博客的目的:
博主最近在刷有关树的一些题目,遇到了很多需要递归,以及dfs(深度优先搜索的题目),但无奈大一数据结构只学了一点皮毛,最近实在忍不了,所以打算开一篇博文,记录有关dfs的理解心得,并持续更新:
对DFS的概念理解:
大致理解:也许不接触过DFS的同学会困惑,DFS是什么?我在查阅了众多博文资料总结了一下:DFS是一种搜索手段,他从某个状态开始,不断转移状态,直到无法转移,并且没有达成目的,就退回到前一步的状态,然后从其他没有尝试过的状态入手,直到达成目的,或者将所有的状态都尝试完。(上面是挑战算法程序这本书所给出的定义),尽管它所说的状态有点抽象但我们大致也可以知道,DFS的算法思路是将所有可能都尝试,当遇到不成立的路径时,它可以回退到前一种状态,从未尝试过的路径,继续;
CSDN上也有通俗的说法:一种用于遍历或搜索树或者图的算法,沿着树的深度去遍历素的节点,尽可能深的搜索树的分支,当节点v的所有边都已被探索过后,发现节点不满足条件,搜索将回溯到发现节点v的那条边的起始节点,,整个过程反复进行直到所有节点都被访问为止。
DFS的用途:
相信看了上面你大致理解了DFS是什么东西,可这东西到底有什么用?问的好:博主目前是为了刷题所以才了解的DFS,所以DFS对于我的用处就是刷题:什么类型的题?问得好,当你发现你刷到一种需要遍历所有图树的路径的题,应该就会用到DFS:
需要用到DFS的例题(来自力扣):
DFS在树中的一些应用:
例1:给你二叉树的根节点 root 和一个表示目标和的整数 targetSum ,判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。叶子节点 是指没有子节点的节点。
//深度优先遍历二叉树,每深入一次,sum-根节点的值,当到达叶子节点的时候,判断sum是否等于当前的节点值,如果等于,说明找到了,否则尝试另外一条路径
//谈谈这道题:深度优先遍历,其实现在仔细想想之前所做的递归的遍历的题的思路根这道题的深度度优先遍历思路很像不过之前的递归
//都是递归到最后,然后再回溯的过程中进行相应的操作,而这道题是在递推的过程中进行操作,照着这个思路应该就可以将代码写出来了
class Solution {
public boolean hasPathSum(TreeNode root, int targetSum) {
//判断所给树是否为空树,如果为空树,直接返回false
if(root==null){
return false;
}
//递归出口,,因为DFS是自顶向下的操作,所以当搜索到叶子节点后,回溯到前一个状态节点
if(root.left==null&&root.right==null){
return root.val==targetSum;
}
//深度优先搜索的递归过程,搜索过程的操作为targetSum-root.val
boolean pdl=hasPathSum(root.left, targetSum-root.val);
boolean pdr=hasPathSum(root.right, targetSum-root.val);
//返回值因为每个节点都有两个分支,也就是说每个节点下面都有两种状态,只要其中一种符合要求就能说明存在路径
return pdl||pdr;
}
}
}
}
新理解1:DFS算法在二叉树中好像是一个自顶向下的递归过程,并且一些相关的操作也是在递推过程中进行
例题2:给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。(力扣:235)
//利用搜索二叉树的性质,左子树的值小于根节点,根节点小于有子树的值所以我们可以以之为递归出口,从上往下进行DFS操作
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//深度搜索到叶子节点依旧没有找到结果的返回出口
if(root==null){
return null;
}
//找到结果后的返回出口
if(root.val>=p.val&&root.val<=q.val||root.val<=p.val&&root.val>=q.val){
return root;
}
//使用一个变量来保存结果
TreeNode pd=lowestCommonAncestor(root.left,p,q);
//判断是否找到了结果,如果没有找到回退到前一个root,并从零一个分支开始搜索,这里似乎还涉及了另一个语法,减枝
if(pd==null){
pd=lowestCommonAncestor(root.right,p,q);
}
return pd;
}
}
理解:似乎没什麽新的理解,如果你理解了上面的题,这题应该也没问题
例题3:给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。叶子节点 是指没有子节点的节点。(力扣:257. 二叉树的所有路径)
//同样是自顶而下的深度优先遍历,DFS,出口为该节点是否为叶子节点
//做完这道题后我对DFS以及递归的理解似乎更加深刻了一些:理解DFS似乎就是一种自顶而下递归,所以要理解DFS的同学必须好好理解
//递归的层次思想也就是栈结构
//其实对于这种返回值为List的递归函数我们一般可以自己写一个函数,将List返回值变为形参,这样函数写起来更易于理解
class Solution {
public List<String> binaryTreePaths(TreeNode root) {
//判断是否为空,避免出现异常
if(root==null){
return null;
}
//创建一个String变量,以及List作为形参
String path="";
List <String> list=new ArrayList <String>();
addpath(root,list,path);
return list;
}
//至于为什么可以想到将path作为形参,同样是有递归的层次结构所决定的,每层递归都有其相应的path方便添加路径
public void addpath(TreeNode root,List list,String path){
//判断是否为空,避免出现异常
if(root==null){
return ;
}
//这里是为了规范答案的输出形式
if(path==""){
path=path+String.valueOf(root.val);
}
else{
//更新每一层的path
path=path+"->"+String.valueOf(root.val);
}
//递归出口,一旦搜索到了叶子节点就将该层次的path添加到List中
if(root.left==null&&root.right==null){
list.add(path);
return;
}
//不断递归,深度优先搜索
addpath(root.left,list,path);
addpath(root.right,list,path);
}
}
新理解3:独立做完这道题后我对DFS以及递归的理解似乎更加深刻了一些:理解DFS似乎就是一种自顶而下递归,所以要理解DFS的同学必须好好理解,递归的层次思想也就是栈结构,同样我也产生了一个困惑我目前所理解的到底是不是DFS,还是说我目前所理解的只是简单的递归?望路过的高手解惑
例题4:给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。(力扣:543)
//主要思路就是求出每个节点的最长路径,然后每次递归都将目前的最长路径与当前节点的路径进行比较从而不断更新最长路径,思路是理解了但是要写出这个递归还是很有难度
//通过这道题我们可以学到什么:1.递归过程中我们要做什么:找出节点的最长路径
// 2.在什么地方做
//3.有关递归过程中返回值的理解:即使这个函数是有返回值的,但是我们没必要一看到有返回值就reteun 函数;我们只要在改层函数中有返回值就行
class Solution {
int max=0;
public int diameterOfBinaryTree(TreeNode root) {
//判断根节点是否为null避免出现异常
if(root==null){
return 0;
}
dfs(root);
return max;
}
//为什么要额外写一个递归函数,因为我们每层递归返回的值为该点的深度,而题目要求的是最长路径,所以我们递归的最终的返回值也不是所求
public int dfs(TreeNode root) {
//递归的出口
if(root==null){
return 0;
}
//下面是深度搜索的过程
int left= dfs(root.left);
int right= dfs(root.right);
//每搜索完一个节点就判断最长路径是否需要更新
max=Math.max(left+right,max);
//返回值为该层节点的深度,方便上一层递归的比较
return Math.max(left,right)+1;
}
}
新理解:DFS就是一种先搜索然后不断回退的思想,我管你是自顶向下操作,还是自下向上操作,只要有搜索然后不断回退的思想,那它就是DFS
例题4:给定一个二叉树,计算 整个树 的坡度 。一个树的 节点的坡度 定义即为,该节点左子树的节点之和和右子树节点之和的 差的绝对值 。如果没有左子树的话,左子树的节点之和为 0 ;没有右子树的话也是一样。空结点的坡度是 0 。整个树 的坡度就是其所有节点的坡度之和。(力扣:563)
//同样利用DFS
class Solution {
int sum;
public int findTilt(TreeNode root) {
if(root==null){
return 0;
}
//有没有发现这个地方即使dfs是一个由返回值的函数,可即使我不用存储返回值也没由什么问题,之前陷入了一个误区
//该函数有返回值就一定要使用返回值
dfs(root);
return sum;
}
public int dfs(TreeNode root){
//同样的递归出口
if(root==null){
return 0;
}
//深度搜索的过程,分别用两个变量将左右子树的节点值保存起来
int leftcout= dfs(root.left);
int rightcout= dfs(root.right);
//递归过程中需要进行的操作
sum=sum+Math.abs(leftcout-rightcout);
//将该节点及其左右子树的节点值之和作为该层递归的返回值
return leftcout+rightcout+root.val;
}
}
理解:这道题的思路与前面的例题差不多,但是结合两道理解可以更深刻的理解DFS中的操作以及DFS的返回值问题,建议理解不了的同学将这道题的思路看成自底向顶的递归过程
例题5:给你两棵二叉树 root 和 subRoot 。检验 root 中是否包含和 subRoot 具有相同结构和节点值的子树。如果存在,返回 true ;否则,返回 false 。
二叉树 tree 的一棵子树包括 tree 的某个节点和这个节点的所有后代节点。tree 也可以看做它自身的一棵子树。(力扣:572)
本人的解题思路:这道题十分考验你对DFS理解的深度,本人也是十分激动啊,花了半个多小时独立完成了这道题,首先这道题呢我是使用了两个DFS函数,一个是自底向上的搜索过程,一个是自顶向下的判断结构是否相同的函数。首先是在root中自下向上搜索与subRoot根节点的值相等的节点,一定那找到这写节点就调用函数判断该节点所在的子树的结构与subRoot树的结构是否相同,细节在代码中会有讲解
class Solution {
//使用一个全局变量,对结构的判断结果进行保存
boolean bool;
public boolean isSubtree(TreeNode root, TreeNode subRoot) {
//下面的两个语句都是对根节点进行判断,避免出现异常
if(root==null&&subRoot==null){
return true;
}
if(root==null&&subRoot!=null||root!=null&&subRoot==null){
return false;
}
//调用搜索函数
dfs(root, subRoot);
return bool;
}
//首先写一个DFS遍历函数,在root中寻找与snbRoot数值相等的节点,一旦找到该节点,调用判断函数
public void dfs(TreeNode root, TreeNode subRoot){
//搜索过程的出口
if(root==null){
return ;
}
dfs(root.left,subRoot);
//下面的语句十分重要,我就是在这里卡了十几分钟,避免多次判断,而导致已经判断出来的true被覆盖,
//我在前面就说过一定要明白每层递归函数都做了什么
if(bool==true){
return;
}
if(root.val==subRoot.val){
//调用结构判断函数,并保存结果
bool=pd(root,subRoot);
}
if(bool){
return;
}
dfs(root.right,subRoot);
}
//判断结构是否相同的函数
public boolean pd(TreeNode root, TreeNode subRoot){
if(root==null&&subRoot==null){
return true;
}
if(root==null&&subRoot!=null||root!=null&&subRoot==null){
return false;
}
boolean pd1= root.val == subRoot.val;
boolean pd2= pd(root.left, subRoot.left);
boolean pd3= pd(root.right, subRoot.right);
return pd1&&pd2&&pd3;
}
}
理解:没什麽新的理解,但这到题可以加深你对DFS的理解
例题六:给你二叉树的根节点 root 和一个整数目标和 targetSum ,找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。叶子节点 是指没有子节点的节点。(力扣:113)
//典型的dfs算法,这道题的难点在于如何设计递归函数,其次就是要理解List是引用类型,在递归过程中始终是同一List,即List贯穿所有层次
//即使你将其加入集合后,你对其进行更改,你加入集合中的那个List也会改变,所以你要设计一个新的集合复制那时的List并将其加入listsum,
//还有一个问题就是list始终是同一个list所以你在回溯过程中,要将在该层次所加入的节点删除
class Solution {
List<List<Integer>> listsum =new ArrayList<List<Integer>>();
public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
//判断是否为空树,避免出现异常
if(root==null){
return listsum;
}
//使用一个集合来保存dfs过程中的路径
List<Integer> list=new ArrayList<Integer>();
//记录路径上的节点之和
int sum=0;
addpath(root,list,sum,targetSum);
return listsum;
}
public void addpath(TreeNode root,List list,int sum,int targetSum){
//递归出口,这是是不用删除元素的因为,当该节点为null时是没有往list中添加元素的我就犯了这个错误
if(root==null) {
//list.remove(list.size()-1);
return;
}
if(root.left==null&&root.right==null){
//当该节点为叶子节点时要额外将该节点的值,及该节点加入list
sum=sum+root.val;
list.add(root.val);
if(sum==targetSum)
{
//用一个额外的集合来保存当时的值
List<Integer> list1=new ArrayList<Integer>();
list1.addAll(list);
//list是引用类型,如果直接加入的话,从始至终加入的都是那一个List。
//listsum.add(new ArrayList<>(list));
listsum.add(list1);
}
//将该层所加入的值删除
list.remove(list.size()-1);
return;
}
sum=sum+root.val;
list.add(root.val);
//对左右子树进行深度搜索
addpath(root.left,list,sum,targetSum);
addpath(root.right,list,sum,targetSum);
//将该层所加入的值删除
list.remove(list.size()-1);
}
}
新理解:List是引用类型,它不像sum这些局部变量,每层递归都是一个新的变量,它是贯穿所有的递归层次的,其次就是如何设计出一个递归函数,这在代码中又详细的解释,这里不再多说
前言:一道大哥级别的题目对于理解递归的返回值,深度优先搜索的理解很有帮助
**例题7:**给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”(力扣:236)
细节在代码中:
//大体思路自下往上的进行dfs,也就是说使用后序遍历,如果该节点满足条件就将该节点返回,我大体思路可以所是对的但是对dfs的理解不够深
//好好理解,设置两个全局变量会出错
/**
注意p,q必然存在树内, 且所有节点的值唯一!!!
递归思想, 对以root为根的(子)树进行查找p和q, 如果root == null || p || q 直接返回root
表示对于当前树的查找已经完毕, 否则对左右子树进行查找, 根据左右子树的返回值判断:
1. 左右子树的返回值都不为null, 由于值唯一左右子树的返回值就是p和q, 此时root为LCA
2. 如果左右子树返回值只有一个不为null, 说明只有p和q存在与左或右子树中, 最先找到的那个节点为LCA
3. 左右子树返回值均为null, p和q均不在树中, 返回null
**/
class Solution {
//创建一个全局变量,来保存符合条件的节点
// TreeNode left=null;
// TreeNode right=null;
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//递归出口
if(root==null){
return null;
}
//如果该节点等于p或q返回上一层,并从该层的右子树查找p q
if(root==p||root==q){
return root;
}
//进行后序遍历,遍历到二叉树的最底层,并保存在遍历过程中所找到的节点
TreeNode left=lowestCommonAncestor(root.left,p, q);
TreeNode right=lowestCommonAncestor(root.right,p, q);
//按我的思路改变以下将量保存节点的变量设置为全局变量
// left=lowestCommonAncestor(root.left,p, q);
// right=lowestCommonAncestor(root.right,p, q);
if(left!=null&&right!=null){
//如果left!=null并且right!=null说明该节点就是p q的祖先节点,又因为是后序遍历所以层次已经为最深
//只需一直返回root即可
return root;
}
else if(left!=null){
return left;
}
else if(right!=null){
return right;
}
return null;
}
}
另外代码中也注释了本人一些错误的思路
例题8:给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值 。
前言:其实我知道这题之前是做过的,我也知道大致的思路,可尽然还是浪费了一定的时间,浪费在哪?就是对于该层次下需要做的事情在什么时间做?还是有点欠缺,所以再次将这道题记录下来
代码及细节如下:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
//看到这道题就来了两种思路:1.使用全局变量保存前一个节点,在递归过程中相减,并记录下最小差值
// 2.先进行层次遍历将遍历结果保存在数组中,在迭代一次找出最小差值
//我这种高手当然选择第一种方法
class Solution {
//创建一个变量保存最小差值,并将初值设置为最大的整数
int min=Integer.MAX_VALUE;
//设置一个变量来保存前一个节点
TreeNode pre;
public int minDiffInBST(TreeNode root) {
dfs(root);
return min;
}
//设置一个深度递归函数
public void dfs(TreeNode root){
if(root==null){
return;
}
//先遍历到最左边的节点
dfs(root.left);
if(pre!=null){
min=Math.min(root.val-pre.val,min);
}
pre=root;
dfs(root.right);
}
}
例题9:给定一个 N 叉树,返回其节点值的前序遍历 。
N 叉树在输入中按层序遍历进行序列化表示,每组子节点由空值 null 分隔(请参见示例)。
前言:n插树的前序遍历我没做过,这可怎么办?我只做过二叉树的前序遍历,那不就完事了,二叉树的前序遍历的核心思想不就是,先根节点,然后访问左子树,最后右子树,n叉树不就是先访问根节点,然后访问第一个孩子节点,然后第二个,以此类推:怎么实现;
在这时候我们就要在二叉树dfs思想上拓展一下,其实差不多都是不断搜索回退的过程,唯一的区别就是多了几个子节点
那问题不大我们只要把{dfs(root.left);dfs(root.right)}改为对它的每个孩子节点都dfs(root.child)就完事了
代码及细节如下:
/*
// Definition for a Node.
class Node {
public int val;
public List<Node> children;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
};
*/
//兄弟们,最近可能是一个思维大爆炸的时期,先是dfs,bfs延展到数组,然后又是从二叉树延展到n叉树
//还好在二叉树那边打下了基础
//在这时候我们就要在二叉树dfs思想上拓展一下,其实差不多都是不断搜索回退的过程,唯一的区别就是多了几个子节点
//那问题不大我们只要把{dfs(root.left);dfs(root.right)}改为对它的每个孩子节点都dfs(root.child)就完事了
//如何使每个孩子都dfs,循环不就完事了,我们是java,那就更方便了。直接foreach就完事了,反正他的孩子节点已经有序的
//放在集合中,思路大概就是这样动手:
class Solution {
//创建一个集合保存遍历结果
List<Integer> list=new ArrayList <Integer>();
public List<Integer> preorder(Node root) {
//避免异常的产生
if(root==null){
return list;
}
dfs(root);
return list;
}
//创建一个深度优先搜索函数
public void dfs(Node root){
if(root==null){
return;
}
list.add(root.val);
for(Node child:root.children){
dfs(child);
}
}
}
例题10:给定一个非空特殊的二叉树,每个节点都是正数,并且每个节点的子节点数量只能为 2 或 0。如果一个节点有两个子节点的话,那么该节点的值等于两个子节点中较小的一个。
更正式地说,root.val = min(root.left.val, root.right.val) 总成立。
给出这样的一个二叉树,你需要输出所有节点中的第二小的值。如果第二小的值不存在的话,输出 -1 。(力扣:671)
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
//讲真的这种题,我都不屑于做了
class Solution {
//设置两个全局变量保存最小值,以及第二小值,并将初值设置为-1
int min=Integer.MAX_VALUE;
//这里为什么设置为Long.MAX_VALUE因为例子卡我边界
long min2=Long.MAX_VALUE;
public int findSecondMinimumValue(TreeNode root) {
if(root==null){
return 0;
}
dfs(root);
if(min2==Long.MAX_VALUE){
return -1;
}
return (int)min2;
}
//进行深度优先遍历
public void dfs(TreeNode root){
if(root==null){
return ;
}
if(root.val<min){
min=root.val;
}
if(root.val>min&&root.val<min2){
min2=root.val;
}
dfs(root.left);
dfs(root.right);
}
}
例题11:给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”(剑指 Offer 68 )
差点没做出来
代码及思路:
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode(int x) { val = x; }
* }
*/
//君子报仇十年不晚,上次惨败于你手,这次我必将你首刃
//这道题的大致思路是:
//1.首先他要找的是:最近的公共祖先所以我们的总体思路肯定就是自底向上的深度优先搜索
//如何判断p q是否存在,我们可以每个层次都使用一个left和right来保存p q
//因为是自下往上的搜索,所以如果某节点的left和right都存在,那他肯定就是最近的公共祖先在
//如果返回过程中left一直为空,就说明最近共同祖先在right那边
//思路都整理到这了,这次不血刃你真的没天理
class Solution {
public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
//首先设置两个变量来记录p 和 q
TreeNode left=null;
TreeNode right=null;
//设置后深度优先遍历的出口
if(root==null){
return null;
}
if(root==p||root==q){
return root;
}
//因为是自下向上,所以先遍历到底部
left=lowestCommonAncestor(root.left,p, q);
right=lowestCommonAncestor(root.right,p, q);
//每层递归都要判断p q是否存在
if(left!=null&&right!=null){
return root;
}
if(left==null&&right!=null){
return right;
}
if(left!=null&&right==null){
return left;
}
return null;
}
}
例题12:给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
首先找到需要删除的节点;
如果找到了,删除它。
说明: 要求算法时间复杂度为 O(h),h 为树的高度。(力扣:450)
理解:其实这道题的每一部分并不难,难就难在它要讨论的情况很多
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
class Solution {
//建立一个哨兵节点,可以避免额外讨论根节点的特殊情况
TreeNode nroot=new TreeNode();
//使用一个全局变量来保存要删除的节点的父节点的位置
TreeNode parent;
public TreeNode deleteNode(TreeNode root, int key) {
nroot.right=root;
parent=nroot;
TreeNode md=dfs(root,key);
//如果树中没有该节点直接返回root
if(md==null){
return root;
}
//当删除的节点为前三种情况直接调用删除函数,当为第四种情况
if(md.left!=null&&md.right!=null){
//当为第四种情况时,我们要找到被删除节点的右子树的,然后删除该节点并将其覆盖到当前要被删除的节点
//找到left节点及其父节点,直接使用全局变量parent即可
parent=md;
TreeNode left=four(md.right);
//保存好left的值
int temp=left.val;
//删除left节点
delete(left);
//将md节点的值使用temp覆盖
md.val=temp;
}
else{
delete(md);
}
return nroot.right;
}
//使用dfs在二插树中搜索要删除的节点以及它的父节点
public TreeNode dfs(TreeNode root, int key){
TreeNode md=null;
if(root==null){
return null;
}
if(root.val==key||md!=null){
return root;
}
parent=root;
if(key>root.val){
md=dfs(root.right,key);
}
if(key<root.val){
md=dfs(root.left,key);
}
return md;
}
//删除节点的算法:
// 1.当该节点的左右节点为空时,直接删除该节点
// 2.当该节点的左节点不为空,右节点为空时,用该节的的左节点覆盖该节点
// 3.当该节点的右节点不为空,左节点为空时,用该节的的右节点覆盖该节点
// 4.当左右节点都不为空
public void delete(TreeNode md){
// 1.当该节点的左右节点为空时,直接删除该节点
if(md.left==null&&md.right==null){
//判断要删除的节点时父节点的那边节点
if(parent.left==md){
parent.left=null;
}
if(parent.right==md){
parent.right=null;
}
}
// 2.当该节点的左节点不为空,右节点为空时,用该节的的左节点覆盖该节点
if(md.left!=null&&md.right==null){
//判断要删除的节点时父节点的那边节点
if(parent.left==md){
parent.left=md.left;
}
if(parent.right==md){
parent.right=md.left;
}
}
// 3.当该节点的右节点不为空,左节点为空时,用该节的的右节点覆盖该节点
if(md.left==null&&md.right!=null){
//判断要删除的节点时父节点的那边节点
if(parent.left==md){
parent.left=md.right;
}
if(parent.right==md){
parent.right=md.right;
}
}
}
//针对第四种情况的算法:寻找被删除节点的右子树的最左边节点
public TreeNode four(TreeNode root1){
while(root1.left!=null){
parent=root1;
root1=root1.left;
}
return root1;
}
}
例题13: 给你一个二叉树的根结点,请你找出出现次数最多的子树元素和。一个结点的「子树元素和」定义为以该结点为根的二叉树上所有结点的元素之和(包括结点本身)。
你需要返回出现次数最多的子树元素和。如果有多个元素出现的次数相同,返回所有出现次数最多的子树元素和(不限顺序)。(力扣:508)
/**
* Definition for a binary tree node.
* public class TreeNode {
* int val;
* TreeNode left;
* TreeNode right;
* TreeNode() {}
* TreeNode(int val) { this.val = val; }
* TreeNode(int val, TreeNode left, TreeNode right) {
* this.val = val;
* this.left = left;
* this.right = right;
* }
* }
*/
// 第一感觉:递归得嵌套使用,也就是双重dfs
class Solution {
//创建一个集合保存所有得节点得子树和
List<Integer> list=new ArrayList<Integer>();
// 设置一个变量保存出现最多得次数
int max=1;
// 创建一个Map子树和以及它出现得次数联系起来
Map<Integer,Integer> map=new HashMap<Integer,Integer>();
//用一个变量来保存每个子树的和
int sum=0;
public int[] findFrequentTreeSum(TreeNode root) {
if(root==null){
return new int[0];
}
dfs(root);
// 获取所有得键
Set<Integer> set=map.keySet();
// 将出现次数最多的sum放进list集合里保存
for(Integer i:set){
if(map.get(i)==max){
list.add(i);
}
}
// 将list集合中得元素转化为数组
int[] a=new int[list.size()];
for(int i=0;i<list.size();i++){
a[i]=list.get(i);
}
return a;
}
// 用于遍历所有节点
public void dfs(TreeNode root){
if(root==null){
return;
}
sum=0;
addsum(root);
//如果map中已经含有sum将该sum的value值加一
if(map.containsKey(sum)){
int temp=map.get(sum);
map.put(sum,temp+1);
if(temp+1>max){
max=temp+1;
}
}
//首次将sum加进map
if(!map.containsKey(sum)){
map.put(sum,1);
}
dfs(root.left);
dfs(root.right);
}
// 用于求子树和
public void addsum(TreeNode root){
if(root==null){
return;
}
sum=sum+root.val;
addsum(root.left);
addsum(root.right);
}
}
例题14:给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。假设二叉树中至少有一个节点。(力扣:513)
理解:看到题目的第一思路:最深是吧不就是求二叉树的深度吗?所以先求根节点左子树的深度与右子树的深度进行比较,然后再较深的那一边求出最左边的节点,最左边的节点这不简单?不就是层次遍历最后一层的第一个节点吗?思路可行,开搞
// 第一思路:求二叉树的深度+前序遍历 即dfs+bfs
class Solution {
public int findBottomLeftValue(TreeNode root) {
// 避免异常的产生
if(root.left==null&&root.right==null){
return root.val;
}
TreeNode temp;
int deep=0;
// 判断根节点左子树高还是右子树高
int l=deep(root.left);
int r=deep(root.right);
if(l>=r){
temp=root.left;
deep=l;
}
else{
temp=root.right;
deep=r;
}
//编写一个求最左边节点的函数,我想了一想好像使用广度优先遍历求解比较稳妥
// 创建一个队列来使用广度优先遍历
Queue<TreeNode> queue=new LinkedList<TreeNode>();
// 将根节点入队列
queue.offer(temp);
// 进行广度优先遍历
while(!queue.isEmpty()){
// 没进行一次层次遍历,deep减一
deep--;
// 如果deep==0说明到达了最底层,跳出循环
if(deep==0){
break;
}
int cout=queue.size();
for(int i=0;i<cout;i++){
TreeNode node=queue.poll();
// 将当前节点的左右节点入队列
if(node.left!=null){
queue.offer(node.left);
}
if(node.right!=null){
queue.offer(node.right);
}
}
}
// 跳出循环后队列中就只剩下0层的元素,此时队列中的第一个元素就为最左边元素
return queue.poll().val;
}
// 编写一个求二叉树深度的函数
public int deep(TreeNode root){
if(root==null){
return 0;
}
int lh=deep(root.left);
int rh=deep(root.right);
return Math.max(lh,rh)+1;
}
}
例题15:给定一个二叉树,根节点为第1层,深度为 1。在其第 d 层追加一行值为 v 的节点。
添加规则:给定一个深度值 d (正整数),针对深度为 d-1 层的每一非空节点 N,为 N 创建两个值为 v 的左子树和右子树。
将 N 原先的左子树,连接为新节点 v 的左子树;将 N 原先的右子树,连接为新节点 v 的右子树。
如果 d 的值为 1,深度 d - 1 不存在,则创建一个新的根节点 v,原先的整棵树将作为 v 的左子树。(力扣:623)
理解:有难度的一题,不过细节都在代码中了,这里不多讲了
// 这道题又点难度:第一时间我都不知道如何下手,看了评论有了一些思路自顶向下的深度优先遍历
// 实际上这道题就是二叉树的路径问题
// 对于每条路径上到达指定的深度后都插入一个值为1的节点
// 要注意一些甚么?因为要插入节点,所以要记录下前一个节点的位置
class Solution {
int val;
int depth;
public TreeNode addOneRow(TreeNode root, int val, int depth) {
// 创建一个哨兵节点,就不需要额外讨论根节点
TreeNode head=new TreeNode();
head.left=root;
// 为全局变量赋值
this.val=val;
this.depth=depth;
dfs( root,head,1);
return head.left;
}
// 创建一个深度优先遍历的函数
public void dfs(TreeNode root,TreeNode pre,int deep){
if(deep==depth){
TreeNode temp=new TreeNode(val);
if(pre.left==root){
temp.left=root;
temp.right=null;
pre.left=temp;
// 这里一定要加上return,,不能在下面注释处加,不然当root为根节点是会改变两次root.right
return;
}
if(pre.right==root){
temp.right=root;
temp.left=null;
pre.right=temp;
return;
}
// return;
}
if(root==null){
return;
}
dfs(root.left,root,deep+1);
dfs(root.right,root,deep+1);
}
}
例题15:给定一棵二叉树,返回所有重复的子树。对于同一类的重复子树,你只需要返回其中任意一棵的根结点即可。
两棵树重复是指它们具有相同的结构以及相同的结点值。(力扣:652)
第一思路:dfs+HashMap,将子树转化形成的字符串与它出现的次数建立联系,并将出现次数为2的根节点保存起来,好像还涉及了一个什么二叉树序列化的概念
// 第一次接触到二叉树序列化这个概念:简单来说就是将二叉树转化为字符串
// 那么这道题的思路:将树中的所有子树都序列化并将每个序列出现的次数与该序列建立联系
// 所以大致算法的组成就是双重dfs+hashMap
class Solution {
// 建立一个HashMap来保存序列与次数之间的关系
Map<String,Integer> map=new HashMap<String,Integer>();
String str;
// 创建一个list来保存重复子树的根节点
List<TreeNode> list=new ArrayList<TreeNode>();
public List<TreeNode> findDuplicateSubtrees(TreeNode root) {
// 避免异常的产生
if(root==null){
return list;
}
dfs(root);
return list;
}
// 建立一个dfs遍历每个节点
public void dfs(TreeNode root){
if(root==null){
return;
}
str="";
dfs1(root);
Integer temp=map.get(str);
if(map.containsKey(str)){
if(temp==1){
// 同时将该子树的根节点加入List
list.add(root);
}
map.put(str,temp+1);
}
if(!map.containsKey(str)){
map.put(str,1);
}
dfs(root.left);
dfs(root.right);
}
// 建立一个dfs将该二叉树转化为字符串
public void dfs1(TreeNode root){
if(root!=null){
// 避免出现1,11这种搞人心态的节点,在每个节点值前加上“,”分隔
str=str+","+String.valueOf(root.val);
}
if(root==null){
str=str+","+"#";
return ;
}
dfs1(root.left);
dfs1(root.right);
}
}
例题16:树可以看成是一个连通且 无环 的 无向 图。
给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。
请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的边。(力扣:684)
做题过程中的收获:
- 这道题最让我困惑的地方就在于复原visited数组,与全排列的时候有点不一样,全排列是不断的对同一数组进行递归,而者这道题是邻接表,而且这道题是遇到相同的直接return
- 其次就是队列的api的使用问题:list.get(),如果直接在方框里面写数字会被当做索引,如果向要取具体的某一项应该将其转化为对象
// 第一思路:首先使用邻接表将图中的关系保存好,然后将两个节点直接相连的边去掉
// 如果依旧能从边的以端访问到另一端则说明这条边是重复的
class Solution {
// 创建一个全局的邻接矩阵保存节点之间的关系
List<List <Integer>> list=new ArrayList<List <Integer>>();
boolean pd=false;
public int[] findRedundantConnection(int[][] edges) {
// 为邻接表创建空间,因为它的节点是从以开始的所以要多创建一个空间
for(int j=0;j<=edges.length;j++){
list.add(new ArrayList<Integer>());
}
// 将图中的关系保存到邻接矩阵中去
for(int i=0;i<edges.length;i++){
// 注意边的关系是双向的所以要添加两次
// 将边的关系保存到数组中去
list.get(edges[i][0]).add(edges[i][1]);
list.get(edges[i][1]).add(edges[i][0]);
}
// 创建一个数组俩记录已经访问过的节点
int[] visited=new int[edges.length+1];
// 从后往前遍历,将两节点直接相连的边进行删除
// 这样最先找处的边就是最后添加的边
for(int i=edges.length-1;i>=0;i--){
// 将start包装成对象避免其认为为index
int start=edges[i][0];
int end=edges[i][1];
list.get(start).remove(new Integer(end));
list.get(end).remove(new Integer(start));
// 判断删除直接俄相连的边后,判断两节点是否依然相连
dfs(visited,start,end);
if(pd){
// 如果相连那么连接的两个节点就是多余的边
int[] result=new int[2];
result[0]=start>end?end:start;
result[1]=start<end?end:start;
return result;
}
// 将list集合复原方便下一次的深搜
list.get(start).add(end);
list.get(end).add(start);
}
int[] a=new int[2];
return a;
}
// 创建一个深搜函数
public void dfs(int[] visited,int start,int end){
// 深搜的出口
if(visited[start]==1){
return;
}
// 将该节点标记为已访问
visited[start]=1;
List<Integer> temp=list.get(start);
for(int a:temp){
if(a==end){
pd=true;
return;
}
// 深搜
dfs(visited,a,end);
}
// 返回是上一层之前先将visit数组复原
visited[start]=0;
}
}
DFS在数组中的一些应用:
例题1:有一幅以二维整数数组表示的图画,每一个整数表示该图画的像素值大小,数值在 0 到 65535 之间。
给你一个坐标 (sr, sc) 表示图像渲染开始的像素值(行 ,列)和一个新的颜色值 newColor,让你重新上色这幅图像。
为了完成上色工作,从初始坐标开始,记录初始坐标的上下左右四个方向上像素值与初始坐标相同的相连像素点,接着再记录这四个方向上符合条件的像素点与他们对应四个方向上像素值与初始坐标相同的相连像素点,……,重复该过程。将所有有记录的像素点的颜色值改为新的颜色值。最后返回经过上色渲染后的图像。
理解:其实DFS在数组中的应用的思路也是层次思想,并没有其他的一些复杂的东西,只要理解出口大概就能做出这道题目
代码及细节如下:
//一开始我是真的没想到深度优先遍历可以在数组中应用,不过一旦你理解递归的层次思想理解这个也不难
//解开这道题的关键是要理解深度优先遍历的出口
class Solution {
public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
int color=image[sr][sc];
dfs(image, sr, sc,color,newColor);
return image;
}
//编写一个递归函数
public void dfs(int[][] image, int sr, int sc, int color,int newColor) {
//递归出口:1.超出2维度数组的范围 2.该位置的原来渲染值为不需要渲染(第三种值) 3.该节点已经渲染为新颜色了
if(sr<0||sc<0||sr>=image.length||sc>=image[0].length||image[sr][sc]!=color&&image[sr][sc]!=newColor||image[sr][sc]==newColor){
return ;
}
//在该层次下需要完成的事情
image[sr][sc]=newColor;
//王该节点的四个方向进行dfs
dfs(image, sr+1, sc,color,newColor);
dfs(image, sr, sc-1,color,newColor);
dfs(image, sr, sc+1,color,newColor);
dfs(image, sr-1, sc,color,newColor);
}
}
例题2:小朋友 A 在和 ta 的小伙伴们玩传信息游戏,游戏规则如下:
1.有 n 名玩家,所有玩家编号分别为 0 ~ n-1,其中小朋友 A 的编号为 0
每个玩家都有固定的若干个可传信息的其他玩家(也可能没有)。传信息的2.关系是单向的(比如 A 可以向 B 传信息,但 B 不能向 A 传信息)。
每轮信息必须需要传递给另一个人,且信息可重复经过同一个人
3.给定总玩家数 n,以及按 [玩家编号,对应可传递玩家编号] 关系组成的二维数组 relation。返回信息从小 A (编号 0 ) 经过 k 轮传递到编号为 n-1 的小伙伴处的方案数;若不能到达,返回 0。(力扣:LCP 07. 传递信息)
前言:这道题有两种解法:1.基于邻接矩阵的广度优先遍历 2.基于邻接表的广度优先遍历,这道题对于我理解邻接矩阵与邻接表有很大的帮助,一些思想及细节都在代码里面这里就不再多说了:
代码及细节如下:
//第一种方法:基于邻接矩阵的深度优先遍历
//这题好像是邻接矩阵的深度优先遍历的题型,要解决的问题有两个:1:将他们之间的关系存储到邻接矩阵中去
// 2.理解邻接矩阵中的广度优先搜索
class Solution {
//设置一个全局变量保存结果
int path=0;
public int numWays(int n, int[][] relation, int k) {
//设置一个二维数组作为邻接,将关系放进邻接矩阵中去,
int[][] a=new int[n][n];
//将关系用邻接矩阵保存起来,如果他们之间存在关系就将值设为1
for(int i=0;i<relation.length;i++){
a[relation[i][0]][relation[i][1]]=1;
}
int start=0;
dfs(n, a, k,start);
return path;
}
//创建一个深度优先搜索函数
public void dfs(int n, int[][] a, int k,int start){
//好像也没有越界的机会
//理解递归函数的出口
if(k==0){
if(start==n-1){
path++;
}
return;
}
//现在邻接矩阵中找到与起点有关系的第一个节点,然后深度优先搜索:一条路走到底
//不愧为暴力搜索,果真牛
for(int i=0;i<n;i++){
if(a[start][i]==1){
dfs(n,a,k-1,i);
}
}
}
}
//第二种方法:基于邻接表的深度优先遍历,也是官方的解法
class Solution {
//将邻接表也设为全局变量
List<ArrayList<Integer>> list;
//创建一个全局变量来记录方法的个数
int path=0;
//因为n和k的值遍历过程中基本没有变化所以可以将其变为全局变量
int n,k;
public int numWays(int n, int[][] relation, int k) {
this.n=n;
this.k=k;
//第一步创建一个邻接表,官方的实现方法是通过集合嵌套的方法实现的
//由于我也是第一次见到,只能说学到了
list=new ArrayList <ArrayList<Integer>> ();
//为上面所创建的邻接表赋予空间,上面只不过是创建另一个变量罢了
//邻接表的层数就是图中的元素个数,也就是题目中的小伙伴的个数
for(int i=0;i<n;i++){
list.add(new ArrayList<Integer>());
}
//官解中使用的是foreach,为了更好理解我直接改成了for循环
//为邻接表赋值,将与0 1 2 3..有关系的节点添加到相应的列表中去
for(int i=0;i<relation.length;i++){
list.get(relation[i][0]).add(relation[i][1]);
}
dfs(0,0);
return path;
}
public void dfs(int start,int step){
//出口
if(step==k){
if(start==n-1){
path++;
}
return;
}
//将start所对应的行取出来,方便后面取第一个元素
List<Integer> temp=list.get(start);
for(int next:temp){
dfs(next,step+1);
}
}
}
例题三:给定一个 row x col 的二维网格地图 grid ,其中:grid[i][j] = 1 表示陆地, grid[i][j] = 0 表示水域。
网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。
岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。(力扣:463)
// 如何在 DFS 遍历时求岛屿的周长
// 求岛屿的周长其实有很多种方法,如果用 DFS 遍历来求的话,有一种很简单的思路:岛屿的周长就是岛屿方格和
//非岛屿方格相邻的边的数量。注意,这里的非岛屿方格,既包括水域方格,也包括网格的边界。我们可以画一张图,看得更清晰:
//思路如果要使用深度优先遍历的话要1:确定好递归的出口问题
//2:避免在递归的过程中出现死循环的问题
//3.如何计算岛屿的周长问题
//4.解决深度优先遍历的起点问题
class Solution {
//创建一个变量来保存陆地空格的个数
int sum=0;
public int islandPerimeter(int[][] grid) {
//首先我们来解决深度优先遍历的起点问题
//我们可以使用双重循环来找一个值为1的点
int r=0;
int c=0;
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++){
//找到一个值为一的节点就行,找到了就跳出双重循环
if(grid[i][j]==1){
r=i;
c=j;
break;
}
}
if(grid[r][c]==1){
break;
}
}
if(r==0&&c==0&&grid[0][0]!=1){
return 0;
}
//以刚才找到的节点作为深度优先遍历的起点
dfs(grid,r,c);
return sum;
}
//创建一个基于二维数组的深度优先遍历
//dfs的思想是:向陆地空格的四个方向进行搜索
public void dfs(int[][] grid,int row ,int col){
//深度优先遍历的出口
//if(row<0||row>=grid.length||col<0||col>=grid[0].length||grid[row][col]!=1){
if(row<0||row>=grid.length||col<0||col>=grid[0].length||grid[row][col]==0){
//在返回之前先统计周长
//加上限制,避免遍历到之前遍历的陆地也统计边数
//加上限制会出错算了我直接将出口分成两部分算了,上面的注释是之前的出口
sum++;
return;
}
if(grid[row][col]==2){
return ;
}
//为了避免在深度优先遍历的过程中出现死循环,我们将遍历过程中的陆地节点标记为2
grid[row][col]=2;
//向四个方向进行搜索
dfs(grid,row+1 ,col);
dfs(grid,row,col-1);
dfs(grid,row,col+1);
dfs(grid,row-1 ,col);
}
}
例题4:给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围。(力扣:200)
//虽然这道题归类在了bfs中,但我第一感觉想要使用dfs
class Solution {
//使用一个全局变量,cout保存结果
int cout=0;
public int numIslands(char[][] grid) {
//首先在所给的二维数组中找到一块陆地,作为深度优先遍历的起始位置,并使用两个变量保存好
for(int i=0;i< grid.length;i++){
for(int j=0;j<grid[0].length;j++){
if(grid[i][j]=='1'){
//调用深度优先搜索函数,并且调用了多少次dfs就说明右多少片岛屿,因为每次调用dfs都会将相邻的'1',标记为'2'
cout++;
dfs(grid,i,j);
}
}
}
return cout;
}
//编写深度优先遍历函数
public void dfs(char[][] grid,int rstart,int cstart){
//深度优先遍历的出口
if(rstart<0||rstart>=grid.length||cstart<0||cstart>=grid[0].length||grid[rstart][cstart]!='1'){
return;
}
//将访问过的节点标记为2避免出现死循环
grid[rstart][cstart]='2';
//向当前位置的四个方向进行搜索
dfs(grid,rstart+1,cstart);
dfs(grid,rstart,cstart-1);
dfs(grid,rstart,cstart+1);
dfs(grid,rstart-1,cstart);
}
}
例题5:给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。
规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。
请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。(力扣:417)
// 写着广度优先遍历,实际上由是一道深度搜索的题目
//大致思路就是找到太平洋所能到达的地方记录下来,然后找到大西洋所能到达的点记录下来,然后找到重叠的点,且值都为1
//的点就是所求,看了一个评论区大佬的思路就是设置一个数组将太平洋能到达的地点加一,然后将大西洋能到达的地点在加一
//最后值为2的点就是所求,忽然想了想这种思路可能不太好,因为深度优先遍历过程中可能会重复遍历到某个点
// 还是设置两个数组的好
class Solution {
int min=Integer.MIN_VALUE;
// 创建一个二维数组记录海洋所能到达的点
int[][] preach;
int[][] areach;
// 创建一个集合保存结果
List<List<Integer>> list=new ArrayList <List<Integer>>();
public List<List<Integer>> pacificAtlantic(int[][] heights) {
// 分别创建两个数组记录两个海洋所能到达的陆地
preach=new int[heights.length][heights[0].length];
areach=new int[heights.length][heights[0].length];
//将太平洋边上的每个点都进行深搜
for(int i=0;i<heights[0].length;i++){
dfs(heights, preach,0,i, min);
}
for(int i=0;i<heights.length;i++){
dfs(heights, preach,i,0, min);
}
// 将大西洋边上的每个点都进行深搜
for(int i=0;i<heights[0].length;i++){
dfs(heights, areach,heights.length-1,i, min);
}
for(int i=0;i<heights.length;i++){
dfs(heights, areach,i,heights[0].length-1, min);
}
//创建一个集合保存坐标
List<Integer> temp;
//比较重复的点,且值为1的点,并将他们的坐标放进集合中去
for(int i=0;i<heights.length;i++){
for(int j=0;j<heights[0].length;j++){
if(preach[i][j]==1&&areach[i][j]==1){
temp=new ArrayList<Integer>();
temp.add(i);
temp.add(j);
list.add(temp);
}
}
}
return list;
}
// 创建一个深度优先搜索函数
public void dfs(int[][] heights, int[][] reach,int rstart,int cstart,int pre){
// 深度优先搜索的出口
// 当超出二维数组的范围时:rstart<0||cstart<0||rstart>=heights.length||cstart>=heights[0].length
// 当某个点的值必它本身要小的时候:heights[rstart][cstart]<pre
// 由或者该点已经被搜索过了 : reach[rstart][cstart]==1
if(rstart<0||cstart<0||rstart>=heights.length||cstart>=heights[0].length||heights[rstart][cstart]<pre||reach[rstart][cstart]==1){
return;
}
int temp=heights[rstart][cstart];
reach[rstart][cstart]=1;
// 想该点的四个方向进行搜索
dfs(heights, reach,rstart+1,cstart,temp);
dfs(heights, reach,rstart,cstart-1,temp);
dfs(heights, reach,rstart,cstart+1,temp);
dfs(heights, reach,rstart-1,cstart,temp);
}
}
例题5:让我们一起来玩扫雷游戏!
给定一个代表游戏板的二维字符矩阵。 ‘M’ 代表一个未挖出的地雷,‘E’ 代表一个未挖出的空方块,‘B’ 代表没有相邻(上,下,左,右,和所有4个对角线)地雷的已挖出的空白方块,数字(‘1’ 到 ‘8’)表示有多少地雷与这块已挖出的方块相邻,‘X’ 则表示一个已挖出的地雷。
现在给出在所有未挖出的方块中(‘M’或者’E’)的下一个点击位置(行和列索引),根据以下规则,返回相应位置被点击后对应的面板:
- 如果一个地雷(‘M’)被挖出,游戏就结束了- 把它改为 ‘X’。
- 如果一个没有相邻地雷的空方块(‘E’)被挖出,修改它为(‘B’),并且所有和其相邻的未挖出方块都应该被递归地揭露。
- 如果一个至少与一个地雷相邻的空方块(‘E’)被挖出,修改它为数字(‘1’到’8’),表示相邻地雷的数量。
- 如果在此次点击中,若无更多方块可被揭露,则返回面板。(力扣:529)
// 深度优先遍历:遍历该节点的上下左右及其边角的地方
//扫雷的大致规则:1.如果某格子四周都没有地雷就把改字符串改为'E'
// 2.如果四周如果有地雷则改为地雷的个数
// 3.如果无更多的方块可以挖出则返回数组
// 4.如果炸弹被点中直接结束函数,返回此时的数组
// 5.如果一个方块被点开了,就将与其相邻的方块都揭开,直到遇见数字,或者炸弹
//所以思路大致就出来了:出口:1.超出数组范围
// 2.遇到数字
// 3.或者是遇到已经翻开的
class Solution {
public char[][] updateBoard(char[][] board, int[] click) {
int r=click[0];
int c=click[1];
if(board[r][c]=='M'){
board[r][c]='X';
return board;
}
dfs(board,r,c);
return board;
}
// 创建一个深度优先遍历函数
public void dfs(char[][] board,int rowstart,int cstart){
// 递归的出口
if(rowstart<0||rowstart>=board.length||cstart<0||cstart>=board[0].length||board[rowstart][cstart]!='E'){
return ;
}
// 创建一个变量记录周围炸弹的个数
int cout=0;
// 判断周围炸弹的个数
if(rowstart+1>=0&&rowstart+1<board.length&&cstart>=0&&cstart<board[0].length&&board[rowstart+1][cstart]=='M'){
cout++;
}
if(rowstart-1>=0&&rowstart-1<board.length&&cstart>=0&&cstart<board[0].length&&board[rowstart-1][cstart]=='M'){
cout++;
}
if(rowstart>=0&&rowstart<board.length&&cstart+1>=0&&cstart+1<board[0].length&&board[rowstart][cstart+1]=='M'){
cout++;
}
if(rowstart>=0&&rowstart<board.length&&cstart-1>=0&&cstart-1<board[0].length&&board[rowstart][cstart-1]=='M'){
cout++;
}
if(rowstart+1>=0&&rowstart+1<board.length&&cstart+1>=0&&cstart+1<board[0].length&&board[rowstart+1][cstart+1]=='M'){
cout++;
}
if(rowstart+1>=0&&rowstart+1<board.length&&cstart-1>=0&&cstart-1<board[0].length&&board[rowstart+1][cstart-1]=='M'){
cout++;
}
if(rowstart-1>=0&&rowstart-1<board.length&&cstart-1>=0&&cstart-1<board[0].length&&board[rowstart-1][cstart-1]=='M'){
cout++;
}
if(rowstart-1>=0&&rowstart-1<board.length&&cstart+1>=0&&cstart+1<board[0].length&&board[rowstart-1][cstart+1]=='M'){
cout++;
}
// 根据炸弹的个数来更改当前节点的值
if(cout==0){
board[rowstart][cstart]='B';
// 想该点的四周进行深度优先遍历
// 注意一个十分重要的点就是只有该点周围没有炸弹才会向四周深搜
dfs(board,rowstart,cstart+1);
dfs(board,rowstart-1,cstart);
dfs(board,rowstart+1,cstart);
dfs(board,rowstart,cstart-1);
// 对角线
dfs(board,rowstart+1,cstart+1);
dfs(board,rowstart-1,cstart-1);
dfs(board,rowstart-1,cstart+1);
dfs(board,rowstart+1,cstart-1);
}
if(cout!=0){
board[rowstart][cstart]=(char)('0'+cout);
}
}
}
例题6 给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。(力扣:79)
// 思考了一下,应该能使用深搜解题
// 思路1:直接给它暴力干就完事了,先把所有的组合都找出来然后看看找出来的字符笑话中是否包含了word
// 思路2: 在搜索的过程中判断是否有相应的字母
// 先使用思路2:来试试
class Solution{
boolean pd=false;
char[][] board;
String word;
int row;
int col;
public boolean exist(char[][] board, String word) {
this.board=board;
this.word=word;
this.row=board.length;
this.col=board[0].length;
// 在board中寻找与word首字母相等的空格作为起点
for(int i=0;i<row;i++){
for(int j=0;j<col;j++){
if(board[i][j]==word.charAt(0)){
int[][] visited=new int[row][col];
dfs(i,j,visited,"",0);
if(pd){
return true;
}
}
}
}
return false;
}
// 创建一个深度优先遍历函数
public void dfs(int sr,int sc,int[][] visited,String str,int index){
//首先如果在搜索的过程中超出了数组的范围,直接返回上一层
if(sr<0||sr>=row||sc<0||sc>=col||visited[sr][sc]==1){
return ;
}
// 判断当前str的长度,如果>word.length也返回
if(index>=word.length()){
return;
}
// 如果当前空格的字符与word相对应的字符不相等直接返回
if(board[sr][sc]!=word.charAt(index)){
return ;
}
str=str+board[sr][sc];
if(str.length()==word.length()){
// 判断当前的字符串是否和word相等
if(str.equals(word)){
pd=true;
return ;
}
}
// 并记录下已经访问过的节点
visited[sr][sc]=1;
// 向当前节点的四个方向进行搜索
dfs(sr+1,sc,visited,str,index+1);
dfs(sr,sc-1,visited,str,index+1);
dfs(sr,sc+1,visited,str,index+1);
dfs(sr-1,sc,visited,str,index+1);
// 返回上以层之前先将本层标记的visited恢复原样
visited[sr][sc]=0;
}
}
例题7:有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。(力扣:547)
//都不知道什么是并查集,只能使用dfs来解题,总的思路就是不断深搜,然后将深搜过的关系去掉,进行了多少次深搜,就有多少条不连通的路径
class Solution {
int rlen;
int clen;
public int findCircleNum(int[][] isConnected) {
this.rlen=isConnected.length;
this.clen=isConnected[0].length;
// 创建一个变量来记录省份的数量
int sum=0;
/*
将对角线设置为零还是不妥,会将对角线上的一些孤立点也去点,
我笑了搞了半天原来不去掉也可以只要我将访问过的关系去掉即可
// 首先将对角线中1,赋值为0
for(int i=0;i<rlen;i++){
isConnected[i][i]=0;
}
// 对数组进行一波判断将对角线的值设置为0后,如果所有的值都为0,说明各个城市之间是独立的
boolean pd=false;
for(int i=0;i<rlen;i++){
for(int j=0;j<clen;j++){
if(isConnected[i][j]==1){
pd=true;
break;
}
if(pd) break;
}
}
if(!pd){
return rlen;
}
*/
// 如果数组中不全为0,则可以对其进行深搜
// 寻找不为0的节点进行深搜
for(int i=0;i<rlen;i++){
for(int j=0;j<clen;j++){
if(isConnected[i][j]==1){
// 每进行一次深搜sum的数量加一
sum++;
// 进行深搜
dfs(isConnected,i);
}
}
}
return sum;
}
// 创建一个深搜函数
public void dfs(int[][] isConnected,int sr){
// 每必要写出口,因为我的目的就只是为了将他相连的值改为0
for(int i=0;i<clen;i++){
if(isConnected[sr][i]==1){
// 将访问过的节点改为0,将访问过的关系断掉
isConnected[sr][i]=0;
isConnected[i][sr]=0;
// 继续沿一条路径进行深搜
dfs(isConnected,i);
}
}
}
}
例题8 :给你一个大小为 m x n 的二进制矩阵 grid 。
岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
岛屿的面积是岛上值为 1 的单元格的数目。
计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。(力扣:695)
// 这道题第一感觉深度优先遍历:思路:遍历二维数组,一旦找到土地立即使用深搜找出与该土地相连的土地
// 的数量,然后将访问过的陆地都设置为访问过的数字,以免对同一块陆地多次求值
class Solution {
// 创建一个全局变量来记录,最大的土地面积
int max=0;
// 创建一个变量来记录每次深搜得到的陆地的面积
int sum=0;
public int maxAreaOfIsland(int[][] grid) {
// 在二维数组中搜索未被访问过的土地
for(int i=0;i<grid.length;i++){
for(int j=0;j<grid[0].length;j++)
if(grid[i][j]==1){
// 每次访问前先将sum的值赋值为0
sum=0;
dfs(i,j,grid);
if(sum>max){
max=sum;
}
}
}
return max;
}
// 创建一个深度优先搜索函数,求该陆地的面积
public void dfs(int sr,int sc,int[][] grid){
// 出口
if(sr<0||sr>=grid.length||sc<0||sc>=grid[0].length){
return;
}
if(grid[sr][sc]==0){
return;
}
// 土地数量加一
sum++;
// 并且将访问过的土地的值设置为0,避免出现死循环,或者重复求值
grid[sr][sc]=0;
// 向土地的四个方向进行搜索
dfs(sr+1, sc, grid);
dfs(sr, sc-1, grid);
dfs(sr, sc+1, grid);
dfs(sr-1, sc, grid);
}
}
例题9: 给你一个大小为 m x n 的二进制矩阵 grid ,其中 0 表示一个海洋单元格、1 表示一个陆地单元格。
一次 移动 是指从一个陆地单元格走到另一个相邻(上、下、左、右)的陆地单元格或跨过 grid 的边界。返回网格中 无法 在任意次数的移动中离开网格边界的陆地单元格的数量。(力扣:1020)
// 看完题目的第一思路就是深搜:就是先将所有于边界1相连的陆地都记录,然后再去
// 寻找不与边界相邻的最大的陆地的深度
class Solution {
int[][] grid;
int row;
int col;
public int numEnclaves(int[][] grid) {
// 创建一个变量记录陆地的数量
int sum=0;
this.grid=grid;
this.row=grid.length;
this.col=grid[0].length;
// 开始搜索处于边界的1
for(int i=0;i<col;i++){
if(grid[0][i]==1){
dfs(0,i);
}
}
for(int i=0;i<col;i++){
if(grid[row-1][i]==1){
dfs(row-1,i);
}
}
for(int i=0;i<row;i++){
if(grid[i][0]==1){
dfs(i,0);
}
}
for(int i=0;i<row;i++){
if(grid[i][col-1]==1){
dfs(i,col-1);
}
}
// 遍历数组找出剩余的陆地的数量和
for(int i=1;i<row-1;i++){
for(int j=1;j<col-1;j++){
if(grid[i][j]==1){
sum++;
}
}
}
return sum;
}
// 创建一个深度优先搜索函数,将与边界1相邻的1都改为0
public void dfs(int srow,int scol){
// 出口
if(srow<0||srow>=row||scol<0||scol>=col||grid[srow][scol]==0){
return;
}
// 深搜,并将搜索过程中的1改为0
grid[srow][scol]=0;
dfs(srow-1,scol);
dfs(srow+1,scol);
dfs(srow,scol+1);
dfs(srow,scol-1);
}
}
例题10: 用一个大小为 m x n 的二维网格 grid 表示一个箱子。你有 n 颗球。箱子的顶部和底部都是开着的。
箱子中的每个单元格都有一个对角线挡板,跨过单元格的两个角,可以将球导向左侧或者右侧。
将球导向右侧的挡板跨过左上角和右下角,在网格中用 1 表示。
将球导向左侧的挡板跨过右上角和左下角,在网格中用 -1 表示。
在箱子每一列的顶端各放一颗球。每颗球都可能卡在箱子里或从底部掉出来。如果球恰好卡在两块挡板之间的 “V” 形图案,或者被一块挡导向到箱子的任意一侧边上,就会卡住。
返回一个大小为 n 的数组 answer ,其中 answer[i] 是球放在顶部的第 i 列后从底部掉出来的那一列对应的下标,如果球卡在盒子里,则返回 -1 。(leetcode:1706)
class Solution {
// 创建一个数组保存结果
int[] result;
// 保存网格数组的行和列数
int row;
int col;
public int[] findBall(int[][] grid) {
this.result=new int[grid[0].length];
this.row=grid.length;
this.col=grid[0].length;
// 将每一颗小球都使用深搜进行判断,并保存结果
for(int i=0;i<col;i++){
result[i]=dfs(0,i,grid);
}
// 返回结果
return result;
}
// 创建一个深搜函数
public int dfs(int r,int l,int[][] grid){
// 创建一个变量记录小球最后到达的列方便返回结果
int rescol=-1;
// 如果小球成功到达了row,则说明它可以出去,返回此时对应的列即可
if(r==row){
return l;
}
// 首先判断小球所处的方格的隔板的朝向
int direction=grid[r][l];
// 如果小球目前所处的方格是向右边倾斜,则它能否到达g[r+1][l+1]是取决于它右边的方格
if(direction==1){
// 那么此时的出口应该就是超出边界,或者于右边的方格形成了v:grid[r][l+1]==-1
if(l+1>=col||grid[r][l+1]==-1){
return -1;
}else{
// 否则小球可以到达下一行的方格
rescol=dfs(r+1,l+1,grid);
}
}
// 同样如果小球目前所处的方格是向左边倾斜,则它能否到达g[r+1][l-1]是取决于它左边的方格
if(direction==-1){
// 那么此时的出口应该就是超出边界,或者于右边的方格形成了v:grid[r][l-1]==1
if(l-1<0||grid[r][l-1]==1){
return -1;
}else{
// 否则小球可以到达下一行的方格
rescol=dfs(r+1,l-1,grid);
}
}
// 将结果返回主函数
return rescol;
}
}
例题11: 振兴中华
题目:小明参加了学校的趣味运动会,其中的一个项目是:跳格子
地上画着一些格子,每个格子里写一个字;如下图
从我做起振
我做起振兴
做起振兴中
起振兴中华
比赛时从左上角的的"从"字开始,只能横向或纵向跳到相邻的格子里,但不能跳到对角的格子,要求跳到"华"字结束。要求跳过的路线刚好是"从我做起振兴中华"这句话,请问小明有几种可能的跳跃路线
这道题就我个人认为应该是使用深搜解题得,但是由于题目得特殊性所以可以使用动态规划:
解法一:深搜
package one;
class Solutatin{
char[][] chars;
String str;
// 创建一个集合记录访问过的地方
int[][] visited;
int count=0;
public int slution(char[][] chars,String str) {
this.chars=chars;
this.str=str;
this.visited=new int[chars.length][chars[0].length];
String temp="";
dfs(temp,0,0);
return count;
}
public void dfs(String temp,int rstr,int cstr) {
// 深搜的出口
if(rstr>=chars.length||rstr<0||cstr>=chars[0].length||cstr<0||visited[rstr][cstr]==1) {
return;
}
// 因为最后的界限问题所以导致最后的一次temp无法判断就直接返回了,
// 所以这里 先加上chars[rstr][cstr]
// if((temp).equals(str))
if((temp+chars[rstr][cstr]).equals(str)) {
count++;
return;
}
visited[rstr][cstr]=1;
//向下项做进行搜索
dfs(temp+chars[rstr][cstr],rstr+1,cstr);
dfs(temp+chars[rstr][cstr],rstr,cstr+1);
// 返回上一层之前将本层的标记去掉
visited[rstr][cstr]=0;
}
}
public class None_3 {
public static void main(String[] args) {
String str="从我做起振兴中华";
char[][] chars={{'从','我','做','起','振'},
{'我','做','起','振','兴'},
{'做','起','振','兴','中'},
{'起','振','兴','中','华'}};
Solutatin a=new Solutatin();
int result=a.slution(chars,str);
System.out.print(result);
}
}
解法2: 动态规划
package one;
import java.util.Arrays;
class Solution_3_2{
// 动态规划数组代表到达,该点的方法的数量
int[][] dp1;
char[][] chars;
String str;
public int solution(char[][] chars,String str) {
this.chars=chars;
this.str=str;
this.dp1=new int[chars.length][chars[0].length];
// 初始华动态规划数组dp[0][0]=1
dp();
return dp1[3][4];
}
// 仔细观察了一下发现,这道题只要到达了最右下角,就一定满足条件,所以可以转化为到达右下角的路径问题
public void dp() {
// 初始华第1行,和第1列的值
Arrays.fill(dp1[0], 1);
for(int i=0;i<chars.length;i++) {
dp1[i][0]=1;
}
for(int i=1;i<chars.length;i++) {
for(int j=1;j<chars[0].length;j++) {
dp1[i][j]=dp1[i-1][j]+dp1[i][j-1];
}
}
}
}
public class None_3_2 {
public static void main(String[] args) {
String str="从我做起振兴中华";
char[][] chars={{'从','我','做','起','振'},
{'我','做','起','振','兴'},
{'做','起','振','兴','中'},
{'起','振','兴','中','华'}};
Solution_3_2 a=new Solution_3_2();
int result=a.solution(chars,str);
System.out.print(result);
}
}
Last
一旦博主有新的理解再更新博客,另外这些只是本人的理解,不一定正确,有错误的理解欢迎指出,共同进步