20道面试题001

常考语法就是指针,指针与数组、指针与字符串、指针与结构体、指针与函数之间的关系与使用, 以上课为准,辅助《深度理解C指针》这本书。
C++ 复制代码
1. 指针与数组
定义: 数组名在表达式中通常被视为指向数组首元素的指针。
访问元素: 可以通过指针访问数组元素。例如,arr[i]等效于*(arr + i)。
传递数组: 在函数中可以通过指针参数传递数组,以避免复制整个数组
    
2. 指针与字符串
定义: 字符串在C中实际上是一个字符数组,字符串常量(如 "hello")被视为字符指针。
操作: 可以使用指针来遍历字符串
传递字符串: 字符串可以作为指向字符的指针传递给函数

3. 指针与结构体
定义: 结构体可以通过指针进行操作,指针可以指向结构体类型。
访问成员: 使用箭头操作符(->)来访问结构体的成员
动态分配: 可以使用指针动态分配结构体的内存
    
4. 指针与函数
函数指针: 可以定义指向函数的指针,允许将函数作为参数传递或返回。
回调函数: 使用函数指针实现回调机制
传递指针: 可以通过指针传递参数,实现对函数外变量的修改    
---------------------------------------------------------------------
2、数据结构与算法:
常见的八大排序,特别的:快排、堆排,
C++ 复制代码
3. 快速排序
快速排序是基于分治法的排序算法。它通过选择一个"基准"元素,将数组分为两个部分,小于基准的放左边,大于基准的放右边,然后递归地对这两部分进行排序。平均时间复杂度为 (O(n \log n)),最坏情况为 (O(n^2))。

4. 归并排序
归并排序也是一种基于分治法的算法。它将数组分成两半,分别对两部分进行递归排序,然后将两部分合并成一个有序的数组。归并排序的时间复杂度为 (O(n \log n)),并且是稳定的排序算法。

5. 希尔排序
希尔排序是对插入排序的一种改进。它通过将数组分成若干个子序列,对每个子序列进行插入排序,然后逐步减少子序列的数量,最终完成整个数组的排序。希尔排序的时间复杂度因增量选择的不同而有所不同,平均一般为 (O(n^{1.3})) 到 (O(n^{2}))。

6. 堆排序
堆排序利用堆这种数据结构来进行排序。首先将待排序的数组构建成一个最大堆,然后将堆顶元素(最大值)与数组的最后一个元素交换,并重新调整堆。这个过程重复进行,直到堆为空。时间复杂度为 (O(n \log n))。

7. 插入排序
插入排序通过构建一个有序序列,将未排序的元素逐个插入到已排序序列中,直到所有元素都有序。它适合于数据量较小的场景,时间复杂度为 (O(n^2)),但在数据基本有序时效率较高。  
    
1. 选择排序
基本思想是:首先找到数组中的最小元素,将其与数组的第一个元素交换;然后在剩余的元素中继续寻找最小元素,并将其与第二个元素交换,依此类推,直到整个数组排序完成。时间复杂度为 (O(n^2))。

2. 冒泡排序
冒泡排序通过重复比较相邻的元素并交换它们来实现排序。如果在一趟比较中没有发生任何交换,则说明已经排序完成。这个过程重复进行,直到整个数组有序。时间复杂度同样为 (O(n^2))。    
    
数据结构中:链表的操作(创建、插入、删除、逆置、判断环等)、
C++ 复制代码
#include <stdio.h>
#include <stdlib.h>

// 链表节点结构
typedef struct ListNode {
    int value;
    struct ListNode* next;
} ListNode;


// 创建链表
ListNode* create_linked_list(int* values, int size) {
    if (size == 0) return NULL;

    ListNode* head = (ListNode*)malloc(sizeof(ListNode));
    head->value = values[0];
    head->next = NULL;

    ListNode* current = head;
    for (int i = 1; i < size; i++) {
        ListNode* new_node = (ListNode*)malloc(sizeof(ListNode));
        new_node->value = values[i];
        new_node->next = NULL;
        current->next = new_node;
        current = new_node;
    }
    return head;
}
C++ 复制代码
// 在头部插入
ListNode* insert_at_head(ListNode* head, int value) {
    ListNode* new_node = (ListNode*)malloc(sizeof(ListNode));
    new_node->value = value;
    new_node->next = head;
    return new_node;
}

// 在尾部插入
ListNode* insert_at_tail(ListNode* head, int value) {
    ListNode* new_node = (ListNode*)malloc(sizeof(ListNode));
    new_node->value = value;
    new_node->next = NULL;

    if (!head) return new_node;

    ListNode* current = head;
    while (current->next) {
        current = current->next;
    }
    current->next = new_node;
    return head;
}

// 在指定位置插入
ListNode* insert_at_position(ListNode* head, int value, int position) {
    if (position == 0) {
        return insert_at_head(head, value);
    }

    ListNode* new_node = (ListNode*)malloc(sizeof(ListNode));
    new_node->value = value;

    ListNode* current = head;
    for (int i = 0; current != NULL && i < position - 1; i++) {
        current = current->next;
    }

    if (current == NULL) {
        free(new_node); // 释放内存
        return head; // 超出范围
    }

    new_node->next = current->next;
    current->next = new_node;
    return head;
}
C++ 复制代码
// 删除指定值的节点
ListNode* delete_node(ListNode* head, int value) {
    if (!head) return head;

    if (head->value == value) {
        ListNode* temp = head->next;
        free(head);
        return temp; // 删除头节点
    }

    ListNode* current = head;
    while (current->next) {
        if (current->next->value == value) {
            ListNode* temp = current->next;
            current->next = temp->next;
            free(temp);
            return head;
        }
        current = current->next;
    }
    return head;
}
C++ 复制代码
// 逆置链表
ListNode* reverse_linked_list(ListNode* head) {
    ListNode* prev = NULL;
    ListNode* current = head;
    ListNode* next_node = NULL;

    while (current) {
        next_node = current->next; // 保存下一个节点
        current->next = prev;      // 逆转指针
        prev = current;            // 移动 prev
        current = next_node;       // 移动 current
    }
    return prev; // 新的头节点
}
C++ 复制代码
// 判断链表是否存在环        快慢指针
int has_cycle(ListNode* head) {
    ListNode* slow = head;
    ListNode* fast = head;

    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;

        if (slow == fast) {
            return 1; // 存在环
        }
    }
    return 0; // 不存在环
}
C++ 复制代码
int main() {
    int values[] = {1, 2, 3, 4, 5};
    ListNode* head = create_linked_list(values, 5);

    // 测试插入
    head = insert_at_head(head, 0);
    head = insert_at_tail(head, 6);
    head = insert_at_position(head, 7, 3);

    // 测试删除
    head = delete_node(head, 3);

    // 逆置链表
    head = reverse_linked_list(head);

    // 检查环
    printf("Has cycle: %d\n", has_cycle(head));

    // 释放链表内存
    while (head) {
        ListNode* temp = head;
        head = head->next;
        free(temp);
    }

    return 0;
}
树(平衡二叉树、B+树(数据库索引底层数据结构)、
C++ 复制代码
//1. 树的基本概念
树是一种非线性数据结构,由节点组成,节点之间通过边连接。树有以下基本属性:

根节点:树的顶层节点,没有父节点。
叶子节点:没有子节点的节点。
高度:树中节点到叶子的最长路径长度。
深度:节点到根节点的路径长度。
子树:从某个节点开始的子结构。
    
    
//2. 平衡二叉树
	//2.1 定义
平衡二叉树(AVL树)是一种特殊的二叉搜索树(BST),它保证了任何节点的两个子树的==高度差==不超过 1,从而保持树的平衡。这种平衡性确保了查找、插入和删除操作的时间复杂度为 O(log n)。

	//2.2 特点
自平衡:在每次插入或删除之后,AVL树会自动进行==旋转==以保持平衡。
旋转操作:主要有四种旋转操作:
    
右旋 (Right Rotation):当树的左子树比右子树高两个层级,并且插入操作发生在左子树的左侧时(左左情况),需要进行右旋。用于解决左子树过高的问题。
左旋(Left Rotation):当树的右子树比左子树高两个层级,并且插入操作发生在右子树的右侧时(右右情况),需要进行左旋用于解决右子树过高的问题。
左-右旋 (Left-Right Rotation):当树的左子树比右子树高两个层级,并且插入操作发生在左子树的右侧时(左右情况),首先进行左旋,然后进行右旋。用于处理左子树右侧过高的情况。
右-左旋 (Right-Left Rotation):当树的右子树比左子树高两个层级,并且插入操作发生在右子树的左侧时(右左情况),首先进行右旋,然后进行左旋。用于处理右子树左侧过高的情况。
    
	//2.3 时间复杂度
查找:O(log n)
插入:O(log n)
删除:O(log n)
	//2.4 应用
数据库索引
内存中的动态数据结构
实现优先队列
    
    
//3. B+ 树
	//3.1 定义
B+ 树是一种自平衡的树数据结构,广泛用于数据库和文件系统中,特别适合于大规模数据的存储与检索。B+ 树是 B 树的一种变体,它具有所有叶子节点在同一层,并且所有数据都存储在叶子节点中。

	//3.2 特点
多路平衡树:每个节点可以有多个子节点,而不仅仅是两个。B+ 树的每个节点通常包含多个键值对。
叶子节点链接:所有叶子节点通过指针相互链接,便于范围查询。
顺序访问:由于叶子节点的顺序链接,B+ 树能够高效地进行区间查询。
	//3.3 节点结构
内部节点:存储指向子节点的指针和键的值,用于导航。
叶子节点:存储实际的数据记录,并且与其他叶子节点通过指针相连。
	//3.4 操作
查找:与 B 树类似,使用关键字进行查找,复杂度为 O(log n)。
插入:插入操作可能需要分裂节点,复杂度为 O(log n)。
删除:也会涉及节点的合并和调整,复杂度为 O(log n)。
	//3.5 应用
数据库索引(如 MySQL 的 InnoDB 存储引擎)
文件系统(如 NTFS 和 ext4)
大数据存储与检索
C++ 复制代码
#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int key;
    struct Node* left;
    struct Node* right;
    int height;
} Node;

// 函数声明
Node* createNode(int key);            // 创建新节点
int getHeight(Node* N);               // 获取节点高度
int getBalance(Node* N);              // 获取平衡因子
Node* rightRotate(Node* y);           // 右旋
Node* leftRotate(Node* x);            // 左旋
Node* insert(Node* node, int key);    // 插入节点(内部包括四种旋转)
Node* minValueNode(Node* node);       // 获取最小值节点
Node* deleteNode(Node* root, int key);// 删除节点
void inorder(Node* root);             // 中序遍历
void preorder(Node* root);            // 前序遍历
void postorder(Node* root);           // 后序遍历
Node* search(Node* root, int key);
C++ 复制代码
// 创建新节点
Node* createNode(int key) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->key = key;
    newNode->left = NULL;
    newNode->right = NULL;
    newNode->height = 1; // 新节点初始高度为 1
    return newNode;
}

// 获取节点高度
int getHeight(Node* N) {
    if (N == NULL)
        return 0;
    return N->height;
}

// 获取平衡因子
int getBalance(Node* N) {
    if (N == NULL)
        return 0;
    return getHeight(N->left) - getHeight(N->right);
}
C++ 复制代码
// 右旋
Node* rightRotate(Node* y) {
    Node* x = y->left;
    Node* T2 = x->right;

    // 旋转
    x->right = y;
    y->left = T2;

    // 更新高度
    y->height = 1 + max(getHeight(y->left), getHeight(y->right));
    x->height = 1 + max(getHeight(x->left), getHeight(x->right));

    return x;
}

// 左旋
Node* leftRotate(Node* x) {
    Node* y = x->right;
    Node* T2 = y->left;

    // 旋转
    y->left = x;
    x->right = T2;

    // 更新高度
    x->height = 1 + max(getHeight(x->left), getHeight(x->right));
    y->height = 1 + max(getHeight(y->left), getHeight(y->right));

    return y;
}
C++ 复制代码
// 插入节点
Node* insert(Node* node, int key) {
    // 1. 执行常规的 BST 插入
    if (node == NULL)
        return createNode(key);
    if (key < node->key)
        node->left = insert(node->left, key);
    else if (key > node->key)
        node->right = insert(node->right, key);
    else // 不允许重复值
        return node;

    // 2. 更新节点的高度
    node->height = 1 + max(getHeight(node->left), getHeight(node->right));

    // 3. 获取平衡因子并检查是否失衡
    int balance = getBalance(node);

    // 如果失衡,则进行旋转

    // 左左情况
    if (balance > 1 && key < node->left->key)
        return rightRotate(node);

    // 右右情况
    if (balance < -1 && key > node->right->key)
        return leftRotate(node);

    // 左右情况
    if (balance > 1 && key > node->left->key) {
        node->left = leftRotate(node->left);
        return rightRotate(node);
    }

    // 右左情况
    if (balance < -1 && key < node->right->key) {
        node->right = rightRotate(node->right);
        return leftRotate(node);
    }

    return node;
}
C++ 复制代码
// 获取最小值节点
Node* minValueNode(Node* node) {
    Node* current = node;
    while (current->left != NULL)
        current = current->left;
    return current;
}

// 删除节点
Node* deleteNode(Node* root, int key) {
    // 1. 执行常规的 BST 删除
    if (root == NULL)
        return root;

    if (key < root->key)
        root->left = deleteNode(root->left, key);
    else if (key > root->key)
        root->right = deleteNode(root->right, key);
    else {
        // 找到该节点
        if ((root->left == NULL) || (root->right == NULL)) {
            Node* temp = root->left ? root->left : root->right;

            // 没有子节点
            if (temp == NULL) {
                temp = root;
                root = NULL;
            } else // 一个子节点
                *root = *temp; // 复制非空子节点的内容

            free(temp);
        } else {
            // 有两个子节点
            Node* temp = minValueNode(root->right);
            root->key = temp->key; // 复制后继节点的值
            root->right = deleteNode(root->right, temp->key); // 删除后继节点
        }
    }

    // 如果树只有一个节点,返回
    if (root == NULL)
        return root;

    // 更新节点高度
    root->height = 1 + max(getHeight(root->left), getHeight(root->right));

    // 获取平衡因子并检查是否失衡
    int balance = getBalance(root);

    // 如果失衡,则进行旋转

    // 左左情况
    if (balance > 1 && getBalance(root->left) >= 0)
        return rightRotate(root);

    // 左右情况
    if (balance > 1 && getBalance(root->left) < 0) {
        root->left = leftRotate(root->left);
        return rightRotate(root);
    }

    // 右右情况
    if (balance < -1 && getBalance(root->right) <= 0)
        return leftRotate(root);

    // 右左情况
    if (balance < -1 && getBalance(root->right) > 0) {
        root->right = rightRotate(root->right);
        return leftRotate(root);
    }

    return root;
}
C++ 复制代码
// 中序遍历
void inorder(Node* root) {
    if (root != NULL) {
        inorder(root->left);
        printf("%d ", root->key);
        inorder(root->right);
    }
}

// 前序遍历
void preorder(Node* root) {
    if (root != NULL) {
        printf("%d ", root->key);
        preorder(root->left);
        preorder(root->right);
    }
}

// 后序遍历
void postorder(Node* root) {
    if (root != NULL) {
        postorder(root->left);
        postorder(root->right);
        printf("%d ", root->key);
    }
}

// 查找节点
Node* search(Node* root, int key) {
    if (root == NULL || root->key == key)
        return root;

    if (key < root->key)
        return search(root->left, key);
    return search(root->right, key);
}
C++ 复制代码
// 主程序
int main() {
    Node* root = NULL;

    // 插入节点
    int keys[] = {10, 20, 30, 40, 50, 25};
    for (int i = 0; i < sizeof(keys) / sizeof(keys[0]); i++) {
        root = insert(root, keys[i]);
    }

    printf("中序遍历结果:");
    inorder(root);
    printf("\n");

    printf("前序遍历结果:");
    preorder(root);
    printf("\n");

    printf("后序遍历结果:");
    postorder(root);
    printf("\n");

    // 查找节点
    int searchKey = 25;
    Node* foundNode = search(root, searchKey);
    if (foundNode) {
        printf("找到节点: %d\n", foundNode->key);
    } else {
        printf("未找到节点: %d\n", searchKey);
    }

    // 删除节点
    root = deleteNode(root, 30);
    printf("删除节点 30 后的中序遍历结果:");
    inorder(root);
    printf("\n");

    return 0;
}
红黑树(STL中关联式容器的底层数据结构,这个比较繁琐一些))、
C++ 复制代码
红黑树是一种自平衡的二叉搜索树,其主要特性是节点被涂成红色或黑色,以确保树的平衡性。红黑树的"红黑"指的是节点的颜色属性。具体来说,红黑树遵循以下几个性质:

节点颜色:每个节点要么是红色,要么是黑色。
根节点:树的根节点是黑色。
叶子节点:每个叶子节点(空节点)是黑色。
红色节点的限制:如果一个节点是红色,则它的两个子节点必须是黑色(即不能有两个连续的红色节点)。
黑色高度:从任意节点到其每个叶子节点的路径都包含相同数量的黑色节点。

这些性质保证了树的高度在一定范围内,从而在最坏情况下也能保持对数时间复杂度的查找、插入和删除操作。通过使用红黑树,能够有效地保持二叉搜索树的平衡性,避免退化为链表的情况。
    
这些特性确保了树的高度是对数级别,从而保证了插入、删除和查找操作的时间复杂度为 (O(\log n))。
C++ 复制代码
节点结构:RBNode 包含键、颜色、左右子节点和父节点指针。
红黑树结构:RBTree 包含根节点和哨兵节点。
创建节点和树:createNode 和 createRBTree 用于初始化节点和树。
左旋和右旋:leftRotate 和 rightRotate 用于维护树的平衡。
插入操作:insert 方法插入新节点并调用 fixInsert 来修复树的性质。
中序遍历:inOrder 和 inOrderHelper 用于打印树的节点。
    
#include <stdio.h>
#include <stdlib.h>

// 节点颜色定义
typedef enum { RED, BLACK } NodeColor;

// 红黑树节点定义
typedef struct RBNode {
    int key;
    NodeColor color;
    struct RBNode *left, *right, *parent;
} RBNode;

// 红黑树定义
typedef struct RBTree {
    RBNode *root;
    RBNode *TNULL; //TNULL: 一个特殊的哨兵节点,用于简化树的操作(如插入和删除)
} RBTree;






// 创建新节点
RBNode* createNode(int key) {
    RBNode* node = (RBNode*)malloc(sizeof(RBNode));
    node->key = key;
    node->left = NULL;
    node->right = NULL;
    node->parent = NULL;
    node->color = RED; // 新节点默认为红色
    return node;
}

// 创建红黑树及哨兵节点
RBTree* createRBTree() {
    RBTree* tree = (RBTree*)malloc(sizeof(RBTree));
    tree->TNULL = createNode(0); // 哨兵节点
    tree->TNULL->color = BLACK; // 哨兵节点为黑色
    tree->root = tree->TNULL;
    return tree;
}

// 左旋转
void leftRotate(RBTree *tree, RBNode *x) {
    RBNode *y = x->right;
    x->right = y->left;

    if (y->left != tree->TNULL) {
        y->left->parent = x;
    }

    y->parent = x->parent;

    if (x->parent == tree->TNULL) {
        tree->root = y;
    } else if (x == x->parent->left) {
        x->parent->left = y;
    } else {
        x->parent->right = y;
    }

    y->left = x;
    x->parent = y;
}

// 右旋转
void rightRotate(RBTree *tree, RBNode *x) {
    RBNode *y = x->left;
    x->left = y->right;

    if (y->right != tree->TNULL) {
        y->right->parent = x;
    }

    y->parent = x->parent;

    if (x->parent == tree->TNULL) {
        tree->root = y;
    } else if (x == x->parent->right) {
        x->parent->right = y;
    } else {
        x->parent->left = y;
    }

    y->right = x;
    x->parent = y;
}

// 插入修正函数
void fixInsert(RBTree *tree, RBNode *k) {
    RBNode *u; // 叔叔节点

    while (k->parent->color == RED) {
        if (k->parent == k->parent->parent->left) {
            u = k->parent->parent->right;
            if (u->color == RED) {
                // 情况 1: 叔叔是红色
                k->parent->color = BLACK;
                u->color = BLACK;
                k->parent->parent->color = RED;
                k = k->parent->parent;
            } else {
                if (k == k->parent->right) {
                    // 情况 2: 当前节点是右孩子
                    k = k->parent;
                    leftRotate(tree, k);
                }
                // 情况 3: 当前节点是左孩子
                k->parent->color = BLACK;
                k->parent->parent->color = RED;
                rightRotate(tree, k->parent->parent);
            }
        } else {
            u = k->parent->parent->left;
            if (u->color == RED) {
                // 情况 1: 叔叔是红色
                k->parent->color = BLACK;
                u->color = BLACK;
                k->parent->parent->color = RED;
                k = k->parent->parent;
            } else {
                if (k == k->parent->left) {
                    // 情况 2: 当前节点是左孩子
                    k = k->parent;
                    rightRotate(tree, k);
                }
                // 情况 3: 当前节点是右孩子
                k->parent->color = BLACK;
                k->parent->parent->color = RED;
                leftRotate(tree, k->parent->parent);
            }
        }
        if (k == tree->root) {
            break;
        }
    }
    tree->root->color = BLACK;
}

// 插入节点
void insert(RBTree *tree, int key) {
    RBNode *node = createNode(key);
    node->parent = tree->TNULL;

    RBNode *y = tree->TNULL;
    RBNode *x = tree->root;

    while (x != tree->TNULL) {
        y = x;
        if (node->key < x->key) {
            x = x->left;
        } else {
            x = x->right;
        }
    }

    node->parent = y;
    if (y == tree->TNULL) {
        tree->root = node;
    } else if (node->key < y->key) {
        y->left = node;
    } else {
        y->right = node;
    }

    node->left = tree->TNULL;
    node->right = tree->TNULL;

    // 修复红黑树
    fixInsert(tree, node);
}

// 中序遍历
void inOrderHelper(RBNode *node, RBTree *tree) {
    if (node != tree->TNULL) {
        inOrderHelper(node->left, tree);
        printf("%d ", node->key);
        inOrderHelper(node->right, tree);
    }
}

// 打印中序遍历
void inOrder(RBTree *tree) {
    inOrderHelper(tree->root, tree);
}

// 主函数测试红黑树
int main() {
    RBTree *tree = createRBTree();

    // 插入节点
    insert(tree, 55);
    insert(tree, 40);
    insert(tree, 65);
    insert(tree, 30);
    insert(tree, 50);
    insert(tree, 60);
    insert(tree, 70);

    // 打印中序遍历
    printf("中序遍历结果: ");
    inOrder(tree);
    printf("\n");

    // 释放内存(在实际使用中需要实现完整的释放逻辑)
    free(tree->TNULL);
    free(tree);
    return 0;
}
红黑树的哨兵节点
C++ 复制代码
哨兵节点(不实际存储数据)在红黑树等数据结构中能够简化操作,主要是因为它提供了一个统一的处理方式,避免了对空指针的特殊情况处理。具体来说,哨兵节点的优势包括:

消除空指针检查:使用哨兵节点允许我们在树的每个节点都可以访问左右子节点,即使是叶子节点。这样一来,很多操作(如插入、删除和遍历)就无需特别判断当前节点是否为空,从而简化代码逻辑。

统一结构:哨兵节点充当了一个虚拟的叶节点(通常为黑色),使得所有非叶节点都遵循相同的结构。这种一致性使得实现各种操作时的逻辑更加直观。

减少边界条件:在执行旋转或调整颜色等操作时,哨兵节点可以作为一个稳定的参考点,减少了边界条件的复杂性。例如,在插入新节点时,无需单独处理根节点的情况,因为根节点的父指针可以指向哨兵节点,而不是 NULL。

简化算法实现:许多维护红黑树性质的算法(如修复插入后的性质)可以通过统一的逻辑实现,避免了多次重复代码。因为哨兵节点的存在,算法的实现可以更加简洁且易于理解
C++ 复制代码
          [10B]
         /      \
     [5R]        [15R]
     /   \       /    \
  [2B]  [7B] [12B]   [20B]
   |      |     |      |
 [NIL]  [NIL] [NIL] [NIL]
哈希表(这个在STL中无序关联式容器也会被问到)。
C++ 复制代码
//哈希表的定义
哈希表(Hash Table)是一种数据结构,通过将键(Key)映射到值(Value)的方式来实现高效的数据存储和查找。它利用哈希函数将键转换为数组的索引,从而支持快速的数据访问。

//哈希表的基本组成
哈希函数:将输入的键转换为固定大小的整数索引。
数组:存储值的容器,通常被称为"桶"或"槽"。
冲突处理机制:当多个键映射到同一索引时,采用的方法以解决此问题(如链地址法、开放寻址法等)。
    
//哈希表的问题
1.hash冲突:多个键可能被哈希到同一个索引。解决方法包括:

链地址法:在每个索引处使用链表存储冲突的键值对。
开放寻址法:在表中寻找下一个空槽来存储冲突的键值对。
负载因子:负载因子是哈希表中元素的数量与数组大小的比率。负载因子过高会影响性能,因此通常会在达到一定负载因子时进行扩容。

2.hash函数的选择:一个好的哈希函数能均匀地分配键值,避免冲突。差的哈希函数会导致大量冲突,从而降低性能。
3.动态扩展:当哈希表装填达到一定阈值时,需要动态扩展,这涉及重新计算每个元素的哈希值并插入新的数组。

//哈希表的优点
快速查找:平均时间复杂度为 O(1),非常高效。
灵活性:可以存储各种类型的数据,不仅限于数字。
哈希表的缺点
内存消耗:为了减少冲突,可能会预留较多的空间。
不支持有序操作:哈希表内部没有顺序,无法按键的顺序遍历。    
C++ 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define TABLE_SIZE 10  // 哈希表大小

// 键值对结构
typedef struct KeyValue {
    char *key;
    int value;
    struct KeyValue *next; // 指向下一个节点
} KeyValue;

// 哈希表结构
typedef struct HashTable {
    KeyValue **table; // 哈希桶数组
} HashTable;

// 哈希函数
unsigned int hash(const char *key) {
    unsigned long int hashval = 0;
    while (*key != '\0') {
        hashval = (hashval << 5) + *key++; // 计算哈希值
    }
    return hashval % TABLE_SIZE;
}

// 创建哈希表
HashTable *create_table() {
    HashTable *ht = malloc(sizeof(HashTable));
    ht->table = malloc(sizeof(KeyValue *) * TABLE_SIZE);
    for (int i = 0; i < TABLE_SIZE; i++) {
        ht->table[i] = NULL; // 初始化为NULL
    }
    return ht;
}

// 插入键值对
void insert(HashTable *ht, const char *key, int value) {
    unsigned int index = hash(key);
    KeyValue *new_pair = malloc(sizeof(KeyValue));
    new_pair->key = strdup(key); // 复制键
    new_pair->value = value;
    new_pair->next = ht->table[index]; // 将新节点插入链表头
    ht->table[index] = new_pair;
}

// 查找值
int search(HashTable *ht, const char *key) {
    unsigned int index = hash(key);
    KeyValue *pair = ht->table[index];
    while (pair != NULL) {
        if (strcmp(pair->key, key) == 0) {
            return pair->value; // 找到返回值
        }
        pair = pair->next; // 移动到下一个节点
    }
    return -1; // 未找到返回-1
}

// 删除键值对
void delete(HashTable *ht, const char *key) {
    unsigned int index = hash(key);
    KeyValue *pair = ht->table[index];
    KeyValue *prev = NULL;

    while (pair != NULL) {
        if (strcmp(pair->key, key) == 0) {
            if (prev == NULL) {
                ht->table[index] = pair->next; // 删除头节点
            } else {
                prev->next = pair->next; // 删除中间或尾节点
            }
            free(pair->key);
            free(pair);
            return;
        }
        prev = pair;
        pair = pair->next;
    }
}

// 释放哈希表
void free_table(HashTable *ht) {
    for (int i = 0; i < TABLE_SIZE; i++) {
        KeyValue *pair = ht->table[i];
        while (pair != NULL) {
            KeyValue *temp = pair;
            pair = pair->next;
            free(temp->key);
            free(temp);
        }
    }
    free(ht->table);
    free(ht);
}

int main() {
    HashTable *ht = create_table();
    
    insert(ht, "apple", 1);
    insert(ht, "banana", 2);
    insert(ht, "orange", 3);

    printf("Value for 'apple': %d\n", search(ht, "apple"));
    printf("Value for 'banana': %d\n", search(ht, "banana"));
    
    delete(ht, "banana");
    printf("Value for 'banana' after deletion: %d\n", search(ht, "banana"));

    free_table(ht);
    return 0;
}
---------------------------------------------------------------------
3、Linux:
进程与线程的基本概念、
C++ 复制代码
进程和线程是操作系统中两个重要的概念,它们用于管理程序的执行。

//进程(Process)
定义:进程是一个正在运行的程序实例,是操作系统资源分配的基本单位。每个进程都有自己的地址空间、数据栈和其他辅助数据。

特性:
独立性:进程之间相互独立,一个进程的崩溃不会直接影响到其他进程。
资源拥有:每个进程有自己的资源,如内存、文件句柄等。
状态:进程可以处于创建、就绪、运行、阻塞和终止等状态。
调度:操作系统通过==调度算法==来管理多个进程的运行,确保系统资源的合理利用。

//线程(Thread)
定义:线程是进程中的一个执行单元,是程序执行的最小单位。一个进程可以包含多个线程,这些线程共享进程的资源。

特性:
轻量级:线程比进程更轻量,创建和销毁的开销较小。
共享资源:同一进程内的线程可以共享该进程的内存和资源,从而能够更高效地进行通信。
并发执行:多个线程可以并发执行,提高程序的运行效率。
调度:线程调度通常由操作系统或运行时环境负责,支持多线程并发执行。

//进程与线程的比较
特性	      进程	      			线程
资源拥有	拥有独立的资源			 共享进程的资源
创建开销	较大	        		 较小
通信方式	通过进程间通信(IPC)	通过共享内存、信号量等
切换开销	较大					较小
独立性		 进程独立	 			线程依赖于所在的进程
//总结
进程是资源分配的基本单位,而线程是程序执行的基本单位。
理解进程和线程的区别有助于更好地进行程序设计和优化,以提高系统性能和响应速度。
进程间通信的几种方式以及区别;
C++ 复制代码
进程间通信(Inter-Process Communication, IPC)是指在不同进程之间交换数据和信息的机制。以下是几种常见的进程间通信方式及其区别:

1. 管道(Pipe)
定义:管道是一种半双工的通信方式,允许一个进程将数据写入管道,而另一个进程则从管道中读取数据。
特点:
只能在具有亲缘关系的进程间使用(如父子进程)。
数据传输是顺序的,先进先出(FIFO)。
适合小量数据的传输。
    
2. 命名管道(Named Pipe)
定义:命名管道也是一种管道,但它通过一个名字在文件系统中创建,可以在不相关的进程间通信。
特点:
支持双向通信。
允许不相关的进程进行数据传输。
仍然保持先进先出的特性。
    
3. 消息队列(Message Queue)
定义:消息队列允许进程以消息的形式异步地发送和接收数据。
特点:
支持多个发送者和接收者。
消息可以按照优先级处理。
适合需要异步处理的大量数据传输。
    
4. 共享内存(Shared Memory)
定义:共享内存允许多个进程直接访问同一块内存区域,以进行数据交换。
特点:
速度快,因为数据直接在内存中共享。
需要同步机制(如信号量)来防止竞争条件。
适合大规模数据传输。
    
5. 信号(Signal)
定义:信号是一种用于通知进程某个事件发生的机制,例如外部中断或进程状态变化。
特点:
主要用于进程之间的事件通知。
信号本身不传递数据,只传递控制信息。
信号处理较为复杂,易出错。
    
6. 套接字(Socket)
定义:套接字是一种用于在网络上或本地计算机上进行进程间通信的机制。
特点:
支持跨网络的进程通信。
可以是面向连接(TCP)或无连接(UDP)的。
适合需要网络通信的应用场景。
    
7. 文件映射(Memory-Mapped Files)
定义:文件映射允许多个进程访问同一文件的内容,就像访问内存一样。
特点:
适合大数据量的共享。
通过操作系统的文件系统进行管理。
需要注意同步问题。
OSI七层模型,每一层常规协议;
C++ 复制代码
//从下到上,物链网传会表应

1. 物理层 (Physical Layer)
功能: 负责数据的物理传输,包括电气信号、光信号或无线信号等。
常规协议/标准:Ethernet (IEEE 802.3),USB,DSL,RS-232
    
2. 数据链路层 (Data Link Layer)
功能: 提供节点间的数据帧传输,处理物理地址和错误检测。
常规协议/标准:Ethernet (IEEE 802.3),Wi-Fi (IEEE 802.11),PPP (Point-to-Point Protocol)
HDLC (High-Level Data Link Control)
    
3. 网络层 (Network Layer)
功能: 负责数据包的路由选择和转发,处理逻辑地址(如IP地址)。
常规协议/标准:IP (Internet Protocol),ICMP (Internet Control Message Protocol)
IGMP (Internet Group Management Protocol),RIP (Routing Information Protocol)
    
4. 传输层 (Transport Layer)
功能: 提供端到端的通信,确保数据完整性和顺序。
常规协议/标准:TCP (Transmission Control Protocol),UDP (User Datagram Protocol)
SCTP (Stream Control Transmission Protocol)
    
5. 会话层 (Session Layer)
功能: 管理会话和连接,控制对话的建立、维护和终止。
常规协议/标准:NetBIOS,RPC (Remote Procedure Call),PPTP (Point-to-Point Tunneling Protocol)
    
6. 表示层 (Presentation Layer)
功能: 数据格式化和转换,处理加密和解密。
常规协议/标准:SSL/TLS (Secure Sockets Layer / Transport Layer Security)
JPEG, GIF, PNG (图像格式),ASCII, EBCDIC (字符编码)
    
7. 应用层 (Application Layer)
功能: 提供用户与应用程序之间的接口,处理高层协议。
常规协议/标准:HTTP/HTTPS (Hypertext Transfer Protocol),FTP (File Transfer Protocol)
SMTP (Simple Mail Transfer Protocol),DNS (Domain Name System)
TCP三次握手与四次挥手;
C++ 复制代码
//TCP三次握手(建立连接)
第一次握手:客户端发送一个SYN(同步)报文段,带有初始序列号,表示请求建立连接。
第二次握手:服务器收到SYN报文段后,回复一个SYN-ACK(同步-确认)报文段,表示同意建立连接,同时也发送			 自己的初始序列号。
第三次握手:客户端收到SYN-ACK报文段后,发送一个ACK(确认)报文段,确认收到服务器的SYN,此时连接建			  立完成。

//TCP四次挥手(终止连接)
第一次挥手:主动关闭连接的一方(例如客户端)发送一个FIN(结束)报文段,表示希望关闭连接。
第二次挥手:另一方(例如服务器)收到FIN报文段后,发送一个ACK报文段,确认收到关闭请求,这一阶段连接		  的一方进入FIN_WAIT_1状态。
第三次挥手:服务器在处理完所有数据后,发送一个FIN报文段,表示也希望关闭连接。
第四次挥手:客户端收到FIN报文段后,发送一个ACK报文段,确认关闭请求,此时客户端进入TIME_WAIT状态,		  等待可能的重发报文段,之后最终关闭连接。

//总结
三次握手用于建立连接,确保双方都准备好进行数据传输。
四次挥手用于安全地终止连接,确保所有数据都已成功传输并接收。
socket网络编程的流程;
C++ 复制代码
//server
int server_socket, client_socket;
struct sockaddr_in server_addr, client_addr;
socklen_t addr_len = sizeof(client_addr);

// 创建 socket
server_socket = socket(AF_INET, SOCK_STREAM, 0);

// 设置地址结构 sockaddr_in
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY; // 监听所有 IP
server_addr.sin_port = htons(PORT);

// 绑定 bind
bind(server_socket, (struct sockaddr*)&server_addr, sizeof(server_addr));

// 监听 listen
listen(server_socket, BACKLOG);

// 接受连接 accept
client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &addr_len);

// 数据传输 send recv
recv(client_socket, buffer, sizeof(buffer), 0);
send(client_socket, response, sizeof(response), 0);

// 关闭连接 close
close(client_socket);
close(server_socket);
C++ 复制代码
//client
int client_socket;
struct sockaddr_in server_addr;

// 创建 socket
client_socket = socket(AF_INET, SOCK_STREAM, 0);

// 设置地址结构 address_in
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
inet_pton(AF_INET, "SERVER_IP", &server_addr.sin_addr); // 服务器 IP

// 连接到服务器 connect
connect(client_socket, (struct sockaddr*)&server_addr, sizeof(server_addr));

// 数据传输 send recv
send(client_socket, request, sizeof(request), 0);
recv(client_socket, buffer, sizeof(buffer), 0);

// 关闭连接 close
close(client_socket);
三种IO多路复用的原理、底层数据结构、使用方法,
C++ 复制代码
IO多路复用是指在单个线程中同时监控多个文件描述符的状态,以便在某个文件描述符准备好进行读写操作时,能够及时响应。常见的三种IO多路复用机制是:select、poll和epoll。以下是它们的原理和底层数据结构的简要介绍。

1. select
//原理
select函数允许程序监视多个文件描述符,以查看它们是否可以进行读、写或异常处理。
它使用一个集合(fd_set)来表示要监视的文件描述符。
//底层数据结构
fd_set:一个位域数组,每一位对应一个文件描述符。最大文件描述符的数量受限于系统常量FD_SETSIZE(通常为1024)。
使用宏FD_SET、FD_CLR、FD_ISSET来操作这个集合。
//缺点
文件描述符的数量受到FD_SETSIZE的限制,且每次调用select都需要重新设置监视的集合。
效率较低,在高并发场景下性能下降明显。
    
2. poll
//原理
poll函数与select类似,但它不使用固定大小的集合,而是使用一个可扩展的数组来存储待监视的文件描述符及其事件。
它能监视更多的文件描述符,没有FD_SETSIZE的限制。
//底层数据结构
struct pollfd:包含文件描述符和事件类型的结构体。
poll函数接受一个pollfd数组和其大小作为参数。
//缺点
每次调用poll都需传递整个数组,仍然存在线性扫描的问题,性能在大量文件描述符时可能会下降。

3. epoll
//原理
epoll是Linux特有的高效IO多路复用机制,设计用于处理大量并发连接。
它通过内核空间与用户空间的分离来提高效率,只在感兴趣的文件描述符上进行操作。
//底层数据结构
epoll_event:包含文件描述符及其关注的事件。
epoll接口提供了epoll_create、epoll_ctl和epoll_wait等函数来创建、控制和等待事件。
//优点
可以处理大规模的文件描述符,性能优于select和poll,尤其是在大量连接的情况下。
支持边缘触发(edge-triggered)和水平触发(level-triggered)两种模式。
C++ 复制代码
struct pollfd {
    int fd;         // 文件描述符
    short events;   // 关注的事件(如可读、可写等)
    short revents;  // 返回的事件(实际发生的事件)
};

struct epoll_event {
    uint32_t events;    // 关注的事件(如EPOLLIN、EPOLLOUT等)
    epoll_data_t data;  // 用户自定义的数据
};
特别是epoll的用法、边沿触发与水平触发的区别与联系;
C++ 复制代码
//边沿触发与水平触发的区别与联系
水平触发(Level Triggered)
定义:在水平触发模式下,只要文件描述符的状态满足条件(例如可读或可写),epoll_wait就会返回该事件。
特点:
只要数据尚未被读取,事件就会持续触发。
程序需要确保在处理完事件后尽快读取数据,以避免反复触发同一个事件。
使用场景:适合大多数应用,因为简单易理解。
    
边缘触发(Edge Triggered)
定义:在边缘触发模式下,只有当文件描述符的状态从未准备好变为准备好时,epoll_wait才会返回该事件。
特点:
一旦事件被触发,如果没有新数据到来,之后不会再次触发。
程序必须在事件触发时尽可能多地读取数据,否则可能会遗漏后续的可读事件。
使用场景:适合高性能、低延迟的应用,能够减少不必要的系统调用,但需要更复杂的逻辑处理。
    
联系
性能:边缘触发通常在高负载情况下提供更好的性能,因为它减少了重复唤醒的次数。
实现复杂性:边缘触发的实现需要开发者更加小心,以确保数据完全被读取,避免漏掉后续事件。
选择:开发者需要根据应用场景和性能需求选择使用水平触发或边缘触发。
C++ 复制代码
// 添加文件描述符时设置边缘触发
ev.events = EPOLLIN | EPOLLET; // 边缘触发
epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);

// 处理事件时确保尽可能多地读取数据
while (1) {
    int n = read(fd, buffer, sizeof(buffer));
    if (n <= 0) {
        // 处理错误或EOF
        break;
    }
    // 处理读取的数据
}
同步、异步、阻塞、非阻塞;
C++ 复制代码
阻塞/非阻塞关注的是/*==用户态进程/线程==*/的状态
其要访问的数据是否就绪,进程/线程是否需要等待。当前接口数据还未准备就绪时,线程是否被阻塞挂起。
    //阻塞挂起
    就是当前线程还处于CPU时间片当中,调用了阻塞的方法,由于数据未准备就绪,则时间片还未到就让出CPU。
    //非阻塞
    就是当前接口数据还未准备就绪时,线程不会被阻塞挂起,可以不断轮询请求接口,看看数据是否已经准备就绪。


同步/异步关注的是/*消息通信机制*/
    同步,就是在发出一个调用时,自己需要参与等待结果的过程,则为同步。同步需要主动读写数据,在读写数据的过程中还是会阻塞。
    异步IO,则指出发出调用以后到数据准备完成,自己都未参与,则为异步。异步只需要关注IO操作完成的通知,并不主动读写数据,由操作系统内核完成数据的读写。
五种网络IO模型;
C++ 复制代码
1. 阻塞I/O (Blocking I/O)
定义:在阻塞I/O模型中,调用I/O操作时,程序会被挂起,直到I/O操作完成。
特点:
程序在进行I/O操作时无法执行其他任务。
简单易用,但在高并发情况下可能导致性能瓶颈。
    
2. 非阻塞I/O (Non-blocking I/O)
定义:在非阻塞I/O模型中,I/O操作不会使程序挂起。如果数据不可用,操作会立即返回。
特点:
程序可以执行其他任务,不会因I/O操作而阻塞。
通常需要轮询或使用回调机制来检查数据是否可用。
    
3. I/O复用 (I/O Multiplexing)
定义:通过使用select、poll或epoll等系统调用,程序可以监视多个I/O流,并处理其中有数据的流。
特点:
适合于高并发场景,可以同时处理多个连接。
程序在等待I/O时不会被阻塞,而是等待多个文件描述符的状态变化。
    
4. 信号驱动I/O (Signal-driven I/O)
定义:在信号驱动I/O模型中,程序注册一个信号处理程序,以便在I/O操作就绪时接收信号通知。
特点:
可以避免轮询,提高效率。
复杂性较高,需要处理信号安全和信号处理的细节。
    
5. 异步I/O (Asynchronous I/O)
定义:在异步I/O模型中,程序发起I/O请求后立即返回,并在I/O操作完成时通过回调函数或其他机制获得结果。
特点:
无需轮询或等待,能够有效利用CPU资源。
编程模型相对复杂,需要管理回调和状态。
常见的并发服务器模型。
C++ 复制代码
1. 单线程模型 (Single-threaded Model)
定义:服务器在一个线程中处理所有请求,通常使用非阻塞I/O或I/O复用来管理多个连接。
特点:
简单实现,无需多线程管理。
适合小型应用和低并发场景。
可能在高负载下成为瓶颈。
    
2. 多线程模型 (Multi-threaded Model)
定义:每个客户端请求由一个独立的线程处理。
特点:
充分利用多核CPU,能处理多个请求。
线程切换开销较高,可能导致性能下降。
需要管理线程的创建、销毁和同步。
    
3. 进程模型 (Multi-process Model)
定义:每个请求由一个独立的进程处理,通常使用系统的多进程支持。
特点:
提供更强的隔离性和安全性,避免了线程间共享内存的问题。
进程间通信开销较大。
启动和管理进程的开销高于线程。
    
4. 线程池模型 (Thread Pool Model)
定义:预先创建一组线程,客户端请求到达时从线程池中获取线程处理,处理完成后返回线程池。
特点:
降低了线程创建和销毁的开销。
可以控制并发量,提高资源利用率。
适合高并发场景。
    
5. 事件驱动模型 (Event-driven Model)
定义:使用事件循环和回调机制响应客户端的请求,通常结合非阻塞I/O。
特点:
高效处理大量连接,适合I/O密集型应用。
复杂性较高,需管理事件和回调。
Node.js 和 Nginx 是基于此模型的典型例子。
    
6. 混合模型 (Hybrid Model)
定义:结合多线程和事件驱动模型,例如在每个线程中使用事件循环。
特点:
灵活性高,可以根据需求进行调整。
适合复杂的应用场景,能够同时处理计算密集型和I/O密集型任务。
---------------------------------------------------------------------
4、C++的基础:
常见的基础语法,比如:const可以修饰哪些(指针常量与常量指针的区别、
C++ 复制代码
const int *ptr:指向的值是常量,不能通过指针修改它的值,但指针本身可以指向其他变量。
int *const ptr:指针是常量,不能改变指向,但可以修改指向的值。
    
const 修饰   变量,指针,成员函数,参数    
数组指针与指针数组、函数指针与指针数组的区别,这个上课怎么讲的);
C++ 复制代码
int (*ptr)[5] = &arr; // ptr是指向包含5个int类型元素的数组的指针
int *arr[5]; // arr是一个包含5个指针的数组
void (*funcPtr)(int) = myFunction; // funcPtr是指向myFunction的指针
void (*funcArr[2])(int) = {func1, func2}; // funcArr是一个包含2个函数指针的数组

1. 数组指针与指针数组
数组指针(Pointer to Array)
定义:指向一个数组的指针。        声明:通常使用type (*ptr)[size]的形式。
指针数组(Array of Pointers)
定义:数组中的每个元素都是指针。  声明:通常使用type *arr[size]的形式。 
    
2. 函数指针与指针数组
函数指针(Pointer to Function)
定义:指向函数的指针,可以用来调用指向的函数。  return_type (*ptr)(parameter_types)的形式    
指针数组(Array of Function Pointers)
定义:数组中的每个元素都是指向函数的指针。  return_type (*arr[size])(parameter_types)的形式    
四种强制转换叫什么,有什么区别;
C++ 复制代码
float b = static_cast<float>(a); // int 转 float
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 安全转换,基类转派生类
int* b = const_cast<int*>(&a); // 去掉const修饰
void* ptr = reinterpret_cast<void*>(&a); // int* 转 void*

static_cast
用途:用于在相关类型之间进行安全的转换,如基本数据类型(int、float等)之间的转换,或者类层次结构中的基类和派生类之间的转换。
特点:编译时检查,确保类型转换的安全性。
    
dynamic_cast
用途:主要用于类的多态性,安全地将基类指针或引用转换为派生类指针或引用。
特点:运行时检查,如果转换不安全,将返回nullptr(对于指针)或抛出std::bad_cast(对于引用)
    
const_cast
用途:用于去掉对象的常量性(const),允许修改原本被声明为const的对象。
特点:只能添加或去除常量性,不改变对象的类型
    
reinterpret_cast
用途:用于进行底层的类型转换,可以将指针类型转换为任何其他指针类型,或将整数类型转换为指针类型等。
特点:不进行任何检查,转换结果可能是不安全的,使用时需谨慎    
相关推荐
ChoSeitaku26 分钟前
链表循环及差集相关算法题|判断循环双链表是否对称|两循环单链表合并成循环链表|使双向循环链表有序|单循环链表改双向循环链表|两链表的差集(C)
c语言·算法·链表
娅娅梨28 分钟前
C++ 错题本--not found for architecture x86_64 问题
开发语言·c++
兵哥工控32 分钟前
MFC工控项目实例二十九主对话框调用子对话框设定参数值
c++·mfc
Fuxiao___35 分钟前
不使用递归的决策树生成算法
算法
我爱工作&工作love我40 分钟前
1435:【例题3】曲线 一本通 代替三分
c++·算法
娃娃丢没有坏心思1 小时前
C++20 概念与约束(2)—— 初识概念与约束
c语言·c++·现代c++
lexusv8ls600h1 小时前
探索 C++20:C++ 的新纪元
c++·c++20
lexusv8ls600h1 小时前
C++20 中最优雅的那个小特性 - Ranges
c++·c++20
白-胖-子1 小时前
【蓝桥等考C++真题】蓝桥杯等级考试C++组第13级L13真题原题(含答案)-统计数字
开发语言·c++·算法·蓝桥杯·等考·13级
workflower1 小时前
数据结构练习题和答案
数据结构·算法·链表·线性回归