力求用最短的代码实现算法模板——来自Dryad和carl
目录
模板篇
二叉树dfs
- 前序遍历
class Solution {
public:
vector<int> ans;
void preorder(TreeNode* root){
if(root == NULL) return;
ans.push_back(root->val);
preorder(root->left);
preorder(root->right);
}
vector<int> preorderTraversal(TreeNode* root) {
preorder(root);
return ans;
}
};
- 中序遍历
class Solution {
public:
vector<int> ans;
void inorder(TreeNode* root){
if(root == NULL) return;
inorder(root->left);
ans.push_back(root->val);
inorder(root->right);
}
vector<int> inorderTraversal(TreeNode* root) {
inorder(root);
return ans;
}
};
- 后序遍历
class Solution {
public:
vector<int> ans;
void postorder(TreeNode* root){
if(root == NULL) return;
postorder(root->left);
postorder(root->right);
ans.push_back(root->val);
}
vector<int> postorderTraversal(TreeNode* root) {
postorder(root);
return ans;
}
};
二分查找模板
class Solution {
public:
int searchInsert(vector<int>& nums, int target)
{
int n = nums.size();
int left = 0;
int right = n; // 我们定义target在左闭右开的区间里,[left, right)
while (left < right) // 因为left == right的时候,在[left, right) 是无效的空间
{
int middle = left + ((right - left) >> 1);
if (nums[middle] > target) {
right = middle; // target在左区间,因为是左闭右开的区间,nums[middle]一定不是我们的目标值,所以right = middle,在[left, middle)中继续寻找目标值。
} else if (nums[middle] < target) {
left = middle + 1; // target在右区间,在 [middle+1, right)中
} else { // nums[middle] == target
return middle; // 数组中找到目标值的情况,直接返回下标
}
}
return right;
}
};
比如要求平方根问题:
class Solution {
public:
int mySqrt(int x) {
int bottom = 0, upper = x, ans = -1;
while (bottom <= upper) {
int mid = bottom + (upper - bottom) / 2;
if ((long long) mid * mid <= x) {
ans = mid;
bottom = mid + 1;
} else {
upper = mid - 1;
}
}
return ans;
}
};
并查集
方便记忆:
int Find(int x) {
// 查找根结点,也就是找目前这棵树上,最上面的“帮主”,通过递归来实现。
int b = x;
while (F[x] != x) {
x = F[x];
}
// 路径压缩的实现
// 将路径上的每个点指向根结点x,将帮主的直接查询权赋予给该点
while (F[b] != x) {
int p = F[b];
F[b] = x;
b = p;
}
return x;
}
合并的技巧
int i = b;
int j = m;
int to = b;
// 将两个子数组进行合并, 注意下面是一个很重要的模板
// 这里的判断条,是只要两个子数组中还有元素
while (i < m || j < e) {
// 如果右子数组没有元素 或 左子数组开头的元素小于右子数组开头的元素
// 那么取走左子数组开头的元素
// 考点:a[i] <= a[j]这样可以保证合并排序是稳定的,不要写错!
if (j >= e || i < m && a[i] <= a[j]) {
t[to++] = a[i++];
} else {
// 否则就是取右子数组开头的元素
t[to++] = a[j++];
}
}
快速排序
快速排序的特点:
- 根节点的处理,需要执行“三路切分”操作,将一个数组切分为三段
- 左右子区间是由切分动态生成的,并不像二叉树由指针固定。
void qsort(vector<int> A, int b, int e) {
// 像二叉树一样,如果空树/只有一个结点,那么不需要再递归了
// 如果给定的区间段为空,或者只有一个结点。
if (b >= e || b + 1 >= e) {
return;
}
// 取数组中间的元素作为x
int m = b + ((e - b) >> 1);
int x = A[m];
// 三路切分,这部分代码在例 3详细介绍!
int l = b, i = b, r = e - 1;
while (i <= r) {
if (A[i] < x) {
swap(A, l++, i++);
} else if (A[i] == x) {
i++;
} else {
swap(A, r--, i);
}
}
// 像二叉树的前序遍历一样,分别遍历左子树与右子树。
qsort(A, b, l);
qsort(A, i, e);
}
lowerbound和upperbound
lowerbound用于查找有序数组中,最左边出现的元素(闭)
upperbound用于查找有序数组中,最右边出现的元素(开)
当给定数组 A[] = {1,2,2,2,3},运行 lowerBound(A, 2) 返回下标 1,而运行 upperBound(A,2) 却返回下标 4,但此时 A[4] = 3。
注意跟二分法的区别
- 用途不一样,边界问题是当排序数组有重复元素时,找到重复元素的上下边界。二分法是查询元素的一种方式,并不能准确的确定重复元素的具体位置,而且也往往没有重复元素。
- 算法不一样,边界问题返回的是left位置和right位置,对找左边界来说,当遇到A[m] < target的时候,l需要不停自增,当等于的时候,此时的l已经增到指定位置;对找右边界来说,当遇到A[m] <= target的时候,l需要自增,为什么呢,这是因为upperbound找到的都是" 更右边 "的位置,因此要把所有小于连同等于的元素都略过,才能遇到最右边的r,即右边界。对二分法来说,直接三个if判断,判断当前middle位置是否已经是target,如果不是,要么更新left,要么right,同时更新middle。
int lower_bound(vector<int> A, int target)
{
int l = 0, r = A.size();
while (l < r)
{
int m = l + ((r - l) >> 1);
if (A[m] < target) {
l = m + 1;
} else {
r = m;
}
}
return l;
}
int upper_bound(vector<int> A, int target)
{
int l = 0, r = A.size();
while (l < r)
{
int m = l + ((r - l) >> 1);
if (A[m] <= target) {
l = m + 1;
} else {
r = m;
}
}
return l;
}
双指针的区间问题
最长区间
- 两个指针,left指针和right指针,两个指针形成的区间为(left, right]
- left指针要等到条件不满足了,才向右移动
int maxLength(vector<int> A)
{
int N = A.size();
int left = -1;
int ans = 0;
for (int i = 0; i < N; i++)
{
// step1 直接将A[i]加到区间中,形成(left, i]
// step2 将A[i]加入之后,惰性原则
while (check((left, i]))/*TODO 检查区间状态是否满足条件*/) {
++left; // 如果不满足条件,移动左指针
// TODO 修改区间的状态
}
// assert 此时(left, i]必然满足条件
ans = max(ans, i - left);
}
return ans; // 返回最优解
}
定长区间
通常也被称为“滑动窗口算法”
class Solution {
public:
string minWindow(string s, string t) {
int M = s.size();
int N = t.size();
unordered_map<char, int> scount; // 该字典变量保存s[left, right]闭区间中各个元素出现的次数
unordered_map<char, int> tcount; // 该字典变量保存t中元素出现次数。
// left and right means [left, right] of s
// count means the same chars of s[left, right] with t
int left = 0, right = 0, count = 0; // count变量储存s[left, right]闭区间中已经覆盖了t中的多少个元素,如果cnt == t.size()说明该子数组覆盖了t。
int minLen = INT_MAX; // 该变量存储当前满足题目要求的最短子串长度,目的是用substr将res从s中取出来,其更新方法为right - left + 1。如果不是遇到子数组已经满足覆盖,开始收缩的时候,left是不会动的
string res;
// 给tcount字典初始化
for (char c : t)
++tcount[c];
// 开始准备滑动窗口
while (right < M) {
char c = s[right]; // 取出来,才能进行用字典的count方法查询是否存在
scount[c] += 1; // 滑动窗口嘛,所以记录数据就在改变,当考虑的时候,个数+1,后面又有减一的时候
if (tcount.count(c) && scount[c] <= tcount[c]) {
count += 1;
}
while (left <= right && count == N) { // 如果count == t.size() 说明该子数组覆盖了t。
// 如果覆盖了所有t的元素,说明这个区间是符合要求的一个区间。此时不必移动right指针。
if (minLen > right - left + 1) {
minLen = right - left + 1;
res = s.substr(left, minLen);
}
char l = s[left];
scount[l] -= 1;
if (tcount.count(l) && scount[l] < tcount[l])
count -= 1;
// 要移动left指针,直至s[left, right]子字符串不能覆盖t。
++left;
}
++right;
}
return res;
}
};
另一种滑动窗口的实现方法如下,单调队列
单调队列
- 每次入队的时候,,为了保证队列的单调性,还要剔除尾部的元素。直到尾部的元素大于等于入队元素(因为是单调递减队列)。
- 单调队列在出队时,需要给出一个value,如果value与队首相等,才能将这个数出队。
- 性质:
-
- 队首元素就是最大值
-
- 入队和出队的组合,可以在O(1)时间得到某个区间上的最大值
-
class MyQueue {
public:
deque<int> que;
void pop(int value){
// 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。因为很有可能该value已经不存在了,因为push的时候就有删除操作
// 同时pop之前判断队列当前是否为空。
if (!que.empty() && value == que.front()) {
que.pop_front();
}
}
// 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。
// 这样就保持了队列里的数值是单调从大到小的了。
void push(int value){
while(!que.empty() && value > que.back()) {
que.pop_back();
}
que.push_back(value);
}
// 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
int front() {
return que.front();
}
};
所谓的滑动窗口的最大值,其实并不是使用滑动窗口实现的!!!因为滑动窗口有伸张和收缩两个步骤,而本地的题眼,其实是最大二字。
单调栈
单调栈就是指栈中的元素必须是按照升序排列的栈,或者是降序排列的栈。
一般此类题目用于解决:数组中元素右边第一个比元素自身小的元素的位置。
单调栈的性质
-
当单调递增栈中存放数组下标 i, j, k 时,其中 (i, k] 中的元素 > A[j];
-
当单调递增栈中存放数组下标 i, j,并且当 A[k] 入栈,会把栈顶元素 A[j]“削”出栈时,其中 (j, k) 元素 > A[j]。
用来求解最大矩形
单调栈模板
单调栈有别于单调队列,其往往在“最大矩形”一类题目中出现,
int largestRectangleArea(vector<int>& heights) {
stack<int> st;
heights.insert(heights.begin(), 0); // 数组头部加入元素0
heights.push_back(0); // 数组尾部加入元素0
st.push(0);
int result = 0;
// 第一个元素已经入栈,从下表1开始
for (int i = 1; i < heights.size(); i++) {
// 注意heights[i] 是和heights[st.top()] 比较 ,st.top()是下表
if (heights[i] > heights[st.top()]) {
st.push(i);
} else if (heights[i] == heights[st.top()]) {
st.pop(); // 这个可以加,可以不加,效果一样,思路不同
st.push(i);
} else {
while (heights[i] < heights[st.top()]) { // 注意是while
int mid = st.top();
st.pop();
int left = st.top();
int right = i;
int w = right - left - 1;
int h = heights[mid];
result = max(result, w * h);
}
st.push(i);
}
}
return result;
}
普通栈模板
基本上,所有括号的问题,都要用到栈的操作,其实匹配问题都可以考虑用栈实现,模板如下:
class Solution {
public:
bool isValid(string s) {
stack<int> st;
for (int i = 0; i < s.size(); i++) {
if (s[i] == '(') st.push(')');
else if (s[i] == '{') st.push('}');
else if (s[i] == '[') st.push(']');
// 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false
// 第二种情况:遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false
else if (st.empty() || st.top() != s[i]) return false;
else st.pop(); // st.top() 与 s[i]相等,栈弹出元素
}
// 第一种情况:此时我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true
return st.empty();
}
};
哦,对了,在补充一些面试考点
- C++中stack 是容器么?
答:栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。所以STL中栈往往不被归类为容器,而被归类为容器适配器(container adapter)。
另外可以拓展了解 “堆 ”和“ 栈 ”之间的区别。从数据结构角度来说,是两种不同的数据结构,前者由二叉树构建,后者先入后出;从计算机系统角度来说,栈区由编译器自动分配释放,存放函数的参数值,局部变量的值等;堆区一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收。
堆是由程序员自己申请并指明大小,在c中malloc函数 如p1 = (char *)malloc(10);
栈由系统自动分配,如声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间 - 我们使用的STL中stack是如何实现的?
答:deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。 - stack 提供迭代器来遍历stack空间么?
答:栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。不像是set 或者map 提供迭代器iterator来遍历所有元素。
例题如:
- 有效括号
- 删除相邻相同元素(本质上还是类似括号问题,可以把字符串顺序放到一个栈中,然后如果相同的话 栈就弹出,这样最后栈里剩下的元素都是相邻不相同的元素了)
最短区间
贪心算法
虽然说贪心算法是一种思想,不过还是有一些代码模板需要掌握。比如无重叠区间。
按照右边界排序,就要从左向右遍历,因为右边界越小越好,只要右边界越小,留给下一个区间的空间就越大,所以从左向右遍历,优先选右边界小的。
从左向右记录非交叉区间的个数,最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了
右边界排序之后,局部最优:优先选右边界小的区间,所以从左向右遍历,留给下一个区间的空间大一些,从而尽量避免交叉。全局最优:选取最多的非交叉区间。局部最优可以推出全局最优。
区间,1,2,3,4,5,6都按照右边界排好序。
每次取非交叉区间的时候,都是可右边界最小的来做分割点(这样留给下一个区间的空间就越大),所以第一条分割线就是区间1结束的位置。
接下来就是找大于区间1结束位置的区间,是从区间4开始。那有同学问了为什么不从区间5开始?别忘已经是按照右边界排序的了。
区间4结束之后,在找到区间6,所以一共记录非交叉区间的个数是三个。
总共区间个数为6,减去非交叉区间的个数3。移除区间的最小数量就是3。
class Solution {
public:
// 按照区间右边界排序
static bool cmp (const vector<int>& a, const vector<int>& b) {
return a[1] < b[1];
}
int eraseOverlapIntervals(vector<vector<int>>& intervals) {
if (intervals.size() == 0) return 0;
sort(intervals.begin(), intervals.end(), cmp);
int count = 1; // 记录非交叉区间的个数
int end = intervals[0][1]; // 记录区间分割点
for (int i = 1; i < intervals.size(); i++) {
if (end <= intervals[i][0]) {
end = intervals[i][1];
count++;
}
}
return intervals.size() - count;
}
};
复杂度分析
- 时间复杂度:O(nlogn) ,有一个快排
- 空间复杂度:O(1)
回溯
一个核心,三个条件
- 第 i 个人怎么选?
- 什么样的状态是我们想要的?
- 后面的人还有选项吗?如果后面所有的人都没有选项,就需要返回了。
- 第 i 个人的宝石选项是什么样的?
pop的理解:为了避免弄丢东西,每个人都必须遵守规则:归还的箱子要与借出时一模一样,所以归还箱子的时候,需要把箱子里面的属于自己的东西拿出来。
void backtrace(int[] A,
int i, /*第i个人*/
Box s, /*箱子*/
answer/*存放所有的答案*/) {
final int N = A == null ? 0 : A.length;
if (状态满足要求) {
answer.add(s);
}
if ([i, ...., 后面)的人都没有任何选项了) {
return;
}
for 宝石 in {第i个人当前所有宝石选项} {
s.push(宝石);
backtrace(A, i + 1, s, answer);
s.pop();
}
}
DFS和BFS
DFS 的模板 1
将 opt 设置为每次可以做出的选择
DFS 的模板 2
如果题目中需要求解最优解,那么,DFS 问题就退化为回溯问题,只不过满足约束的条件就变成:“从所有解中找到最优解”
/*
@ start 表示出发点
@ vis 记录每个点是否已经访问
@ path 路径
@ answer 存放最优的答案
*/
void dfs(A, int start, vis, path, answer) {
int N = A == null ? 0 : A.length;
if (状态满足要求) { // 是更好的解吗?
if (s better_than ans) {
ans = s
}
return;
}
for next in {start点的后继点} {
if !vis[next] {
path.append(next);
vis[next] = true;
dfs(A, next, vis, path, answer);
path.pop();
vis[next] = false;
}
}
}
n皇后问题
这类模板解决如n皇后问题
class Solution {
private:
vector<vector<string>> result;
// n 为输入的棋盘大小
// row 是当前递归到***的第几行了
void backtracking(int n, int row, vector<string>& chessboard)
{
if (row == n)
{
result.push_back(chessboard);
return;
}
for (int col = 0; col < n; col++)
{
if (isValid(row, col, chessboard, n)) // 验证合法就可以放
{
chessboard[row][col] = 'Q'; // 放置皇后
backtracking(n, row + 1, chessboard);
chessboard[row][col] = '.'; // 回溯,撤销皇后
}
}
}
bool isValid(int row, int col, vector<string>& chessboard, int n)
{
int count = 0;
// 检查列
for (int i = 0; i < row; i++)
{
if (chessboard[i][col] == 'Q')
{
return false;
}
}
// 检查45度角是否有皇后
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--)
{
if (chessboard[i][j] == 'Q')
return false;
}
// 检查135度角是否有皇后
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++)
{
if (chessboard[i][j] == 'Q')
return false;
}
return true;
}
public:
vector<vector<string>> solveNQueens(int n) {
result.clear();
vector<string> chessboard(n, string(n, '.'));
backtracking(n, 0, chessboard);
return result;
}
};
- 将visited换成了isValid,是因为要满足下一次dfs 的条件
- 将pop换成了清 ’ . ',这是因为要回溯,也就是刚刚考虑了放置的情况,放置之后我们进行了backtracking得到了一系列的结果,然而不放置的情况,又会如何如何,体现在for循环中,将别的地方放置Q。放置方法是独立同分布的,不能互相影响,所以要归位!
- 返回值,之前是为了找到最优解,有个better_than的判断,这儿的result是个private变量,一开始的chessboard全是空,然后用它作为dfs的桥梁,
- vector < string >,比如[".Q…"],可以看成是一个二维数组
DFS 记忆化搜索
将memory数组添加到backtracking函数参数里。
DFS 可以帮助我们找到最优解。不过在搜索的时候,若想知道一些关于“最近/最快/最少”之类问题的答案,往往采用 BFS 更加适合。
单词拆分
给定一个非空字符串s和一个包含非空单词的列表wordDict,判定s是否可以被空格拆分为一个或多个在字典中出现的单词。
使用memory数组保存每次计算的以startIndex起始的计算结果,如果 memory[startIndex]里已经被赋值了,直接用memory[startIndex]的结果。
class Solution {
private:
bool backtracking (const string& s,
const unordered_set<string>& wordSet,
vector<int>& memory,
int startIndex) {
if (startIndex >= s.size()) {
return true;
}
// 如果memory[startIndex]不是初始值了,直接使用memory[startIndex]的结果
if (memory[startIndex] != -1) return memory[startIndex];
for (int i = startIndex; i < s.size(); i++) {
string word = s.substr(startIndex, i - startIndex + 1);
if (wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, memory, i + 1)) {
memory[startIndex] = 1; // 记录以startIndex开始的子串是可以被拆分的
return true;
}
}
memory[startIndex] = 0; // 记录以startIndex开始的子串是不可以被拆分的
return false;
}
public:
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
vector<int> memory(s.size(), -1); // -1 表示初始化状态
return backtracking(s, wordSet, memory, 0);
}
};
复杂度分析
- 时间复杂度:O(2^n),因为每一个单词都有两个状态,切割和不切割
- 空间复杂度:O(n),算法递归系统调用栈的空间
三角形最小路径和
给定一个triangle,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。相邻的结点
指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)
思路一:由于列的选择有限制,所以我们采取自下而上的方式进行dp
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
//很经典的dp题目
int col = triangle.size(); //多少行
vector<int> dp(col+1,0); //根据多少行,我们来确定需要多大的辅助空间 这里将里面的元素都初始化为0
for (int i = col - 1; i >= 0; i--) //从最后一行开始向第一行走 即从下到上
{
for (int j = 0; j <triangle[i].size(); j++) //从第一列向最后一列走, 从左到右
{
dp[j] = min(dp[j], dp[j + 1]) + triangle[i][j]; //先再选择最小的元素 然后再加上要计算的元素
}
}
return dp[0];
}
};
思路二:记忆化搜索
// 其实还是超时了
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
vector<vector<int>> memo(triangle.size(), vector<int>(triangle.size(), 0));
return dfs(triangle, memo, 0, 0);
}
private:
int dfs(vector<vector<int>>& triangle, vector<vector<int>> memo, int i, int j)
{
if (i == triangle.size()) {
return 0;
}
if (memo[i][j] != 0)
{
return memo[i][j];
}
return memo[i][j] = min(dfs(triangle, memo, i+1, j), dfs(triangle, memo, i+1, j+1)) + triangle[i][j];
}
};
其实记忆化搜索,是剪枝的一种方式而已,减少了空间复杂度。我们也可以剪枝来减少时间复杂度,如组合总和问题中, i <= n - (k - path.size()) + 1。
BFS的模板
二叉树的层序遍历模板,需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root){
queue<TreeNode*> que; // que存放的是一个节点
if (root != nullptr) que.push(root);
vector<vector<int>> result;// 返回的结果是一个二维数组,第一个维度代表一层
while(!que.empty())
{
int size = que.size();
vector<int> vec;
// 这里一定要使用固定大小的size,不要使用que.size(),因为que.size()是不断变化的。
for (int i = 0; i < size; i++)
{
TreeNode* node = que.front();
// 先将队列中当前的节点弹出,但该节点的左右子节点会加入队列中去。上一轮while循环push_back进去的另一个节点,在下一次for循环也弹出,同时也加入该节点的左右子节点,于是,便实现了层序遍历。
que.pop();
// 由于que队列的首位元素已经赋值给了node,所以弹出并不影响取val值。其实这儿vec.push_back(que.front->val)也行吧。即将for循环改成如下形式
vec.push_back(node->val);
// 如果子节点存在的话
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
/*
for (int i = 0; i < size; i++)
{
// TreeNode* node = que.front();
// 先将队列中当前的节点弹出,但该节点的左右子节点会加入队列中去。上一轮while循环push_back进去的另一个节点,在下一次for循环也弹出,同时也加入该节点的左右子节点,于是,便实现了层序遍历。
// 由于que队列的首位元素已经赋值给了node,所以弹出并不影响取val值。其实这儿vec.push_back(que.front->val)也行吧。
vec.push_back(que.front()->val);
// 如果子节点存在的话
if (que.front()->left) que.push(que.front()->left);
if (que.front()->right) que.push(que.front()->right);
que.pop();
}
*/
result.push_back(vec);
}
return result;
}
};
BFS(s) { // s表示出发点
queue<TreeNode*> que;
que.push(s), visited[s] = true // 标记s为已访问
while (!que.empty()) {
auto cur = que.front();
que.pop(); // 拿到当前结点
for next in getNext(u) { // 拿到u的后继next,方法比如说当前位置增加四个方向(如下例题),或者像是二叉树的方式直接->left或者->right。在当前层级的循环过程中,处理判断是否满足条件,将走过的位置visit置一,并同时将下一层级的元素push到队列中去,体现了BFS的思想。
if (!visited[next]) { // 如果next还没有访问过
q.push(next);
visited[next] = true;
}
}
}
}
最短路径
给定一个矩阵,0表示可通行的路径,1表示墙,返回从左上角走到右下角的最短路径。如果不存在返回-1。
class Solution {
public:
int shortestPathBinaryMatrix(vector<vector<int>>& A)
{
const int R = A.size();
const int C = A[0].size();
int dir[][2] = {
{0, 1}, {0, -1}, {1, 0}, {-1, 0},
{-1, -1}, {-1, 1}, {1, -1}, {1,1},
};
// 首先处理特殊情况,为空,或者只有一个格子
if (R <= 1 || C <= 1) {
return min(R, C);
}
// 首先处理起始点,如果起点和终点都不可以走
if (A[0][0] == 1 || A[R - 1][C - 1] == 1) {
return -1;
}
queue<pair<int, int>> Q;
Q.push({0, 0});
A[0][0] = 1;
int ans = 0;
while (!Q.empty())
{
ans++;
// 注意这里类似二叉树层序遍历的做法,取出qSize,可以一层一层的遍历
int QSize = Q.size();
while (Qsize--) {
auto cur = Q.front();
Q.pop();
// 如果已经走到了目的地
if (cur.first == R - 1 && cur.second == C - 1) {
return ans;
}
// BFS搜索,在当前点处,扩展8个方向上的位移,完成两件事,一是将走到的位置置1,也就是visit的作用,二是将新位置push到Q当中,类似层序遍历
for (int d = 0; d < 8; d++)
{
int nr = cur.first + dir[d][0];
int nc = cur.second + dir[d][1];
if (!(nr < 0 || nc < 0 || nr >= R || nc >= C)) {
if (A[nr][nc] != 1)
{
A[nr][nc] = 1;
Q.push({nr, nc});
}
}
}
}
}
return -1;
}
}
动态规划
KMP算法
void kmp(int * next, const string& s)
{
next[0] = -1;
int j = -1;
for (int i = 1; i < s.size(); i++)
{
while(j >= 0 && s[i] != s[j+1]) {
j = next[j];
}
if (s[i] == s[j+1]) {
j++;
}
next[i] = j;
}
}
Dijkstra算法
该算法用于查询最短路径
通过该算法解决了从某个源点到其余各顶点的最短路径问题。
https://blog.csdn.net/fatfairyyy/article/details/107378839
Floyd算法
该算法用于查询最短路径
相比于Dijkstra算法,Floyd算法关注的是多元最短路径问题,即一个是从某一个顶点出发求出其达到其他顶点的最短路径,一个是从每个顶点出发到达其他顶点的最短路径。即如果面临需要求所有顶点至所有所有顶点的最短路径问题,Floyd算法应该是不错的选择。
void ShortestPath_Floyd(MGraph *G) {
for (int i = 0; i < G->numVer)
}
关键路径算法
该算法用于查找最短路径
从源点到汇点具有最大长度的路径叫做关键路径,在关键路径上的活动叫做关键活动
求最晚发生时间数组的值,即为求最早发生时间的逆过程
void CriticalPath(GraphAdjList *G){
TopologicalSort(G); // 调用上述拓扑排序代码
for (int i = 0; i < G->numVertexes; i++)
{
ltv[i] = etv[G->numVertexes - 1];
}
while (!s2.empty()) {
int gettop = s2.top();
s2.pop();
EdgeNode *e;
for (e = G->adjList[].FirstEdge; e != nullptr; e = e->next) {
// 求各顶点事件的最晚发生时间
int k = e->adjvex;
if(ltv[k] - e -> weight < ltv[gettop]){
ltv[gettop] = ltv[k] - e -> weight;
} // 因最晚发生时间是从后向前找(栈后进先出),因此此处为“相减”与“小值”。即求etv的逆运算
}
}
for(int j=0;j<G->numVertexes;j++)/*遍历顶点表上各顶点*/{
for(EdgeNode *e = G->adjList[j].FirstEdge;e != NULL ; e = e->next){
int k = e->adjvex;
int ete = etv[j];/*活动最早开始时间*/
int lte = ltv[k] - e->weight;/*最迟开始时间,减去e->weight是因为k的
下标指向的是边表一号结点,而非顶点表结点*/
if(ete == lte){/*二者相等即在关键路径上*/
cout<<"<v"<<G->adjList[j].data<<",v" <<G->adjList[k].data<<"> length:"<<e->weight<<endl;
}
}
}
}
Kruskal算法
该算法用于代价最小的图连接方式
给定一个图的点集,边集和权重,返回构建最小生成树的代价。
输入:N = 2, conn = [[1, 2, 37], [2, 1, 17], [1, 2, 68]]
输出:17
解释:图中只有两个点 [1, 2],当然是选择最小连接 [2, 1, 17]
利用并查集 + 贪心算法,可以生成一个图的最小生成树,这种方法也被称为 Kruskal 算法。并查集可以用来将两个点进行 Union,不过在并查集的 Union 代码中,并没有权重这一项,那我们该怎么办呢?
在 Union 的时候,就直接根据边的权重来排序,然后再处理,这不就是经典的 Kruskal 算法。
这里我们可以讲一下最小生成树的思路:
首先初始化并查集
将边集按照权重排序
利用边集将不同的两点进行 Union
将不同的集合进行 Union 时需要加上新加入的边的代价(即边的权重)。
/* Kruskal算法求无向图的最小生成树 */
int Kruskal(int n, int m, vector<edge>& E)
{
vector<int> father(n); // 并查集数组
int ans = 0; // 所求边权之和
int NumEdge = 0; // 记录最小生成树边数
for (int i = 0; i < n; i++) // 初始化并查集
father[i] = i;
sort(E.begin(), E.end(), cmp); // 所有边按边权从小到大,贪心算法体现
for (int i = 0; i < m; ++i) // 枚举所有边
{
int faU = findFather(father, E[i].u); // 查询端点u所在集合的根节点
int faV = findFather(father, E[i].v); // 查询端点v所在集合的根节点
if (faU != faV) // 如果不在一个集合中
{ // 合并集合 (相当于把测试边加入到最小生成树)
father[faU] = faV;
ans += E[i].cost;
NumEdge++; // 当前生成树边数+1
if (NumEdge == n - 1) // 边数等于顶点数减一,算法结束。
break;
}
}
if (NumEdge != n - 1) // 无法连通时返回-1
return -1;
else
return ans; // 返回最小生成树边权之和
}
- 采用的数据结构:边集数组
const int maxn = 5005;
struct Edge{
int ew, ev, d;
} e[maxn];
- 采用的查找方式:并查集
并查集作用:查找某个元素的“祖先”,在图问题中可以理解为,查找某个结点和另一个结点是否联通。
查找方式:递归的方式
int findf(int x){
while(x != f[x]) x = f[x] = f[f[x]];
return x;
}
- 初始化,用sort()的比较函数cmp等先略过不谈
1). cnt统计已加入生成树的边数,任意一棵生成树,一定满足e = n - 1,故当有n-1条边加入生成树时,最小生成树已经建成了。
2). ev,ew是用来查找边的两个结点的祖先
3). ans是全局变量,统计边的权值,在最小生成树问题中,ans即对应于构建最小生成树需要的最小权值。
bool Kruscal(){
int cnt = 0, ev, ew;
sort(e+1, e+m+1, cmp);
for (int i = 1; i <= m; i++)
{
ev = findf(e[i].ev); // 这儿的findf不明白可以先跳过,按下不表,一会儿解释
ew = findf(e[i].ew);
if (ev == ew) continue;
f[ew] = ev;
ans += e[i].d;
if (++cnt == n - 1) return true;
}
return false;
}
Prim算法
最小生成树
哈夫曼树
工具篇
我们是在学习算法,不是在练习打字!
vscode模板插件——User snippets
- 打开网站
- 打开vscode
- 填充
注意删除scope,或者指定scope
- 重启生效,输入prefix即可