基础数据结构#
数组、字符串、链表、栈、双端队列、单调队列、优先队列、哈希表、哈希集合、有序表的刷题模板。
数据结构#
数组#
虽然 C++ 和 Java 中都有静态数组,但是静态数组不太灵活。我们在刷题时,首要目的是把题解出来,因此,我们统一使用动态数组。同时,也方便我们调用各种库函数。
如果题目给的是静态数组,我们可以先将其转换为动态数组,以节省思考时间。
vector<int> arr(sz, val); // sz 和 val 可选
vector<vector<int>> dp(m, vector<int>(n)); // m * n 的数组
ArrayList<Integer> v = new ArrayList<>();
字符串#
string str = "ABCDEFG";
str += 'B'; // 末尾追加
str.substr(start, len); // 截取子串
str.find("CD"); // 查找子串,返回下标,找不到返回 string::npos
str.rfind('C'); // 从后往前查找
str.compare(other); // 比较(<0 / 0 / >0)
stoi(str);
stoll(str); // 字符串转数字(数字转字符串用 to_string(n))
sort(str.begin(), str.end()); // 字符串排序
reverse(str.begin(), str.end()); // 反转
初始化字符串
String str = "hello world";
StringBuilder sb = new StringBuilder(str);
类型转换
sb.toString();
追加
sb.append(true);
sb.insert(i, "abc");
删除
sb.deleteCharAt(i);
sb.delete(i,j);
修改
sb.setCharAt(i, 'a');
sb.replace(i, j, "abc");
查询
sb.indexOf("abc");
sb.lastIndexOf("abc");
判空
s1.isEmpty();
截取字符串
str.substring(i);
str.substring(i,j);
拼接
str.concat("abc");
链表#
哨兵节点
哨兵节点(dummy):在头节点前加一个虚拟节点,避免单独处理头节点,使头节点的插入/删除与普通节点一致
ListNode dummy = new ListNode(-1, head); // 哨兵节点,指向原头节点
ListNode cur = dummy; // cur 负责向后移动,dummy 不动
// ... 操作链表的 cur.next ...
return dummy.next; // 返回真正的头节点
初始化
// 双向链表
LinkedList<Integer> v = new LinkedList<>();
遍历
for (ListNode cur = head; cur != null; cur = cur.next) {
// 访问 cur.val
}
栈#
后进先出(LIFO),用于括号匹配、函数调用栈、单调栈、撤销操作等。
stack<int> stk;
stk.push(1); // 入栈
stk.pop(); // 出栈(不返回值)
stk.top(); // 查看栈顶
stk.empty();
stk.size();
// Java 官方推荐用 Deque 实现栈(Stack 类已不推荐)
Deque<Integer> stk = new ArrayDeque<>();
stk.push(1); // 入栈
stk.pop(); // 出栈
stk.peek(); // 查看栈顶
stk.isEmpty();
stk.size();
双端队列#
双端队列(deque)支持在两端插入与删除,操作均为 \(O(1)\)。用于需要从两头维护数据的场景(滑动窗口、单调队列、回文匹配等)。
deque<int> dq;
dq.push_back(1); // 尾部入队
dq.push_front(2); // 头部入队
dq.pop_back(); // 尾部出队
dq.pop_front(); // 头部出队
dq.front(); // 查看队首
dq.back(); // 查看队尾
dq.empty();
dq.size();
// Deque 接口,LinkedList 和 ArrayDeque 均可实现
Deque<Integer> dq = new ArrayDeque<>();
dq.addLast(1); // 尾部入队
dq.addFirst(2); // 头部入队
dq.removeLast(); // 尾部出队
dq.removeFirst(); // 头部出队
dq.getFirst(); // 查看队首
dq.getLast(); // 查看队尾
dq.isEmpty();
dq.size();
单调队列#
原理:单调队列 = 滑动窗口 + 单调栈。维护队列内元素的单调性(求窗口最大值时维护从大到小,队首即当前窗口最大值),入队时弹出队尾所有”不如当前元素”的元素。每个元素最多入队、出队各一次,整体复杂度 \(O(n)\)。
Note
队列中存下标而非值:一是方便判断队首是否已滑出窗口(下标 < 左边界即弹出);二是取值时通过下标访问原数组。
滑动窗口最大值模板(模板题 LC239):
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> ans;
deque<int> dq; // 存下标,维护从大到小
for (int i = 0; i < nums.size(); i++) {
// 队尾元素 ≤ 当前值时,它不可能再成为窗口最大值,弹出
while (!dq.empty() && nums[dq.back()] <= nums[i])
dq.pop_back();
dq.push_back(i);
if (dq.front() <= i - k) // 队首滑出窗口左边界
dq.pop_front();
if (i + 1 >= k) // 窗口已形成
ans.push_back(nums[dq.front()]);
}
return ans;
}
// 单调队列,要始终维持队列递增或递减的状态。
// 递增(减)队列的队头是最小(大)值。
int[] maxSlidingWindow(int[] arr, int sz) {
int[] ans = new int[arr.length - sz + 1];
Deque<Integer> deque = new LinkedList<>();
// r 表示滑动窗口右边界
for (int r = 0; r < arr.length; r++) {
// 移除队尾比当前值小的元素的索引
while (!deque.isEmpty()
&& arr[r] >= arr[deque.peekLast()])
deque.removeLast();
deque.addLast(r); // 存储元素下标
int l = r - sz + 1; // 窗口左边界
if (deque.peekFirst() < l)// 超出左边界
deque.removeFirst();
if (r + 1 >= sz) // 若已经形成窗口
ans[l] = arr[deque.peekFirst()];
}
return ans;
}
Note
求窗口最小值时只需把弹出条件改成 >=(维护从大到小改为从小到大),其余逻辑不变。
单调栈#
原理:维护栈内元素单调(递增或递减)。常用于求每个元素左右两侧最近的更大/更小元素,进而解决接雨水、柱状图最大矩形、贡献法计数等问题。每个元素最多入栈、出栈各一次,整体复杂度 \(O(n)\)。
技巧:在栈底预先压入一个边界下标(如 -1 或 n)作为哨兵,保证循环中栈永不为空,简化边界判断。
应用 1:左右两侧最近更小元素(以 \(a[i]\) 为最小值的区间范围):
// left[i] = 左侧第一个 < a[i] 的下标,不存在为 -1
// right[i] = 右侧第一个 < a[i] 的下标,不存在为 n
vector<int> a(n);
vector<int> left(n), right(n);
// 从左到右求 left:维护严格递增栈,弹出 >= 当前值的下标
stack<int> st;
st.push(-1); // 哨兵
for (int i = 0; i < n; i++) {
while (st.top() != -1 && a[st.top()] >= a[i])
st.pop();
left[i] = st.top();
st.push(i);
}
// 从右到左求 right:同理
stack<int> st2;
st2.push(n); // 哨兵
for (int i = n - 1; i >= 0; i--) {
while (st2.top() != n && a[st2.top()] >= a[i])
st2.pop();
right[i] = st2.top();
st2.push(i);
}
// 此时 a[i] 是区间 [left[i]+1, right[i]-1] 内的最小值,
// 以 a[i] 为最小值的子数组个数 = (i-left[i]) * (right[i]-i)
// 单调栈(存下标),思路同 C++ 版
int n = a.length;
int[] left = new int[n], right = new int[n];
Deque<Integer> st = new ArrayDeque<>();
st.push(-1); // 哨兵
for (int i = 0; i < n; i++) {
while (st.peek() != -1 && a[st.peek()] >= a[i])
st.pop();
left[i] = st.peek();
st.push(i);
}
应用 2:下一个更大元素(LC496 模板):
// 从右往左维护递减栈:栈中保留"右边比当前元素大的候选"
vector<int> nextGreater(vector<int>& nums) {
int n = nums.size();
vector<int> ans(n, -1);
stack<int> st;
for (int i = n - 1; i >= 0; i--) {
// 弹出所有 <= 当前值的(它们不可能成为更左边元素的下一个更大元素)
while (!st.empty() && st.top() <= nums[i])
st.pop();
ans[i] = st.empty() ? -1 : st.top();
st.push(nums[i]);
}
return ans;
}
int[] nextGreater(int[] nums) {
int n = nums.length;
int[] ans = new int[n];
Arrays.fill(ans, -1);
Deque<Integer> st = new ArrayDeque<>();
for (int i = n - 1; i >= 0; i--) {
while (!st.isEmpty() && st.peek() <= nums[i])
st.pop();
ans[i] = st.isEmpty() ? -1 : st.peek();
st.push(nums[i]);
}
return ans;
}
Note
求”左侧/更小”时把比较符号反过来即可;
若要处理相等元素,把弹出条件中的
>=改为>(或反之),可控制左右两侧的严格/非严格边界,避免重复计数。
优先队列#
优先队列支持以下几种操作:
push向优先队列中插入一个元素。pop删除并返回优先队列中优先级最高的元素。top查看优先队列中优先级最高的元素。empty判断优先队列是否为空。size返回优先队列中的元素数量。
优先队列的声明方式:
priority_queue<int> PQ;
优先队列默认是降序排列的,也就是最大值在堆顶。如果想创建一个小根堆,声明方式如下:
priority_queue<int, vector<int>, greater<>> PQ;
在很多情况下,我们会想在优先队列中存储自定义的数据类型,并按照某个属性排列。这种情况下,我们需要重载运算符来达到要求:
// 假设:我们想将 hashMap 中的 {key, value} 对存储到 priority queue 中
unordered_map<int, int> hashMap;
for (int num : nums) {
hashMap[num]++;
}
// 创建一个根据哈希表的值升序排列的小根堆,并且只保存 k 个元素
struct HashEntry {
int key;
int value;
// 重载 > 运算符
bool operator>(const HashEntry& other) const { return value > other.value; }
};
priority_queue<HashEntry, vector<HashEntry>, greater<HashEntry>> minHeap;
for (const auto& entry : hashMap) {
minHeap.push({entry.first, entry.second});
if (minHeap.size() > k) {
minHeap.pop();
}
}
// 取出小根堆中的元素
vector<int> ans;
while (!minHeap.empty()) {
ans.push_back(minHeap.top().key);
minHeap.pop();
}
// 默认的初始化方法
PriorityQueue<Integer> pq = new PriorityQueue<>();
// 自定义排序规则
PriorityQueue<Integer> pq = new PriorityQueue<>(
new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2; // (升序) 谁小谁优先
}
});
哈希表#
初始化
unordered_map<string, int> map;
插入
map["hello"] = 1; // 最简单,常用
map.insert({"hello", 1});
map.emplace("hello", 1);
map.insert(std::make_pair("hello", 1));
修改值
map["hello"] = 2;
查找
if (map.find("hello") != map.end()) {
cout << "Key exists." << endl;
}
if (map.count("hello")) {
cout << "Key exists." << endl;
}
int val = map.at("hello"); // 抛出异常如果键不存在
int valOrDefault = map["hello"]; // 如果键不存在,将插入默认值 0 并返回 0
删除一个键
map.erase("hello");
清空哈希表
map.clear();
获取哈希表的大小
size_t size = map.size();
遍历
for (const auto& entry : map) {
const string& key = entry.first;
int value = entry.second;
}
初始化
Map<Integer, String> map = new HashMap<>();
常用操作
map.put(1, "a"); // 插入 / 覆盖
map.get(1); // 取值,键不存在返回 null
map.getOrDefault(1, "默认"); // 键不存在时返回默认值
map.containsKey(1); // 判断键是否存在
map.remove(1); // 删除键
map.size();
map.isEmpty();
遍历
Map<String, String> map = new HashMap<>();
for (Map.Entry<String, String> entry : map.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
}
哈希集合#
初始化
unordered_set<string> set;
插入
set.insert("hello");
删除一个键
set.erase("hello");
查找
if (set.find("hello") != set.end()) {
cout << "Key exists." << endl;
}
// 或者使用 count 方法
if (set.count("hello")) {
cout << "Key exists." << endl;
}
清空哈希集合
set.clear();
获取哈希集合的大小
size_t size = set.size();
遍历
for (const auto& element : set) {
cout << element << endl;
}
初始化
Set<String> set = new HashSet<>();
常用操作
set.add("hello"); // 插入,已存在则忽略
set.remove("hello"); // 删除
set.contains("hello"); // 查找
set.size();
set.isEmpty();
遍历
for (String element : set) {
System.out.println(element);
}
有序表#
有序集合/映射基于平衡树(红黑树)实现,按键有序,支持 \(O(\log n)\) 的插入、删除、查找,以及”找前驱/后继、第 k 小”等有序操作。用于需要动态维护有序数据的场景(TopK、区间统计)。
// 有序集合(元素不重复,自动升序)
TreeSet<Integer> ts = new TreeSet<>();
ts.add(3);
ts.add(1);
ts.first(); // 最小值(1)
ts.last(); // 最大值(3)
ts.ceiling(2); // >= 2 的最小元素(3)
ts.floor(2); // <= 2 的最大元素(1)
ts.contains(1);
ts.remove(1);
// 有序映射(按键升序)
TreeMap<String, Integer> tm = new TreeMap<>();
tm.put("b", 2);
tm.put("a", 1);
tm.firstKey(); // 最小的键("a")
tm.lastKey(); // 最大的键("b")
tm.ceilingKey("ab"); // >= "ab" 的最小键("b")
Note
C++ 对应为 set / map(有序),与 unordered_set / unordered_map(哈希、无序)区别开:
前者按键有序、支持有序操作但为 \(O(\log n)\);后者 \(O(1)\) 均摊但无序。