C++ 数据结构复习笔记

C++ 数据结构复习笔记


目录

  1. 复杂度分析
  2. 数组与字符串
  3. 链表
  4. 栈与队列
  5. 哈希表
  6. 树与二叉树
  7. [二叉搜索树 BST](#二叉搜索树 BST)
  8. [平衡二叉树 AVL](#平衡二叉树 AVL)
  9. 红黑树
  10. 堆与优先队列
  11. [图与遍历 BFS / DFS](#图与遍历 BFS / DFS)
  12. 排序算法
  13. 动态规划基础
  14. 数据结构面试概念考点

1. 复杂度分析

1.1 时间复杂度

复杂度 含义 常见算法
O(1) 常数 数组下标访问、哈希查找
O(log n) 对数 二分查找、平衡树查找
O(n) 线性 遍历数组、链表
O(n log n) 线性对数 归并排序、堆排序、快速排序(平均)
O(n²) 平方 冒泡排序、选择排序、插入排序
O(2ⁿ) 指数 递归求解斐波那契(无记忆化)

1.2 递归复杂度 --- 主定理

对于递归式 T(n) = a·T(n/b) + f(n)

情况 条件 复杂度
1 f(n) = O(n^(log_b(a) - ε)) T(n) = Θ(n^(log_b(a)))
2 f(n) = Θ(n^(log_b(a))) T(n) = Θ(n^(log_b(a)) · log n)
3 f(n) = Ω(n^(log_b(a) + ε)) T(n) = Θ(f(n))

常考例子:

cpp 复制代码
// 二分查找: T(n) = T(n/2) + O(1)           => O(log n)
// 归并排序: T(n) = 2T(n/2) + O(n)          => O(n log n)
// 遍历二叉树: T(n) = 2T(n/2) + O(1)        => O(n)

1.3 空间复杂度

指算法运行所需的额外存储空间,不包括输入数据本身。重点关注递归调用栈深度和临时数据结构的大小。


2. 数组与字符串

2.1 数据结构基本概念

数组 是一段连续 的内存空间,存储相同类型的元素,支持通过下标**O(1)**随机访问。

核心性质:

  • 连续存储 → 缓存友好,遍历效率高
  • 固定大小(C 风格数组)或动态扩容(std::vector
  • 中间插入/删除需要移动元素 → O(n)

2.2 STL array / vector 对比

特性 std::array<T, N> std::vector<T>
大小 编译期固定 动态可变
内存位置 栈上(或作为对象成员) 堆上分配
随机访问 O(1) O(1)
尾插/尾删 --- 均摊 O(1)
中间插入/删除 --- O(n)
cpp 复制代码
int a[5] = {1, 2, 3, 4, 5};
std::array<int, 5> arr = {1, 2, 3, 4, 5};
std::vector<int> vec = {1, 2, 3};
vec.push_back(4);                        // 均摊 O(1)
vec.pop_back();                          // O(1)
vec.insert(vec.begin() + 1, 10);        // O(n) --- 后续元素全部后移

2.3 vector 扩容机制(面试重点)

cpp 复制代码
// vs/g++ 扩容因子通常为 1.5x 或 2x
// size() = 当前元素个数    capacity() = 已分配空间能容纳的元素个数
// 扩容步骤: 分配新内存 → 拷贝/移动旧元素 → 释放旧内存
// 均摊复杂度 O(1) 的证明(聚合分析法 / 记账法)

std::vector<int> v;
v.reserve(100);  // 预分配,避免多次扩容

为什么扩容因子选 1.5 或 2? --- 选 2 是经典的翻倍策略,选 1.5 可以让之前释放的内存块被复用,减少内存碎片。

2.4 基础操作模板

cpp 复制代码
// ---------- 反转数组 ----------
void reverseArray(std::vector<int>& arr) {
    int l = 0, r = arr.size() - 1;
    while (l < r) std::swap(arr[l++], arr[r--]);
}

// ---------- 二分查找(有序数组)----------
int binarySearch(std::vector<int>& arr, int target) {
    int l = 0, r = arr.size() - 1;
    while (l <= r) {
        int mid = l + (r - l) / 2;  // 防溢出
        if (arr[mid] == target) return mid;
        else if (arr[mid] < target) l = mid + 1;
        else r = mid - 1;
    }
    return -1;
}

3. 链表

3.1 数据结构基本概念

链表 由一系列节点 组成,每个节点包含数据域和指向下一节点的指针 。节点在内存中不必连续

核心性质:

  • 动态大小,不需要预分配
  • 插入/删除节点只需修改指针 → O(1)(前提是已定位到插入位置)
  • 不支持随机访问,查找必须从头遍历 → O(n)
  • 额外指针开销

3.2 数组 vs 链表(概念对比,高频)

维度 数组 链表
内存布局 连续 分散,通过指针链接
随机访问 O(1) O(n)
插入/删除 O(n)(需移动元素) O(1)(修改指针,已定位时)
缓存友好 好(连续内存) 差(内存跳跃)
空间开销 无额外开销 每个节点额外存指针
扩容 需整体重新分配 按需逐个分配
大小 固定或动态扩容 完全动态

3.3 单链表节点定义

cpp 复制代码
struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode* n) : val(x), next(n) {}
};

3.4 核心操作

cpp 复制代码
// ---------- 反转链表(迭代)O(n), O(1) ----------
ListNode* reverseList(ListNode* head) {
    ListNode *prev = nullptr, *cur = head;
    while (cur) {
        ListNode* nxt = cur->next;  // 保存后继
        cur->next = prev;           // 反转指针
        prev = cur;
        cur = nxt;
    }
    return prev;
}

// ---------- 反转链表(递归)----------
ListNode* reverseListRecursive(ListNode* head) {
    if (!head || !head->next) return head;
    ListNode* newHead = reverseListRecursive(head->next);
    head->next->next = head;
    head->next = nullptr;
    return newHead;
}

// ---------- 快慢指针找中点 ----------
ListNode* findMiddle(ListNode* head) {
    ListNode *slow = head, *fast = head;
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
    }
    return slow;
}

// ---------- 判断环 ----------
bool hasCycle(ListNode* head) {
    ListNode *slow = head, *fast = head;
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) return true;
    }
    return false;
}

// ---------- 找环的入口 ----------
ListNode* detectCycle(ListNode* head) {
    ListNode *slow = head, *fast = head;
    while (fast && fast->next) {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast) {
            ListNode* ptr = head;
            while (ptr != slow) { ptr = ptr->next; slow = slow->next; }
            return ptr;
        }
    }
    return nullptr;
}

// ---------- 合并两个有序链表 ----------
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
    ListNode dummy(0);
    ListNode* tail = &dummy;
    while (l1 && l2) {
        if (l1->val < l2->val) { tail->next = l1; l1 = l1->next; }
        else                   { tail->next = l2; l2 = l2->next; }
        tail = tail->next;
    }
    tail->next = l1 ? l1 : l2;
    return dummy.next;
}

// ---------- 删除倒数第 N 个节点 ----------
ListNode* removeNthFromEnd(ListNode* head, int n) {
    ListNode dummy(0, head);
    ListNode *fast = &dummy, *slow = &dummy;
    for (int i = 0; i <= n; ++i) fast = fast->next;
    while (fast) { slow = slow->next; fast = fast->next; }
    slow->next = slow->next->next;
    return dummy.next;
}

3.5 链表常用技巧

技巧 原理 用途
快慢指针 fast 每次走两步,slow 走一步 找中点、判环、找环入口
哑节点 (dummy) 额外节点指向 head 统一头节点处理,避免特殊判断
双指针(前后指针) 一个指针固定落后另一个 n 步 倒数第 k 个、相交链表

4. 栈与队列

4.1 数据结构基本概念

栈 (Stack):**后进先出(LIFO)**的线性结构。只允许在一端(栈顶)进行插入和删除。类比:一摞盘子,后放的先取。

队列 (Queue):**先进先出(FIFO)**的线性结构。在一端(队尾)插入,在另一端(队头)删除。类比:排队,先到的先服务。

维度 队列
操作原则 LIFO(后进先出) FIFO(先进先出)
插入端 栈顶 (top) 队尾 (back/rear)
删除端 栈顶 (top) 队头 (front)
典型应用 函数调用栈、括号匹配、撤销操作 BFS、消息队列、打印队列

4.2 STL 封装

cpp 复制代码
std::stack<int> stk;    // 默认 deque 底层
stk.push(1); stk.pop(); stk.top();

std::queue<int> q;      // 默认 deque 底层
q.push(1); q.pop(); q.front(); q.back();

std::deque<int> dq;     // 双端队列,头尾均可插入删除
dq.push_front(1); dq.push_back(2);
dq.pop_front();  dq.pop_back();

4.3 用栈实现队列 / 用队列实现栈

cpp 复制代码
// ---------- 双栈实现队列 ----------
// 核心思想:in栈负责入队,out栈负责出队
// out为空时,将in的所有元素弹出并压入out,顺序反转,实现FIFO
class MyQueue {
    std::stack<int> in, out;
    void flush() {
        if (out.empty())
            while (!in.empty()) { out.push(in.top()); in.pop(); }
    }
public:
    void push(int x)  { in.push(x); }
    int pop()  { flush(); int v = out.top(); out.pop(); return v; }
    int peek() { flush(); return out.top(); }
    bool empty() { return in.empty() && out.empty(); }
};

4.4 单调栈与单调队列

cpp 复制代码
// ---------- 单调栈: 找每个元素右侧第一个比它大的元素 ----------
// 核心思想: 维护栈内元素值单调递减,遇到新元素时弹栈
std::vector<int> nextGreaterElement(std::vector<int>& nums) {
    int n = nums.size();
    std::vector<int> ans(n, -1);
    std::stack<int> stk;
    for (int i = 0; i < n; ++i) {
        while (!stk.empty() && nums[stk.top()] < nums[i]) {
            ans[stk.top()] = nums[i];
            stk.pop();
        }
        stk.push(i);
    }
    return ans;
}

// ---------- 单调队列: 滑动窗口最大值 ----------
std::vector<int> maxSlidingWindow(std::vector<int>& nums, int k) {
    std::deque<int> dq;  // 存下标,队头=窗口最大值下标
    std::vector<int> ans;
    for (int i = 0; i < nums.size(); ++i) {
        if (!dq.empty() && dq.front() <= i - k) dq.pop_front();     // 出窗口
        while (!dq.empty() && nums[dq.back()] <= nums[i]) dq.pop_back(); // 维护递减
        dq.push_back(i);
        if (i >= k - 1) ans.push_back(nums[dq.front()]);
    }
    return ans;
}

5. 哈希表

5.1 数据结构基本概念

哈希表 (Hash Table) 通过哈希函数 将键 (key) 映射到存储位置(桶/bucket),实现均摊 O(1) 的查找、插入和删除。

核心组成:

  • 哈希函数:将 key 转换为整数索引
  • 桶数组:实际存储数据的数组
  • 冲突解决机制:两个不同 key 映射到同一位置时的处理方法

5.2 STL 容器对比

容器 底层实现 查找 插入 有序性
std::unordered_map 哈希表(拉链法) O(1) 均摊 O(1) 均摊 无序
std::unordered_set 哈希表 O(1) 均摊 O(1) 均摊 无序
std::map 红黑树 O(log n) O(log n) 有序(按 key 排序)
std::set 红黑树 O(log n) O(log n) 有序

5.3 哈希冲突解决方法

方法 原理 优点 缺点
拉链法 (Separate Chaining) 每个桶指向一个链表,冲突的元素链到后面 装载因子可 >1,实现简单 额外指针开销,缓存不友好
开放寻址法 (Open Addressing) 冲突时按一定规则找下一个空位(线性探测/平方探测/双重哈希) 无指针开销,缓存友好 装载因子必须 <1,删除麻烦

5.4 装载因子 (Load Factor)

定义λ = n / m(n = 已存元素数,m = 桶数量)

  • λ 过大 → 冲突增加,性能下降
  • λ 过小 → 浪费空间
  • unordered_map 默认 max_load_factor = 1.0
  • 当 λ 超过阈值时触发 rehash → 扩容桶数组,重新映射所有元素

5.5 一个好的哈希函数的要求

  1. 确定性:相同 key 始终产生相同哈希值
  2. 均匀性:哈希值尽量均匀分布到所有桶
  3. 高效性:计算速度快
  4. 雪崩效应:key 的微小变化导致哈希值巨大变化

6. 树与二叉树

6.1 基本概念

复制代码
        ①          ← 根节点 (root)
       / \
      ②   ③        ← ② 是 ① 的左孩子,③ 是右孩子
     / \   \
    ④  ⑤   ⑥      ← ④⑤⑥ 是叶子节点 (leaf),度为 0

深度 (depth):  从根到该节点的路径长度
高度 (height): 从该节点到最远叶子的路径长度
度 (degree):   一个节点拥有的子树个数

满二叉树:      每层节点数都达到最大值(每层都满)
完全二叉树:    除最后一层外全满,且最后一层节点从左到右连续排列
               → 堆的底层结构

6.2 二叉树重要性质(选择题高频)

  • 第 i 层最多有 2^(i-1) 个节点(i ≥ 1)
  • 深度为 k 的二叉树最多有 2^k - 1 个节点
  • 对任意非空二叉树,若叶子节点数为 n₀,度为 2 的节点数为 n₂,则 n₀ = n₂ + 1
  • n 个节点的完全二叉树深度为 ⌊log₂(n)⌋ + 1
  • n 个节点能构成的不同二叉树个数 = 卡特兰数 C(2n, n) / (n+1)

6.3 二叉树节点定义

cpp 复制代码
struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode* l, TreeNode* r) : val(x), left(l), right(r) {}
};

6.4 四种遍历(必背)

遍历是二叉树最核心的操作,需要能口头描述遍历过程,以及知道每种遍历的典型用途

复制代码
       1
      / \
     2   3
    / \
   4   5

前序 (根左右): 1 2 4 5 3
中序 (左根右): 4 2 5 1 3
后序 (左右根): 4 5 2 3 1
层序 (按层):   1 2 3 4 5
遍历方式 顺序 典型用途
前序 根→左→右 序列化、复制树、前缀表达式
中序 左→根→右 BST 得到有序序列
后序 左→右→根 删除树(先删子节点)、后缀表达式、计算子树属性
层序 (BFS) 逐层从左到右 最短路径、树宽、层级操作
cpp 复制代码
// ---------- 1. 前序遍历 ----------
// 递归
void preorder(TreeNode* root) {
    if (!root) return;
    std::cout << root->val << " ";
    preorder(root->left);
    preorder(root->right);
}
// 迭代(栈:根入栈 → 弹出 → 先压右再压左)
void preorderIter(TreeNode* root) {
    if (!root) return;
    std::stack<TreeNode*> stk;
    stk.push(root);
    while (!stk.empty()) {
        TreeNode* node = stk.top(); stk.pop();
        std::cout << node->val << " ";
        if (node->right) stk.push(node->right);
        if (node->left)  stk.push(node->left);
    }
}

// ---------- 2. 中序遍历 ----------
// 递归
void inorder(TreeNode* root) {
    if (!root) return;
    inorder(root->left);
    std::cout << root->val << " ";
    inorder(root->right);
}
// 迭代(一路向左)
void inorderIter(TreeNode* root) {
    std::stack<TreeNode*> stk;
    TreeNode* cur = root;
    while (cur || !stk.empty()) {
        while (cur) { stk.push(cur); cur = cur->left; }
        cur = stk.top(); stk.pop();
        std::cout << cur->val << " ";
        cur = cur->right;
    }
}

// ---------- 3. 后序遍历 ----------
// 后序迭代:前序"根右左"再反转 → "左右根"
void postorderIter(TreeNode* root) {
    if (!root) return;
    std::stack<TreeNode*> stk;
    std::vector<int> ans;
    stk.push(root);
    while (!stk.empty()) {
        TreeNode* node = stk.top(); stk.pop();
        ans.push_back(node->val);
        if (node->left)  stk.push(node->left);
        if (node->right) stk.push(node->right);
    }
    std::reverse(ans.begin(), ans.end());
    for (int v : ans) std::cout << v << " ";
}

// ---------- 4. 层序遍历 (BFS) ----------
void levelOrder(TreeNode* root) {
    if (!root) return;
    std::queue<TreeNode*> q;
    q.push(root);
    while (!q.empty()) {
        TreeNode* node = q.front(); q.pop();
        std::cout << node->val << " ";
        if (node->left)  q.push(node->left);
        if (node->right) q.push(node->right);
    }
}

// 层序按层输出
std::vector<std::vector<int>> levelOrderByLevel(TreeNode* root) {
    std::vector<std::vector<int>> ans;
    if (!root) return ans;
    std::queue<TreeNode*> q;
    q.push(root);
    while (!q.empty()) {
        int sz = q.size();
        std::vector<int> level;
        for (int i = 0; i < sz; ++i) {
            TreeNode* node = q.front(); q.pop();
            level.push_back(node->val);
            if (node->left)  q.push(node->left);
            if (node->right) q.push(node->right);
        }
        ans.push_back(level);
    }
    return ans;
}

6.5 常见操作

cpp 复制代码
// 最大深度
int maxDepth(TreeNode* root) {
    if (!root) return 0;
    return 1 + std::max(maxDepth(root->left), maxDepth(root->right));
}

// 判断对称二叉树(递归比较镜像位置)
bool isSymmetric(TreeNode* root) {
    if (!root) return true;
    std::function<bool(TreeNode*,TreeNode*)> dfs = [&](TreeNode* l, TreeNode* r) {
        if (!l && !r) return true;
        if (!l || !r) return false;
        return l->val == r->val && dfs(l->left, r->right) && dfs(l->right, r->left);
    };
    return dfs(root->left, root->right);
}

// 翻转二叉树
TreeNode* invertTree(TreeNode* root) {
    if (!root) return nullptr;
    std::swap(root->left, root->right);
    invertTree(root->left);
    invertTree(root->right);
    return root;
}

// 从前序+中序构造二叉树
TreeNode* buildTree(std::vector<int>& preorder, std::vector<int>& inorder) {
    std::unordered_map<int, int> inMap;
    for (int i = 0; i < inorder.size(); ++i) inMap[inorder[i]] = i;
    std::function<TreeNode*(int,int,int,int)> build = [&](int pL, int pR, int iL, int iR) -> TreeNode* {
        if (pL > pR) return nullptr;
        int rootVal = preorder[pL];
        int iMid = inMap[rootVal];
        int leftCnt = iMid - iL;
        TreeNode* root = new TreeNode(rootVal);
        root->left  = build(pL + 1, pL + leftCnt, iL, iMid - 1);
        root->right = build(pL + leftCnt + 1, pR, iMid + 1, iR);
        return root;
    };
    return build(0, preorder.size() - 1, 0, inorder.size() - 1);
}

7. 二叉搜索树 BST

7.1 数据结构基本概念

二叉搜索树 (Binary Search Tree) 是一棵二叉树,满足:

  • 左子树所有节点的值 < 根节点的值
  • 右子树所有节点的值 > 根节点的值
  • 左右子树也分别是 BST

核心性质:中序遍历 BST 得到有序递增序列 ← 这是 BST 最重要的性质。

7.2 核心操作

cpp 复制代码
// ---------- 查找 ----------
TreeNode* searchBST(TreeNode* root, int val) {
    if (!root || root->val == val) return root;
    return val < root->val ? searchBST(root->left, val)
                           : searchBST(root->right, val);
}

// ---------- 插入 ----------
TreeNode* insertBST(TreeNode* root, int val) {
    if (!root) return new TreeNode(val);
    if (val < root->val) root->left  = insertBST(root->left, val);
    else                 root->right = insertBST(root->right, val);
    return root;
}

// ---------- 删除(三种情况)----------
TreeNode* deleteBST(TreeNode* root, int val) {
    if (!root) return nullptr;
    if (val < root->val) {
        root->left = deleteBST(root->left, val);
    } else if (val > root->val) {
        root->right = deleteBST(root->right, val);
    } else {
        // 情况1/2: 只有一个孩子或没有孩子
        if (!root->left)  { TreeNode* r = root->right; delete root; return r; }
        if (!root->right) { TreeNode* l = root->left;  delete root; return l; }
        // 情况3: 有两个孩子 → 找后继(右子树最小节点)替换
        TreeNode* succ = root->right;
        while (succ->left) succ = succ->left;
        root->val = succ->val;
        root->right = deleteBST(root->right, succ->val);
    }
    return root;
}

// ---------- 验证是否为合法 BST ----------
bool isValidBST(TreeNode* root, long long minV = LLONG_MIN, long long maxV = LLONG_MAX) {
    if (!root) return true;
    if (root->val <= minV || root->val >= maxV) return false;
    return isValidBST(root->left, minV, root->val)
        && isValidBST(root->right, root->val, maxV);
}

7.3 BST 退化问题

操作 平均 最坏(退化成链表)
查找 O(log n) O(n)
插入 O(log n) O(n)
删除 O(log n) O(n)

退化原因:插入顺序不当(如插入 1,2,3,4,5 → 退化为右斜链表)。

解决方案:保证树保持平衡 → 这就是 AVL 树和红黑树的动机。


8. 平衡二叉树 AVL

8.1 数据结构基本概念

AVL 树 (以发明者 Adelson-Velsky 和 Landis 命名)是最早的自平衡二叉搜索树。

核心定义:任意节点的左右子树高度差不超过 1 。这个高度差称为平衡因子 (Balance Factor)

复制代码
平衡因子 = 左子树高度 - 右子树高度
合法范围: -1, 0, 1

8.2 旋转(自旋)--- AVL 的核心

当插入或删除导致某个节点的平衡因子超出 -1, 1 范围时,需要通过旋转 恢复平衡。旋转是理解平衡树的关键概念

复制代码
LL 型(左子树的左子树过高)→ 单次右旋 (Right Rotate)

        z (BF=2)                   y (BF=0)
       / \                         /   \
  (BF=1)y  T4                     x     z
       / \          ======>       / \   / \
      x   T3                    T1 T2 T3 T4
     / \
   T1 T2

RR 型(右子树的右子树过高)→ 单次左旋 (Left Rotate)

    z (BF=-2)                     y (BF=0)
   / \                           /   \
 T1   y (BF=-1)                 z     x
     / \        ======>        / \   / \
   T2   x                     T1 T2 T3 T4
       / \
     T3  T4

LR 型(左子树的右子树过高)→ 先左旋左子树,再右旋根
RL 型(右子树的左子树过高)→ 先右旋右子树,再左旋根

8.3 四种情况总结

类型 失衡模式 旋转操作 旋转次数
LL 在左子树的左子树插入 右旋根节点 1
RR 在右子树的右子树插入 左旋根节点 1
LR 在左子树的右子树插入 先左旋左孩子,再右旋根 2
RL 在右子树的左子树插入 先右旋右孩子,再左旋根 2

8.4 旋转实现

cpp 复制代码
struct AVLNode {
    int val, height;
    AVLNode *left, *right;
    AVLNode(int v) : val(v), height(1), left(nullptr), right(nullptr) {}
};

int getHeight(AVLNode* node) { return node ? node->height : 0; }
int getBalance(AVLNode* node) { return getHeight(node->left) - getHeight(node->right); }

// 右旋
AVLNode* rightRotate(AVLNode* y) {
    AVLNode* x = y->left;
    AVLNode* T2 = x->right;
    x->right = y;
    y->left = T2;
    y->height = 1 + std::max(getHeight(y->left), getHeight(y->right));
    x->height = 1 + std::max(getHeight(x->left), getHeight(x->right));
    return x;
}

// 左旋
AVLNode* leftRotate(AVLNode* x) {
    AVLNode* y = x->right;
    AVLNode* T2 = y->left;
    y->left = x;
    x->right = T2;
    x->height = 1 + std::max(getHeight(x->left), getHeight(x->right));
    y->height = 1 + std::max(getHeight(y->left), getHeight(y->right));
    return y;
}

8.5 带自平衡的插入

cpp 复制代码
AVLNode* insertAVL(AVLNode* root, int val) {
    // 1. 普通 BST 插入
    if (!root) return new AVLNode(val);
    if (val < root->val) root->left  = insertAVL(root->left, val);
    else                 root->right = insertAVL(root->right, val);

    // 2. 更新高度
    root->height = 1 + std::max(getHeight(root->left), getHeight(root->right));

    // 3. 检查平衡并旋转
    int balance = getBalance(root);

    if (balance > 1 && val < root->left->val)   return rightRotate(root);        // LL
    if (balance < -1 && val > root->right->val) return leftRotate(root);         // RR
    if (balance > 1 && val > root->left->val) {                                  // LR
        root->left = leftRotate(root->left);
        return rightRotate(root);
    }
    if (balance < -1 && val < root->right->val) {                                // RL
        root->right = rightRotate(root->right);
        return leftRotate(root);
    }
    return root;
}

8.6 AVL vs 红黑树

特性 AVL 树 红黑树
平衡条件 高度差 ≤ 1(严格) 最长路径 ≤ 2×最短路径(宽松)
树高 更低 略高(最多 2log(n+1))
查找性能 更快(更严格平衡) 略慢
插入旋转 最多 2 次 最多 2 次(需染色)
删除旋转 最多 O(log n) 最多 3 次
适用场景 查多写少 写多 --- C++ std::map/std::set

9. 红黑树

9.1 数据结构基本概念

红黑树 是一种自平衡二叉搜索树,通过给节点染色(红/黑)和一组规则来保证树的近似平衡。

9.2 五条性质(必背)

  1. 每个节点是红色黑色
  2. 根节点 始终是黑色
  3. 叶节点(NIL / 空节点)视为黑色
  4. 红色节点的两个直接孩子必须是黑色 → 不能出现连续的红节点
  5. 从任意节点到其每个后代叶节点的所有路径上,黑色节点数量相同

性质 4 和 5 共同保证:最长路径(红黑交替)≤ 最短路径(全黑)的 2 倍 → 树的近似平衡。

9.3 为什么不需要像 AVL 那么严格地旋转

红黑树放弃严格的高度平衡(AVL 的要求),改为维护颜色约束。这使得:

  • 插入/删除时需要的结构调整更少
  • 虽然树比 AVL 略高,但写操作平均更快
  • 这是典型的以读换写的权衡

9.4 插入调平

新插入节点默认为红色(插入红色不违反性质 5,但可能违反性质 4)。

情况 叔节点颜色 处理方式
case1 红色 父、叔变黑,祖父变红,问题节点上移到祖父继续处理
case2(LR) 黑色 + 当前节点是右孩子 对父节点左旋 → 转为 case3(LL)
case3(LL) 黑色 + 当前节点是左孩子 父变黑、祖父变红,对祖父右旋

9.5 为什么 std::map 用红黑树而不是 AVL

  • 写操作更少旋转:红黑树插入最多旋转 2 次,删除最多旋转 3 次;AVL 删除可能 O(log n) 次旋转
  • STL 容器修改频繁,红黑树的颜色调整比 AVL 的高度调整开销更小
  • 工程上已充分验证(Linux 内核、Java TreeMap 等都使用红黑树)

9.6 B 树 / B+ 树速查

特性 B 树 B+ 树
数据存放 所有节点都存数据 只有叶子节点存数据
叶子间链接 叶子节点用指针串成有序链表
内节点 存数据和索引 只存索引(充当路由)
范围查询 需中序遍历 叶子链表 → O(log n + k) 极快
典型用途 文件系统 数据库索引(MySQL InnoDB)

10. 堆与优先队列

10.1 数据结构基本概念

堆 (Heap) 是一种完全二叉树,分为两种:

  • 大顶堆 (Max-Heap):每个节点的值 ≥ 其孩子节点的值 → 根是最大值
  • 小顶堆 (Min-Heap):每个节点的值 ≤ 其孩子节点的值 → 根是最小值

核心性质:

  • 堆只保证父子之间的大小关系,不保证兄弟节点之间的大小关系
  • 堆用数组存储(利用完全二叉树的性质),索引 i 节点的左孩子为 2i+1,右孩子为 2i+2
  • 插入/删除的时间复杂度为 O(log n)(上浮/下沉),建堆为 O(n)

10.2 priority_queue

cpp 复制代码
// 默认大顶堆
std::priority_queue<int> pq;
pq.push(3); pq.push(1); pq.push(5);
pq.top();  // 5
pq.pop();

// 小顶堆
std::priority_queue<int, std::vector<int>, std::greater<int>> minPq;

10.3 手写堆

cpp 复制代码
class MinHeap {
    std::vector<int> h;

    void siftUp(int i) {
        while (i > 0) {
            int p = (i - 1) / 2;
            if (h[p] <= h[i]) break;
            std::swap(h[p], h[i]); i = p;
        }
    }
    void siftDown(int i) {
        int n = h.size();
        while (true) {
            int smallest = i, l = 2 * i + 1, r = 2 * i + 2;
            if (l < n && h[l] < h[smallest]) smallest = l;
            if (r < n && h[r] < h[smallest]) smallest = r;
            if (smallest == i) break;
            std::swap(h[i], h[smallest]); i = smallest;
        }
    }
public:
    void push(int v) { h.push_back(v); siftUp(h.size() - 1); }
    void pop()       { h[0] = h.back(); h.pop_back(); siftDown(0); }
    int  top()       { return h[0]; }
    bool empty()     { return h.empty(); }
};

10.4 堆与二叉搜索树的区别

维度 二叉搜索树
结构要求 完全二叉树 无形状要求
节点关系 父子有序,兄弟无序 左 < 根 < 右
查找任意元素 O(n) --- 需遍历 O(log n) 平均
获取最值 O(1) O(log n)(最左/最右)
主要用途 优先队列、Top-K、堆排序 查找、有序遍历

11. 图与遍历 BFS / DFS

11.1 数据结构基本概念

图 (Graph)顶点集合 V边集合 E 组成,记作 G = (V, E)。

分类 定义
无向图 边没有方向,(u, v) 和 (v, u) 是同一条边
有向图 边有方向,u→v 和 v→u 是不同的边
带权图 每条边有权重
连通图 无向图中任意两个顶点之间都有路径
强连通图 有向图中任意两个顶点之间都有路径(双向可达)

11.2 图的存储方式

方式 空间复杂度 适用场景
邻接矩阵 O(V²) 稠密图(边数接近 V²)、需要快速判断两点是否相连
邻接表 O(V + E) 稀疏图(边数远小于 V²)、遍历邻接点(最常用)
cpp 复制代码
int n = 5;
// 邻接矩阵
int graph[100][100] = {0};
graph[0][1] = graph[1][0] = 1;

// 邻接表(最常用)
std::vector<std::vector<int>> adj(n);
adj[0].push_back(1);
adj[1].push_back(0);

// 带权图
std::vector<std::vector<std::pair<int,int>>> wAdj(n);
wAdj[0].push_back({1, 5});  // 边 0→1, 权重 5

11.3 广度优先搜索 BFS

核心思想 :从起点出发,逐层向外扩展访问。使用队列实现。

关键性质 :BFS 在无权图中找到的一定是最短路径(边数最少)。

cpp 复制代码
void bfs(int start, std::vector<std::vector<int>>& adj) {
    std::vector<bool> visited(adj.size(), false);
    std::queue<int> q;
    q.push(start);
    visited[start] = true;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        std::cout << u << " ";
        for (int v : adj[u]) {
            if (!visited[v]) {
                visited[v] = true;
                q.push(v);
            }
        }
    }
}

// BFS 求无权图最短路径
std::vector<int> bfsShortestPath(int start, std::vector<std::vector<int>>& adj) {
    int n = adj.size();
    std::vector<int> dist(n, -1);
    std::queue<int> q;
    q.push(start);
    dist[start] = 0;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        for (int v : adj[u]) {
            if (dist[v] == -1) {
                dist[v] = dist[u] + 1;
                q.push(v);
            }
        }
    }
    return dist;
}

11.4 深度优先搜索 DFS

核心思想 :一条路走到底,走不通再回溯。使用(或递归)实现。

cpp 复制代码
// 递归 DFS
void dfs(int u, std::vector<std::vector<int>>& adj, std::vector<bool>& visited) {
    visited[u] = true;
    std::cout << u << " ";
    for (int v : adj[u])
        if (!visited[v]) dfs(v, adj, visited);
}

// 迭代 DFS(显式栈)
void dfsIter(int start, std::vector<std::vector<int>>& adj) {
    std::vector<bool> visited(adj.size(), false);
    std::stack<int> stk;
    stk.push(start);
    while (!stk.empty()) {
        int u = stk.top(); stk.pop();
        if (visited[u]) continue;
        visited[u] = true;
        std::cout << u << " ";
        for (int v : adj[u])
            if (!visited[v]) stk.push(v);
    }
}

11.5 BFS vs DFS(概念对比,高频)

维度 BFS DFS
数据结构 队列 (queue) 或 递归调用栈
遍历顺序 按层(宽度优先) 按深度(一条路到底)
路径性质 无权图中找到最短路径 不保证最短
空间复杂度 O(w),w = 图的最大宽度 O(h),h = 图的深度
使用场景 最短路径、层级遍历、社交网络"几度好友" 拓扑排序、连通分量、回溯、迷宫所有路径
递归能否实现 一般不递归(用队列迭代) 递归实现自然简洁

11.6 经典图算法

cpp 复制代码
// ---------- 拓扑排序 (Kahn 算法 / BFS 实现) ----------
// 概念: 对有向无环图(DAG),将所有顶点排成线性序列,
//       使对每条边 u→v,u 在序列中都出现在 v 之前
std::vector<int> topoSort(int n, std::vector<std::vector<int>>& adj) {
    std::vector<int> inDeg(n, 0), ans;
    for (int u = 0; u < n; ++u)
        for (int v : adj[u]) inDeg[v]++;
    std::queue<int> q;
    for (int i = 0; i < n; ++i) if (inDeg[i] == 0) q.push(i);
    while (!q.empty()) {
        int u = q.front(); q.pop();
        ans.push_back(u);
        for (int v : adj[u])
            if (--inDeg[v] == 0) q.push(v);
    }
    return ans.size() == n ? ans : std::vector<int>{}; // 返回空 = 图中有环
}

// ---------- Dijkstra 最短路径 ----------
// 限制: 边权必须非负
std::vector<int> dijkstra(int start, std::vector<std::vector<std::pair<int,int>>>& adj) {
    int n = adj.size();
    std::vector<int> dist(n, INT_MAX);
    using P = std::pair<int,int>;
    std::priority_queue<P, std::vector<P>, std::greater<P>> pq;
    dist[start] = 0;
    pq.push({0, start});
    while (!pq.empty()) {
        auto [d, u] = pq.top(); pq.pop();
        if (d > dist[u]) continue;
        for (auto& [v, w] : adj[u]) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }
    return dist;
}

12. 排序算法

12.1 各排序算法对比

算法 最佳 平均 最坏 空间 稳定 核心思想
冒泡 O(n) O(n²) O(n²) O(1) 相邻比较,大的后移
选择 O(n²) O(n²) O(n²) O(1) 每轮选最小放到前面
插入 O(n) O(n²) O(n²) O(1) 将当前元素插入已排序部分
归并 O(n log n) O(n log n) O(n log n) O(n) 分治,合并两个有序数组
快速 O(n log n) O(n log n) O(n²) O(log n) 分治,选基准分区
O(n log n) O(n log n) O(n log n) O(1) 利用堆性质取最值

12.2 稳定性的含义

稳定排序:值相等的元素在排序后保持原来的相对顺序。

  • 稳定:冒泡、插入、归并
  • 不稳定:选择、快速、堆

例如按成绩排序时,稳定排序能让同分的人保持原来学号顺序。

12.3 三大排序实现

cpp 复制代码
// ---------- 快速排序 ----------
int partition(std::vector<int>& arr, int l, int r) {
    int pivot = arr[r];
    int i = l;
    for (int j = l; j < r; ++j)
        if (arr[j] <= pivot) std::swap(arr[i++], arr[j]);
    std::swap(arr[i], arr[r]);
    return i;
}
void quickSort(std::vector<int>& arr, int l, int r) {
    if (l >= r) return;
    int p = partition(arr, l, r);
    quickSort(arr, l, p - 1);
    quickSort(arr, p + 1, r);
}

// ---------- 归并排序 ----------
void merge(std::vector<int>& arr, int l, int mid, int r) {
    std::vector<int> tmp(r - l + 1);
    int i = l, j = mid + 1, k = 0;
    while (i <= mid && j <= r)
        tmp[k++] = arr[i] <= arr[j] ? arr[i++] : arr[j++];
    while (i <= mid) tmp[k++] = arr[i++];
    while (j <= r)   tmp[k++] = arr[j++];
    for (int p = 0; p < k; ++p) arr[l + p] = tmp[p];
}
void mergeSort(std::vector<int>& arr, int l, int r) {
    if (l >= r) return;
    int mid = l + (r - l) / 2;
    mergeSort(arr, l, mid);
    mergeSort(arr, mid + 1, r);
    merge(arr, l, mid, r);
}

// ---------- 堆排序 ----------
void heapSort(std::vector<int>& arr) {
    int n = arr.size();
    auto siftDown = [&](int root, int sz) {
        while (true) {
            int largest = root, l = 2 * root + 1, r = 2 * root + 2;
            if (l < sz && arr[l] > arr[largest]) largest = l;
            if (r < sz && arr[r] > arr[largest]) largest = r;
            if (largest == root) break;
            std::swap(arr[root], arr[largest]);
            root = largest;
        }
    };
    for (int i = n / 2 - 1; i >= 0; --i) siftDown(i, n);  // 建堆 O(n)
    for (int i = n - 1; i > 0; --i) {                       // 排序 O(n log n)
        std::swap(arr[0], arr[i]);
        siftDown(0, i);
    }
}

13. 动态规划基础

13.1 概念与解题框架

动态规划 (DP) 的核心:将大问题分解为重叠的子问题,记录子问题的解避免重复计算。

复制代码
1. 定义状态 dp[i] 的含义(最关键的一步)
2. 推导状态转移方程
3. 确定初始条件和边界
4. 确定遍历顺序
5. 举例验证

13.2 DP 的两大要素

要素 含义
最优子结构 问题的最优解包含子问题的最优解
重叠子问题 递归过程中反复出现相同的子问题(这是DP区别于分治的关键)

13.3 经典例题

cpp 复制代码
// ---------- 斐波那契(理解重叠子问题)----------
int fib(int n) {
    if (n <= 1) return n;
    int a = 0, b = 1;
    for (int i = 2; i <= n; ++i) { int c = a + b; a = b; b = c; }
    return b;
}

// ---------- 爬楼梯(每次 1 或 2 级)----------
int climbStairs(int n) {
    if (n <= 2) return n;
    int a = 1, b = 2;
    for (int i = 3; i <= n; ++i) { int c = a + b; a = b; b = c; }
    return b;
}

// ---------- 最长递增子序列 LIS ----------
// O(n²) DP 版本
int lengthOfLIS(std::vector<int>& nums) {
    int n = nums.size(), ans = 1;
    std::vector<int> dp(n, 1);  // dp[i] = 以 nums[i] 结尾的 LIS 长度
    for (int i = 1; i < n; ++i) {
        for (int j = 0; j < i; ++j)
            if (nums[j] < nums[i]) dp[i] = std::max(dp[i], dp[j] + 1);
        ans = std::max(ans, dp[i]);
    }
    return ans;
}

// O(n log n) 贪心+二分
int lengthOfLIS_optimized(std::vector<int>& nums) {
    std::vector<int> tails;  // tails[i] = 长度为 i+1 的 LIS 的最小末尾值
    for (int x : nums) {
        auto it = std::lower_bound(tails.begin(), tails.end(), x);
        if (it == tails.end()) tails.push_back(x);
        else *it = x;
    }
    return tails.size();
}

// ---------- 最长公共子序列 LCS ----------
int longestCommonSubsequence(std::string a, std::string b) {
    int m = a.size(), n = b.size();
    std::vector<std::vector<int>> dp(m + 1, std::vector<int>(n + 1, 0));
    for (int i = 1; i <= m; ++i)
        for (int j = 1; j <= n; ++j)
            if (a[i-1] == b[j-1]) dp[i][j] = dp[i-1][j-1] + 1;
            else dp[i][j] = std::max(dp[i-1][j], dp[i][j-1]);
    return dp[m][n];
}

// ---------- 0/1 背包 ----------
int knapsack01(std::vector<int>& wt, std::vector<int>& val, int W) {
    int n = wt.size();
    std::vector<int> dp(W + 1, 0);
    for (int i = 0; i < n; ++i)
        for (int w = W; w >= wt[i]; --w)  // 倒序:保证每个物品只用一次
            dp[w] = std::max(dp[w], dp[w - wt[i]] + val[i]);
    return dp[W];
}

14. 数据结构面试概念考点

以下按照数据结构本身的面试考查角度组织,每题从概念、原理、对比层面回答。

14.1 基础概念

Q1: 什么是数据结构?逻辑结构和物理结构的区别?

  • 数据结构 = 数据元素之间存在一种或多种特定关系的集合
  • 逻辑结构(面向问题):线性结构(数组、链表)、树形结构、图形结构
  • 物理结构/存储结构(面向计算机):顺序存储(连续内存)、链式存储(指针链接)

Q2: 什么是抽象数据类型(ADT)?

ADT 定义了一组数据及在其上的操作,但不指定具体实现。例如栈 ADT 定义了 push/pop/top,实现可以是数组也可以是链表。ADT 把"做什么"和"怎么做"分离开。

Q3: 顺序存储和链式存储的本质区别?

维度 顺序存储 链式存储
内存 连续的一块 分散,通过指针链接
寻址 下标 O(1) 遍历 O(n)
插入删除 需移动元素 修改指针
空间利用率 无指针开销 有指针开销
缓存 友好 不友好
扩容 整体重新分配 逐个按需

14.2 线性结构

Q4: 数组和链表的区别?各适合什么场景?

  • 数组:连续内存,O(1) 随机访问,插入删除 O(n)。适合读多写少、需要随机访问的场景
  • 链表:非连续,O(n) 查找,但插入删除 O(1)。适合频繁增删、不需要随机访问的场景
  • 内存方面:数组需预分配(或动态扩容),链表完全按需分配但有指针开销

Q5: vector 扩容为什么用 1.5 倍或 2 倍?

翻倍扩容实现简单,均摊插入复杂度为 O(1)。设扩容因子为 k:

  • k = 2:每次扩容翻倍,之前释放的内存块总和一定小于新分配的大小,无法复用
  • k 选择 1.5:多次扩容后,之前释放的内存块总大小可能够下一次扩容用,减少内存碎片

Q6: 栈和队列的根本区别?各自的实际应用?

  • 栈 LIFO:函数调用栈(递归)、表达式求值、括号匹配、撤销操作、浏览器的后退
  • 队列 FIFO:消息队列、BFS、打印队列、任务调度
  • 本质区别:数据离开结构的顺序不同

Q7: 什么是循环队列?为什么需要它?

队尾指针到达数组末尾时绕回到开头,形成一个逻辑上的环。普通队列在多次出队后,队头空出的空间无法复用(假溢出),循环队列通过取模运算复用数组前端的空间。


14.3 哈希表

Q8: 哈希表为什么能做到 O(1) 查找?会一直 O(1) 吗?

哈希函数将 key 直接映射为数组下标,理论上一次定位。但:

  • 装载因子过高时冲突增多,退化到 O(n)(拉链法链表很长 / 开放寻址探测次数多)
  • 哈希函数设计不好导致聚类,也会降低性能
  • 需要通过 rehash 保持 O(1) 均摊

Q9: 拉链法 vs 开放寻址法,各自适用场景?

  • 拉链法:装载因子可 >1,删除简单。适合元素大小变化大、不确定容量的场景(C++ unordered_map)
  • 开放寻址法:无指针、缓存友好。适合元素小、装载因子可控的场景(Java ThreadLocal)

Q10: 哈希表 vs map(红黑树),什么时候用哪个?

  • 需要 O(1) 查找,不关心顺序 → unordered_map(哈希表)
  • 需要按 key 有序遍历、范围查询 → map(红黑树)
  • 内存敏感 → 哈希表空间开销可能更大(桶数组 + 链表指针)
  • key 不支持哈希 → 只能用 map(需支持 < 比较)

14.4 树

Q11: 什么是树?树和图的核心区别?

  • 树是 n 个节点的有限集合,n=0 为空树;n>0 时有且仅有一个根节点,其余节点分为 m 个互不相交的子集,每个子集本身又是一棵树(递归定义)
  • 树 vs 图 :树是连通无环的无向图,任意两个节点间有且仅有一条路径。树有明确的层级概念,图没有

Q12: 完全二叉树和满二叉树的区别?

  • 满二叉树:每层节点数都达到最大值(第 i 层有 2^(i-1) 个节点)
  • 完全二叉树 :除最后一层外全满,并且最后一层节点从左到右连续排列

Q13: 二叉树为什么有 n₀ = n₂ + 1 这个性质?

设度为 0、1、2 的节点数分别 n₀、n₁、n₂,总边数 E = n₁ + 2n₂。又因为除根外每个节点有一条入边,E = n₀ + n₁ + n₂ - 1。联立得 n₀ = n₂ + 1。

Q14: 四种遍历方式分别有什么实际用途?

  • 前序:复制树结构、序列化/反序列化(先根后子)
  • 中序:BST 输出有序序列(最核心的用途)
  • 后序:删除树(先删孩子再删父亲)、计算子树属性(汇总子节点信息后更新父节点)
  • 层序:求树的宽度、求最短路径(BFS)、判断完全二叉树

Q15: 为什么二叉树通常用链式存储而不用数组?

  • 一般二叉树可能很不"满",用数组存储会导致大量空间浪费
  • 完全二叉树(如堆)用数组存储效率很高,可通过下标计算父子关系
  • 一般二叉树用链式存储更灵活,按需分配节点

14.5 二叉搜索树 & 平衡树

Q16: 什么是二叉搜索树?它的核心价值是什么?

BST 满足左子树 < 根 < 右子树。核心价值是利用二分思想将查找复杂度降到 O(log n),同时中序遍历即可获得有序序列。

Q17: BST 为什么可能退化成 O(n)?

插入顺序是关键:如果数据本身有序(如 1,2,3,4,5),每次都在最右侧插入 → 树变成一条右斜链 → 查找从 O(log n) 退化为 O(n)。这是裸 BST 的根本缺陷。

Q18: 平衡二叉树解决了什么问题?怎么解决的?

解决 BST 在极端插入顺序下退化成链表的问题。通过旋转操作 维护树的高度平衡,保证查找/插入/删除始终保持在 O(log n)

Q19: 什么是旋转(自旋)?旋转为什么不会破坏 BST 性质?

旋转是通过改变少数父子关系的指针来调整树的结构。以右旋为例:将左孩子提为新的根,原根变为左孩子的右子树,左孩子的原右子树移给原根的左指针。旋转前后中序遍历结果不变 → BST 性质得以保持。

Q20: AVL 树和红黑树的平衡策略有什么不同?为什么 C++ STL 选红黑树?

  • AVL 追求严格高度差 ≤ 1 → 树更矮、查找更快
  • 红黑树追求近似平衡(最长路径 ≤ 最短路径 2 倍)→ 插入删除代价更小
  • STL 选红黑树因为:容器场景下插入/删除频繁,红黑树旋转更少,整体性能更优

Q21: B+ 树为什么适合做数据库索引?

  • 数据只在叶子节点,内节点只存索引 → 每个内节点能存更多索引项 → 树更矮,磁盘 I/O 更少
  • 叶子节点串成链表 → 范围查询极快,顺着链表扫即可,不需要中序遍历
  • 查询任何数据都在固定高度(根→叶子),时间稳定

14.6 堆

Q22: 什么是堆?堆和二叉搜索树的区别?

  • 堆:完全二叉树,只保证父子间的大小关系(大顶堆/小顶堆),兄弟之间无序
  • BST:保证 左 < 根 < 右,兄弟之间有序
  • 堆获取最值 O(1),但查找任意元素需要遍历 O(n);BST 查找 O(log n),获取最值也需 O(log n)
  • 堆用数组存储效率高,BST 一般链式存储

Q23: 建堆为什么是 O(n) 而不是 O(n log n)?

虽然每次 siftDown 最坏 O(log n),但越靠近底层的节点越多,且它们下沉的距离越短。从 n/2 到 n-1 的叶子节点下沉距离为 0。精确求和得 O(n)。粗略理解:大部分节点只需很少的比较次数。


14.7 图

Q24: 什么是图?有向图和无向图的区别?

图 G = (V, E) 由顶点和边组成。无向图的边没有方向,有向图的边有方向。连通性定义也不同:无向图连通指任意两点可互达,有向图强连通指任意两点双向可互达。

Q25: 邻接矩阵 vs 邻接表?各自适用场景?

  • 邻接矩阵 O(V²):判断任意两点是否直接相连 O(1),适合稠密图或需要快速判断边的存在
  • 邻接表 O(V+E):遍历邻接点快速,适合稀疏图,是实际中最常用的图的存储方式

Q26: BFS 和 DFS 的本质区别?为什么 BFS 能求最短路径?

  • BFS 按层扩展,从起点向外一圈圈遍历。当第一次访问到某节点时,一定经过了最少边数 → 在无权图中即最短路径
  • DFS 一条路走到底再回溯,先访问的路径不一定最短,因为可能绕了一大圈
  • BFS 用队列(先进先出 → 按层),DFS 用栈(后进先出 → 按深度)

Q27: BFS/DFS 为什么需要 visited 标记?

图可以存在环路,没有 visited 会陷入无限循环。同时避免对同一节点的重复访问造成指数级膨胀。

Q28: 什么是拓扑排序?有哪些应用?

将有向无环图 (DAG) 的所有顶点排成线性序列,使每条边 u→v 中 u 出现在 v 之前。应用:编译器的依赖解析、任务调度(先修课程安排)、构建系统 (Makefile)。


14.8 排序

Q29: 什么是排序的稳定性?哪些排序是稳定的?

稳定 = 相等元素的相对顺序在排序后保持不变。稳定的有:冒泡、插入、归并。不稳定的有:选择、快速、堆。实际场景中多关键字排序时稳定性很重要(先按姓名排序,再按成绩排序时,同分的姓名有序)。

Q30: 快速排序 vs 归并排序?为什么快排通常更快?

  • 都是分治,平均 O(n log n)
  • 快排在原数组上交换(原地排序),缓存友好,常数因子小
  • 归并需要临时数组和来回拷贝
  • 但快排不稳定、最坏 O(n²);归并稳定、最坏 O(n log n)
  • C++ std::sort 使用内省排序(快排 + 堆排序兜底)

14.9 STL 综合

Q31: 各 STL 容器的底层数据结构是什么?

容器 底层数据结构 特点
vector 动态数组 尾插快,随机访问
deque 分段连续数组 头尾都快
list 双向链表 任意位置插入 O(1)
stack / queue 默认基于 deque 适配器
priority_queue 堆(vector 实现) 最值 O(1)
set / map 红黑树 有序,O(log n)
unordered_set / unordered_map 哈希表 无序,均摊 O(1)

Q32: stack / queue 为什么是"容器适配器"?

它们自己不直接管理存储,而是对底层容器(默认 deque)的接口进行了"适配":stack 只暴露 push/pop/top,屏蔽了底层容器的其他操作。可以指定底层容器为 vector 或 list。


14.10 面试答题模板

面试中回答数据结构概念问题时,按这个框架组织:

  1. 先给定义 --- 一句话说清楚这是什么
  2. 核心性质 --- 最重要的 2-3 个特点
  3. 时间复杂度 --- 主要操作的时间/空间
  4. 对比替代方案 --- 与类似数据结构的区别、各自适用场景
  5. 实际应用举例 --- 在哪里用到(STL、数据库、操作系统等)

示例:"请你说说红黑树"

红黑树是一种自平衡二叉搜索树,通过给节点涂色和五条性质来保持树的近似平衡。插入删除最多需要 O(log n),但实际旋转次数很少(插入最多 2 次旋转)。相比 AVL,红黑树放弃了严格高度平衡,换来更少的写操作开销。C++ 的 std::mapstd::set 底层就是红黑树,这也是它被广泛使用的工程验证。


持续更新中:后续补充并查集、字典树 (Trie)、跳表、线段树、LRU/LFU 缓存等内容。

相关推荐
至乐活着1 小时前
深入解析跳表SkipList:原理、实现与性能优化实战
数据结构·算法·跳表·skiplist·java实现
tntxia1 小时前
C++ 基础教程:从入门到精通
c++
Drone_xjw1 小时前
从 GDB 到 CDB:C/C++ 程序调试的两把“手术刀”
c语言·开发语言·c++
r_oo_ki_e_1 小时前
Java Map 集合学习笔记
java·笔记·学习
liuyicenysabel3 小时前
大模型学习笔记 · 第八篇 · 进阶:偏好对齐与多卡训练
人工智能·笔记·学习
熊猫_豆豆5 小时前
QT6 Android C++ 自制美观闹钟
android·c++·qt·闹钟
fpcc5 小时前
数据结构—表
数据结构
随意起个昵称5 小时前
状压dp-基础题目2([USACO12MAR] Cows in a Skyscraper G)
c++·算法·动态规划
Funing76 小时前
FreeRTOS学习day1:Keil 工程配置与 FreeRTOS 链表机制理解
数据结构·学习·链表