cpp-刷题常用数据结构

62 阅读6分钟

如果不是为了应付逐渐内卷的面试算法,很多数据结构在工作中是用不到的。

以下是刷题常用的数据结构 API 总结:

1. vector(动态数组)

#include <vector>
#include <algorithm>
using namespace std;

vector<int> vec;

// 增
vec.push_back(1);           // 尾部添加
vec.emplace_back(2);        // 尾部原地构造(C++11)
vec.insert(vec.begin(), 0); // 指定位置插入

// 删
vec.pop_back();             // 删除尾部
vec.erase(vec.begin());     // 删除指定位置
vec.erase(vec.begin(), vec.begin() + 2); // 删除范围
vec.clear();                // 清空

// 查
vec[0];                     // 随机访问(不检查边界)
vec.at(0);                  // 随机访问(检查边界)
vec.front();                // 第一个元素
vec.back();                 // 最后一个元素

// 容量
vec.size();                 // 元素个数
vec.empty();                // 是否为空
vec.capacity();             // 容量
vec.reserve(100);           // 预留空间

// 其他
sort(vec.begin(), vec.end());           // 排序
reverse(vec.begin(), vec.end());        // 反转
find(vec.begin(), vec.end(), 5);        // 查找

2. unordered_map(哈希表)

#include <unordered_map>
using namespace std;

unordered_map<string, int> map;

// 增/改
map["apple"] = 5;           // 键不存在则创建,存在则修改
map.insert({"banana", 3});  // 插入
map.emplace("cherry", 7);   // 原地构造

// 删
map.erase("apple");         // 按键删除
map.erase(map.begin());     // 按迭代器删除
map.clear();                // 清空

// 查
map["apple"];               // 访问(键不存在会创建)
map.at("apple");            // 访问(键不存在抛异常)
map.find("apple");          // 返回迭代器,找不到返回 end()
map.count("apple");         // 返回1或0
map.contains("apple");      // C++20,返回 bool

// 遍历
for (auto& [key, value] : map) {
    cout << key << ": " << value << endl;
}

// 容量
map.size();
map.empty();

3. unordered_set(哈希集合)

#include <unordered_set>
using namespace std;

unordered_set<int> set;

// 增
set.insert(1);
set.emplace(2);

// 删
set.erase(1);
set.erase(set.begin());
set.clear();

// 查
set.find(1);                // 返回迭代器
set.count(1);               // 返回1或0
set.contains(1);            // C++20

// 遍历
for (int num : set) {
    cout << num << endl;
}

// 容量
set.size();
set.empty();

4. map(红黑树,有序映射)

#include <map>
using namespace std;

map<string, int> map;

// API 与 unordered_map 基本相同,但有序
map["apple"] = 5;
map["banana"] = 3;

// 额外功能(利用有序性)
map.lower_bound("b");       // 第一个 >= key 的迭代器
map.upper_bound("b");       // 第一个 > key 的迭代器

// 遍历时按键顺序输出
for (auto& [key, value] : map) {
    // 按键升序输出
}

5. set(红黑树,有序集合)

#include <set>
using namespace std;

set<int> set;

// API 与 unordered_set 基本相同,但有序
set.insert(3);
set.insert(1);
set.insert(2);

// 额外功能
set.lower_bound(2);         // 第一个 >= value 的迭代器
set.upper_bound(2);         // 第一个 > value 的迭代器

// 遍历时按值升序输出
for (int num : set) {
    // 1, 2, 3
}

6. priority_queue(优先队列)

#include <queue>
#include <functional>
using namespace std;

// 默认最大堆
priority_queue<int> maxHeap;

显式声明是 
priority_queue<int, vector<int>, less<int>> maxHeap;

// 最小堆
priority_queue<int, vector<int>, greater<int>> minHeap;

这里比较费解的是,默认的less是大顶堆,帮助理解的方法是,less第一个参数是当前节点,第二个参数是子节点,如果当前节点比子节点小,那么当前节点下沉,大的节点上浮。

这个priority传参需要的是类,实例化的类(greater函数类是模板,需要传)。

与之相反的是 sort 方法,如果你传参greater函数类,它会默认从大到小排序,你还需要构建实例。比如 priority 传的是 less<int>, 但是你用sort方法,你就需要传递 less<int>()。

// 自定义比较器的堆
auto cmp = [](int a, int b) { return a > b; };
priority_queue<int, vector<int>, decltype(cmp)> customHeap(cmp);

// 操作
maxHeap.push(5);            // 插入
maxHeap.emplace(3);         // 原地构造
maxHeap.top();              // 访问顶部元素(不删除)
maxHeap.pop();              // 删除顶部元素
maxHeap.size();
maxHeap.empty();

7. stack(栈)

#include <stack>
using namespace std;

stack<int> st;

st.push(1);                 // 入栈
st.emplace(2);              // 原地构造
st.top();                   // 访问栈顶
st.pop();                   // 出栈
st.size();
st.empty();

8. queue(队列)

#include <queue>
using namespace std;

queue<int> q;

q.push(1);                  // 入队
q.emplace(2);               // 原地构造
q.front();                  // 队首
q.back();                   // 队尾
q.pop();                    // 出队
q.size();
q.empty();

9. deque(双端队列)

#include <deque>
using namespace std;

deque<int> dq;

// 两端操作
dq.push_front(1);           // 头部插入
dq.push_back(2);            // 尾部插入
dq.pop_front();             // 头部删除
dq.pop_back();              // 尾部删除
dq.front();                 // 头部元素
dq.back();                  // 尾部元素

// 随机访问
dq[0];
dq.at(0);

dq.size();
dq.empty();

刷题常用模式

1. 遍历模式

// vector 遍历
for (int i = 0; i < vec.size(); i++) {}
for (int num : vec) {}
for (auto it = vec.begin(); it != vec.end(); it++) {}

// map/set 遍历
for (auto& [key, val] : map) {}
for (auto it = map.begin(); it != map.end(); it++) {}

2. 查找模式

// vector 查找
auto it = find(vec.begin(), vec.end(), target);
if (it != vec.end()) {}

// map/set 查找
if (map.find(key) != map.end()) {}
if (map.count(key)) {}
if (map.contains(key)) {}  // C++20

3. 堆的常用模式

// Top K 问题
priority_queue<int, vector<int>, greater<int>> minHeap;
for (int num : nums) {
    minHeap.push(num);
    if (minHeap.size() > k) {
        minHeap.pop();
    }
}

4. 栈的常用模式

// 括号匹配、单调栈等
stack<int> st;
for (char c : s) {
    if (c == '(') {
        st.push(c);
    } else {
        if (st.empty()) return false;
        st.pop();
    }
}
return st.empty();

这些 API 覆盖了刷题中 90% 以上的使用场景,熟练掌握后可以高效解决各种算法问题。

list

C++ 提供了双链表 std::list,它是一个双向链表容器。

1. 基本用法

#include <iostream>
#include <list>
using namespace std;

int main() {
    // 创建双链表
    list<int> lst = {1, 2, 3, 4, 5};
    
    // 遍历
    for (int num : lst) {
        cout << num << " ";
    }
    cout << endl;  // 输出: 1 2 3 4 5
    
    return 0;
}

2. 常用 API

头部操作

#include <iostream>
#include <list>
using namespace std;

int main() {
    list<int> lst;
    
    // 头部操作
    lst.push_front(1);      // 头部插入: 1
    lst.push_front(2);      // 头部插入: 2 -> 1
    lst.emplace_front(3);   // 头部原地构造: 3 -> 2 -> 1
    
    cout << "头部元素: " << lst.front() << endl;  // 3
    
    lst.pop_front();        // 删除头部: 2 -> 1
    cout << "删除后头部: " << lst.front() << endl;  // 2
    
    return 0;
}

尾部操作

#include <iostream>
#include <list>
using namespace std;

int main() {
    list<int> lst = {1, 2};
    
    // 尾部操作
    lst.push_back(3);       // 尾部插入: 1 -> 2 -> 3
    lst.emplace_back(4);    // 尾部原地构造: 1 -> 2 -> 3 -> 4
    
    cout << "尾部元素: " << lst.back() << endl;  // 4
    
    lst.pop_back();         // 删除尾部: 1 -> 2 -> 3
    cout << "删除后尾部: " << lst.back() << endl;  // 3
    
    return 0;
}

插入和删除

#include <iostream>
#include <list>
using namespace std;

int main() {
    list<int> lst = {1, 2, 3, 4, 5};
    
    // 在指定位置插入
    auto it = lst.begin();
    advance(it, 2);  // 移动到第三个位置
    lst.insert(it, 99);  // 1 -> 2 -> 99 -> 3 -> 4 -> 5
    
    // 删除指定位置
    it = lst.begin();
    advance(it, 3);
    lst.erase(it);  // 删除 3: 1 -> 2 -> 99 -> 4 -> 5
    
    // 删除指定值
    lst.remove(2);  // 删除所有 2: 1 -> 99 -> 4 -> 5
    
    // 清空
    // lst.clear();
    
    for (int num : lst) {
        cout << num << " ";  // 1 99 4 5
    }
    cout << endl;
    
    return 0;
}

3. 迭代器操作

#include <iostream>
#include <list>
using namespace std;

int main() {
    list<int> lst = {1, 2, 3, 4, 5};
    
    // 双向迭代器
    cout << "正向遍历: ";
    for (auto it = lst.begin(); it != lst.end(); ++it) {
        cout << *it << " ";
    }
    cout << endl;
    
    cout << "反向遍历: ";
    for (auto it = lst.rbegin(); it != lst.rend(); ++it) {
        cout << *it << " ";
    }
    cout << endl;
    
    return 0;
}

4. 特殊操作(链表特有)

#include <iostream>
#include <list>
using namespace std;

int main() {
    list<int> lst1 = {1, 2, 3};
    list<int> lst2 = {4, 5, 6};
    
    // 拼接(转移元素,不拷贝)
    lst1.splice(lst1.end(), lst2);  // lst1: 1->2->3->4->5->6, lst2 为空
    
    cout << "拼接后 lst1: ";
    for (int num : lst1) cout << num << " ";  // 1 2 3 4 5 6
    cout << endl;
    
    cout << "拼接后 lst2 大小: " << lst2.size() << endl;  // 0
    
    // 排序
    list<int> lst3 = {3, 1, 4, 1, 5, 9, 2};
    lst3.sort();
    cout << "排序后: ";
    for (int num : lst3) cout << num << " ";  // 1 1 2 3 4 5 9
    cout << endl;
    
    // 去重(需要先排序)
    lst3.unique();
    cout << "去重后: ";
    for (int num : lst3) cout << num << " ";  // 1 2 3 4 5 9
    cout << endl;
    
    // 反转
    lst3.reverse();
    cout << "反转后: ";
    for (int num : lst3) cout << num << " ";  // 9 5 4 3 2 1
    cout << endl;
    
    return 0;
}

6. 实际应用:LRU Cache

#include <iostream>
#include <list>
#include <unordered_map>
using namespace std;

class LRUCache {
private:
    int capacity;
    list<pair<int, int>> cache;  // (key, value) 双链表
    unordered_map<int, list<pair<int, int>>::iterator> keyToNode;
    
public:
    LRUCache(int cap) : capacity(cap) {}
    
    int get(int key) {
        if (keyToNode.find(key) == keyToNode.end()) {
            return -1;
        }
        
        // 移动到头部(最近使用)
        auto it = keyToNode[key];
        int value = it->second;
        cache.erase(it);
        cache.push_front({key, value});
        keyToNode[key] = cache.begin();
        
        return value;
    }
    
    void put(int key, int value) {
        if (keyToNode.find(key) != keyToNode.end()) {
            // 键已存在,删除旧位置
            cache.erase(keyToNode[key]);
        } else if (cache.size() == capacity) {
            // 删除最久未使用的(尾部)
            int lruKey = cache.back().first;
            keyToNode.erase(lruKey);
            cache.pop_back();
        }
        
        // 插入到头部
        cache.push_front({key, value});
        keyToNode[key] = cache.begin();
    }
    
    void print() {
        cout << "LRU Cache: ";
        for (auto& [k, v] : cache) {
            cout << "(" << k << "," << v << ") ";
        }
        cout << endl;
    }
};

int main() {
    LRUCache cache(2);
    
    cache.put(1, 1);
    cache.put(2, 2);
    cache.print();  // (2,2) (1,1)
    
    cout << "get(1): " << cache.get(1) << endl;  // 1
    cache.print();  // (1,1) (2,2)
    
    cache.put(3, 3);  // 删除 key 2
    cache.print();  // (3,3) (1,1)
    
    cout << "get(2): " << cache.get(2) << endl;  // -1
    
    return 0;
}

7. 常用 API 总结

操作方法时间复杂度
头部操作push_front(), emplace_front()O(1)
pop_front()O(1)
front()O(1)
尾部操作push_back(), emplace_back()O(1)
pop_back()O(1)
back()O(1)
插入删除insert()O(1)
erase()O(1)
remove()O(n)
容量size()O(1)
empty()O(1)
特殊操作splice()O(1)
sort()O(n log n)
reverse()O(n)
merge()O(n)

总结

  • std::list 是 C++ 的标准双链表实现
  • 优点:任意位置插入删除 O(1),不需要连续内存
  • 缺点:不支持随机访问,访问元素需要遍历
  • 适用场景:频繁的插入删除,LRU Cache,需要稳定迭代器

在刷题中,std::list 常用于需要频繁在中间插入删除的场景,或者实现 LRU Cache 等数据结构。