点个赞,看一看,好习惯!本文 GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了 3 个月总结的一线大厂 Java 面试总结,本人已拿大厂 offer。
另外,原创文章首发在个人我的博客:blog.ouyangsihai.cn,欢迎访问。html
今天来聊聊 dfs 的解题方法,这些方法都是总结以后的出来的经验,有值得借鉴的地方。java
二叉树的思想其实很简单,咱们刚刚开始学习二叉树的时候,在作二叉树遍历的时候是否是最多见的方法就是递归遍历,其实,你会发现,二叉树的题目的解题方法基本上都是递归来解题,咱们只须要走一步,其余的由递归来作。mysql
咱们先来看一下二叉树的前序遍历、中序遍历、后序遍历的递归版本。git
//前序遍历 void traverse(TreeNode root) { System.out.println(root.val); traverse(root.left); traverse(root.right); } //中序遍历 void traverse(TreeNode root) { traverse(root.left); System.out.println(root.val); traverse(root.right); } //后续遍历 void traverse(TreeNode root) { traverse(root.left); traverse(root.right); System.out.println(root.val); }
其实你会发现,二叉树的遍历的过程就可以看出二叉树遍历的一个总体的框架,其实这个也是二叉树的解题的总体的框架就是下面这样的。程序员
void traverse(TreeNode root) { //这里将输出变成其余操做,咱们只完成第一步,后面的由递归来完成。 traverse(root.left); traverse(root.right); }
咱们在解题的时候,咱们只须要去想当前的操做应该怎么实现,后面的由递归去实现,至于用前序中序仍是后序遍历,由具体的状况来实现。github
下面来几个二叉树的热身题,来体会一下这种解题方法。面试
另外,这些知识的话,我都写了原创文章,比较系统的讲解了,你们能够看看,会有必定得收获的。算法
序号 | 原创精品 |
---|---|
1 | 【原创】分布式架构系列文章 |
2 | 【原创】实战 Activiti 工做流教程 |
3 | 【原创】深刻理解Java虚拟机教程 |
4 | 【原创】Java8最新教程 |
5 | 【原创】MySQL的艺术世界 |
首先仍是同样,咱们先写出框架。sql
void traverse(TreeNode root) { //这里将输出变成其余操做,咱们只完成第一步,后面的由递归来完成。 traverse(root.left); traverse(root.right); }
接下来,考虑当前的一步须要作什么事情,在这里,固然是给当前的节点加一。后端
void traverse(TreeNode root) { if(root == null) { return; } //这里改成给当前的节点加一。 root.val += 1; traverse(root.left); traverse(root.right); }
发现是否是水到渠成?
不爽?再来一个简单的。
这个问题咱们直接考虑当前一步须要作什么,也就是什么状况,这是同一颗二叉树?
1)两棵树的当前节点等于空:root1 == null && root2 == null,这个时候返回 true。
2)两棵树的当前节点任意一个节点为空:root1 == null || root2 == null,这个时候固然是 false。
3)两棵树的当前节点都不为空,可是 val 不同:root1.val != root2.val,返回 false。
因此,答案就显而易见了。
boolean isSameTree(TreeNode root1, TreeNode root2) { // 都为空的话 if (root1 == null && root2 == null) return true; // ⼀个为空,⼀个⾮空 if (root1 == null || root2 == null) return false; // 两个都⾮空,但 val 不⼀样 if (root1.val != root2.val) return false; // 递归去作 return isSameTree(root1.left, root2.left) && isSameTree(root1.right, root2.right); }
有了上面的讲解,我相信你已经有了基本的思路了,下面咱们来点有难度的题目,小试牛刀。
这个题目是二叉树中的中等难度题目,可是经过率很低,那么咱们用上面的思路来看看是否能够轻松解决这个题目。
这个题目乍一看,根据前面的思路,你能够能首先会选择前序遍历的方式来解决,是能够的,可是,比较麻烦,由于前序遍历的方式会改变右节点的指向,致使比较麻烦,那么,若是前序遍历不行,就考虑中序和后序遍历了,因为,在展开的时候,只须要去改变左右节点的指向,因此,这里其实最好的方式仍是用后续遍历,既然是后续遍历,那么咱们就能够快速的把后续遍历的框架写出来了。
public void flatten(TreeNode root) { if(root == null){ return; } flatten(root.left); flatten(root.right); //考虑当前一步作什么 }
这样,这个题目的基本思路就出来了,那么,咱们只须要考虑当前一步须要作什么就能够把这个题目搞定了。
当前一步:因为是后序遍历,因此顺序是左右中
,从展开的顺序咱们能够看出来,明显是先链接左节点,后链接右节点,因此,咱们确定要先保存右节点的值,而后链接左节点,同时,咱们的展开以后,只有右节点,因此,左节点应该设置为null。
通过分析,代码直接就能够写出来了。
public void flatten(TreeNode root) { if(root == null){ return; } flatten(root.left); flatten(root.right); //考虑当前一步作什么 TreeNode temp = root.right;// root.right = root.left;//右指针指向左节点 root.left = null;//左节点值为空 while(root.right != null){ root = root.right; } root.right = temp;//最后再将右节点连在右指针后面 }
最终这就是答案了,这不是最佳的答案,可是,这多是解决二叉树这种题目的最好的理解方式,同时,很是有助于你理解dfs这种算法的思想。
这个题目也是挺不错的题目,并且其实在咱们学习数据结构的时候,这个题目常常会以解答题的方式出现,让咱们考试的时候来作,确实印象深入,这里,咱们看看用代码怎么解决。
仍是一样的套路,一样的思路,已经一样的味道,再来把这道菜炒一下。
首先,肯定先序遍历、中序遍历仍是后序遍历,既然是由前序遍历和中序遍从来推出二叉树,那么,前序遍历是更好一些的。
这里咱们直接考虑当前一步应该作什么,而后直接作出来这道菜。
当前一步:回想一下之前作这个题目的思路你会发现,咱们去构造二叉树的时候,思路是这样的,前序遍历第一个元素确定是根节点a,那么,当前前序遍历的元素a,在中序遍历中,在a这个元素的左边就是左子树的元素,在a这个元素右边的元素就是左子树的元素,这样是否是就考虑清楚了当前一步,那么咱们惟一要作的就是在中序遍历数组中找到a这个元素的位置,其余的递归来解决便可。
话很少说,看代码。
public TreeNode buildTree(int[] preorder, int[] inorder) { //当前前序遍历的第一个元素 int rootVal = preorder[0]; root = new TreeNode(); root.val = rootVal; //获取在inorder中序遍历数组中的位置 int index = 0; for(int i = 0; i < inorder.length; i++){ if(rootVal == inorder[i]){ index = i; } } //递归去作 }
这一步作好了,后面就是递归要作的事情了,让计算机去工做吧。
public TreeNode buildTree(int[] preorder, int[] inorder) { //当前前序遍历的第一个元素 int rootVal = preorder[0]; root = new TreeNode(); root.val = rootVal; //获取在inorder中序遍历数组中的位置 int index = 0; for(int i = 0; i < inorder.length; i++){ if(rootVal == inorder[i]){ index = i; } } //递归去作 root.left = buildTree(Arrays.copyOfRange(preorder,1,index+1),Arrays.copyOfRange(inorder,0,index)); root.right = buildTree(Arrays.copyOfRange(preorder,index+1,preorder.length),Arrays.copyOfRange(inorder,index+1,inorder.length)); return root; }
最后,再把边界条件处理一下,防止root为null的状况出现。
TreeNode root = null; if(preorder.length == 0){ return root; }
ok,这道菜就这么按照模板炒出来了,相信你,后面的菜你也会抄着炒的。
这一类题目在leetcode仍是很是多的,并且在笔试当中你都会常常遇到这种题目,因此,找到解决的方法很重要,其实,最后,你会发现,这类题目,你会了以后就是再也不以为难的题目了。
咱们先来看一下题目哈。
题目的意思很简单,有一个二维数组,里面的数字都是0和1,0表明水域,1表明陆地,让你计算的是陆地的数量,也就是岛屿的数量。
那么这类题目怎么去解决呢?
其实,咱们能够从前面说的从二叉树看dfs的问题来看这个问题,二叉树的特征很明显,就是只有两个分支能够选择。
因此,就有了下面的遍历模板。
//前序遍历 void traverse(TreeNode root) { System.out.println(root.val); traverse(root.left); traverse(root.right); }
可是,回归到这个题目的时候,你会发现,咱们的整个数据结构是一张二维的图,以下所示。
当你遍历这张图的时候,你会怎么遍历呢?是否是这样子?
在(i,j)的位置,是否是能够有四个方向都是能够进行遍历的,那么是否是这个题目就有了新的解题思路了。
这样咱们就能够把这个的dfs模板代码写出来了。
void dfs(int[][] grid, int i, int j) { // 访问上、下、左、右四个相邻方向 dfs(grid, i - 1, j); dfs(grid, i + 1, j); dfs(grid, i, j - 1); dfs(grid, i, j + 1); }
你会发现是否是和二叉树的遍历很像,只是多了两个方向而已。
最后还有一个须要考虑的问题就是:base case,其实二叉树也是须要讨论一下base case的,可是,很简单,当root == null
的时候,就是base case。
这里的base case其实也不难,由于这个二维的图是有边界的,当dfs的时候发现超出了边界,是否是就须要判断了,因此,咱们再加上边界条件。
void dfs(int[][] grid, int i, int j) { // 判断 base case if (!inArea(grid, i, j)) { return; } // 若是这个格子不是岛屿,直接返回 if (grid[i][j] != 1) { return; } // 访问上、下、左、右四个相邻方向 dfs(grid, i - 1, j); dfs(grid, i + 1, j); dfs(grid, i, j - 1); dfs(grid, i, j + 1); } // 判断坐标 (r, c) 是否在网格中 boolean inArea(int[][] grid, int i, int j) { return 0 <= i && i < grid.length && 0 <= j && j < grid[0].length; }
到这里的话其实这个题目已经差很少完成了,可是,还有一点咱们须要注意,当咱们访问了某个节点以后,是须要进行标记的,能够用bool也能够用其余数字标记,否则可能会出现循环递归的状况。
因此,最后的解题就出来了。
void dfs(int[][] grid, int i, int j) { // 判断 base case if (!inArea(grid, i, j)) { return; } // 若是这个格子不是岛屿,直接返回 if (grid[i][j] != 1) { return; } //用2来标记已经遍历过 grid[i][j] = 2; // 访问上、下、左、右四个相邻方向 dfs(grid, i - 1, j); dfs(grid, i + 1, j); dfs(grid, i, j - 1); dfs(grid, i, j + 1); } // 判断坐标 (r, c) 是否在网格中 boolean inArea(int[][] grid, int i, int j) { return 0 <= i && i < grid.length && 0 <= j && j < grid[0].length; }
没有爽够?再来一题。
这个题目跟上面的那题很像,可是这里是求最大的一个岛屿的面积,因为每个单元格的面积是1,因此,最后的面积就是单元格的数量。
这个题目的解题方法跟上面的那个基本同样,咱们把上面的代码复制过去,改改就能够了。
class Solution { public int maxAreaOfIsland(int[][] grid) { if(grid == null){ return 0; } int max = 0; for(int i = 0; i < grid.length; i++){ for(int j = 0; j < grid[0].length; j++){ if(grid[i][j] == 1){ max = Math.max(dfs(grid, i, j), max); } } } return max; } int dfs(int[][] grid, int i, int j) { // 判断 base case if (!inArea(grid, i, j)) { return 0; } // 若是这个格子不是岛屿,直接返回 if (grid[i][j] != 1) { return 0; } //用2来标记已经遍历过 grid[i][j] = 2; // 访问上、下、左、右四个相邻方向 return 1 + dfs(grid, i - 1, j) + dfs(grid, i + 1, j) + dfs(grid, i, j - 1) + dfs(grid, i, j + 1); } // 判断坐标 (r, c) 是否在网格中 boolean inArea(int[][] grid, int i, int j) { return 0 <= i && i < grid.length && 0 <= j && j < grid[0].length; } }
基本思路: 每次进行dfs的时候都对岛屿数量进行+1的操做,而后再求全部岛屿中的最大值。
咱们看一下咱们代码的效率如何。
看起来是否是还不错哟,对的,就是这么搞事情!!!
最后,这篇文章前先后后写了快一周的时间把,不知道写的怎么样,可是,我尽力的把本身所想的表达清楚,主要是一种思路跟解题方法,确定还有不少其余的方法,去LeetCode去看就明白了。
好了,写的也够久了,下篇文章再来看看其余的,但愿对你们有帮助,再次再见!!
最后,再分享我历时三个月总结的 Java 面试 + Java 后端技术学习指南,这是本人这几年及春招的总结,已经拿到了大厂 offer,整理成了一本电子书,拿去不谢,目录以下:
如今免费分享你们,在下面个人公众号 程序员的技术圈子 回复 面试 便可获取。