📋 摘要

数组和链表有什么区别?各自适用场景?

数组与链表是最基础的两种线性结构,核心差异在于内存布局:数组是连续内存,链表通过指针把分散节点串起来。这一差异直接决定了它们的访问、插入、删除复杂度。

对比维度数组(std::vector/std::array)链表(std::list/std::forward_list)
内存布局连续,支持 CPU 缓存预取分散,指针跳跃,缓存不友好
随机访问O(1)O(n)
头部插入/删除O(n)O(1)
尾部插入/删除均摊 O(1)O(1)(双向)/O(n)(单向)
中间插入/删除O(n)(移动元素)O(1)(已知节点位置)
空间开销无额外指针,可能有预留容量每个节点多存 1~2 个指针
迭代器失效扩容/插入可能全部失效只有被删除节点失效
C++
#include <vector>
#include <list>
#include <iostream>

int main() {
    // 数组:随机访问 O(1),适合频繁按下标读取
    std::vector<int> arr{1, 2, 3, 4, 5};
    std::cout << arr[2] << '\n';          // 3,O(1)

    // 链表:中间插入 O(1),适合频繁插入删除
    std::list<int> lst{1, 2, 4, 5};
    auto it = lst.begin();
    std::advance(it, 2);                   // 指向 4
    lst.insert(it, 3);                     // 变成 1 2 3 4 5
    return 0;
}
💡 面试加分项

现代 CPU 缓存让数组在遍历时远比链表快:std::vector 连续内存能命中 cache line,链表指针跳跃会频繁 cache miss。所以「链表中点插入 O(1)」只是理论值,实际工程里除非数据量巨大且频繁在头部/中部增删,否则优先用 vector。

  • 数组适用场景:需要按下标随机访问、元素数量相对稳定、遍历密集型(如矩阵、缓冲区、LRU 里的连续哈希表槽位)。
  • 链表适用场景:频繁在头部/中部插入删除、元素数量动态变化且无法预估、需要稳定迭代器(如 LRU 的访问顺序链、内存分配器的空闲块管理)。

用 C++ 实现一个单链表(增删查改)?

手写单链表考察对指针操作的理解。核心是维护 head 指针,插入/删除时正确处理前驱指针,避免断链。

C++
#include <iostream>

struct ListNode {
    int val;
    ListNode* next;
    ListNode(int x) : val(x), next(nullptr) {}
};

class SinglyLinkedList {
public:
    SinglyLinkedList() : head(nullptr) {}

    // 尾部插入
    void push_back(int val) {
        ListNode* node = new ListNode(val);
        if (!head) { head = node; return; }
        ListNode* p = head;
        while (p->next) p = p->next;
        p->next = node;
    }

    // 按值查找,返回第一个匹配节点,找不到返回 nullptr
    ListNode* find(int val) {
        for (ListNode* p = head; p; p = p->next)
            if (p->val == val) return p;
        return nullptr;
    }

    // 删除第一个值为 val 的节点,返回是否删除成功
    bool remove(int val) {
        ListNode** cur = &head;            // 二级指针,优雅处理头节点删除
        while (*cur && (*cur)->val != val)
            cur = &(*cur)->next;
        if (!*cur) return false;
        ListNode* tmp = *cur;
        *cur = (*cur)->next;               // 把前驱的 next 指向被删节点的 next
        delete tmp;
        return true;
    }

    // 打印
    void print() {
        for (ListNode* p = head; p; p = p->next)
            std::cout << p->val << " ";
        std::cout << '\n';
    }

    ~SinglyLinkedList() {                  // 析构释放全部节点
        while (head) {
            ListNode* tmp = head;
            head = head->next;
            delete tmp;
        }
    }

private:
    ListNode* head;
};

int main() {
    SinglyLinkedList l;
    l.push_back(1); l.push_back(2); l.push_back(3);
    l.remove(2);
    l.print();                             // 1 3
    return 0;
}
💡 面试加分项

删除节点时用指向指针的指针(ListNode**)可统一处理「删除头节点」和「删除中间节点」两种情况,避免单独判断 head,是链表题的高分写法。

⚠️ 警告

手写链表务必注意:插入前先把新节点 next 接上再断前驱(否则后半段丢失);删除后记得 delete 释放内存;析构函数要遍历释放,避免内存泄漏。


栈和队列有什么区别?如何用栈实现队列?

栈是后进先出(LIFO),队列是先进先出(FIFO),两者都限制「只能在特定端操作」。C++ 中 std::stack 默认用 std::deque 作底层容器,std::queue 同样默认 std::deque。

对比栈 std::stack队列 std::queue
出队顺序LIFO(后进先出)FIFO(先进先出)
操作push/pop/toppush/pop/front/back
应用函数调用、括号匹配、表达式求值、DFS任务调度、BFS、消息队列、缓存淘汰

用两个栈实现队列:一个栈 in 负责入队,一个栈 out 负责出队。出队时若 out 空,把 in 全部倒入 out,翻转顺序后栈顶即队首。每个元素最多被搬移两次,均摊 O(1)。

C++
#include <stack>

class MyQueue {
    std::stack<int> in, out;

    void transfer() {                      // 只有 out 空时才搬
        if (out.empty()) {
            while (!in.empty()) {
                out.push(in.top());
                in.pop();
            }
        }
    }

public:
    void push(int x) { in.push(x); }       // 入队 O(1)
    int pop() {                            // 出队均摊 O(1)
        transfer();
        int v = out.top(); out.pop();
        return v;
    }
    int peek() {                           // 取队首均摊 O(1)
        transfer();
        return out.top();
    }
    bool empty() { return in.empty() && out.empty(); }
};
ℹ️ 信息

用队列实现栈则相反:入队时先入队新元素,再把队列前 size-1 个元素依次出队再入队,让新元素始终在队首,出栈即队首出队 O(1)。

  • 实际场景:栈用于函数调用栈、括号匹配、撤销/重做、DFS 迭代版;队列用于 BFS、线程池任务队列、消息队列、LRU 里的访问顺序(std::list 充当)。

哈希表的原理?如何解决哈希冲突?

哈希表通过哈希函数把 key 映射到数组下标,实现「平均 O(1)」的查找。理想情况下一次定位,但不同 key 可能映射到同一下标,即哈希冲突。

解决冲突的两大类方法:

方法思路代表实现特点
链地址法(拉链)每个槽位挂一条链表/红黑树,冲突元素追加到链表unordered_map实现简单、负载因子可 >1,但多一次指针跳转
开放定址法冲突时按探测序列找下一个空槽(线性/二次/双重哈希)Java HashMap(8 后混合)、Google dense_hash缓存友好、无需指针,但删除需墓碑、负载因子低

链地址法示意(每个 bucket 一条链表):

C++
// 简化版链地址哈希表:vector<list<pair<K,V>>>
#include <vector>
#include <list>
#include <utility>
#include <cstddef>

template <typename K, typename V>
class MyHashMap {
    std::vector<std::list<std::pair<K, V>>> buckets;
    std::size_t count = 0;

    std::size_t hash(const K& key) const {
        return std::hash<K>{}(key) % buckets.size();
    }

public:
    MyHashMap(std::size_t n = 16) : buckets(n) {}

    void put(const K& key, const V& val) {
        auto& list = buckets[hash(key)];
        for (auto& kv : list) {
            if (kv.first == key) { kv.second = val; return; } // 覆盖
        }
        list.emplace_back(key, val);       // 拉链追加
        ++count;
    }

    bool get(const K& key, V& out) const {
        const auto& list = buckets[hash(key)];
        for (const auto& kv : list)
            if (kv.first == key) { out = kv.second; return true; }
        return false;
    }
};
💡 面试加分项

平均复杂度 O(1) 的前提是哈希函数分布均匀 + 负载因子受控;最坏情况(所有 key 撞同一槽)退化为 O(n)。工程上链长超阈值(如 8)会把链表转红黑树(Java)或触发 rehash(C++ unordered_map),保证最坏 O(log n) 或重新摊平。

  • 实际场景:缓存(如 LRU 的 key→节点映射)、去重(集合)、词频统计、数据库索引的哈希索引、分布式一致性哈希(对象→节点)。

unordered_map 的底层实现原理?

std::unordered_map 采用链地址法:底层是一个桶数组(bucket array,即 vector),每个桶是一条单向链表,存 pair<const Key, T> 节点。插入/查找流程:先 hash(key) 得到哈希值,再对桶数取模定位桶,最后在该桶链表上顺序查找。

C++
#include <unordered_map>
#include <string>
#include <iostream>

int main() {
    std::unordered_map<std::string, int> m;

    // 底层可观测接口
    std::cout << "bucket_count = " << m.bucket_count() << '\n';      // 初始桶数
    std::cout << "max_load_factor = " << m.max_load_factor() << '\n'; // 默认 1.0

    m["apple"] = 1;
    m["banana"] = 2;
    std::cout << "load_factor = " << m.load_factor() << '\n';        // 元素数/桶数

    // 查询 key 所在桶
    std::cout << "apple 在桶 " << m.bucket("apple") << '\n';
    return 0;
}

核心机制:

概念说明
桶数组(bucket)连续内存,下标由 hash(key) % bucket_count 决定
负载因子size / bucket_count,超过 max_load_factor 触发 rehash
rehash桶数翻倍(通常取质数),重新分配并重哈希所有元素,O(n)
节点单向链表节点,unordered_map 只支持前向迭代、迭代器不稳定
ℹ️ 信息

与 map(红黑树,有序,操作 O(log n))相比,unordered_map 平均 O(1) 但无序、占用内存更大(桶数组 + 链表指针)、迭代器在 rehash 后失效。需要有序遍历或范围查询时选 map,只做点查选 unordered_map。

💡 面试加分项

可提前 reserve(n) 预留桶数,避免频繁 rehash;rehash(n) 直接设桶数。对已知规模的数据,合理设置桶数能显著减少重哈希开销。

  • 实际场景:计数器(词频)、缓存映射、两数之和等哈希类题目、符号表、配置 key→value 存储。

哈希函数如何设计?什么是哈希碰撞?

哈希函数是把任意长度 key 映射为固定范围整数(桶下标)的函数;哈希碰撞(冲突)指不同 key 得到相同哈希值/桶下标,碰撞不可避免(鸽巢原理),目标是均匀分布 + 快速计算。

常用设计方法:

方法公式/思路说明
除留余数法key % m,m 取质数或不含小因子的数最常用,m 取质数减少碰撞
乘法散列floor(m * (A*key mod 1)),A≈0.618对 m 取值不敏感
字符串哈希如 hash = hash*31 + c(多项式)Java String 同款,31 是经典乘子

好的哈希函数三要素:① 分布均匀(雪崩效应,输入微小变化引起哈希值大幅变化);② 计算快;③ 确定性(同 key 同值)。

C++
#include <string>
#include <cstddef>

// 多项式滚动哈希(类 Java String.hashCode 思路,用 31 乘子)
std::size_t poly_hash(const std::string& s) {
    std::size_t h = 0;
    for (char c : s)
        h = h * 31 + static_cast<unsigned char>(c);
    return h;
}

// 更稳健的字符串哈希:FNV-1a,分布更均匀
std::size_t fnv1a(const std::string& s) {
    std::size_t h = 1469598103934665603ULL;  // FNV offset basis
    for (char c : s) {
        h ^= static_cast<unsigned char>(c);
        h *= 1099511628211ULL;               // FNV prime
    }
    return h;
}
⚠️ 警告

自定义类型作 unordered_map 的 key 时,必须同时提供 std::hash<T> 特化(哈希函数)和 operator==(判等,用于碰撞后在同桶内确认是同一 key)。二者缺一不可:哈希只缩小范围,== 才最终判定相等。

💡 面试加分项

「雪崩效应」是加分词:好的哈希函数让一位输入变化引起约一半输出位变化,例如 murmurhash、xxhash、FNV-1a 都是工程常用实现;C++ 标准库 std::hash 对整数基本是恒等映射,对字符串实现由库决定。

  • 实际场景:密码存储(加盐哈希)、负载均衡(按请求哈希路由到固定后端)、分布式缓存一致性哈希、去重(哈希指纹)。

堆(优先队列)的原理?如何用堆求 TopK?

堆是完全二叉树,满足堆序:大顶堆每个节点 ≥ 其子节点(堆顶最大),小顶堆反之(堆顶最小)。堆用数组紧凑存储,节点 i 的左子 2i+1、右子 2i+2、父 (i-1)/2。

核心操作(以数组实现的小顶堆为例):

  • push:追加到末尾,向上「上浮」(sift-up)O(log n)
  • pop:用末尾元素替换堆顶,向下「下沉」(sift-down)O(log n)
  • top:直接取 a[0] O(1)
C++
#include <vector>
#include <algorithm>

class MinHeap {
    std::vector<int> a;

    void sift_up(int i) {
        while (i > 0 && a[i] < a[(i - 1) / 2]) {
            std::swap(a[i], a[(i - 1) / 2]);
            i = (i - 1) / 2;
        }
    }

    void sift_down(int i) {
        int n = a.size();
        while (true) {
            int l = 2 * i + 1, r = 2 * i + 2, small = i;
            if (l < n && a[l] < a[small]) small = l;
            if (r < n && a[r] < a[small]) small = r;
            if (small == i) break;
            std::swap(a[i], a[small]);
            i = small;
        }
    }

public:
    void push(int x) { a.push_back(x); sift_up(a.size() - 1); }
    int top() const { return a[0]; }
    void pop() { a[0] = a.back(); a.pop_back(); sift_down(0); }
    bool empty() const { return a.empty(); }
    int size() const { return a.size(); }
};

用堆求 TopK:维护一个大小为 K 的小顶堆,遍历所有元素,若当前元素 > 堆顶则弹出堆顶、插入当前元素。遍历结束堆中即最大的 K 个,堆顶即第 K 大。

C++
#include <queue>
#include <vector>

std::vector<int> topK(const std::vector<int>& nums, int k) {
    std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
    for (int x : nums) {
        minHeap.push(x);
        if (minHeap.size() > k) minHeap.pop();   // 维持堆大小为 k
    }
    std::vector<int> res;
    while (!minHeap.empty()) { res.push_back(minHeap.top()); minHeap.pop(); }
    return res;                                  // 从第 K 大到最大
}
💡 面试加分项

TopK 有两条路线:小顶堆(时间 O(n log k),空间 O(k),适合 k 远小于 n,可处理流式数据)和快速选择 quickselect(平均 O(n),但会修改原数组、非流式)。求「第 K 大」小顶堆堆顶就是答案;求「第 K 小」则用大顶堆。

  • 实际场景:TopK 排行榜(微博热搜、电商销量前 N)、优先队列调度(操作系统就绪队列、Dijkstra 最短路径、定时器管理)、流式数据求中位数(双堆)。

priority_queue 的底层实现?

std::priority_queue 是容器适配器,底层默认用 std::vector 存储,并用堆算法(std::make_heap/push_heap/pop_heap)维持堆序。它只暴露 top/push/pop 三个核心接口,不支持遍历。

C++
#include <queue>
#include <vector>
#include <iostream>

int main() {
    // 默认大顶堆(less 比较,堆顶最大)
    std::priority_queue<int> maxHeap;
    maxHeap.push(3); maxHeap.push(1); maxHeap.push(5);
    std::cout << maxHeap.top() << '\n';          // 5

    // 小顶堆:greater 比较
    std::priority_queue<int, std::vector<int>, std::greater<int>> minHeap;
    minHeap.push(3); minHeap.push(1); minHeap.push(5);
    std::cout << minHeap.top() << '\n';          // 1
    return 0;
}

模板参数:priority_queue<T, Container, Compare> —— Container 默认 vector<T>,Compare 默认 std::less<T>(大顶堆)。注意 Compare 的语义与直觉相反:std::greater 得到小顶堆。

复杂度:push/pop 都是 O(log n),top O(1)。由于底层是 vector,入堆可能触发扩容。

💡 面试加分项

自定义比较器三种写法:① 传函数对象(如 std::greater<int>);② 传 lambda(C++11 起需配合 decltype);③ 自定义类型重载 operator<。若元素是自定义结构体,通常重载 operator< 并用 std::less 即可。

⚠️ 警告

priority_queue 没有 iterator、不能遍历、不能直接修改堆中元素(改了不会自动维护堆序)。需要「删除任意元素 / 更新优先级」时,改用 std::set(有序)或手写堆 + 懒删除标记。

  • 实际场景:任务优先级调度、TopK、Dijkstra/A* 优先队列、事件驱动框架的定时器堆。

二叉树的遍历(前/中/后/层序)如何实现?

二叉树遍历分深度优先(DFS)与广度优先(BFS)。DFS 按根节点访问顺序分前序(根左右)、中序(左根右)、后序(左右根);BFS 即层序遍历,按层从左到右。

C++
#include <iostream>

struct TreeNode {
    int val;
    TreeNode *left, *right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

// 前序:根 -> 左 -> 右
void preorder(TreeNode* root) {
    if (!root) return;
    std::cout << root->val << " ";
    preorder(root->left);
    preorder(root->right);
}

// 中序:左 -> 根 -> 右(BST 中序得到有序序列)
void inorder(TreeNode* root) {
    if (!root) return;
    inorder(root->left);
    std::cout << root->val << " ";
    inorder(root->right);
}

// 后序:左 -> 右 -> 根
void postorder(TreeNode* root) {
    if (!root) return;
    postorder(root->left);
    postorder(root->right);
    std::cout << root->val << " ";
}

非递归(迭代) 需要显式用栈模拟递归的调用栈。以中序为例:

C++
#include <stack>
#include <vector>

std::vector<int> inorder_iter(TreeNode* root) {
    std::vector<int> res;
    std::stack<TreeNode*> st;
    TreeNode* cur = root;
    while (cur || !st.empty()) {
        while (cur) {              // 一路向左压栈
            st.push(cur);
            cur = cur->left;
        }
        cur = st.top(); st.pop();  // 弹出访问
        res.push_back(cur->val);
        cur = cur->right;          // 转向右子树
    }
    return res;
}

层序遍历用队列,逐层处理:

C++
#include <queue>
#include <vector>

std::vector<std::vector<int>> levelorder(TreeNode* root) {
    std::vector<std::vector<int>> res;
    if (!root) return res;
    std::queue<TreeNode*> q;
    q.push(root);
    while (!q.empty()) {
        int n = q.size();
        std::vector<int> level;
        for (int i = 0; i < n; ++i) {          // 一次处理一层
            TreeNode* cur = q.front(); q.pop();
            level.push_back(cur->val);
            if (cur->left)  q.push(cur->left);
            if (cur->right) q.push(cur->right);
        }
        res.push_back(level);
    }
    return res;
}
遍历顺序迭代实现要点典型用途
前序根左右栈,先压右再压左序列化、复制树、前缀表达式
中序左根右栈,一路向左再回退BST 输出有序序列
后序左右根栈 + 标记,或双栈释放树、后缀表达式、子树信息聚合
层序逐层从左到右队列,按层计数BFS、最短路径、树的宽度
💡 面试加分项

后序遍历的非递归是三者中最难的,常用技巧:**「前序(根右左)反转」**得到后序(左右根),即用一个栈实现前序但先访问右子,最后 reverse。或给节点加访问标记(第二次弹出才输出)。

  • 实际场景:表达式解析(前缀/中缀/后缀)、BST 有序遍历、树的序列化/反序列化、文件系统遍历、游戏 AI 决策树评估。

二叉搜索树 BST 的特点?为什么容易退化?

二叉搜索树(BST) 满足:左子树所有节点 < 根 < 右子树所有节点。中序遍历得到递增有序序列,查找/插入/删除平均 O(log n)。

为什么容易退化:BST 的形态完全取决于插入顺序。若按有序/近似有序顺序插入,树会退化成单链(每个节点只有右子或左子),此时高度从 O(log n) 变 O(n),查找/插入/删除全部退化为 O(n)。

C++
#include <iostream>

struct TreeNode {
    int val;
    TreeNode *left, *right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};

// BST 查找:平均 O(log n),退化 O(n)
TreeNode* search(TreeNode* root, int key) {
    while (root) {
        if (key < root->val)      root = root->left;
        else if (key > root->val) root = root->right;
        else                      return root;
    }
    return nullptr;
}

// BST 插入
TreeNode* insert(TreeNode* root, int key) {
    if (!root) return new TreeNode(key);
    if (key < root->val) root->left  = insert(root->left, key);
    else                 root->right = insert(root->right, key);
    return root;
}
⚠️ 警告

依次插入 1,2,3,4,5 会得到一条全右链,高度 5;查找 5 要比较 5 次。这是 BST 的致命弱点,也是「平衡树」诞生的原因。

💡 面试加分项

解决退化的三条路径:AVL 树(严格平衡,旋转多)、红黑树(近似平衡,工程首选)、Treap/Splay(引入随机/伸展摊平)。C++ std::map 用红黑树正是为了避免 BST 退化。

  • 实际场景:有序符号表、范围查询(找区间内所有 key)、数据库索引(B+ 树是 BST 的多路扩展)、有序容器 std::set/std::map。

平衡二叉树 AVL 的原理?

AVL 树是最早的自平衡二叉搜索树,要求任意节点左右子树高度差(平衡因子)≤ 1。插入/删除后若失衡,通过旋转恢复平衡,保证树高 O(log n)。

四种失衡与旋转:

失衡情况形状修复
LL(左左)左子树的左子树过高右旋(单旋)
RR(右右)右子树的右子树过高左旋(单旋)
LR(左右)左子树的右子树过高先左旋再右旋(双旋)
RL(右左)右子树的左子树过高先右旋再左旋(双旋)
C++
#include <algorithm>

struct AVLNode {
    int val, height;
    AVLNode *left, *right;
    AVLNode(int x) : val(x), height(1), left(nullptr), right(nullptr) {}
};

int h(AVLNode* n) { return n ? n->height : 0; }
int bf(AVLNode* n) { return n ? h(n->left) - h(n->right) : 0; } // 平衡因子

void update(AVLNode* n) { n->height = 1 + std::max(h(n->left), h(n->right)); }

AVLNode* rotateRight(AVLNode* y) {          // 右旋
    AVLNode* x = y->left;
    y->left = x->right;
    x->right = y;
    update(y); update(x);
    return x;
}

AVLNode* rotateLeft(AVLNode* x) {           // 左旋
    AVLNode* y = x->right;
    x->right = y->left;
    y->left = x;
    update(x); update(y);
    return y;
}

AVLNode* insert(AVLNode* n, int val) {
    if (!n) return new AVLNode(val);
    if (val < n->val) n->left  = insert(n->left, val);
    else              n->right = insert(n->right, val);
    update(n);

    int b = bf(n);
    if (b > 1 && val < n->left->val)  return rotateRight(n);       // LL
    if (b > 1 && val > n->left->val)  { n->left = rotateLeft(n->left); return rotateRight(n); }  // LR
    if (b < -1 && val > n->right->val) return rotateLeft(n);       // RR
    if (b < -1 && val < n->right->val) { n->right = rotateRight(n->right); return rotateLeft(n); }// RL
    return n;
}
ℹ️ 信息

AVL 是严格平衡(高度差 ≤1),查找性能最优;但插入/删除为维持严格平衡旋转更频繁。因此更适合读多写少的场景,写入密集时红黑树更有优势。

💡 面试加分项

平衡因子 = 左子树高 - 右子树高,正常取 -1/0/1;插入后失衡节点的平衡因子必为 ±2,只需在其最近失衡祖先处旋转一次即可恢复,不影响整体树高。

  • 实际场景:内存中的有序字典(读密集)、需要保证最坏 O(log n) 查找的场景、Windows 内核地址空间管理(AVL 树的变体)。

红黑树的特点?为什么 map 用红黑树不用 AVL?

红黑树是近似平衡的二叉搜索树,靠五条性质维持「最长路径 ≤ 2 × 最短路径」,保证树高 O(log n):

  1. 节点非红即黑
  2. 根是黑
  3. 叶子(NIL)是黑
  4. 红节点的子节点必须是黑(红不连续)
  5. 任一节点到其所有叶子的路径上黑节点数相同(黑高一致)
对比AVL 树红黑树
平衡程度严格(高度差 ≤1)近似(最长 ≤2×最短)
查找更快(树更矮)略慢(树略高)
插入/删除旋转多,可能 O(log n) 次旋转最多 2 次旋转 + 重着色,更快
适用场景读多写少读写均衡、工程通用
C++
#include <map>
#include <string>
#include <iostream>

int main() {
    std::map<std::string, int> m;   // 底层红黑树,key 有序
    m["banana"] = 2;
    m["apple"]  = 1;
    m["cherry"] = 3;
    for (auto& [k, v] : m)          // 按键有序输出:apple banana cherry
        std::cout << k << ':' << v << '\n';

    // 范围查询:找 key >= "b" 的区间,红黑树的中序有序性带来
    auto it = m.lower_bound("b");
    return 0;
}
💡 面试加分项

std::map 用红黑树而非 AVL 的核心原因:写入成本更低。红黑树插入/删除最多旋转 O(1)(常数次)+ O(log n) 重着色,而 AVL 可能沿路径旋转 O(log n) 次;在读写均衡的通用容器中,红黑树的整体吞吐更高。AVL 只在读远多于写时才更优。

⚠️ 警告

红黑树的「有序」是 map/set 区别于 unordered_map/set 的关键:支持 lower_bound、范围遍历、按序迭代;代价是单点操作 O(log n),且内存占用小于哈希表。

  • 实际场景:std::map/std::set、Linux 内核完全公平调度器 CFS(用红黑树管理进程 vruntime)、epoll 事件管理、Java TreeMap、MySQL 内存索引。

图的存储方式:邻接矩阵 vs 邻接表?

图有两种经典存储:邻接矩阵(二维数组,g[i][j] 表示边)和邻接表(每个顶点一条链表/vector,存邻接点)。

对比邻接矩阵邻接表
空间复杂度O(V²)O(V + E)
判断边是否存在O(1)O(deg(v)),需遍历邻接链表
遍历某点所有邻居O(V)O(deg(v))
加边/删边O(1)O(1)(尾部)
适用场景稠密图(E 接近 V²)、频繁查边、Floyd稀疏图(E 远小于 V²)、频繁遍历邻居
C++
#include <vector>
#include <iostream>

// 邻接矩阵
std::vector<std::vector<int>> build_matrix(int n, const std::vector<std::pair<int,int>>& edges) {
    std::vector<std::vector<int>> g(n, std::vector<int>(n, 0));
    for (auto& [u, v] : edges) g[u][v] = 1;   // 无向图再设 g[v][u] = 1
    return g;
}

// 邻接表(C++ 首选,稀疏图高效)
std::vector<std::vector<int>> build_adj(int n, const std::vector<std::pair<int,int>>& edges) {
    std::vector<std::vector<int>> g(n);
    for (auto& [u, v] : edges) {
        g[u].push_back(v);
        g[v].push_back(u);                    // 无向图双向
    }
    return g;
}
💡 面试加分项

带权图用邻接表时,节点存 (邻接点, 权重),即 vector<vector<pair<int,int>>>;邻接矩阵则直接把 g[i][j] 存权重。工程上绝大多数图是稀疏的,邻接表是默认选择;只有稠密图或需要 O(1) 查边时才用矩阵。

ℹ️ 信息

另有用边列表 vector<tuple<u,v,w>> 存边,适合 Kruskal 最小生成树等只关心边的算法。

  • 实际场景:社交网络好友关系(邻接表)、地图导航(带权邻接表)、路由表、编译依赖图、网络拓扑。

图的遍历 BFS 和 DFS?

BFS(广度优先) 用队列,按「层」扩散,天然求无权图最短路径;DFS(深度优先) 用栈/递归,一路深入再回溯,适合连通性、拓扑排序、环检测。

C++
#include <vector>
#include <queue>
#include <iostream>

// BFS:从 s 出发遍历,返回访问顺序(求无权图最短距离同理)
std::vector<int> bfs(const std::vector<std::vector<int>>& g, int s) {
    std::vector<bool> visited(g.size(), false);
    std::vector<int> order;
    std::queue<int> q;
    q.push(s); visited[s] = true;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        order.push_back(u);
        for (int v : g[u])
            if (!visited[v]) { visited[v] = true; q.push(v); }
    }
    return order;
}

// DFS:递归版
void dfs_rec(const std::vector<std::vector<int>>& g, int u,
             std::vector<bool>& visited, std::vector<int>& order) {
    visited[u] = true;
    order.push_back(u);
    for (int v : g[u])
        if (!visited[v]) dfs_rec(g, v, visited, order);
}

DFS 迭代版用显式栈:

C++
#include <stack>

std::vector<int> dfs_iter(const std::vector<std::vector<int>>& g, int s) {
    std::vector<bool> visited(g.size(), false);
    std::vector<int> order;
    std::stack<int> st;
    st.push(s);
    while (!st.empty()) {
        int u = st.top(); st.pop();
        if (visited[u]) continue;
        visited[u] = true;
        order.push_back(u);
        for (int v : g[u]) if (!visited[v]) st.push(v);
    }
    return order;
}
对比BFSDFS
数据结构队列栈(递归调用栈)
特性逐层扩散,先访问近的一路深入到底再回溯
无权最短路能求(首次到达即最短)不能
空间复杂度O(最宽一层宽度)O(树高/递归深度)
典型应用最短路、层序、二分图、连通分量计数拓扑排序、环检测、强连通分量、迷宫、回溯
💡 面试加分项

求「无权图最短路径 / 最少步数」优先 BFS;「是否存在路径、拓扑序、环检测」DFS 更直观(配合三色标记:白未访问/灰在栈中/黑已结束,灰→灰即发现环)。递归 DFS 在深层图上可能爆栈,可用迭代版或改 BFS。

  • 实际场景:BFS——最短路径导航、社交「N 度人脉」、爬虫按层抓取;DFS——编译器的依赖解析、游戏迷宫求解、有向图拓扑排序(任务调度)。

跳表的原理?为什么 Redis 用跳表?

跳表(Skip List) 在有序链表之上建立多层索引:第 0 层是完整有序链表,更高层是下一层的「抽样」快车道。查找时从最高层开始,能跳就跳、不能跳就下沉,把链表 O(n) 查找降到平均 O(log n)。

C++
#include <cstdlib>
#include <climits>
#include <iostream>

// 简化跳表节点:多层 next 指针
struct SkipNode {
    int val;
    std::vector<SkipNode*> next;
    SkipNode(int v, int level) : val(v), next(level, nullptr) {}
};

class SkipList {
    static const int MAX_LEVEL = 16;
    SkipNode* head;

    int random_level() {                 // 随机层高:50% 概率升一层
        int level = 1;
        while (level < MAX_LEVEL && (rand() & 1)) ++level;
        return level;
    }

public:
    SkipList() : head(new SkipNode(INT_MIN, MAX_LEVEL)) {}

    SkipNode* find(int target) {
        SkipNode* cur = head;
        for (int i = MAX_LEVEL - 1; i >= 0; --i) {
            while (cur->next[i] && cur->next[i]->val < target)
                cur = cur->next[i];      // 当前层向右跳
        }
        return cur->next[0] && cur->next[0]->val == target ? cur->next[0] : nullptr;
    }

    void insert(int val) {
        int level = random_level();
        SkipNode* node = new SkipNode(val, level);
        SkipNode* cur = head;
        for (int i = MAX_LEVEL - 1; i >= 0; --i) {
            while (cur->next[i] && cur->next[i]->val < val)
                cur = cur->next[i];
            if (i < level) {             // 在低层插入新节点
                node->next[i] = cur->next[i];
                cur->next[i] = node;
            }
        }
    }
};
💡 面试加分项

跳表本质是「用空间换时间 + 随机化」:通过多级索引把有序链表的顺序查找变成「跳着找」,且实现比红黑树简单、支持高效范围查询。与红黑树相比,跳表插入删除无需复杂旋转,只用局部指针调整,并发实现更容易(可以只锁相邻节点)。

为什么 Redis 用跳表(zskiplist):有序集合 ZSet 需要按 score 排序 + 范围查询 + 排名(rank):

  1. 跳表天然有序,范围查询(ZRANGEBYSCORE)直接沿第 0 层顺序走,比红黑树中序遍历更直观高效;
  2. 实现简单、易于调试和维护;
  3. 并发/持久化下,跳表的局部性修改比红黑树旋转更友好;
  4. 每个节点额外存 span 跨度,可 O(log n) 算出排名(rank),满足 ZRANK 需求。
  • 实际场景:Redis ZSet、LevelDB/RocksDB 的 MemTable、Lucene 的倒排索引 term 字典、并发有序容器(ConcurrentSkipListMap)。

并查集的原理和适用场景?

并查集(Union-Find / Disjoint Set) 维护若干不相交集合,每个集合用一棵树表示,树的根是该集合代表元。支持两个近乎 O(1) 的操作:find(x) 找 x 所属集合根,union(x,y) 合并两集合。核心优化是路径压缩 + 按秩合并。

C++
#include <vector>

class UnionFind {
    std::vector<int> parent, rank;
public:
    UnionFind(int n) : parent(n), rank(n, 0) {
        for (int i = 0; i < n; ++i) parent[i] = i;   // 各自独立成集合
    }

    // 查找根 + 路径压缩:把沿途节点直接挂到根
    int find(int x) {
        if (parent[x] != x) parent[x] = find(parent[x]);
        return parent[x];
    }

    // 按秩合并:把矮树挂到高树下,避免退化成链
    void unite(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return;
        if (rank[rx] < rank[ry]) std::swap(rx, ry);
        parent[ry] = rx;
        if (rank[rx] == rank[ry]) ++rank[rx];
    }

    bool connected(int x, int y) { return find(x) == find(y); }
};
💡 面试加分项

同时使用路径压缩 + 按秩合并时,单次操作均摊复杂度为反阿克曼函数 α(n),几乎可视为常数 O(1)。只做路径压缩是 O(log n) 均摊,两者结合才是最优。

优化作用
路径压缩find 时把节点直接指向根,摊平树高
按秩/按大小合并合并时小树挂大树,控制树高增长
  • 实际场景:
    • 图连通性:判断两节点是否连通、连通分量计数、Kruskal 最小生成树判环;
    • 朋友圈/岛屿数量类题目(「省份数量」);
    • 等价关系判定(变量相等性)、网络连通性(两主机是否可达)。

Trie 树(前缀树)的原理和适用场景?

Trie(前缀树/字典树) 是多叉树,节点存「下一字符 → 子节点」映射,从根到某节点的路径即一个字符串前缀。共享公共前缀可大幅压缩空间,且前缀查询、按字典序遍历天然高效。

C++
#include <unordered_map>
#include <string>

struct TrieNode {
    std::unordered_map<char, TrieNode*> children; // 字符映射
    bool is_end = false;                          // 是否为某单词结尾
};

class Trie {
    TrieNode* root;
public:
    Trie() : root(new TrieNode()) {}

    void insert(const std::string& word) {
        TrieNode* cur = root;
        for (char c : word) {
            if (!cur->children.count(c))
                cur->children[c] = new TrieNode();
            cur = cur->children[c];
        }
        cur->is_end = true;
    }

    bool search(const std::string& word) {        // 完整单词是否存在
        TrieNode* cur = root;
        for (char c : word) {
            if (!cur->children.count(c)) return false;
            cur = cur->children[c];
        }
        return cur->is_end;
    }

    bool startsWith(const std::string& prefix) {  // 是否存在该前缀
        TrieNode* cur = root;
        for (char c : prefix) {
            if (!cur->children.count(c)) return false;
            cur = cur->children[c];
        }
        return true;
    }
};

复杂度:插入/查询均为 O(m),m 为字符串长度,与词表规模无关。

💡 面试加分项

内存敏感场景可用数组代替哈希表存子节点(TrieNode* children[26],小写字母时),速度快但空间可能浪费;或用「压缩 Trie(Patricia)」合并单分支路径进一步省空间。Trie 的核心价值是共享前缀与前缀匹配。

  • 实际场景:
    • 搜索引擎/输入法的自动补全、前缀联想;
    • 敏感词过滤(AC 自动机基于 Trie);
    • IP 路由最长前缀匹配、拼写检查;
    • 词频统计、字符串集合的公共前缀查找。

布隆过滤器的原理?如何控制误判率?

布隆过滤器(Bloom Filter) 是空间高效的概率型结构,用长度为 m 的位数组 + k 个哈希函数表示集合。插入时把 key 经 k 个哈希函数映射到的 k 个位置全部置 1;查询时若 k 个位置全为 1 则「可能存在」,有任一为 0 则「一定不存在」。

C++
#include <vector>
#include <string>
#include <functional>

class BloomFilter {
    std::vector<bool> bits;                 // 位数组
    std::vector<std::hash<std::string>> hashers; // 简化:k 个哈希用不同种子
    int k;

    std::size_t nth_hash(const std::string& s, int i) {
        // 用双哈希技巧模拟 k 个哈希:h_i = h1 + i*h2
        std::size_t h1 = hashers[0](s);
        std::size_t h2 = hashers[1](s);
        return (h1 + i * h2) % bits.size();
    }

public:
    BloomFilter(std::size_t m, int k) : bits(m), hashers(2), k(k) {}

    void add(const std::string& s) {
        for (int i = 0; i < k; ++i) bits[nth_hash(s, i)] = true;
    }

    bool might_contain(const std::string& s) {
        for (int i = 0; i < k; ++i)
            if (!bits[nth_hash(s, i)]) return false;   // 一定不存在
        return true;                                   // 可能存在(可能误判)
    }
};

如何控制误判率:设 n 为元素数、m 为位数、k 为哈希函数个数,误判率近似:

p≈(1−e−kn/m)kp \approx (1 - e^{-kn/m})^k

  • 最优哈希函数个数:k=mnln⁡2≈0.693mnk = \frac{m}{n} \ln 2 \approx 0.693\frac{m}{n},此时误判率约 p≈0.6185m/np \approx 0.6185^{m/n}。
  • 位越多(m/n 越大)误判率越低:例如 m/n=10 时 p≈1%;m/n=20 时 p≈0.001%。
  • 权衡:误判率越低需要越多的位和哈希计算。
⚠️ 警告

布隆过滤器不支持删除(多个 key 可能共享同一位置,置 0 会误删他人);且只会有假阳性(误判存在),不会有假阴性(漏判不存在)——这是它的核心性质。需要删除时改用「计数布隆过滤器」。

💡 面试加分项

典型应用是「缓存穿透防护」:查询前先过布隆过滤器,不在集合内的 key 直接返回,避免打穿到 DB。同理用于:垃圾邮件过滤、爬虫 URL 去重、LevelDB/RocksDB 用布隆过滤器快速判断 SSTable 中是否可能含某 key。


B 树和 B+ 树的区别?为什么数据库索引用 B+ 树?

B 树/B+ 树都是多路平衡搜索树,一个节点可存多个 key、多个子节点,高度极低(百万级数据仅 3~4 层),专为磁盘/外存设计——减少磁盘 I/O 次数。

对比B 树B+ 树
数据存储每个节点(含内部节点)都存数据只有叶子节点存全部数据,内部节点只存索引
叶子节点连接叶子间无指针叶子用双向链表串联,支持顺序遍历
查找可能在内部节点命中即返回必须一直走到叶子节点
范围查询需中序遍历,可能跨层回溯叶子链表顺序扫,高效
稳定性查找到命中节点位置不定每次查找路径长度固定(都到叶子)
适用部分文件系统、内存索引数据库索引(MySQL InnoDB)
C++
// B+ 树节点示意(仅结构,非完整实现)
#include <vector>

template <typename K, typename V>
struct BPlusNode {
    bool is_leaf;
    std::vector<K> keys;                       // 内部节点存 key 索引
    std::vector<BPlusNode*> children;          // 内部节点的子指针
    std::vector<V> values;                     // 叶子节点存数据
    BPlusNode* next_leaf = nullptr;            // 叶子节点串成链表
};
💡 面试加分项

数据库索引用 B+ 树而非 B 树/红黑树的核心原因:

  1. 磁盘 I/O 友好:多路、矮胖,一次磁盘页读入更多 key,树高 34 层即可容纳千万数据,查询只需 34 次 I/O;红黑树是二叉树,高度高、I/O 次数多。
  2. 范围查询/顺序扫描高效:叶子链表 + 全部数据在叶子,ORDER BY/范围查询/全表扫描都顺着叶子链表走即可。
  3. 查询稳定:每次都走到叶子,路径等长,最坏性能可预期。
  4. 内部节点只存 key 不存数据,单页能容纳更多索引项,进一步降低树高。
ℹ️ 信息

B 树「内部节点也存数据」意味着命中内部节点可提前返回,个别点查略快,但范围查询要跨层回溯,不适合数据库主键排序扫描为主的负载。MySQL InnoDB 主键索引(聚簇索引)即 B+ 树,叶子存完整行;辅助索引叶子存主键值,回表再查主键索引。

  • 实际场景:MySQL InnoDB 索引、大多数关系型数据库索引、文件系统目录结构(NTFS、ext4 用类 B+ 树/B 树变体)。

如何实现 LRU 缓存?

LRU(Least Recently Used,最近最少使用) 缓存淘汰策略:容量满时淘汰「最久未被访问」的项。经典实现是哈希表 + 双向链表:哈希表负责 O(1) 定位 key→节点,双向链表维护访问顺序(头部最新、尾部最旧)。

C++
#include <unordered_map>
#include <list>

class LRUCache {
    int cap;
    std::list<std::pair<int, int>> lru;                 // 双向链表:头新尾旧
    std::unordered_map<int, std::list<std::pair<int, int>>::iterator> mp;

public:
    LRUCache(int capacity) : cap(capacity) {}

    int get(int key) {
        auto it = mp.find(key);
        if (it == mp.end()) return -1;
        lru.splice(lru.begin(), lru, it->second);       // 移到头部(最新)
        return it->second->second;
    }

    void put(int key, int value) {
        auto it = mp.find(key);
        if (it != mp.end()) {                           // 已存在:更新并移到头部
            lru.splice(lru.begin(), lru, it->second);
            it->second->second = value;
            return;
        }
        if (lru.size() == cap) {                        // 满了:淘汰尾部(最旧)
            mp.erase(lru.back().first);
            lru.pop_back();
        }
        lru.emplace_front(key, value);
        mp[key] = lru.begin();
    }
};
💡 面试加分项

std::list::splice 是 O(1) 地把节点移动到头部的高效写法,避免「erase + push_front」两次操作。核心考点是为什么用双向链表而不是单向:删除尾节点时,双向链表能通过尾节点的 prev O(1) 定位到新的尾,单向链表则需 O(n) 遍历。

⚠️ 警告

常见坑:① 用 std::list 存迭代器到 map 时,splice/erase 后要同步更新 map;② get 命中也算「访问」,必须刷新到头部,否则淘汰顺序错;③ 若用 std::map 代替 unordered_map 定位,整体会从 O(1) 退化 O(log n)。

  • 实际场景:操作系统页面置换、CPU 缓存、浏览器历史/缓存、Redis 内存淘汰策略(allkeys-lru)、数据库缓冲池(InnoDB Buffer Pool 的 LRU 链表)。

线段树的原理和适用场景?

线段树(Segment Tree) 是一棵平衡二叉树,把区间 [l, r] 递归二分:根节点代表整个区间,每个节点代表一个子区间,叶子节点代表单点。每个节点保存该区间的聚合信息(和、最值等),从而把「区间查询」和「区间更新」都降到 O(log n)。

原理示意:根节点 [0, n-1],节点 [l, r] 的左子是 [l, mid]、右子是 [mid+1, r](mid=(l+r)/2)。用数组紧凑存储,节点 i 的左子 2i、右子 2i+1(下标从 1 起),空间需开 4n。懒惰标记(lazy):区间更新时若当前节点区间被完全覆盖,只在该节点打标记、不递归到叶子,等下次访问其子区间时再「下传」(pushdown),从而把区间加法也优化到 O(log n)。

C++
#include <vector>

// 线段树:维护区间和,支持「区间加法 + 区间查询」
class SegmentTree {
    std::vector<long long> tree, lazy;   // 各开 4n 空间
    int n;

    void build(const std::vector<int>& a, int node, int l, int r) {
        if (l == r) { tree[node] = a[l]; return; }
        int mid = (l + r) >> 1;
        build(a, node * 2, l, mid);
        build(a, node * 2 + 1, mid + 1, r);
        tree[node] = tree[node * 2] + tree[node * 2 + 1];   // 自底向上合并
    }

    void pushdown(int node, int l, int r) {                  // 懒标记下传
        if (lazy[node] == 0) return;
        int mid = (l + r) >> 1;
        tree[node * 2]     += lazy[node] * (mid - l + 1);
        tree[node * 2 + 1] += lazy[node] * (r - mid);
        lazy[node * 2]     += lazy[node];
        lazy[node * 2 + 1] += lazy[node];
        lazy[node] = 0;
    }

    void range_add(int node, int l, int r, int ql, int qr, long long val) {
        if (ql <= l && r <= qr) {            // 完全覆盖:直接打懒标记
            tree[node] += val * (r - l + 1);
            lazy[node] += val;
            return;
        }
        pushdown(node, l, r);                // 部分覆盖:先下传再递归
        int mid = (l + r) >> 1;
        if (ql <= mid) range_add(node * 2, l, mid, ql, qr, val);
        if (qr > mid)  range_add(node * 2 + 1, mid + 1, r, ql, qr, val);
        tree[node] = tree[node * 2] + tree[node * 2 + 1];
    }

    long long query(int node, int l, int r, int ql, int qr) {
        if (ql <= l && r <= qr) return tree[node];
        pushdown(node, l, r);
        int mid = (l + r) >> 1;
        long long res = 0;
        if (ql <= mid) res += query(node * 2, l, mid, ql, qr);
        if (qr > mid)  res += query(node * 2 + 1, mid + 1, r, ql, qr);
        return res;
    }

public:
    SegmentTree(const std::vector<int>& a) : n(a.size()) {
        tree.assign(4 * n, 0);
        lazy.assign(4 * n, 0);
        build(a, 1, 0, n - 1);
    }
    void range_add(int l, int r, long long val) { range_add(1, 0, n - 1, l, r, val); }
    long long query(int l, int r) { return query(1, 0, n - 1, l, r); }
};
操作复杂度说明
建树O(n)每个节点恰好访问一次
单点/区间更新O(log n)懒标记避免递归到叶子
区间查询O(log n)至多覆盖约 2·log n 个节点
空间O(4n)数组存储,实际约 4n
💡 面试加分项

线段树与「树」章节的递归分治一脉相承:本质是区间二分 + 懒标记。两个高频考点:① 懒标记必须在 pushdown 中下传并清零,否则区间更新会重复累加;② 空间要开 4n(不是 2n,最坏情况节点数接近 4n),避免数组越界。进阶还有「线段树合并」「动态开点(值域线段树/主席树)」「扫描线求矩形面积并」。

ℹ️ 信息

对比 树状数组:线段树功能更强,能维护区间最值、区间修改、区间最大子段和等任意可合并信息,但常数和代码量更大。若只需要「前缀和 + 单点更新」,树状数组更轻量。

  • 实际场景:区间求和/最值(RMQ)、排行榜区间统计、LeetCode 区间类题目(区域和检索、区间加法)、计算几何扫描线(矩形面积并/周长并)、线段树维护区间最大公约数/最大子段和、持久化(主席树)求区间第 K 大。

树状数组(BIT)的原理和适用场景?

树状数组(BIT,Binary Indexed Tree / Fenwick Tree) 用一棵「前缀和」思想维护的数组,支持 O(log n) 单点更新 + O(log n) 前缀和查询,空间 O(n),常数极小、实现只有几行。核心是 lowbit:lowbit(x) = x & (-x),取 x 二进制最低位的 1。

原理:tree[i] 维护区间 [i - lowbit(i) + 1, i] 的和。更新时沿 i += lowbit(i) 向上「通知」所有覆盖它的节点;查询前缀和时沿 i -= lowbit(i) 向下累加。因 lowbit 把下标按 2 的幂分组,更新和查询都只经过 O(log n) 个节点。区间和 [l, r] = sum(r) - sum(l-1)。

C++
#include <vector>

// 树状数组:单点更新 + 前缀和查询(下标从 1 开始)
class Fenwick {
    std::vector<long long> tree;
    int n;

    static int lowbit(int x) { return x & (-x); }

public:
    Fenwick(int n) : tree(n + 1, 0), n(n) {}

    // 单点更新:位置 i 加 val,O(log n)
    void add(int i, long long val) {
        for (; i <= n; i += lowbit(i)) tree[i] += val;
    }

    // 前缀和 [1, i],O(log n)
    long long sum(int i) {
        long long res = 0;
        for (; i > 0; i -= lowbit(i)) res += tree[i];
        return res;
    }

    // 区间和 [l, r]
    long long range_sum(int l, int r) {
        return sum(r) - sum(l - 1);
    }
};
💡 面试加分项

树状数组经典变体——差分 + BIT 实现「区间更新 + 单点查询」:对原数组建差分数组 d[i] = a[i] - a[i-1],[l, r] 区间加 val 等价于 d[l] += val、d[r+1] -= val 两次单点更新,单点 a[i] 即 sum(d[1..i])。还可结合离散化求逆序对:倒序遍历,用 BIT 统计已出现且小于当前值的个数。

维度树状数组 BIT线段树
代码量极小(10 行内)较大
常数/内存小(O(n))大(O(4n))
能维护前缀和/可差分信息、单点更新区间和/最值/任意可合并信息、区间更新(懒标记)
是否支持区间最值不支持支持
适用动态前缀和、逆序对、排名通用区间操作
ℹ️ 信息

树状数组本质是线段树的「压缩版」:它只保留了线段树中「前缀和」这条主链的信息,因此只能维护前缀和这类可相减(差分)的信息,不能像线段树那样维护区间最值、区间修改。能用 BIT 解决时优先 BIT(快、省内存),需要通用区间操作时用 线段树。

  • 实际场景:动态维护前缀和(数据流式累加 + 查询)、求逆序对/顺序对(离散化)、排行榜的实时排名与分数更新、算法题「区域和检索 - 数组可修改」(LeetCode 307)、差分数组求区间更新后单点值。