一、递归
递归就是一个函数调用自身的方法,通俗点说,就是把一个大型复杂的问题层层转化为一个与原问题相同的更小规模的问题来求解。
- 递:将问题拆解成子问题来解决, 子问题再拆解成子子问题,...,直到被拆解的子问题无需再拆分成更细的子问题(即可以求解)
- 归:最小的子问题解决了,那么它的上一层子问题也就解决了,上一层的子问题解决了,上上层子问题自然也就解决了,....,直到最开始的问题解决。
我们可以把” 递归 “比喻成 “查字典 “,当你查一个词,发现这个词的解释中某个词仍然不懂,于是你开始查这第二个词。可惜,第二个词里仍然有不懂的词,于是查第三个词,这样查下去,直到有一个词的解释是你完全能看懂的,那么递归走到了尽头,然后你开始后退,逐个明白之前查过的每一个词,最终,你明白了最开始那个词的意思。(摘自网络)
-
递归和栈的关系
实质:递归的过程就是出入栈的过程,以阶乘为例:
int Factorial(int n){
if (n == 0) return 1;
return
n * Factorial(n - 1);
}
第 1~4 步,都是入栈过程,
Factorial(3)
调用了Factorial(2)
,Factorial(2)
又接着调用Factorial(1)
,直到Factorial(0)
;第 5 步,因 0 是递归结束条件,故不再入栈,此时栈高度为 4,即为我们平时所说的递归深度;
第 6~9 步,
Factorial(0)
做完,出栈,而Factorial(0)
做完意味着Factorial(1)
也做完,同样进行出栈,重复下去,直到所有的都出栈完毕,递归结束。
-
什么时候用递归(三个条件)
- 一个问题的解可以分解为若干个子问题的解;
- 这个问题与分解之后的子问题,除了数据规模不同,求解的思路完全相同;
- 存在终止条件。
-
如何编写递归代码:
- 需要找到递归的终止条件
-
找到问题与子问题间的关系,即递归的递推公式
注意:编写递归代码时,只要遇到递归,我们就把其抽象为一个递推公式,不用去想递归层层调用的关系,去分析递归的每步在做啥,这样会把我们搞混,因为递归和我们的思维方式正好相反。
-
需要避免的问题:
1.堆栈的溢出问题。
函数调用会用栈来保存临时变量,每调用一次函数,都会将临时变量压入栈中,等函数执行完返回时,出栈,如果数据规模大或者递归调用的层数深,一般就有堆栈溢出的风险
2.重复子问题。
1.汉罗塔问题(leetcode 面试题08.06)
- 题目描述:
在经典汉诺塔问题中,有 3 根柱子及 N 个不同大小的穿孔圆盘,盘子可以滑入任意一根柱子。一开始,所有盘子自上而下按升序依次套在第一根柱子上(即每一个盘子只能放在更大的盘子上面)。移动圆盘时受到以下限制:
(1) 每次只能移动一个盘子;
(2) 盘子只能从柱子顶端滑出移到下一根柱子;
(3) 盘子只能叠在比它大的盘子上。
请编写程序,用栈将所有盘子从第一根柱子移到最后一根柱子。
你需要原地修改栈。
示例1:
输入:A = [2, 1, 0], B = [], C = []
输出:C = [2, 1, 0]
示例2:
输入:A = [1, 0], B = [], C = []
输出:C = [1, 0]
提示:
A中盘子的数目不大于14个。
- 分析:
做递归时,假设A前n-1个盘子借助C都成功移到了B上,A上还剩下一个面积最大的盘子,因为要把最大的放在下面,所以将A的最后一个盘子移动到C,然后A上就没有了,再将B上的n-1个盘子借助A移动到C上,当只剩一个盘子时,就直接移动。
class Solution {
public:
void move(int n,vector<int>&A,vector<int>& B, vector<int>& C)
{
//终止条件
if(n==1)
{
C.push_back(A.back());//将A的最后一个移到C
A.pop_back();//A清空
return;
}
move(n-1,A,C,B);//将A上的n-1个借助C移动到B
C.push_back(A.back());//将A的最后一个移到C
A.pop_back();//A清空
move(n-1,B,A,C);//将B上的n-1个借助A移动到c
return;
}
void hanota(vector<int>& A, vector<int>& B, vector<int>& C) {
int n=A.size();
return move(n,A,B,C);
}
};
2.从前序与中序遍历序列构造二叉树(leetcode 105)
- 题目描述:【中等难度】
根据一棵树的前序遍历与中序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
- 分析:
首先,我们需要知道二叉树的前序遍历与中序遍历是如何遍历的,如下图所示(图片来源于网络),接着:
- 我们可以发现先序遍历的第一个节点为根节点,而在中序遍历(9 | 3 | 15 20 7)中根节点将中序遍历的结果分为左子树(9)和右子树(15 20 7)两部分。
- 在先序遍历的结果中,除去第一个根节点后,也分为两部分:左子树(9)和右子树(20 15 7)。
- 左子树和右子树又可以看成一棵树,这样就可以用递归进行求解。
- 当前序遍历或中序遍历只要一个元素的时候,也就是叶子节点,这是也就为递归的终止条件。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if(preorder.empty()||inorder.empty())
return NULL;
//创建根节点
TreeNode* root=new TreeNode;
root->val=preorder[0];
if(preorder.size()==1)
{
return root;
}
//找到根节点的位置
int root_index;
for(int i=0;i<inorder.size();i++)
{
if(preorder[0]==inorder[i])
{
root_index=i;
break;
}
}
vector<int> left_inorder,right_inorder;
left_inorder.insert(left_inorder.end(),inorder.begin(),inorder.begin()+root_index);
right_inorder.insert(right_inorder.end(),inorder.begin()+root_index+1,inorder.end());
vector<int> left_preorder,right_preorder;
left_preorder.insert(left_preorder.end(),preorder.begin()+1,preorder.begin()+left_inorder.size()+1);
right_preorder.insert(right_preorder.end(),preorder.begin()+left_inorder.size()+1,preorder.end());
TreeNode* left=buildTree(left_preorder,left_inorder);
TreeNode* right=buildTree(right_preorder,right_inorder);
root->left=left;
root->right=right;
return root;
}
};
3.对称二叉树(leetcode 101)
- 题目描述:
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:1
/ \
2 2
\ \
3 3
- 分析:
两个树互为镜像的条件(图来源于leetcode):
- 它们的两个根结点具有相同的值
- 每个树的右子树都与另一个树的左子树镜像对称
那么,根据上面的条件我们可以使用递归进行求解,参数为两个节点。
- 当两个节点都为空时,返回true;
- 当两个节点都不为空,且值相等时,判断(左->左,右->右)(左->右,右->左)
- 当为其他情况时,返回false。
- 递归方法:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool tree(TreeNode* left,TreeNode* right)
{
if(left==NULL&&right==NULL)
return true;
else if(left!=NULL&&right!=NULL&&left->val==right->val)
{
return tree(left->left,right->right)&&tree(left->right,right->left);
}
else
return false;
}
bool isSymmetric(TreeNode* root) {
if(root==NULL)
return true;
return tree(root->left,root->right);
}
};
- 迭代方法(见3、广度优先第二题)
4.二叉树中的最大路径和(leetcode 124)
- 题目描述:
给定一个非空二叉树,返回其最大路径和。
本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点。
示例 1:
输入: [1,2,3]
1
/ \
2 3输出: 6
示例 2:
输入: [-10,9,20,null,null,15,7]
-10
/ \
9 20
/ \
15 7输出: 42
首先,路径被定义为一条从树中任意节点出发,达到任意节点的序列。该路径至少包含一个节点,且不一定经过根节点,而且路径不能重复。
当处于一个节点的时候,可以选择不走,或者走左边,或者走右边。如果做出了选择到达了下一个节点,那么又会面临相同的选择,只不过相对上一个节点是规模更小的子树。因此可以用递归实现。
首先,终止条件是当当前节点为空时,那么返回0,表示不走了。
接着,三种选择:
- 走入左子树,收益:root->val + dfs(root->left)
- 走入右子树,收益:root->val + dfs(root->right)
- 停在当前子树的根节点,收益:root->val
其次,如果当前节点为负,需要特殊处理,别走它了(没想到)。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
int maxsum=INT_MIN;
int dfs(TreeNode* root)
{
if(root==NULL)
return 0;
int left=max(0,dfs(root->left));
int right=max(0,dfs(root->right));
maxsum=max(maxsum,left+right+root->val);
return root->val+max(left,right);
}
int maxPathSum(TreeNode* root) {
dfs(root);
return maxsum;
}
};
5.将有序数组转换为二叉搜索树(leetcode 108)
- 题目描述:
将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
示例:
给定有序数组: [-10,-3,0,5,9],
一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树:
0
/ \
-3 9
/ /
-10 5
- 分析:
- 可以选择中间数字作为二叉搜索树的根节点,这样分给左右子树的数字个数相同或只相差 1,可以使得树保持平衡。
- 如果数组长度是奇数,则根节点的选择是唯一的;
- 如果数组长度是偶数,则可以选择中间位置左边的数字作为根节点或者选择中间位置右边的数字作为根节点;
- 选择不同的数字作为根节点则创建的平衡二叉搜索树也是不同的。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* helper(vector<int>& nums,int start,int end)
{
if(start>end)
return NULL;
int index=(end+start)/2;
TreeNode* root=new TreeNode(nums[index]);
root->left=helper(nums,start,index-1);
root->right=helper(nums,index+1,end);
return root;
}
TreeNode* sortedArrayToBST(vector<int>& nums) {
return helper(nums,0,nums.size()-1);
}
};
二、深度优先遍历
深度优先遍历,通俗点说,不撞南墙,不回头。DFS本质是一种枚举,通过枚举状态空间中所有的可能性,找到一种或者若干种可行解。
解决的问题:能枚举全部状态的问题。
- 图的遍历中求可行路径。
- 排列/组合方案。
- 问题过于复杂,只能通过枚举+判断的问题
深度优先搜索,最重要的是如何确定状态空间,也就是说我们要搜什么,怎么搜,搜得一干二净。
回溯法
回溯法,又称为“试探法”,解决问题时,每进行一步,都抱着试试看的态度,如果发现当前选择并不是最好的,或者这么走下去肯定达不到目的,立刻做回退操作,重新选择,这样走不通就回退再选择的方法就是回溯法。
“回溯”指的是“状态重置”,可以理解为“回到过去”、“恢复现场”。
树形问题
1.深度优先例题
1.对称二叉树(leetcode 101)
解析:广度优先中的另解
2.N叉树的最大深度(leetcode 559)
广度优先:
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
public:
int dfs(Node * root)
{
if(root->children.empty())
return 1;
int ans=0;
for(int i=0;i<root->children.size();i++)
{
ans=max(ans,dfs(root->children[i]));
// cout<<"node : "<<root->val<<" "<<ans<<endl;;
}
return ans+1;
}
int maxDepth(Node* root) {
if(root==NULL)
return 0;
return dfs(root);
}
};
1.验证二叉搜索树(leetcode 98)
- 题目描述:
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
- 节点的左子树只包含小于当前节点的数。
- 节点的右子树只包含大于当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
- 分析:
二叉搜索树有如下性质:
- 节点的左子树只包含小于当前节点的数。
- 节点的右子树只包含大于当前节点的数。
- 所有左子树和右子树自身必须也是二叉搜索树。
从性质3中我们可以发现这道题的方法——递归,首先验证父节点是否满足性质1,2,然后递归调用再验证左右子树是否也是二叉搜索树。
注意(很重要):这里有一个区间限制,左子树上的节点都比根节点的值小,同理,右子树上的节点都比根节点的大,下面这个就不是二叉搜索树。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool helper(TreeNode* root,long long lower,long long upper)
{
if(!root)
return true;
bool flag_left;
bool flag_right;
//左节点存在
if(root->left)
{
//节点的左子树只包含小于当前节点的数。
if(root->val>root->left->val&&root->left->val>lower&&root->left->val<upper)
flag_left=helper(root->left,lower,root->val);
else
return false;
}
else
flag_left=true;//左为空,为二叉搜索树
//右节点存在
if(root->right)
{
//节点的右子树只包含大于当前节点的数
if(root->val<root->right->val&&root->right->val>lower&&root->right->val<upper)
flag_right=helper(root->right,root->val,upper);
else
return false;
}
else
flag_right = true;//右为空,为二叉搜索树
//左右子树都为二叉搜索树
return flag_left&&flag_right;
}
bool isValidBST(TreeNode* root) {
return helper(root,LONG_MIN,LONG_MAX);
}
};
2.另一个数的子树(leetcode 572)
- 题目描述:
给定两个非空二叉树 s 和 t,检验 s 中是否包含和 t 具有相同结构和节点值的子树。s 的一个子树包括 s 的一个节点和这个节点的所有子孙。s 也可以看做它自身的一棵子树。
- 分析:
- 首先,我们需要在主树中找到与子树相同的节点。
- 然后,进行深度遍历。
- 接着,判断两个节点是否为空,如果是,则为true,否则,判断是否其中一个节点为空,如果是,则为false,否则,判断节点的值是否相等,如果是,按照上述条件判断左节点和右节点。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool dfs(TreeNode* s,TreeNode* t)
{
if(s==NULL&&t==NULL)
return true;
if(s==NULL||t==NULL)
return false;
if(s->val!=t->val)
return false;
return dfs(s->left,t->left)&&dfs(s->right,t->right);
}
bool isSubtree(TreeNode* s, TreeNode* t) {
while(s)
{
if(dfs(s,t))
return true;
return isSubtree(s->left,t)||isSubtree(s->right,t);
}
return false;
}
};
3. 二叉树的最近公共祖先(leetcode 236)
- 题目描述:(中等难度)
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
说明:
所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉树中。
- 分析1:
注:来源于leetcode 官方题解
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* ans;
bool dfs(TreeNode* root, TreeNode* p, TreeNode* q)
{
//遍历到底为空
if(root==NULL)
return false;
bool l_flag=dfs(root->left,p,q);
bool r_flag=dfs(root->right,p,q);
if((l_flag&&r_flag)||((root->val==p->val||root->val==q->val)&&(l_flag||r_flag)))
{
ans=root;
}
return l_flag || r_flag||(root->val==p->val||root->val==q->val);
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
dfs(root,p,q);
return ans;
}
};
- 分析2:
- 如果当前结点 root 等于NULL,则直接返回NULL
- 如果 root 等于 p或者 q ,那返回 p 或者 q
- 然后递归左右子树,递归结果,用 left 和 right表示
- 如果 left 和 right都非空,说明左右子树一边一个,因此 root是他们的最近公共祖先
- 如果 left为空,返回right,否则left;
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* dfs(TreeNode* root, TreeNode* p, TreeNode* q)
{
if(root==NULL||root==p||root==q)
return root;
TreeNode* left= dfs(root->left,p,q);
TreeNode* right= dfs(root->right,p,q);
if(left&&right)
return root;
return left?left:right;
}
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
return dfs(root,p,q);
}
};
4.电话号码的字母组合(leetcode 17)
- 题目描述:
给定一个仅包含数字 2-9
的字符串,返回所有它能表示的字母组合。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
示例:
输入:"23"
输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].
- 分析:
这道题是字母组合的问题,我们可以先画出如下的树形结构得到我们所需要的所有可能。我们采用回溯法解决。
class Solution {
public:
string lettermap[8]={
"abc",
"def",
"ghi",
"jkl",
"mno",
"pqrs",
"tuv",
"wxyz"
};
vector<string> ans;
void helper(string digits,int index,string s)
{
if(index==digits.size())
{
ans.push_back(s);
return;
}
//取出对应的数字
char c=digits[index];
if(c>='2'&&c<='9')
{
string letter=lettermap[c-'2'];
//做选择,选择列表letter
for(int i=0;i<letter.size();i++)
{
helper(digits,index+1,s+letter[i]);
}
}
}
vector<string> letterCombinations(string digits) {
if(digits.empty())
return ans;
string s;
helper(digits,0,s);
return ans;
}
};
5.全排列(leetcode 46)
- 分析:
对[1,2,3]进行全排列={取出其中一个数字}+对剩余的数字进行全排列,这也就是递归操作。
- 实现:
class Solution {
public:
vector<vector<int>> res;
vector<bool> used;
//
void permute(vector<int>&nums,int index,vector<int>&ans)
{
if(index==nums.size())
{
res.push_back(ans);
return;
}
for(int i=0;i<nums.size();i++)
{
ans.push_back(nums[i]);
permute(nums,index+1,ans);
//回溯操作
ans.pop_back();
}
return;
}
vector<vector<int>> permute(vector<int>& nums) {
vector<int> ans;
permute(nums,0,ans);
return res;
}
};
结果: [[1,1,1],[1,1,2],[1,1,3],[1,2,1],[1,2,2],[1,2,3],[1,3,1],[1,3,2],[1,3,3],[2,1,1],[2,1,2],[2,1,3],[2,2,1],[2,2,2],[2,2,3],[2,3,1],[2,3,2],[2,3,3],[3,1,1],[3,1,2],[3,1,3],[3,2,1],[3,2,2],[3,2,3],[3,3,1],[3,3,2],[3,3,3]]
class Solution {
public:
vector<vector<int>> res;
vector<bool> used;
void permute(vector<int>&nums,int index,vector<int>&ans)
{
if(index==nums.size())
{
res.push_back(ans);
return;
}
for(int i=0;i<nums.size();i++)
{
if(!used[i])
{
ans.push_back(nums[i]);
used[i]=true;
permute(nums,index+1,ans);
//当递归到底时,需要把插入的元素退出,状态恢复原样
ans.pop_back();
used[i]=false;
}
}
return;
}
vector<vector<int>> permute(vector<int>& nums) {
//状态变量,判断是否被使用
used=vector<bool>(nums.size(),false);
vector<int> ans;
permute(nums,0,ans);
return res;
}
};
结果:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
三、广度优先遍历:
广度优先搜索算法(英语:Breadth-First Search,缩写为BFS),又译作宽度优先搜索,或横向优先搜索,通俗点说,一层一层的剥开你的心。
-
原理:
从某个结点出发,BFS 首先遍历到距离为 1 的结点,然后是遍历距离为 2、3、4…… 的结点。因此,BFS 可以用来求最短路径问题。BFS 先搜索到的结点,一定是距离最近的结点。
- 什么时候用广度优先遍历(BFS)
- 层级遍历
- 由点到面
- 拓扑排序
- 最短路径:简单图求最短路径,即图中每条边为1,且没有方向。
- 实现模板
//初始的起点
for(...)
//插入到队列中
queue.push();
//队列非空
while queue 非空:
//取出队首的元素
n=queue.front();
//出队
queue.pop();
//判断相邻的节点
for node 的所有相邻结点 m:
if m 未访问过:
//改变m的属性
//入队
queue.push(m)
对于上述模板,无法区分每一层是否遍历完成。因为在遍历的时候,第1层的节点出队,如果条件满足,第2层的节点紧接着入队,这使得队列中第 1 层和第 2 层的结点会紧挨在一起,无法区分,也就无法知道每个结点的距离 depth 了。
因此,我们需要稍微修改一下代码,在每一层遍历开始前,记录当前队列中的结点数量 num ,然后一口气处理完这一层的num 个结点 。这样,我们就知道这num个结点位于同一层了。然后遍历下一层的时候,把变量 depth 加一。代码框架是这样的:
//初始的起点
for(...)
//插入到队列中
queue.push();
//队列非空
while queue 非空:
int size=queue.size();
for(i:size)
//取出队首的元素
n=queue.front();
//出队
queue.pop();
//判断相邻的节点
for node 的所有相邻结点 m:
if m 未访问过:
//改变m的属性
//入队
queue.push(m)
相关例题
1.从上到下打印二叉树 II(剑指offer 32)
- 题目描述:
从上到下按层打印二叉树,同一层的节点按从左到右的顺序打印,每一层打印到一行。
例如:
给定二叉树: [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:[
[3],
[9,20],
[15,7]
]
提示:
节点总数 <= 1000
- 分析:
二叉树的层级遍历
/**
* Definition for a binary tree node.
* 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) {
vector<vector<int> > ans;
if(root==NULL)
return ans;
queue<TreeNode* > q;
q.push(root);
while(!q.empty())
{
int size=q.size();
vector<int> cur;
for(int i=0;i<size;i++)
{
auto top=q.front();
q.pop();
cur.push_back(top->val);
if(top->left)
q.push(top->left);
if(top->right)
q.push(top->right);
}
ans.push_back(cur);
cur.clear();
}
return ans;
}
};
2.二叉树的层次遍历(leetcode 102)
给定一个二叉树,返回其按层次遍历的节点值。 (即逐层地,从左到右访问所有节点)。
例如:
给定二叉树: [3,9,20,null,null,15,7]
,
3
/ \
9 20
/ \
15 7
返回其层次遍历结果:
[
[3],
[9,20],
[15,7]
]
- 实现
判断根节点是否为NULL,如果是,直接返回空。
将根节点插入到队列中,遍历整个队列。
队列的顶部元素出队,存储其值。
判断左右节点是否为空,如果不为空,则将其入队。
重复步骤3.
注意:需要记得队列中现目前的元素数量。
/**
* Definition for a binary tree node.
* 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) {
vector<vector<int>> res;
queue<TreeNode* >q;
if(root==NULL)
return res;
q.push(root);
while(!q.empty())
{
vector<int> ans;
int size=q.size();
for(int i=0;i<size;i++)
{
auto p=q.front();
q.pop();
ans.push_back(p->val);
if(p->left!=NULL)
{
q.push(p->left);
}
if(p->right!=NULL)
{
q.push(p->right);
}
}
res.push_back(ans);
ans.clear();
}
return res;
}
};
- 另外的解法:
本题使用 DFS 同样能做。由于题目要求每一层的节点都是从左到右遍历,因此递归时也要先递归左子树、再递归右子树。
DFS 做本题的主要问题是: DFS 不是按照层次遍历的。为了让递归的过程中同一层的节点放到同一个列表中,在递归时要记录每个节点的深度 level。递归到新节点要把该节点放入 level 对应列表的末尾。
当遍历到一个新的深度 level,而最终结果 res 中还没有创建 level 对应的列表时,应该在 res 中新建一个列表用来保存该 level 的所有节点。
/**
* Definition for a binary tree node.
* 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) {
vector<vector<int>> res;
dfs(res, root, 0);
return res;
}
void dfs(vector<vector<int>>& res, TreeNode* root, int level) {
if (!root) return;
if (level >= res.size())
res.push_back(vector<int>());
res[level].push_back(root->val);
dfs(res, root->left, level + 1);
dfs(res, root->right, level + 1);
}
};
3.二叉树的层次遍历II(leetcode 107)
- 题目描述:
给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
例如:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回其自底向上的层次遍历为:
[
[15,7],
[9,20],
[3]
]
-
分析:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root) {
vector<vector<int>> ans;
if(root==NULL)
return ans;
queue<TreeNode* > q;
q.push(root);
while(!q.empty())
{
int size=q.size();
vector<int> cur;
for(int i=0;i<size;i++)
{
auto top=q.front();
q.pop();
cur.push_back(top->val);
if(top->left!=NULL)
q.push(top->left);
if(top->right!=NULL)
q.push(top->right);
}
ans.push_back(cur);
cur.clear();
}
vector<vector<int>> res;
for(int i=ans.size()-1;i>=0;i--)
{
res.push_back(ans[i]);
}
return res;
}
};
4.二叉树的最小深度(leetcode 111)
- 题目描述:
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
返回它的最小深度 2.
- 分析:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
int minDepth(TreeNode* root) {
int ans=0;
if(root==NULL)
return ans;
queue<TreeNode* > q;
q.push(root);
ans+=1;
while(!q.empty())
{
int size=q.size();
for(int i=0;i<size;i++)
{
auto top=q.front();
q.pop();
if(top->left==NULL&&top->right==NULL)
return ans;
if(top->left!=NULL)
q.push(top->left);
if(top->right!=NULL)
q.push(top->right);
}
ans++;
}
return ans;
}
};
5.N叉树的最大深度(leetcode 559)
- 题目描述:
给定一个 N 叉树,找到其最大深度。
最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。
例如,给定一个 3叉树 :
我们应返回其最大深度,3。
说明:
树的深度不会超过 1000。
树的节点总不会超过 5000。
- 分析:
广度优先,一层一层的遍历,也就是这棵树有多少层。
/*
// Definition for a Node.
class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
public:
int maxDepth(Node* root) {
if(root==NULL)
return 0;
int ans=0;
queue<Node *> q;
q.push(root);
while(!q.empty())
{
int size=q.size();
for(int i=0;i<size;i++)
{
auto top=q.front();
q.pop();
for(int i=0;i<top->children.size();i++)
{
q.push(top->children[i]);
}
}
ans++;
}
return ans;
}
};
2.对称二叉树(leetcode 101)
给定一个二叉树,检查它是否是镜像对称的。
例如,二叉树 [1,2,2,3,4,4,3] 是对称的。
1
/ \
2 2
/ \ / \
3 4 4 3
但是下面这个 [1,2,2,null,3,null,3] 则不是镜像对称的:1
/ \
2 2
\ \
3 3
-
分析:
- 对整棵二叉树进行层次遍历,节点分为左子树和右子树进行存储,
- 首先,入队。
- 左右队列队首出队,判断是不是都为空节点。
- 如果都是空节点,则跳过;
- 如果不是,则判断是不是都不为空且值相等,如果是,则将左孩子和右孩子入队。
- 重复上述步骤
- 实现:
bfs:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool isSymmetric(TreeNode* root) {
//标志位
bool flag=true;
if(!root)
return true;
//将二叉树分为左子树、右子树
queue<TreeNode* > left;
queue<TreeNode* > right;
//入队
left.push(root->left);
right.push(root->right);
while(!left.empty()&&!right.empty())
{
//出队
TreeNode* l=left.front();
TreeNode* r=right.front();
left.pop();
right.pop();
//左右都为空的时候,成立
if(l==NULL&&r==NULL)
continue;
//左右不为空且值相等时,成立
if(l!=NULL&&r!=NULL&&l->val==r->val)
{
//入队
left.push(l->left);
left.push(l->right);
right.push(r->right);
right.push(r->left);
}
else
{
//其余情况,不成立
flag=false;
break;
}
}
return flag;
}
};
dfs:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
bool dfs(TreeNode* left,TreeNode* right)
{
if(left==NULL&&right==NULL)
return true;
else if(left!=NULL&&right!=NULL&&left->val==right->val)
return dfs(left->left,right->right)&&dfs(left->right,right->left);
else
return false;
}
bool isSymmetric(TreeNode* root) {
if(root==NULL)
return true;
return dfs(root->left,root->right);
}
};
3.二叉树的右视图(leetcode 199)
- 日期:2020/4/22
- 题目描述:
给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
示例:
输入: [1,2,3,null,5,null,4]
输出: [1, 3, 4]
解释:1 <---
/ \
2 3 <---
\ \
5 4 <---
- 分析:
其实这道题是二叉树层次遍历的变种,我们层次遍历的时候是从左往右进行存储节点,但是这里我们需要从右往左进行存储节点,其次,我们每层只取第一个节点放到我们的最终结果中。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<int> rightSideView(TreeNode* root) {
vector<int> res;
if(!root)
return res;
queue<TreeNode*> q;
q.push(root);
while(!q.empty())
{
int size=q.size();
for(int i=0;i<size;i++)
{
auto temp=q.front();
q.pop();
// cout<<"val: "<<temp->val<<" i: "<<i<<endl;
if(i==0)
res.push_back(temp->val);
if(temp->right)
q.push(temp->right);
if(temp->left)
q.push(temp->left);
}
// cout<<"-------------"<<endl;
}
return res;
}
};
4.腐烂的橘子(leetcode 994)
在给定的网格中,每个单元格可以有以下三个值之一:
- 值
0
代表空单元格; - 值
1
代表新鲜橘子; - 值
2
代表腐烂的橘子。
每分钟,任何与腐烂的橘子(在 4 个正方向上)相邻的新鲜橘子都会腐烂。
返回直到单元格中没有新鲜橘子为止所必须经过的最小分钟数。如果不可能,返回 -1
。
示例 1:
输入:[[2,1,1],[1,1,0],[0,1,1]]
输出:4
示例 2:
输入:[[2,1,1],[0,1,1],[1,0,1]]
输出:-1
解释:左下角的橘子(第 2 行, 第 0 列)永远不会腐烂,因为腐烂只会发生在 4 个正向上。
示例 3:
输入:[[0,2]]
输出:0
解释:因为 0 分钟时已经没有新鲜橘子了,所以答案就是 0 。
提示:
1 <= grid.length <= 10
1 <= grid[0].length <= 10
grid[i][j]
仅为0
、1
或2
- 实现
- 找出初始腐烂橘子的位置,插入到队列中。
- 判断队列是否为空,如果不为空,队列的顶部元素出队,对上、下、左、右四个方向的橘子进行腐烂操作,腐烂的橘子置为2。注意:需要在边界处判断是否越界。
- 新的腐烂橘子插入到队列中,重复2。
- 然后再次遍历整个区域,判断是否把所有的橘子都腐烂了。
class Solution {
public:
int orangesRotting(vector<vector<int>>& grid) {
int rows=grid.size();
int cols=grid[0].size();
std::queue<std::pair<int,int>> orange;
for(int i=0;i<rows;i++)
{
for(int j=0;j<cols;j++)
{
if(grid[i][j]==2)
orange.push({i,j});
}
}
int time=0;
while (!orange.empty())
{
int size=orange.size();
bool flag= false;
for(int i=0;i<size;i++)
{
std::pair<int,int> temp=orange.front();
int row=temp.first;
int col=temp.second;
orange.pop();
if(col+1<cols&&grid[row][col+1]==1)
{
flag= true;
grid[row][col+1]=2;
orange.push({row,col+1});
}
if(col-1>=0&&grid[row][col-1]==1)
{
flag= true;
grid[row][col-1]=2;
orange.push({row,col-1});
}
if(row+1<rows&&grid[row+1][col]==1)
{
flag= true;
grid[row+1][col]=2;
orange.push({row+1,col});
}
if(row-1>=0&&grid[row-1][col]==1)
{
flag= true;
grid[row-1][col]=2;
orange.push({row-1,col});
}
}
if(flag)
time++;
}
for(int i=0;i<rows;i++)
{
for(int j=0;j<cols;j++)
{
if(grid[i][j]==1)
return -1;
}
}
return time;
}
};
5.水壶问题(leetcode 365)
-
日期:2020/3/21
有两个容量分别为 x升 和 y升 的水壶以及无限多的水。请判断能否通过使用这两个水壶,从而可以得到恰好 z升 的水?
如果可以,最后请用以上水壶中的一或两个来盛放取得的 z升 水。
你允许:
- 装满任意一个水壶
- 清空任意一个水壶
- 从一个水壶向另外一个水壶倒水,直到装满或者倒空
示例 1: (From the famous "Die Hard" example)
输入: x = 3, y = 5, z = 4
输出: True
示例 2:
输入: x = 2, y = 6, z = 5
输出: False
- 补充学习知识:std::unordered_set自定义函数
参考:STL: unordered_map 自定义键值类型的使用(C++)
以 x = 1, y = 3, z = 2 为例,一共有八种状态:(0,0), (1,0),(0,1), (1,1),(0,2), (1,2),(0,3), (1,3)。可以以这八种状态构建如下的图:
- 节点表示两个水壶的状态
- 边表示操作方法:分别为倒满A/B,倒空A/B,A倒入B,B倒入A 六种方法。
- 这是一个有向图,因为有些状态并不能护互为转移。比如 (1,1) 和 (1,0)。
广度优先搜索(BFS):该过程总是从一个或若干个起始点开始,沿着边像水波一样逐层向外遍历,直到所有的点已被访问或者到达目标状态。
- 实现
1.初始时,队列和set均为空。将起始点放入队列及set。
2.如果队列为空则 bfs 结束。
3.弹出队首元素并访问其周围元素,设为 p。
4.如果p为目标状态则 bfs 结束。
5. p 执行6种操作,将不在set中的元素放入队列及set。跳转第 2 步。
class Solution {
public:
pair<int,int> op(int type,pair<int,int> &state,int x,int y)
{
switch(type)
{
case 0:
return make_pair(x,state.second);
case 1:
return make_pair(state.first,y);
case 2:
return make_pair(0,state.second);
case 3:
return make_pair(state.first,0);
case 4:
{
int move = min(state.first, y-state.second);
return make_pair(state.first - move, state.second + move);
}
case 5:
{
int move = min(x-state.first, state.second);
return make_pair(state.first + move, state.second - move);
}
}
return make_pair(0,0);
}
struct HashPair {
size_t operator()(const pair<int, int> &key) const noexcept
{
return size_t(key.first)*100000007 + key.second;
}
};
bool canMeasureWater(int x, int y, int z) {
unordered_set<pair<int,int>,HashPair> s;
queue<pair<int,int>> q;
q.push({0,0});
while(!q.empty())
{
auto p=q.front();
q.pop();
if(p.first+p.second==z)
return true;
for(int i=0;i<6;i++)
{
auto points=op(i,p,x,y);
if(s.find(points)!=s.end())
continue;
s.insert(points);
q.push(points);
}
}
return false;
}
};
6.地图分析(leetcode 1162)
你现在手里有一份大小为 N x N 的『地图』(网格) grid,上面的每个『区域』(单元格)都用 0 和 1 标记好了。其中 0 代表海洋,1 代表陆地,你知道距离陆地区域最远的海洋区域是是哪一个吗?请返回该海洋区域到离它最近的陆地区域的距离。
我们这里说的距离是『曼哈顿距离』( Manhattan Distance):(x0, y0) 和 (x1, y1) 这两个区域之间的距离是 |x0 - x1| + |y0 - y1| 。
如果我们的地图上只有陆地或者海洋,请返回 -1。
示例 1:
输入:[[1,0,1],[0,0,0],[1,0,1]]
输出:2
解释:
海洋区域 (1, 1) 和所有陆地区域之间的距离都达到最大,最大距离为 2。
示例 2:
输入:[[1,0,0],[0,0,0],[0,0,0]]
输出:4
解释:
海洋区域 (2, 2) 和所有陆地区域之间的距离都达到最大,最大距离为 4。
提示:
1 <= grid.length == grid[0].length <= 100
grid[i][j] 不是 0 就是 1
参考:
https://leetcode-cn.com/problems/as-far-from-land-as-possible/solution/li-qing-si-lu-wei-shi-yao-yong-bfs-ru-he-xie-bfs-d/
7.机器人的运动范围
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
示例 1:
输入:m = 2, n = 3, k = 1
输出:3
示例 1:
输入:m = 3, n = 1, k = 0
输出:1
提示:
1 <= n,m <= 100
0 <= k <= 20
【记住】错误: 哎,本来思路很清晰,没一会就将代码写完了,但是由于自己在遍历方向数组的时候,命名命成了范围K,导致变量命名重复,浪费了时间。
class Solution {
public:
int getsum(int x)
{
int sum=0;
while(x)
{
sum+=x%10;
x=x/10;
}
return sum;
}
int movingCount(int m, int n, int k) {
if(k==0)return 1;
int dx[2] = {0, 1};
int dy[2] = {1, 0};
vector<vector<int>> grid(m,vector<int>(n,0));
queue<pair<int,int>> q;
q.push({0,0});
grid[0][0]=1;
int d=1;
while(!q.empty())
{
auto top=q.front();
q.pop();
for(int j=0;j<2;j++)
{
int row=top.first+dx[j];
int col=top.second+dy[j];
int sum=getsum(row)+getsum(col);
if(row < 0 || row >= m || col < 0 || col >= n || grid[row][col] || sum > k)
continue;
d++;
q.push({row,col});
grid[row][col]=1;
}
}
return d;
}
};
8.01矩阵(leetcode 542)
- 题目描述:
给定一个由 0 和 1 组成的矩阵,找出每个元素到最近的 0 的距离。
两个相邻元素间的距离为 1 。
示例 1:
输入:0 0 0
0 1 0
0 0 0
输出:0 0 0
0 1 0
0 0 0
示例 2:
输入:0 0 0
0 1 0
1 1 1
输出:0 0 0
0 1 0
1 2 1
注意:给定矩阵的元素个数不超过 10000。
给定矩阵中至少有一个元素是 0。
矩阵中的元素只在四个方向上相邻: 上、下、左、右。
9.岛屿数量(leetcode 200)
- 日期:2020/4/20
- 题目描述:
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:
11110
11010
11000
00000
输出: 1
示例 2:
输入:
11000
11000
00100
00011
输出: 3
解释: 每座岛屿只能由水平和/或竖直方向上相邻的陆地连接而成。
- 分析:
首先,“1”表示陆地,“0"表示海洋,每座岛屿只能由水平和/或竖直方向上相邻的陆地连接而成,我们可以通过深度优先搜索的方法通过起点寻找相邻的陆地(连通区域)构成岛屿,直到找到陆地的边界才返回,对于寻找过程中找过的陆地赋予新值:"2"。
- 代码实现:
- 深度优先搜索
class Solution {
public:
int dx[4]={-1,1,0,0};
int dy[4]={0,0,-1,1};
void islands(vector<vector<char>> &grid,int i,int j)
{
if(i>=grid.size()||i<0||j>=grid[0].size()||j<0)
return;
if(grid[i][j]=='0'||grid[i][j]=='2')
return;
if(grid[i][j]=='1')
{
grid[i][j]='2';
// cout<<grid[i][j]<<endl;
for(int k=0;k<4;k++)
{
islands(grid,i+dx[k],j+dy[k]);
}
}
}
int numIslands(vector<vector<char>>& grid) {
int num=0;
if(grid.empty()||grid[0].empty())
return 0;
for(int i=0;i<grid.size();i++)
{
for(int j=0;j<grid[0].size();j++)
{
if(grid[i][j]=='1')
{
num++;
islands(grid,i,j);
}
else
continue;
// cout<<grid[i][j]<<" ";
}
// cout<<endl;
}
return num;
}
};
- 广度优先搜索
class Solution {
public:
int numIslands(vector<vector<char>>& grid) {
int nr = grid.size();
if (!nr) return 0;
int nc = grid[0].size();
int num_islands = 0;
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
if (grid[r][c] == '1') {
++num_islands;
grid[r][c] = '0';
queue<pair<int, int>> neighbors;
neighbors.push({r, c});
while (!neighbors.empty()) {
auto rc = neighbors.front();
neighbors.pop();
int row = rc.first, col = rc.second;
if (row - 1 >= 0 && grid[row-1][col] == '1') {
neighbors.push({row-1, col});
grid[row-1][col] = '0';
}
if (row + 1 < nr && grid[row+1][col] == '1') {
neighbors.push({row+1, col});
grid[row+1][col] = '0';
}
if (col - 1 >= 0 && grid[row][col-1] == '1') {
neighbors.push({row, col-1});
grid[row][col-1] = '0';
}
if (col + 1 < nc && grid[row][col+1] == '1') {
neighbors.push({row, col+1});
grid[row][col+1] = '0';
}
}
}
}
}
return num_islands;
}
};
10.课程表II(leetcode 210)
- 题目描述:
现在你总共有 n 门课需要选,记为 0 到 n-1。
在选修某些课程之前需要一些先修课程。 例如,想要学习课程 0 ,你需要先完成课程 1 ,我们用一个匹配来表示他们: [0,1]
给定课程总量以及它们的先决条件,返回你为了学完所有课程所安排的学习顺序。
可能会有多个正确的顺序,你只要返回一种就可以了。如果不可能完成所有课程,返回一个空数组。
示例 1:
输入: 2, [[1,0]]
输出: [0,1]
解释: 总共有 2 门课程。要学习课程 1,你需要先完成课程 0。因此,正确的课程顺序为 [0,1] 。
示例 2:
输入: 4, [[1,0],[2,0],[3,1],[3,2]]
输出: [0,1,2,3] or [0,2,1,3]
解释: 总共有 4 门课程。要学习课程 3,你应该先完成课程 1 和课程 2。并且课程 1 和课程 2 都应该排在课程 0 之后。
因此,一个正确的课程顺序是 [0,1,2,3] 。另一个正确的排序是 [0,2,1,3] 。
说明:
输入的先决条件是由边缘列表表示的图形,而不是邻接矩阵。详情请参见图的表示法。
你可以假定输入的先决条件中没有重复的边。
提示:
这个问题相当于查找一个循环是否存在于有向图中。如果存在循环,则不存在拓扑排序,因此不可能选取所有课程进行学习。
通过 DFS 进行拓扑排序 - 一个关于Coursera的精彩视频教程(21分钟),介绍拓扑排序的基本概念。
拓扑排序也可以通过 BFS 完成。
- 分析:(参考)
我们用 有向图 描述这种 依赖关系 (做事的先后关系):例如:n = 6, 先决条件表:[ [3, 0], [3, 1], [4, 1], [4, 2], [5, 3], [5, 4] ]
同时,有向图 中有 入度 和 出度 概念:如果存在一条有向边 A --> B,则这条边给 A 增加了 1 个出度,给 B 增加了 1 个入度
例如:0、1、2 没有依赖谁,入度为 0。 而 3、4、5 的 入度为 2
不能“跳步”,选你【能上的课】
- 当下只能选【入度为 0 的课】,因为它不依赖别的课。假设先选了 0
- 这导致 依赖 0 的课的入度减小 —— 3 的入度由 2 变 1
- 接着选 1,导致课 3 的入度变 0,课 4 的入度由 2 变 1
- 接着选 2,导致课 4 的入度变 0,当前 3 和 4 入度为 0
- 继续选【入度为 0 的课】……
- ……
- 直到选不到【入度为 0 的课】
这形似【树的BFS】
- 起初让【入度为 0 的课】入列
- 然后 逐个出列,课出列 = 课被选,减小相关课的入度
- 判定是否有 入度转 0 的课,继续入列、出列……
- 直到没有【入度为 0 的课】可入列……
BFS 前的准备工作
- 我们关心【每门课对应的入度】—— 它要被减,它要被监控
- 我们关心【课之间的依赖关系】—— 选这门课会减小哪些课的入度
- 因此我们需要 合适的数据结构,去存储这些关系
构建入度数组
- 每一门课都有一个动态变化的入度
- 课的编号是 0 到 n - 1,让它作为索引,选用 一维数组 存放 入度
- 遍历 先决条件表 (二维数组),计算每门课的初始入度
构建哈希表
- 我们选用 哈希表 即【相邻衔接表】来记录 依赖关系
- map 存什么键值对:
- 键: 课的编号
- 值: 依赖它的后续课程 ( list 数组)
- 比如:修完 2 才能修 4 和 5
- 2: [4, 5]
- 也可以用 邻接矩阵,但二维矩阵它有点大
BFS 思路
- queue 队列中始终是【入度为 0 的课】在里面流动
- 选择一门课,就让它 出列,同时 查看哈希表,看它 对应哪些后续课
- 将这些后续课的 入度 - 1,如果有 减至 0 的,就将它 推入 queue
- 不再有新的入度 0 的课入列 时,此时 queue 为空,退出循环
class Solution {
public:
vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
unordered_map<int,vector<int>> graph;
vector<int> indegree(numCourses,0);
for(auto p:prerequisites)
{
graph[p[0]].push_back(p[1]);
indegree[p[1]]++;
}
queue<int> q;
for(int i=0;i<indegree.size();i++)
{
if(indegree[i]==0)
q.push(i);
}
stack<int> order;
while(!q.empty())
{
int c=q.front();
q.pop();
order.push(c);
for(auto &pre:graph[c])
{
if(--indegree[pre]==0)
q.push(pre);
}
}
vector<int> res;
if(order.size()<numCourses) return res;
while(!order.empty())
{
res.push_back(order.top());
order.pop();
}
return res;
}
};