在这一章节的学习中,我们将要学习一个数据结构——二叉树
(Binary Tree),和基于二叉树上的搜索算法。
在二叉树的搜索中,我们主要使用了分治法(Divide Conquer)来解决大部分的问题。之所以大部分二叉树的问题可以使用分治法,是因为二叉树这种数据结构,是一个天然就帮你做好了分治法中“分”这个步骤的结构。
遍历(Traversal),顾名思义,就是通过某种顺序,一个一个访问一个数据结构中的元素
。比如我们如果需要遍历一个数组,无非就是要么从前往后,要么从后往前遍历。但是对于一棵二叉树来说,他就有很多种方式进行遍历:
我们在之前的课程中,已经学习过了二叉树的层序遍历,也就是使用 BFS 算法来获得二叉树的分层信息。通过 BFS 获得的顺序我们也可以称之为 BFS Order。而剩下的三种遍历,都需要通过深度优先搜索的方式来获得。而这一小节中,我们将讲一下通过深度优先搜索(DFS)来获得的节点顺序,
分治法(Divide & Conquer Algorithm)是说将一个大问题,拆分为2个或者多个小问题,当小问题得到结果之后,合并他们的结果来得到大问题的结果。
在一棵二叉树(Binary Tree)中,如果将整棵二叉树看做一个大问题的话,那么根节点(Root)的左子树(Left subtree)就是一个小问题,右子树(Right subtree)是另外一个小问题。这是一个天然就帮你完成了“分”这个步骤的数据结构。
既可遍历又可分治
分治法,多个return value时可以用resultType
中序遍历从小到大,或者分治法
联系
分治法(Divide & Conquer)与遍历法(Traverse)是两种常见的递归(Recursion)方法。
分治法解决问题的思路
先让左右子树去解决同样的问题,然后得到结果之后,再整合为整棵树的结果。
遍历法解决问题的思路
通过前序/中序/后序的某种遍历,游走整棵树,通过一个全局变量或者传递的参数来记录这个过程中所遇到的点和需要计算的结果。
两种方法的区别
从程序实现角度分治法的递归函数,通常有一个返回值
,遍历法通常没有。
什么是递归 (Recursion) ?
很多书上会把递归(Recursion)当作一种算法。事实上,递归是包含两个层面的意思的:
与之对应的,有非递归(Non-Recursion)和迭代法(Iteration),你可以认为这两个概念是一样的概念(番茄和西红柿的区别)。不需要做区分。
什么是搜索 (Search)?
搜索分为深度优先搜索(Depth First Search)和宽度优先搜索(Breadth First Search),通常分别简写为 DFS 和 BFS。搜索是一种类似于枚举(Enumerate)的算法。比如我们需要找到一个数组里的最大值,我们可以采用枚举法,因为我们知道数组的范围和大小,比如经典的打擂台算法:
int max = nums[0];
for (int i = 1; i < nums.length; i++) {
max = Math.max(max, nums[i]);
}
枚举法通常是你知道循环的范围,然后可以用几重循环就搞定的算法。比如我需要找到 所有 x^2 + y^2 = K 的整数组合,可以用两重循环的枚举法:
// 不要在意这个算法的时间复杂度
for (int x = 1; x <= k; x++) {
for (int y = 1; y <= k; y++) {
if (x * x + y * y == k) {
// print x and y
}
}
}
而有的问题,比如求 N 个数的全排列,你可能需要用 N 重循环才能解决。这个时候,我们就倾向于采用递归的方式去实现这个变化的 N 重循环。这个时候,我们就把算法称之为搜索
。因为你已经不能明确的写出一个不依赖于输入数据的多重循环了。
通常来说 DFS 我们会采用递归的方式实现(当然你强行写一个非递归的版本也是可以的),而 BFS 则无需递归(使用队列 Queue + 哈希表 HashMap就可以)。所以我们在面试中,如果一个问题既可以使用 DFS,又可以使用 BFS 的情况下,一定要优先使用 BFS。
因为他是非递归的,而且更容易实现。
什么是回溯(Backtracking)?
有的时候,深度优先搜索算法(DFS),又被称之为回溯法,所以你可以完全认为回溯法,就是深度优先搜索算法。在我的理解中,回溯实际上是深度优先搜索过程中的一个步骤。比如我们在进行全子集问题的搜索时,假如当前的集合是 {1,2} 代表我正在寻找以 {1,2}开头的所有集合。那么他的下一步,会去寻找 {1,2,3}开头的所有集合,然后当我们找完所有以 {1,2,3} 开头的集合时,我们需要把 3 从集合中删掉,回到 {1,2}。然后再把 4 放进去,寻找以 {1,2,4} 开头的所有集合。这个把 3 删掉回到 {1,2} 的过程,就是回溯。
subset.add(nums[i]);
subsetsHelper(result, subset, nums, i + 1);
subset.remove(list.size() - 1) // 这一步就是回溯
每一个递归函数,都需要有明确的定义,有了正确的定义以后,才能够对递归进行拆解。
一个
大问题
如何拆解为若干个小问题
去解决。
什么时候可以直接知道答案,不用再拆解,直接 return
通常是我们定义在某个文件内部使用的一个类。比如:
class ResultType {
int maxValue, minValue;
ResultType(int maxValue, int minValue) {
this.maxValue = maxValue;
this.minValue = minValue;
}
};
当我们定义的函数需要返回多个值供调用者计算时,就需要使用 ResultType了。
所以如果你只是返回一个值就够用的话,就不需要。
平衡二叉树(Balanced Binary Tree,又称为AVL树,有别于AVL算法)是二叉树中的一种特殊的形态。二叉树当且仅当满足如下两个条件之一,是平衡二叉树:
AVL树的高度为 O(logN)
若二叉搜索树是AVL树,则最大作用是保证查找的最坏时间复杂度为O(logN)。而且较浅的树对插入和删除等操作也更快。
AVL树必定是二叉搜索树,反之则不一定。
Set/Map
Set/Map 是底层运用了红黑树的数据结构
对比 unordered_set/unordered_map
对比 priority_queue(Heap)
priority_queue是基于Heap实现的,它可以保证队头元素是优先级最高的元素,但其余元素是不保证有序的。
Heap 是完全二叉树
二叉搜索树(Binary Search Tree,又名排序二叉树,二叉查找树,通常简写为BST)定义如下:
空树或是具有下列性质的二叉树:
(1)若左子树不空,则左子树上所有节点值均小于或等于它的根节点值;
(2)若右子树不空,则右子树上所有节点值均大于或等于它的根节点值;
(3)左、右子树也为二叉搜索树;
基本操作之查找(Retrieve)
思路
基本操作之修改(Update)
思路
基本操作之增加(Create)
思路
基本操作之删除(Delete)
思路(最为复杂)
与递归和使用栈空间遍历的思想不同,Morris 算法使用二叉树中的叶节点的right指针来保存后面将要访问的节点的信息,当这个right指针使用完成之后,再将它置为null,但是在访问过程中有些节点会访问两次,所以与递归的空间换时间的思路不同,Morris则是使用时间换空间的思想。
class TreeNode{
int val;
TreeNode left;
TreeNode right;
pubic TreeNode(int val) {
this.val = val;
this.left = this.right = null;
}
}
1. 如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点。
2. 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
1. 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。
2. 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空(恢复树的形状)。输出当前节点。当前节点更新为当前节点的右孩子。
3. 重复1、2两步直到当前节点为空。
public class Solution {
public List inorderTraversal(TreeNode root) {
List nums = new ArrayList<>();
TreeNode cur = null;
while (root != null) {
if (root.left != null) {
cur = root.left;
while (cur.right != null && cur.right != root) {
cur = cur.right;
}
if (cur.right == root) {
nums.add(root.val);
cur.right = null;
root = root.right;
} else {
cur.right = root;
root = root.left;
}
} else {
nums.add(root.val);
root = root.right;
}
}
return nums;
}
}
思路
遍历顺序为根、左、右
思路
遍历顺序为左、根、右
思路
遍历顺序为左、右、根