@@ -566,6 +566,204 @@ class Solution {
566
566
567
567
![ ] ( https://labuladong.github.io/pictures/二叉树收官/plugin2.png )
568
568
569
+ ### 以树的视角看动归/回溯/DFS算法的区别和联系
570
+
571
+ 前文我说动态规划/回溯算法就是二叉树算法两种不同思路的表现形式,相信能看到这里的读者应该也认可了我这个观点。但有细心的读者经常问我:东哥,你的理解思路让我豁然开朗,但你好像一直没讲过 DFS 算法?
572
+
573
+ 其实我在 [ 一文秒杀所有岛屿题目] ( https://labuladong.github.io/article/fname.html?fname=岛屿题目 ) 中就是用的 DFS 算法,但我确实没有单独用一篇文章讲 DFS 算法,** 因为 DFS 算法和回溯算法非常类似,只是在细节上有所区别** 。
574
+
575
+ 这个细节上的差别是什么呢?其实就是「做选择」和「撤销选择」到底在 for 循环外面还是里面的区别,DFS 算法在外面,回溯算法在里面。
576
+
577
+ 为什么有这个区别?还是要结合着二叉树理解。这一部分我就把回溯算法、DFS 算法、动态规划三种经典的算法思想,以及它们和二叉树算法的联系和区别,用一句话来说明:
578
+
579
+ ** 动归/DFS/回溯算法都可以看做二叉树问题的扩展,只是它们的关注点不同** :
580
+
581
+ - ** 动态规划算法属于分解问题的思路,它的关注点在整棵「子树」** 。
582
+ - ** 回溯算法属于遍历的思路,它的关注点在节点间的「树枝」** 。
583
+ - ** DFS 算法属于遍历的思路,它的关注点在单个「节点」** 。
584
+
585
+ 怎么理解?我分别举三个例子你就懂了:
586
+
587
+ ** 第一个例子** ,给你一棵二叉树,请你用分解问题的思路写一个 ` count ` 函数,计算这棵二叉树共有多少个节点。代码很简单,上文都写过了:
588
+
589
+ <!-- muliti_language -->
590
+ ``` java
591
+ // 定义:输入一棵二叉树,返回这棵二叉树的节点总数
592
+ int count(TreeNode root) {
593
+ if (root == null ) {
594
+ return 0 ;
595
+ }
596
+ // 我这个节点关心的是我的两个子树的节点总数分别是多少
597
+ int leftCount = count(root. left);
598
+ int rightCount = count(root. right);
599
+ // 后序位置,左右子树节点数加上自己就是整棵树的节点数
600
+ return leftCount + rightCount + 1 ;
601
+ }
602
+ ```
603
+
604
+ ** 你看,这就是动态规划分解问题的思路,它的着眼点永远是结构相同的整个子问题,类比到二叉树上就是「子树」** 。
605
+
606
+ 你再看看具体的动态规划问题,比如 [ 动态规划框架套路详解] ( https://labuladong.github.io/article/fname.html?fname=动态规划详解进阶 ) 中举的斐波那契的例子,我们的关注点在一棵棵子树的返回值上:
607
+
608
+ <!-- muliti_language -->
609
+ ``` java
610
+ int fib(int N ) {
611
+ if (N == 1 || N == 2 ) return 1 ;
612
+ return fib(N - 1 ) + fib(N - 2 );
613
+ }
614
+ ```
615
+
616
+ ![ ] ( https://labuladong.github.io/pictures/动态规划详解进阶/2.jpg )
617
+
618
+ ** 第二个例子** ,给你一棵二叉树,请你用遍历的思路写一个 ` traverse ` 函数,打印出遍历这棵二叉树的过程,你看下代码:
619
+
620
+ <!-- muliti_language -->
621
+ ``` java
622
+ void traverse(TreeNode root) {
623
+ if (root == null ) return ;
624
+ printf(" 从节点 %s 进入节点 %s" , root, root. left);
625
+ traverse(root. left);
626
+ printf(" 从节点 %s 回到节点 %s" , root. left, root);
627
+
628
+ printf(" 从节点 %s 进入节点 %s" , root, root. right);
629
+ traverse(root. right);
630
+ printf(" 从节点 %s 回到节点 %s" , root. right, root);
631
+ }
632
+ ```
633
+
634
+ 不难理解吧,好的,我们现在从二叉树进阶成多叉树,代码也是类似的:
635
+
636
+ <!-- muliti_language -->
637
+ ``` java
638
+ // 多叉树节点
639
+ class Node {
640
+ int val;
641
+ Node [] children;
642
+ }
643
+
644
+ void traverse(Node root) {
645
+ if (root == null ) return ;
646
+ for (Node child : root. children) {
647
+ printf(" 从节点 %s 进入节点 %s" , root, child);
648
+ traverse(child);
649
+ printf(" 从节点 %s 回到节点 %s" , child, root);
650
+ }
651
+ }
652
+ ```
653
+
654
+ 这个多叉树的遍历框架就可以延伸出 [ 回溯算法框架套路详解] ( https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版 ) 中的回溯算法框架:
655
+
656
+ ``` java
657
+ void backtrack(... ) {
658
+ for (int i = 0 ; i < ... ; i++ ) {
659
+ // 做选择
660
+ ...
661
+
662
+ // 进入下一层决策树
663
+ backtrack(... );
664
+
665
+ // 撤销刚才做的选择
666
+ ...
667
+ }
668
+ }
669
+ ```
670
+
671
+ ** 你看,这就是回溯算法遍历的思路,它的着眼点永远是在节点之间移动的过程,类比到二叉树上就是「树枝」** 。
672
+
673
+ 你再看看具体的回溯算法问题,比如 [ 回溯算法秒杀排列组合子集的九种题型] ( https://labuladong.github.io/article/fname.html?fname=子集排列组合 ) 中讲到的全排列,我们的关注点在一条条树枝上:
674
+
675
+ ``` java
676
+ // 回溯算法核心部分代码
677
+ void backtrack(int [] nums) {
678
+ // 回溯算法框架
679
+ for (int i = 0 ; i < nums. length; i++ ) {
680
+ // 做选择
681
+ used[i] = true ;
682
+ track. addLast(nums[i]);
683
+
684
+ // 进入下一层回溯树
685
+ backtrack(nums);
686
+
687
+ // 取消选择
688
+ track. removeLast();
689
+ used[i] = false ;
690
+ }
691
+ }
692
+ ```
693
+
694
+ ![ ] ( https://labuladong.github.io/pictures/排列组合/2.jpeg )
695
+
696
+ ** 第三个例子** ,我给你一棵二叉树,请你写一个 ` traverse ` 函数,把这棵二叉树上的每个节点的值都加一。很简单吧,代码如下:
697
+
698
+ <!-- muliti_language -->
699
+ ``` java
700
+ void traverse(TreeNode root) {
701
+ if (root == null ) return ;
702
+ // 遍历过的每个节点的值加一
703
+ root. val++ ;
704
+ traverse(root. left);
705
+ traverse(root. right);
706
+ }
707
+ ```
708
+
709
+ ** 你看,这就是 DFS 算法遍历的思路,它的着眼点永远是在单一的节点上,类比到二叉树上就是处理每个「节点」** 。
710
+
711
+ 你再看看具体的 DFS 算法问题,比如 [ 一文秒杀所有岛屿题目] ( https://labuladong.github.io/article/fname.html?fname=岛屿题目 ) 中讲的前几道题,我们的关注点是 ` grid ` 数组的每个格子(节点),我们要对遍历过的格子进行一些处理,所以我说是用 DFS 算法解决这几道题的:
712
+
713
+ ``` java
714
+ // DFS 算法核心逻辑
715
+ void dfs(int [][] grid, int i, int j) {
716
+ int m = grid. length, n = grid[0 ]. length;
717
+ if (i < 0 || j < 0 || i >= m || j >= n) {
718
+ return ;
719
+ }
720
+ if (grid[i][j] == 0 ) {
721
+ return ;
722
+ }
723
+ // 遍历过的每个格子标记为 0
724
+ grid[i][j] = 0 ;
725
+ dfs(grid, i + 1 , j);
726
+ dfs(grid, i, j + 1 );
727
+ dfs(grid, i - 1 , j);
728
+ dfs(grid, i, j - 1 );
729
+ }
730
+ ```
731
+
732
+ ![ ] ( https://labuladong.github.io/pictures/岛屿/5.jpg )
733
+
734
+ 好,请你仔细品一下上面三个简单的例子,是不是像我说的:动态规划关注整棵「子树」,回溯算法关注节点间的「树枝」,DFS 算法关注单个「节点」。
735
+
736
+ 有了这些铺垫,你就很容易理解为什么回溯算法和 DFS 算法代码中「做选择」和「撤销选择」的位置不同了,看下面两段代码:
737
+
738
+ <!-- muliti_language -->
739
+ ``` java
740
+ // DFS 算法把「做选择」「撤销选择」的逻辑放在 for 循环外面
741
+ void dfs(Node root) {
742
+ if (root == null ) return ;
743
+ // 做选择
744
+ print(" 我已经进入节点 %s 啦" , root)
745
+ for (Node child : root. children) {
746
+ dfs(child);
747
+ }
748
+ // 撤销选择
749
+ print(" 我将要离开节点 %s 啦" , root)
750
+ }
751
+
752
+ // 回溯算法把「做选择」「撤销选择」的逻辑放在 for 循环里面
753
+ void backtrack(Node root) {
754
+ if (root == null ) return ;
755
+ for (Node child : root. children) {
756
+ // 做选择
757
+ print(" 我站在节点 %s 到节点 %s 的树枝上" , root, child)
758
+ backtrack(child);
759
+ // 撤销选择
760
+ print(" 我站在节点 %s 到节点 %s 的树枝上" , child, root)
761
+ }
762
+ }
763
+ ```
764
+
765
+ 看到了吧,你回溯算法必须把「做选择」和「撤销选择」的逻辑放在 for 循环里面,否则怎么拿到「树枝」的两个端点?
766
+
569
767
### 层序遍历
570
768
571
769
二叉树题型主要是用来培养递归思维的,而层序遍历属于迭代遍历,也比较简单,这里就过一下代码框架吧:
@@ -726,6 +924,7 @@ class Solution {
726
924
- [ 后序遍历的妙用] ( https://labuladong.github.io/article/fname.html?fname=后序遍历 )
727
925
- [ 回溯算法秒杀所有排列/组合/子集问题] ( https://labuladong.github.io/article/fname.html?fname=子集排列组合 )
728
926
- [ 回溯算法解题套路框架] ( https://labuladong.github.io/article/fname.html?fname=回溯算法详解修订版 )
927
+ - [ 图论基础及遍历算法] ( https://labuladong.github.io/article/fname.html?fname=图 )
729
928
- [ 在插件中解锁二叉树专属题解] ( https://labuladong.github.io/article/fname.html?fname=解锁tree插件 )
730
929
- [ 归并排序详解及应用] ( https://labuladong.github.io/article/fname.html?fname=归并排序 )
731
930
- [ 我的刷题心得] ( https://labuladong.github.io/article/fname.html?fname=算法心得 )
0 commit comments