目录
[1. 三路划分铺垫](#1. 三路划分铺垫)
[2. 三路划分快排](#2. 三路划分快排)
[3. 快速选择算法](#3. 快速选择算法)
[十三、BFS && 队列](#十三、BFS && 队列)
[十四、BFS && FloodFill](#十四、BFS && FloodFill)
[十五、BFS && 最短路问题](#十五、BFS && 最短路问题)
[十七、BFS && 拓扑排序](#十七、BFS && 拓扑排序)
[十八、递归 && 搜索 && 回溯](#十八、递归 && 搜索 && 回溯)
前言:
遇见一个问题,不妨先思考:
- 有没有暴力解法,如果有,再考虑如何在暴力解法上优化
- 要考虑特殊情况,比如是否为空、比如是否越界
- 正难则反
- 数据是否有序,如果有序,不妨考虑双指针和二分
- 数据是否具有单调性,如果有,不妨考虑滑动窗口
- 数据是否具有二段性,如果有,不妨考虑二分算法
- 如果要快速求出某一段连续区间的和,不妨考虑前缀和
- top k 问题,堆排序 or 快速选择算法
(笔者对算法的理解还很浅显,这些只是对笔者自己的提示,不构成建议。)
常见排序算法
常见排序算法的时间复杂度及其稳定性:
- 冒泡排序, O(N^2), 稳定的
- 插入排序, O(N^2), 稳定的
- 选择排序, O(N^2), 不稳定
- 希尔排序, O(N*logN), 不稳定
- 堆排序, O(N*logN), 不稳定
- 快速排序, O(N*logN), 不稳定
- 归并排序, O(N*logN), 稳定
- 计数排序, O(N+K), 稳定的(K 为数据范围)
各排序算法博客链接
一、双指针
- 双指针有多种,比如:同向(滑动窗口) 、异向、快慢
- 当数据有序或者存在单调性的时候,不妨考虑双指针
- 但双指针思想并不仅局限于有序时才能使用
例题:
解题思路:
代码:
cpp// 写法一 class Solution { public: void moveZeroes(vector<int>& nums) { int cur = 0; //遍历数组,左侧为已处理,右侧为未处理 int des = -1; //非0元素的最后一个位置,左侧为非0元素,右侧为0 // [非0,des] [des+1,cur-1] [cur, ] while(cur!=nums.size()) { if(nums[cur]!=0) { ++des; swap(nums[cur],nums[des]); } ++cur; } } }; // 写法二 class Solution { public: void moveZeroes(vector<int>& nums) { for(int cur = 0,dest = -1;cur < nums.size();cur++) { if(nums[cur]) { swap(nums[++dest],nums[cur]); } } } };
解题思路:
代码:
cppclass Solution { public: vector<int> twoSum(vector<int>& price, int target) { int left = 0; int right = price.size()-1; while(left < right) { if(price[left]+price[right]>target) { right--; } else if(price[left]+price[right]<target) { left++; } else return {price[left],price[right]}; } // 不会走到这,这一行是为了避免编译器报错 return {-1,-1}; } };
解题思路:
代码:
cppclass Solution { public: vector<vector<int>> threeSum(vector<int>& nums) { sort(nums.begin(),nums.end());//先排序,方便使用双指针 vector<vector<int>> ret; for(int i = 0; i < nums.size(); ) { if(nums[i] > 0) break; // 如果nums[i]大于0,说明其后全为正数,不可能再找到两数之和为其相反数的数 //[-1,0,1,2,-1,-4] int left = i+1; //(-1 [0,1,2,-1,-4]),left指向新区间首元素,将-1摘出来,后面元素作为新区间,在新区间里找两个和为1的数 int right = nums.size()-1; int target = -nums[i];//开始时为 -1 的相反数 while(left < right) { //这一部分与查找总价格为目标值的两个商品思路大致相同 if(nums[left]+nums[right]>target) { --right; } else if(nums[left]+nums[right]<target) { ++left; } else { //先插入,然后在新区间里继续缩小区间查找,保证不漏 ret.push_back({nums[left],nums[right],nums[i]}); left++,right--; // 去重left和right,要注意避免越界 while(left < right && nums[left]==nums[left - 1])//新区间里,下个值相同就跳过 { ++left; } while(left < right && nums[right]==nums[right + 1])//新区间里,下个值相同就跳过 { --right; } } } // 去重i i++; while(i < nums.size() - 1 && nums[i]==nums[i - 1])//如果下个nums[i]值和之前相同也跳过,也要注意避免越界 { i++; } } return ret; } };
二、滑动窗口
滑动窗口是双指针的一种
满足单调性时,发现两个同向指针都可以做到不回退时,就可以用滑动窗口。
正确性:
利用单调性,规避了很多没有必要的枚举行为,正确性等同于暴力枚举,但时间复杂度一般只有O(n)。
基础步骤:
① 定义 left=0, right=0
② 进窗口,移动其中一个指针,比如移动 right 让数据进窗口。
③ 判断,判断窗口内的数据是否满足条件,是否需要移动 left 让 left 旧数据出窗口。
④ 循环执行②和③,直到
right指针遍历完整个数组 / 字符串。注:
其中还有一步是更新结果
但更新结果的时机是就题论题的,有的题是进窗口时更新结果,有的题是判断时更新结果,有的题是判断加出窗口结束后更新结果
注意:解题步骤并不是最重要的,最重要的是如何能分析出某一题需要使用或可以使用滑动窗口
满足单调性时,发现两个同向指针都可以做到不回退时,就可以用滑动窗口。
例题:
解题思想:找出最长的连续子数组,其中0的个数不超过k个。
cppclass Solution { public: int longestOnes(vector<int>& nums, int k) { int left = 0,right = 0,zero = 0; int ret = 0; while(right < nums.size()) { if(nums[right]==0) { ++zero; // 计算窗口内0的数量 } while(zero > k) // 说明窗口内0的个数不符合要求 { if(nums[left]==0) { --zero; } ++left; } // 到这说明窗口内的数据符合要求 ret = max(ret,right-left+1); // 更新结果 ++right; } return ret; } };
三、二分算法
二分不一定需要有序,只要能找出一种规律使得其具有二段性,即能被分成两段,能淘汰其中一段,就能使用。
- 二分并非只能用在有序数组上,它的核心前提其实是二段性,而非 "有序" 本身。
- 所谓 "二段性",指的是:对于区间
[L, R],存在一个分界点mid,使得区间可以被划分为两个部分,且满足 "前一部分都满足条件 A,后一部分都满足条件 B(A、B 互斥)"。- 只要满足这个性质,不管数组是否完全有序,都可以通过判断
mid的归属,直接淘汰一半区间,从而实现 O (log n) 的查找效率。
基础模板:
1. 基础模板代码
cpp// 朴素二分查找模板(适用于有序数组的精确查找) while (left <= right) { // 写法1:向下取整的mid,等价于 (left + right) / 2 int mid = left + (right - left) / 2; if (/* 条件1:mid位置不满足目标,目标在右侧 */) { left = mid + 1; } else if (/* 条件2:mid位置不满足目标,目标在左侧 */) { right = mid - 1; } else { // 找到目标值,返回结果 return /* 结果值或索引 */; } }
2. 向上取整
mid写法常用于避免死循环的场景:
cpp// 写法2:向上取整的mid,等价于 (left + right + 1) / 2 int mid = left + (right - left + 1) / 2;两种
mid写法的区别(以区间[0,3]为例)
- 向下取整:
(0 + 3) / 2 = 1(取区间偏左的中间值)- 向上取整:
(0 + 3 + 1) / 2 = 2(取区间偏右的中间值)
3. 关键细节说明
- 循环条件
left <= right:表示闭区间[left, right]内还有元素未检查,需要继续循环。- 二段性 :模板的核心逻辑依赖区间的二段划分,通过
mid的判断淘汰一半区间,实现O(log n)的效率。- 溢出问题 :用
left + (right - left) / 2而非(left + right) / 2,是为了避免left + right过大导致整数溢出。
进阶模板:
例题:
34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)
四、前缀和
作用:快速求出数组中某一段连续区间的和(一次前缀和时间复杂度可以达到O(1))
解题过程:
- 先预处理出一个前缀和数组或矩阵
- 根据题意使用前缀和数组或矩阵来解决问题
一维前缀和:
cpp#include <iostream> #include <vector> using namespace std; int main() { //获取数据 int n,m; cin >> n >> m; vector<int> arr(n+1); for(int i = 1;i <= n;i++) cin >> arr[i]; //构建前缀和数组 vector<long long> dp(n+1); for(int i = 1;i <= n;i++) dp[i] = dp[i-1] + arr[i]; //使用前缀和数组 int l,r; while(m--) { cin >> l >> r; cout << dp[r] - dp[l-1] << endl; } return 0; }
二维前缀和:
cpp#include <iostream> #include <vector> using namespace std; int main() { //获取数据 int n = 0,m = 0;//行列 int q = 0;// 查询次数 cin >> n >> m >> q; vector<vector<int>> arr(n+1,vector<int>(m+1));// n+1行,m+1列 for(int i = 1;i <= n;i++) { for(int j = 1;j <= m;j++) { cin >> arr[i][j]; } } //构建前缀和矩阵 vector<vector<long long>> dp(n+1,vector<long long>(m+1));// n+1行,m+1列,且要考虑防溢出 for(int i = 1;i <= n;i++) { for(int j = 1;j <= m;j++) { dp[i][j] = dp[i-1][j] + dp[i][j-1] + arr[i][j] -dp[i-1][j-1]; } } //使用前缀和矩阵 int x1 = 0,y1 = 0,x2 = 0,y2 = 0;//坐标 while(q--) { cin >> x1 >> y1 >> x2 >> y2; cout << dp[x2][y2] -dp[x1-1][y2] -dp[x2][y1-1] + dp[x1-1][y1-1] << endl; } return 0; }
五、位运算
1. 基础位运算
<<左移:0100(4) << 1 = 1000(8)>>右移:0100(4) >> 1 = 0010(2)~取反:~0100各位翻转(符号位一起变)&有 0 就是 0:0100 & 0011 = 0000|有 1 就是 1:0100 | 0011 = 0111^相同 0 不同 1:0100 ^ 0011 = 0111
2. 确定二进制第 x 位是 0 还是 1
- 公式:
(n >> x) & 1- 例子:n = 5(
0101),看第 1 位(0101 >> 1) & 1 = 0010 & 1 = 0→ 第 1 位是 0
3. 把第 x 位改成 1
- 公式:
n = n | (1 << x)- 例子:n = 4(
0100),把第 1 位变 10100 | (1<<1) = 0100 | 0010 = 0110(6)
4. 把第 x 位改成 0
- 公式:
n = n & (~(1 << x))- 例子:n = 6(
0110),把第 1 位清 00110 & ~(0010) = 0110 & 1101 = 0100(4)
5. 位图思想(空间压缩)
例子:用 1 个 int 存 32 个状态
- 上面三条操作都是为位图操作服务的
- 哈希表:存 32 个布尔要
32字节- 位图:1 个 int(
4字节)的 32 位分别记 0/1,省 8 倍空间
6. 提取最右侧的 1(lowbit)
- 公式:
n & -n-n :对n的二进制数进行先取再反加1-n的操作本质是将n的二进制数中最右侧的1的右侧部分全部变为相反+12:00001100-12:11110100- 例子:n = 12,00001100 & 11110100 = 00000100 → 十进制是 4,只保留最右边那个 1
7. 干掉最右侧的 1
- 公式:
n & (n-1)n-1的操作本质上是将n二进制数的最右侧1(包含1本身)的右侧部分全变为相反+12:00001100- (12-1):00001011
- 例子:n = 12 ,00001100
& 00001011 = 1000(8)→ 最右边的 1 直接抹掉- 对应题:位 1 的个数、汉明距离
8. 位运算优先级
- 原则:能加括号就加括号
- 反例:
a & b << c容易算错正确:a & (b << c)强制顺序
9. 异或 ^ 三大运算律
a ^ 0 = a例:5 ^ 0 = 5a ^ a = 0(消消乐)例:5 ^ 5 = 0a ^ b ^ c = a ^ (b ^ c)(结合律)例:1^2^3 = 1^(2^3)对应题:只出现一次的数字
题目链接:
cppclass Solution { public: bool isUnique(string astr) { if(astr.size() > 26) return false;//鸽巢原理 int bitmap = 0; for(auto e : astr) { int tmp = e - 'a';// e 在位图中的位置 //先判断e在位图中是否已经存在 if(((bitmap >> tmp) & 1) == 1) { return false; } //到此说明位图中e还未存在,将e放入位图中,即将该位置置1 bitmap |= (1 << tmp); } return true; } };
六、分治快排
不了解基础快排的朋友可以先移步快速排序,基础快排那篇里未提及的三路划分会在这里做讲解。
这里会分三个部分引入
1. 三路划分铺垫
解题思路:
cppclass Solution { public: void sortColors(vector<int>& nums) { int n = nums.size(); int left = -1, right = n, i = 0; while(i < right) { if(nums[i] == 0) swap(nums[++left], nums[i++]); else if(nums[i] == 1) i++; else swap(nums[--right], nums[i]); } } };
2. 三路划分快排
基础快排在遇到数据中有大量相同元素时,效率会降低,三路划分可以解决这种情况。
解题思路:
与颜色分类核心步骤基本类似。
cppclass Solution { public: vector<int> sortArray(vector<int>& nums) { srand(time(NULL)); // 种随机数种子 qsort(nums, 0, nums.size() - 1); return nums; } //快排 void qsort(vector<int>& nums, int l, int r) { // 递归出口 if(l >= r) { return; } // 将数组分成3块 int key = getRandom(nums, l, r); int i = l, left = l - 1, right = r + 1; // 核心操作 while(i < right) { if(nums[i] < key) swap(nums[++left], nums[i++]); else if(nums[i] == key) i++; else swap(nums[--right], nums[i]); } // 递归处理左右子区间,中间元素都相同,不用处理 // [l, left] [left + 1, right - 1] [right, r] qsort(nums, l, left); qsort(nums, right, r); } // 随机选key int getRandom(vector<int>& nums, int left, int right) { int r = rand(); return nums[r % (right - left + 1) + left]; } };
3. 快速选择算法
快速选择算法并未将数据排序,只是借助三路划分将数据分成三块,然后根据规则快速选择出某一部分数据。
215. 数组中的第K个最大元素 - 力扣(LeetCode)解题思路:
七、分治归并
不了解归并排序的朋友可以先移步归并排序
912. 排序数组 - 力扣(LeetCode)
快排和归并的区别:
- 快排是选定key,将数组根据key分为两部分,之后对子数组不断根据key细分
归并是算出mid,将数组均分为两部分,对左子数组排序,对右子数组排序,循环- 快排类似二叉树的前根遍历,将原数组分块,将左子数组分块,最后将右子数组分块
归并类似二叉树的后根遍历,对左子数组排序,对右子数组排序,最后合并左右子数组
八、链表
(一)链表常用技巧:
1. 画图
核心原则:遇到链表问题,一定要画图
作用:直观 + 形象 + 便于我们理解指针的指向变化和节点的连接关系
2. 引入虚拟"头"结点
作用:
便于处理边界情况(如头节点插入、删除头节点等需要特殊判断的场景)
方便我们对链表进行统一操作,无需针对头节点做额外分支判断
示意:引入一个不存储实际数据的
newHead节点,让其next指向真正的第一个节点,形成newHead → 1 → 2 → 3 → null的结构
3. 不要吝啬空间,大胆去定义变量(比如给链表定义临时节点)
在操作节点时,多定义几个指针变量(如
prev、cur、next等),避免在复杂操作中丢失节点引用原则:不要让链表断开
说明(以双向链表节点插入为例):
典型操作步骤(需要讲究顺序,容易出错导致链表断开):
prev->next->prev = cur(将原后继节点的前驱指向新节点)
cur->next = prev->next(新节点的后继指向原后继)
prev->next = cur(前驱节点的后继指向新节点)
cur->prev = prev(新节点的前驱指向前驱)但如果先定义一个next将必要的节点引用保存起来,再修改指针指向,就不必考虑这些
4. 快慢双指针
应用场景:
判环:快指针每次走两步,慢指针每次走一步,若相遇则存在环
找链表中环的入口:相遇后,将一个指针重置到头部,两指针同速前进,再次相遇点即为环入口
找链表中倒数第 n 个结点:快指针先走 n 步,然后快慢指针同速前进,快指针到达末尾时,慢指针即为目标节点
(二)链表中的常用操作
1. 创建一个新节点
new
- 基础操作:申请节点内存并初始化数据域和指针域
2. 尾插
操作:遍历链表找到最后一个节点(
cur指向尾节点,即cur->next == null),将新节点链接到尾部示意:
cur指向尾节点,尾节点的next指向新节点,新节点的next指向null3. 头插(重点操作)
操作:将新节点插入到链表头部(加了哨兵头结点后,这一步很容易)
特殊应用:逆序链表
- 逐个取出原链表节点,使用头插法插入到新链表中,即可完成链表逆序
例题:
九、哈希表
例题:
面试题 01.02. 判定是否互为字符重排 - 力扣(LeetCode)
十、栈
1047. 删除字符串中的所有相邻重复项 - 力扣(LeetCode)
十一、字符串
十二、优先级队列(堆)
C++ 中的
std::priority_queue是一种容器适配器 ,提供优先队列(堆)的功能,默认是大顶堆 (元素降序排列,最大元素在队首)。它定义在<queue>头文件中,底层默认使用std::vector作为存储容器,也可指定std::deque。特点:
- 容器适配器 :不直接存储元素,而是封装其他容器(如
vector/deque)。- 堆结构 :底层通过堆算法(
std::make_heap、std::push_heap、std::pop_heap)实现优先级管理。- 默认大顶堆:可通过自定义比较函数改为小顶堆或其他排序规则。
常用接口(以std::priority_queue<int>为例)1. 构造函数
构造方式 说明 priority_queue<T>默认构造,大顶堆,底层用 vector<T>。priority_queue<T, Container>指定底层容器(如 deque<T>)。priority_queue<T, Container, Compare>指定比较函数(如 greater<T>实现小顶堆)。priority_queue(InputIterator first, InputIterator last)用迭代器范围初始化。 2. 成员函数
函数 说明 时间复杂度 push(const T& val)插入元素 val到队列(自动调整堆)。O(log n) emplace(Args&&... args)原地构造元素并插入(C++11 起)。 O(log n) pop()移除队首(优先级最高)元素。 O(log n) top()返回队首元素的引用(不删除)。 O(1) empty()判断队列是否为空,返回 bool。O(1) size()返回队列中元素个数。 O(1) swap(priority_queue& other)与另一个队列交换内容。 O(1)
cppclass Solution { public: int lastStoneWeight(vector<int>& stones) { // 1.将所有石头全部放入大根堆 priority_queue<int> pq; for(auto st : stones) { pq.push(st); } // 2.每次取堆顶数据碰撞,将碰撞后的结果再放入堆中 while(pq.size() > 1) { int a = pq.top(); pq.pop(); int b = pq.top(); pq.pop(); if(a > b)// 由于是大根堆,a是大于等于b的 { pq.push(a - b); } } return pq.size() ? pq.top() : 0; } };
cppclass KthLargest { public: priority_queue<int,vector<int>,greater<int>> _heap;// 小根堆 int _k;// 小根堆的大小,主要是给add用 public: KthLargest(int k, vector<int>& nums) { _k = k; for(auto e : nums) { _heap.push(e); if(_heap.size() > _k) { _heap.pop(); } } } int add(int val) { _heap.push(val); if(_heap.size() > _k) { _heap.pop(); } return _heap.top(); } }; /** * Your KthLargest object will be instantiated and called as such: * KthLargest* obj = new KthLargest(k, nums); * int param_1 = obj->add(val); */
十三、BFS && 队列
解题思路:
在BFS的过程中统计每一层的元素及其个数
103. 二叉树的锯齿形层序遍历 - 力扣(LeetCode)
515. 在每个树行中找最大值 - 力扣(LeetCode)
十四、BFS && FloodFill
题目基本特征:让寻找性质相同的联通块,执行某种操作。
题目:
cppclass Solution { typedef pair<int,int> PII; public: vector<vector<int>> floodFill(vector<vector<int>>& image, int sr, int sc, int color) { int value = image[sr][sc]; if(value == color)//处理特殊情况 { return image; } //矩阵长宽 int m = image.size(); int n = image[0].size(); //坐标数组,方便快速遍历 int dx[4] = {0,0,1,-1}; int dy[4] = {1,-1,0,0}; queue<PII> q;// 存符合条件需要改色的位置的坐标 q.push({sr,sc}); while(!q.empty()) { //改色 auto [a,b] = q.front(); image[a][b] = color; q.pop(); //遍历 for(int i = 0;i < 4;i++) { int x = a + dx[i]; int y = b + dy[i]; if(x >= 0 && x < m && y >= 0 && y < n && image[x][y] == value) { q.push({x,y}); } } } return image; } };
cppclass Solution { public: //坐标数组 int dx[4] = {0,0,1,-1}; int dy[4] = {1,-1,0,0}; //bool类型的数组,用于标记岛屿是否被遍历过 bool vis[301][301]; public: int numIslands(vector<vector<char>>& grid) { int ret = 0; int m = grid.size();//矩阵高 int n = grid[0].size();//矩阵宽 for(int i = 0;i < m;i++) { for(int j = 0;j < n;j++) { if(grid[i][j]== '1' && !vis[i][j]) { ret++; bfs(grid,i,j,m,n); } } } return ret; } void bfs(vector<vector<char>>& _grid,int i,int j,int m,int n) { queue<pair<int,int>> q;//存岛屿坐标 q.push({i,j}); while(!q.empty()) { auto [a,b] = q.front(); q.pop(); for(int k = 0;k < 4;k++) { int x = a + dx[k]; int y = b + dy[k]; if(x >= 0 && x < m && y >= 0 && y < n && _grid[x][y] == '1' && !vis[x][y]) { q.push({x,y}); vis[x][y] = true; } } } } };
十五、BFS && 最短路问题
例题:
1926. 迷宫中离入口最近的出口 - 力扣(LeetCode)
十六、多源BFS问题
例题:
解题思路:
十七、BFS && 拓扑排序
拓扑排序的前提条件:
图必须是有向无环图(DAG)。如果图中有环,则无法进行拓扑排序。
常见概念:
1.有向无环图(DAG图)
- 有向:边是有方向的
- 无环:选择一个起点出发,顺着箭头走,不能回到起点
- 出度:有多少条边是从该节点出发(比如:1号的出度为2,2号的出度为1)
- 入度:有多少条边指向该节点(比如:1号的入度为0,2号的入度为2)
2. AOV 网(顶点活动图)
AOV 网是一种用图来描述工程或项目活动之间依赖关系的模型:
顶点(Vertex) :表示一个活动(或任务)。
有向边(Edge) :表示活动之间的先后顺序 或依赖关系。
例如:若存在边 A → B,则表示"活动 A 必须在活动 B 之前完成",即 B 依赖于 A。
3. 什么是拓扑排序?
拓扑排序是对有向无环图(DAG)的节点进行线性排序的一种算法。
目的 :找到做事情的先后顺序 ,确保对于图中的每一条有向边
u → v,节点u在排序结果中总是位于节点v的前面,即事件 v 必须要在事件 u 之后才能执行。结果不唯一 :一个 DAG 可能存在多种合法的拓扑排序序列。
比如:我想打手柄游戏,其步骤可以是:
先连接手柄,再打开游戏,最后开始玩游戏;
也可以是先打开游戏,再连接手柄,最后开始玩游戏
但无论哪种顺序,想开始玩游戏,都必须先执行前面两个动作。
3.1 拓扑排序的思想
遵循"先完成没有前置依赖的任务"这一规则:
取出入度为 0 的节点,将其输出:这些节点没有前置依赖,可以立即执行。
删除与该节点相连的所有边:相当于"完成该任务后,解除后续任务的依赖"。
重复步骤 1 和 2 :直到图中没有节点,或者找不到入度为 0 的节点为止。
注意:
- 如果在某一步发现图中还有节点,但不存在入度为 0 的节点 ,说明图中存在环,无法进行拓扑排序(因为环中每个点的入度至少为1,无法进行上面的第一步)
- 重要应用:判断有向图中是否存在环:如果拓扑排序能成功输出所有节点,则无环;反之则有环。
4. 如何实现拓扑排序
例题:
解题思路:
如何建图?
1. 如何存连接关系,即如何存边
邻接矩阵 (二维数组)空间复杂度为
O(V²),当节点数多时非常浪费空间。邻接表 (链表/动态数组)空间复杂度为
O(V + E),只存实际存在的边,空间效率高。结论:绝大多数情况下,直接使用「邻接表」即可,无需纠结稠密稀疏。
邻接表的两种代码实现方式:
方式 1:使用
vector<vector<Integer>>(有局限)方式 2:使用
unordered_map<Integer, List<Integer>>(更万能一些)2. 如何统计入度
- 拓扑排序等算法需要知道每个节点有多少条边"进入"它(入度)。
- 方法:使用一个 int 数组直接记录
十八、递归 && 搜索 && 回溯
感谢阅读,本文如有错漏之处,烦请斧正。





























































































