力扣K神图解算法数据结构点这里
四、搜索与回溯算法
- DFS,本质是递归
- 递推参数
- 终止条件
- 递推工作
- 回溯
- BFS,本质是队列
queue<int> que; que.push(第一个参数); while(!que.empty()) { auto tmp = que.front(); que.pop(); if(终止条件) continue; //进行递推工作 que.push(新的参数); }
- 树的递归调用
- 某函数既然可以递归调用root,那么也一定可以调用root->left和root->right,有时候只需要把任务分解成三部分即可,root节点,递归调用root->left,递归调用root->right
- 树的遍历方式总体分为两类,DFS和BFS
- DFS,前序,中序,后序,其中中序是递增序列
- BFS,层序
- 剑指12,矩阵中的路径
//时间O(mn3^k),空间O(k) //标准dfs+剪枝,值得学习 class Solution { public: bool exist(vector<vector<char>>& board, string word) { //每个位置都要作为起点尝试一次 for(int i=0;i<board.size();++i) { for(int j=0;j<board[0].size();++j) { if(dfs(i,j,0,board,word)) return true; } } return false; } private: bool dfs(int i,int j,int k,vector<vector<char>>& board, string &word) { //如果越界或者字符不匹配 if(i < 0 || i >= board.size() || j < 0 || j >= board[0].size() || board[i][j] != word[k]) return false; //执行到这步默认字符匹配,且是最后一个字符 if(k == word.size()-1) return true; //递推工作,修改值 board[i][j] = '\0'; //如果有一条路径成功就返回 auto res = dfs(i,j+1,k+1,board,word) || dfs(i,j-1,k+1,board,word) || dfs(i+1,j,k+1,board,word) || dfs(i-1,j,k+1,board,word); //回溯,恢复原状 board[i][j] = word[k]; //返回,很重要 return res; } };
- 剑指13,机器人的运动范围
//时间O(mn),空间O(mn) //标准dfs+剪枝,使用二维数组记录已访问过的位置 //注意,从左上到右下,只需关注右和下两个方向 class Solution { public: int movingCount(int m, int n, int k) { vector<vector<int>> vec(m,vector<int>(n)); return dfs(m,n,k,0,0,vec); } private: int dfs(int m, int n, int k,int i, int j, vector<vector<int>> &vec) { //如果越界,数位之和大于k,已访问过 if(i >= m || j >= n || digitSum(i)+digitSum(j) > k || vec[i][j]) return 0; //标记 vec[i][j] = 1; //递推 return 1 + dfs(m,n,k,i,j+1,vec) + dfs(m,n,k,i+1,j,vec); } //数位之和 int digitSum(int num) { int res = 0; int x; while(num) { x = num % 10; num /= 10; res += x; } return res; } }; //时间O(mn),空间O(mn) //标准bfs+剪枝,使用二维数组记录已访问过的位置 //熟记bfs套路 class Solution { public: int movingCount(int m, int n, int k) { queue<pair<int,int>> que; que.push({0,0}); int res = 0; vector<vector<int>> vec(m,vector<int>(n)); while(!que.empty()) { auto temp = que.front(); que.pop(); if(temp.first >= m || temp.second >= n || digitSum(temp.first)+digitSum(temp.second) > k || vec[temp.first][temp.second]) continue; ++res; vec[temp.first][temp.second] = 1; que.push({temp.first,temp.second+1}); que.push({temp.first+1,temp.second}); } return res; } private: //数位之和 int digitSum(int num) { int res = 0; int x; while(num) { x = num % 10; num /= 10; res += x; } return res; } };
- 剑指26,树的子结构
//时间O(mn),空间O(m) //总共分为两步, //第一步,先序遍历node1,因为node2可能是node1中以任意节点开始的,isSubStructure //第二步,查询以node1为根节点的树和以node2为根节点的树是否一致,dfs class Solution { public: bool isSubStructure(TreeNode* node1, TreeNode* node2) { if(!node1 || !node2) return false; return dfs(node1,node2) || isSubStructure(node1->left,node2) || isSubStructure(node1->right,node2); } private: bool dfs(TreeNode* node1, TreeNode* node2) { if(!node1 && node2 || (node1 && node2 && node1->val != node2->val)) return false; if(!node1 && !node2 || node1 && !node2) return true; return dfs(node1->left,node2->left) && dfs(node1->right,node2->right); } };
- 剑指27,二叉树的镜像
//时间O(n),空间O(n) //无论是写在一个函数中,还是再使用一个recur函数都是可以的 //首先要明白这整个函数是干嘛的,调用它的左子树,右子树会起到什么样的效果 class Solution { public: TreeNode* mirrorTree(TreeNode* root) { if(!root) return nullptr; auto p = root->right; root->right = mirrorTree(root->left); root->left = mirrorTree(p); return root; } };
- 剑指28, 对称的二叉树
//时间O(n),空间O(n) //recur函数好难想出来 class Solution { public: bool isSymmetric(TreeNode* root) { if(!root) return true; return recur(root->left,root->right); } private: //该函数用来判断以root1和root2为根节点的两棵子树是否对称 bool recur(TreeNode* root1,TreeNode* root2) { if(!root1 && !root2) return true; if(!root1 && root2 || root1 && !root2 || root1->val != root2->val) return false; return recur(root1->left,root2->right) && recur(root1->right,root2->left); } };
- 剑指32,从上到下打印二叉树1
//时间O(n),空间O(n) //bfs基础题目 class Solution { public: vector<int> levelOrder(TreeNode* root) { if(!root) return {}; vector<int> res; queue<TreeNode*> que; que.push(root); while(!que.empty()) { auto p = que.front(); que.pop(); if(!p) continue; res.push_back(p->val); que.push(p->left); que.push(p->right); } return res; } };
- 剑指32,从上到下打印二叉树2
//时间O(n),空间O(n)
//基于上一题目,需要加上循环控制,有几个细节需要注意下
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root)
{
vector<vector<int>> res;
if(!root) return res;
queue<TreeNode*> que;
que.push(root);
while(!que.empty())
{
int num = que.size();
vector<int> vec;
for(int i=0;i<num;++i)
{
auto p = que.front();
que.pop();
if(!p) continue;
vec.push_back(p->val);
que.push(p->left);
que.push(p->right);
}
if(!vec.empty()) res.push_back(vec);
}
return res;
}
};
- 剑指32,从上到下打印二叉树3
//时间O(n),空间O(n) //使用flag标志奇偶层即可,唯一不足是需要reverse //进一步简化可以将奇偶层分开,不需要reverse class Solution { public: vector<vector<int>> levelOrder(TreeNode* root) { vector<vector<int>> res; if(!root) return res; queue<TreeNode*> que; que.push(root); int flag = 0; while(!que.empty()) { ++flag; int num = que.size(); vector<int> vec; for(int i=0;i<num;++i) { auto p = que.front(); que.pop(); if(!p) continue; vec.push_back(p->val); que.push(p->left); que.push(p->right); } if(!vec.empty()) { if(flag % 2 == 0) reverse(vec.begin(),vec.end()); res.push_back(vec); } } return res; } };
- 剑指34,二叉树中和为某一值的路径
//时间O(n),空间O(n) //标准dfs,虽然知道每一步顺序大概是什么,但总是因为一些细节出问题 //需要好好思考下,基础还是不够扎实 class Solution { public: vector<vector<int>> pathSum(TreeNode* root, int target) { dfs(root,target); return res; } private: vector<vector<int>> res; //为什么vec用全局变量而不用函数传参?因为就算是一条真的路径最后也会进行回溯 //调用pop_back,所以一个全局变量足矣 vector<int> vec; void dfs(TreeNode* root, int target) { //终止条件没有想的那么复杂 if(!root) return; vec.push_back(root->val); //更新target,就算为负也无妨, target -= root->val; //添加路径 if(!target && !root->left && !root->right) { res.push_back(vec); } //先序遍历 dfs(root->left,target); dfs(root->right,target); //回溯,很重要,target不需要回溯 vec.pop_back(); } };
- 剑指36,二叉搜索树与双向链表
//时间O(n),空间O(n) //牢记,中序遍历是升序排列,无非是在中序遍历的基础上增加了其他东西 //三个关键点 //1.中序遍历是升序排列 //2.构建pre和cur的双向指向 //3.对于头尾节点,特殊关照 //以下是中序遍历标准格式 void recur(Node* root) { if(!root) return; recur(root->left); cout<<root->val<<endl; recur(root->right); } /************************************************************************************/ class Solution { public: Node* treeToDoublyList(Node* root) { if(!root) return nullptr; //pre初始化 pre = nullptr; //中序遍历,构建双向链表 recur(root); //加上头尾,构建双向循环链表 //pre最后会指向尾节点 head->left = pre; pre->right = head; //返回头指针 return head; } private: Node* pre; Node* head; void recur(Node* cur) { //终止条件 if(!cur) return; //中序遍历,升序排列 recur(cur->left); //根据pre指针判断head if(!pre) head = cur; else if(pre) { //构建双向指向 pre->right = cur; cur->left = pre; } //更新pre,cur无需更新,其会随着recur函数调用自动更新 pre = cur; recur(cur->right); } };
- 剑指37,序列化二叉树
//时间O(n),空间O(n),这两个函数时间复杂度和空间复杂度均为O(n),O(n) //序列化和反序列化均使用层序遍历,所以说,层序遍历才是YYDS //序列化的关键点 //1.层序遍历所有节点,将其值放在string中,其中null也要加入string中 //2.注意返回值string的格式,逗号分割, //反序列化的关键点 //1.将string转成vector<string>,便于下一步查找左右子树的val,如果改接口,可省略此步骤 //2.层序遍历建立整棵树,不断构建当前节点和其左子树和右子树的关系,该入队就入队 //3.如何根据当前节点找到左子树节点和右子树节点的值很关键,重点是变量i的理解,i是非null节点的索引,根据k神所讲,left=2(m-n)+1,right=2(m-n)+2,这里无需求m-n,直接用i代替,i=m-n,另外,k神,YYDS class Codec { public: // Encodes a tree to a single string. string serialize(TreeNode* root) { //root为空直接返回,不加中括号,否则下面还要处理,无意义 if(!root) return ""; string s; queue<TreeNode*> que; //层序遍历基本操作 que.push(root); while(!que.empty()) { TreeNode* p = que.front(); que.pop(); //如果节点为null,不再push左右子节点,将null加入字符串 if(!p) s.append("null,"); else { //节点非null,push左右子节点,将null加入字符串 //使用逗号分割,空格也可以,下面统一处理 s.append(to_string(p->val)); s.append(","); que.push(p->left); que.push(p->right); } } return s; } // Decodes your encoded data to tree. TreeNode* deserialize(string s) { //如果s为空,直接返回nullptr if(s.empty()) return nullptr; //很重要,将string用逗号分隔开,放进vector<string>里面 //便于下一步查找,其实这一步可以省略,不过需要改两个函数 //的参数,serialize的返回值,deserialize的形参, //改为vector<string>类型的,会方便很多 vector<string> res = spilit(s,','); //new root节点 TreeNode* root = new TreeNode(stoi(res[0])); queue<TreeNode*> que; que.push(root); //i表示所有非null节点的序号,k神讲过 //i=m-n,因此,left=2(m-n)+1,可以简化成left=2i+1 int i = 0; //层序遍历基本操作 while(!que.empty()) { TreeNode* p = que.front(); que.pop(); //左子树 //判断左子树是否为空,否的话,new节点,入队 if(res[2*i+1] == "null") { p->left = nullptr; } else{ p->left = new TreeNode(stoi(res[2*i+1])); que.push(p->left); } //右子树 //判断右子树是否为空,否的话,new节点,入队 if(res[2*i+2] == "null") { p->right = nullptr; } else{ p->right = new TreeNode(stoi(res[2*i+2])); que.push(p->right); } //i是连续的,因此每过一个非null节点就+1 ++i; } return root; } private: //spilit //将字符串函数,字符串函数中用symbol隔开各个元素,中各个元素 //装进vector<string>中,因为没找到相关库函数,只能自己实现 //使用双指针 vector<string> spilit(string s,char symbol) { if(s.empty()) return {}; vector<string> res; int i = 0; int j = 0; while(i < s.size()) { while(j < s.size() && s[j] != symbol) ++j; res.push_back(s.substr(i,j-i)); ++j; i=j; } return res; } };
- 剑指38,字符串的排列
//时间复杂度和空间复杂度挺麻烦的 //这个题是真的草,标准的dfs //有几点需要注意 //1.通过swap来实现数据的固定,x为固定位置,swap(s[x],s[i]);i属于[x,s.size()-1] //2.去重第一必须想到哈希表 //3.递归即固定下一个位置 //4.勿忘回溯,很重要 class Solution { public: vector<string> permutation(string s) { dfs(s,0); return res; } private: vector<string> res; void dfs(string s,int x) { //终止条件,所有位都已固定,0 - s.size()-1 if(x == s.size()-1) { res.push_back(s); return; } //哈希表去重 unordered_set<int> ss; //将固定位之后的数据(包括固定位)都依次放到固定位上 for(int i=x;i<s.size();++i) { //如果重复就跳过 if(ss.find(s[i]) != ss.end()) continue; //插入哈希表 ss.insert(s[i]); //通过交换来实现固定数据 swap(s[i],s[x]); //递推,固定下一位 dfs(s,x+1); //回溯 swap(s[i],s[x]); } } };
- 剑指54,二叉搜索树的第K大节点
//时间O(n),空间O(n) //牢记一点,二叉搜索树中序遍历为递增序列,中序遍历倒叙为递减序列 //因此,其为找中序遍历倒叙的第k个节点 class Solution { public: int kthLargest(TreeNode* root, int k) { recur(root,k); return res; } private: int cnt = 0; int res; void recur(TreeNode* root, int k) { if(!root) return; recur(root->right,k); ++cnt; if(cnt == k) { res = root->val; return; } recur(root->left,k); } };
- 剑指55,二叉树的深度
//时间O(n),空间O(n) //可以使用dfs/bfs class Solution { public: int maxDepth(TreeNode* root) { if(!root) return 0; return 1 + max(maxDepth(root->left),maxDepth(root->right)); } };
- 剑指55,平衡二叉树
//时间O(nlogn),空间O(n) //此方法最容易想到,先序遍历+判断深度,从顶至底 class Solution { public: bool isBalanced(TreeNode* root) { if(!root) return true; bool tmp = abs(recur(root->left)-recur(root->right)) <= 1 ? true : false; return tmp && isBalanced(root->left) && isBalanced(root->right); } private: int recur(TreeNode* root) { if(!root) return 0; return 1+max(recur(root->left),recur(root->right)); } };
- 剑指64,1+2+…+n
//时间O(n),空间O(n) //首先必须是递归,因此n+sumNums(n-1)必不可少 //其次是&&的应用 //最后终止条件判断 //这题真草啊 class Solution { public: int sumNums(int n) { n && (n += sumNums(n-1)); return n; } };
- 剑指68,二叉搜索树的最近公共祖先
//时间O(n),空间O(1) //此题使用二分法最为简单,只要将p,q分开即可得到最近公共祖先 //那么我们利用二叉搜索树的特点,左子树小于根节点,右子树大于根节点,不断进行二分 class Solution { public: TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { //p,q必存在,因此必然能找到最近公共祖先 while(true) { //其实是不断进行二分的过程 //如果p,q在root左边 if(root->val > p->val && root->val > q->val) { root = root->left; } //如果p,q在root右边 else if(root->val < p->val && root->val < q->val) { root = root->right; } //二分成功,得到最近公共祖先 //p,q分异两侧,或p,q之一是root else break; } return root; } }; //时间O(n),空间O(n) //递归,分为三种情况,p,q都在左子树中,p,q都在右子树中,其他(即root为最近公共祖先) //首先明确函数传入root会得出什么样的结果 //然后传入root->left,root->right分别会得出什么样的结果 class Solution { public: TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { if(root->val > p->val && root->val > q->val) { return lowestCommonAncestor(root->left,p,q); } else if(root->val < p->val && root->val < q->val) { return lowestCommonAncestor(root->right,p,q); } else return root; } };
- 剑指68,二叉树的最近公共祖先
//时间O(n),空间O(n) //先序遍历进行递归+四种情况分类讨论 class Solution { public: TreeNode* lowestCommonAncestor (TreeNode* root, TreeNode* p, TreeNode* q) { if(!root) return nullptr; //如果恰好是p,q,直接返回,返回值给了上一级left/right if(root == p || root == q) return root; //先序遍历 auto left = lowestCommonAncestor(root->left,p,q); auto right = lowestCommonAncestor(root->right,p,q);. //四种情况分类讨论 if(!left && !right) return nullptr; else if(left && right) return root; else if(left && !right) return left; else if(!left && right) return right; return nullptr; } };