在这一章节的学习中,咱们将要学习一个数据结构——二叉树
(Binary Tree),和基于二叉树上的搜索算法。html
在二叉树的搜索中,咱们主要使用了分治法(Divide Conquer)来解决大部分的问题。之因此大部分二叉树的问题可使用分治法,是由于二叉树这种数据结构,是一个自然就帮你作好了分治法中“分”这个步骤的结构。java
本章节的先修内容有:node
本章节的补充内容有:面试
遍历(Traversal),顾名思义,就是经过某种顺序,一个一个访问一个数据结构中的元素
。好比咱们若是须要遍历一个数组,无非就是要么从前日后,要么从后往前遍历。可是对于一棵二叉树来讲,他就有不少种方式进行遍历:算法
咱们在以前的课程中,已经学习过了二叉树的层序遍历,也就是使用 BFS 算法来得到二叉树的分层信息。经过 BFS 得到的顺序咱们也能够称之为 BFS Order。而剩下的三种遍历,都须要经过深度优先搜索的方式来得到。而这一小节中,咱们将讲一下经过深度优先搜索(DFS)来得到的节点顺序,数据库
首先访问根结点,而后遍历左子树,最后遍历右子树。遍历左、右子树时,仍按先序遍历。若二叉树为空则返回。api
该过程可简记为根左右,注意该过程是递归的。如图先序遍历结果是:ABDECF。数组
1 // 将根做为root,空ArrayList做为result传入,便可获得整棵树的遍历结果 2 private void traverse(TreeNode root, ArrayList<Integer> result) { 3 if (root == null) { 4 return; 5 } 6 result.add(root.val); 7 traverse(root.left, result); 8 traverse(root.right, result); 9 }
首先遍历左子树,而后访问根结点,最后遍历右子树。遍历左、右子树时,仍按中序遍历。若二叉树为空则返回。简记为左根右。
上图中序遍历结果是:DBEAFC。
核心代码:
Java:markdown
1 private void traverse(TreeNode root, ArrayList<Integer> result) { 2 if (root == null) { 3 return; 4 } 5 traverse(root.left, result); 6 result.add(root.val); // 注意访问根节点放到了遍历左子树的后面 7 traverse(root.right, result); 8 }
首先遍历左子树,而后遍历右子树,最后访问根结点。遍历左、右子树时,仍按后序遍历。若二叉树为空则返回。简记为左右根。
上图后序遍历结果是:DEBFCA。网络
1 private void traverse(TreeNode root, ArrayList<Integer> result) { 2 if (root == null) { 3 return; 4 } 5 traverse(root.left, result); 6 traverse(root.right, result); 7 result.add(root.val); // 注意访问根节点放到了最后 8 }
http://www.lintcode.com/problem/construct-binary-tree-from-inorder-and-postorder-traversal/
http://www.lintcode.com/problem/construct-binary-tree-from-preorder-and-inorder-traversal/
分治法(Divide & Conquer Algorithm)是说将一个大问题,拆分为2个或者多个小问题,当小问题获得结果以后,合并他们的结果来获得大问题的结果。
举一个例子,好比中国要进行人口统计。那么若是使用遍历(Traversal)的办法,作法以下:
人口普查员小张本身一我的带着一个本子,跑遍全中国挨家挨户的敲门查户口
而若是使用分治法,作法以下:
在这里,把全国的任务拆分为省级的任务的过程,就是分治法中分
的这个步骤。把各个小任务派发给别人去完成的过程,就是分治法中治
的这个步骤。可是事实上咱们还有第三个步骤,就是将小任务的结果合并到一块儿的过程,合
这个步骤。所以若是我来取名字的话,我会叫这个算法:分治合算法
。
在一棵二叉树(Binary Tree)中,若是将整棵二叉树看作一个大问题的话,那么根节点(Root)的左子树(Left subtree)就是一个小问题,右子树(Right subtree)是另一个小问题。这是一个自然就帮你完成了“分”这个步骤的数据结构。
分治法(Divide & Conquer)与遍历法(Traverse)是两种常见的递归(Recursion)方法。
先让左右子树去解决一样的问题,而后获得结果以后,再整合为整棵树的结果。
经过前序/中序/后序的某种遍历,游走整棵树,经过一个全局变量或者传递的参数来记录这个过程当中所遇到的点和须要计算的结果。
从程序实现角度分治法的递归函数,一般有一个返回值
,遍历法一般没有。
不少书上会把递归(Recursion)看成一种算法。事实上,递归是包含两个层面的意思的:
与之对应的,有非递归(Non-Recursion)和迭代法(Iteration),你能够认为这两个概念是同样的概念(番茄和西红柿的区别)。不须要作区分。
搜索分为深度优先搜索(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。
由于他是非递归的,并且更容易实现。
有的时候,深度优先搜索算法(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) // 这一步就是回溯
详情请参考:
http://www.jiuzhang.com/solutions/subsets/
咱们以《二叉树的最大深度》和《二叉树的前序遍历》两个题目为例子,来分析一下递归的三要素
。
相关题目连接:
http://www.lintcode.com/problem/maximum-depth-of-binary-tree/
http://www.lintcode.com/problem/binary-tree-preorder-traversal/
每个递归函数,都须要有明确的定义,有了正确的定义之后,才可以对递归进行拆解。
例子:
Java:
int maxDepth(TreeNode root)
Python:
def maxDepth(root):
表明 以 root 开头的子树的最大深度是多少
。
Java:
void preorder(TreeNode root, List<TreeNode> result)
Python:
def preorder(root, result):
表明 将 root 开头的子树的前序遍历放到 result 里面
一个
大问题
如何拆解为若干个小问题
去解决。
例子:
Java:
int leftDepth = maxDepth(root.left); int rightDepth = maxDepth(root.right); return Math.max(leftDepth, rightDepth) + 1;
Python:
leftDepth = maxDepth(root.left)
rightDepth = maxDepth(root.right)
return max(leftDepth, rightDepth) + 1
整棵树的最大深度,能够拆解为先计算左右子树深度,而后在左右子树深度中找到最大值+1来解决。
Java:
result.add(root); preorder(root.left, result); preorder(root.right, result);
Python:
result.append(root) preorder(root.left, result) perorder(root.right, result)
一棵树的前序遍历能够拆解为3个部分:
因此对应的,咱们把这个递归问题也拆分为三个部分来解决:
何时能够直接知道答案,不用再拆解,直接 return
例子:
Java:
// 二叉树的最大深度 if (root == null) { return 0; }
Python:
# 二叉树的最大深度 if not root: return 0
一棵空的二叉树,能够认为是一个高度为0
的二叉树。
Java:
// 二叉树的前序遍历 if (root == null) { return; }
Python:
if not root: return
一棵空的二叉树,天然不用往 result 里听任何的东西。
每个递归函数,都须要有明确的定义,有了正确的定义之后,才可以对递归进行拆解。
例子:
Java:
int maxDepth(TreeNode root)
Python:
def maxDepth(root):
表明 以 root 开头的子树的最大深度是多少
。
Java:
void preorder(TreeNode root, List<TreeNode> result)
Python:
def preorder(root, result):
表明 将 root 开头的子树的前序遍历放到 result 里面
一个
大问题
如何拆解为若干个小问题
去解决。
例子:
Java:
int leftDepth = maxDepth(root.left); int rightDepth = maxDepth(root.right); return Math.max(leftDepth, rightDepth) + 1;
Python:
leftDepth = maxDepth(root.left)
rightDepth = maxDepth(root.right)
return max(leftDepth, rightDepth) + 1
整棵树的最大深度,能够拆解为先计算左右子树深度,而后在左右子树深度中找到最大值+1来解决。
Java:
result.add(root); preorder(root.left, result); preorder(root.right, result);
Python:
result.append(root) preorder(root.left, result) preorder(root.right, result)
一棵树的前序遍历能够拆解为3个部分:
因此对应的,咱们把这个递归问题也拆分为三个部分来解决:
何时能够直接知道答案,不用再拆解,直接 return
例子:
Java:
// 二叉树的最大深度 if (root == null) { return 0; }
Python:
# 二叉树的最大深度 if not root: return 0
一棵空的二叉树,能够认为是一个高度为0
的二叉树。
Java:
// 二叉树的前序遍历 if (root == null) { return; }
Python:
if not root: return
一棵空的二叉树,天然不用往 result 里听任何的东西。
一般是咱们定义在某个文件内部使用的一个类。好比:
Java:
class ResultType { int maxValue, minValue; public ResultType(int maxValue, int minValue) { this.maxValue = maxValue; this.minValue = minValue; } }
当咱们定义的函数须要返回多个值供调用者计算时,就须要使用 ResultType了。
因此若是你只是返回一个值就够用的话,就不须要。
不是全部的语言都须要自定义 ResultType。
像 Python 这样的语言,天生支持你返回多个值做为函数的 return value,因此是不须要的。
二叉搜索树(Binary Search Tree,又名排序二叉树,二叉查找树,一般简写为BST)定义以下:
空树或是具备下列性质的二叉树:
(1)若左子树不空,则左子树上全部节点值均小于或等于它的根节点值;
(2)若右子树不空,则右子树上全部节点值均大于根节点值;
(3)左、右子树也为二叉搜索树;
如图即为BST:
http://www.lintcode.com/en/tag/binary-search-tree/
BST是一种重要且基本的结构,其相关题目也十分经典,并延伸出不少算法。
在BST之上,有许多高级且有趣的变种,以解决各式各样的问题,例如:
平衡二叉搜索树(Balanced Binary Search Tree,又称为AVL树,有别于AVL算法)是二叉树中的一种特殊的形态。二叉树当且仅当知足以下两个条件之一,是平衡二叉树:
如图(图片来自网络),节点旁边的数字表示左右两子树高度差。(a)是AVL树,(b)不是,(b)中5节点不知足AVL树,故4节点,3节点都再也不是AVL树。
当AVL树有N个节点时,高度为O(logN)O(logN)O(logN)。为什么?
试想一棵满二叉树,每一个节点左右子树高度相同,随着树高的增长,叶子容量指数暴增,故树高必定是O(logN)O(logN)O(logN)。而相比于满二叉树,AVL树仅放宽一个条件,容许左右两子树高度差1,当树高足够大时,能够把1忽略。如图是高度为9的最小AVL树,若节点更少,树高毫不会超过8,也即为什么AVL树高会被限制到O(logN)O(logN)O(logN),由于树不可能太稀疏。严格的数学证实复杂,略去。
为什么普通二叉树不是O(logN)O(logN)O(logN)?这里给出最坏的单枝树,若单枝扩展,则树高为O(N)O(N)O(N):
最大做用是保证查找的最坏时间复杂度为O(logN)。并且较浅的树对插入和删除等操做也更快。
判断一棵树是否为平衡树
http://www.lintcode.com/problem/balanced-binary-tree/
提示:能够自下而上递归判断每一个节点是否平衡。若平衡将当前节点高度返回,供父节点判断;不然该树必定不平衡。
用 Morris 算法实现 O(1) 额外空间遍历二叉树
https://www.jiuzhang.com/tutorial/algorithm/402
遍历顺序为根、左、右
http://www.lintcode.com/problem/binary-tree-preorder-traversal/
public class Solution { public List<Integer> preorderTraversal(TreeNode root) { Stack<TreeNode> stack = new Stack<TreeNode>(); List<Integer> preorder = new ArrayList<Integer>(); if (root == null) { return preorder; } stack.push(root); while (!stack.empty()) { TreeNode node = stack.pop(); preorder.add(node.val); if (node.right != null) { stack.push(node.right); } if (node.left != null) { stack.push(node.left); } } return preorder; } }
遍历顺序为左、根、右
http://www.lintcode.com/problem/binary-tree-inorder-traversal/
public class Solution { /** * @param root: The root of binary tree. * @return: Inorder in ArrayList which contains node values. */ public ArrayList<Integer> inorderTraversal(TreeNode root) { Stack<TreeNode> stack = new Stack<>(); ArrayList<Integer> result = new ArrayList<>(); while (root != null) { stack.push(root); root = root.left; } while (!stack.isEmpty()) { TreeNode node = stack.peek(); result.add(node.val); if (node.right == null) { node = stack.pop(); while (!stack.isEmpty() && stack.peek().right == node) { node = stack.pop(); } } else { node = node.right; while (node != null) { stack.push(node); node = node.left; } } } return result; } }
遍历顺序为左、右、根
http://www.lintcode.com/problem/binary-tree-postorder-traversal/
public ArrayList<Integer> postorderTraversal(TreeNode root) { ArrayList<Integer> result = new ArrayList<Integer>(); Stack<TreeNode> stack = new Stack<TreeNode>(); TreeNode prev = null; // previously traversed node TreeNode curr = root; if (root == null) { return result; } stack.push(root); while (!stack.empty()) { curr = stack.peek(); if (prev == null || prev.left == curr || prev.right == curr) { // traverse down the tree if (curr.left != null) { stack.push(curr.left); } else if (curr.right != null) { stack.push(curr.right); } } else if (curr.left == prev) { // traverse up the tree from the left if (curr.right != null) { stack.push(curr.right); } } else { // traverse up the tree from the right result.add(curr.val); stack.pop(); } prev = curr; } return result; }
二叉搜索树能够是一棵空树或者是一棵知足下列条件的二叉树:
均小于
它的根节点值。均大于
它的根节点值。https://www.jiuzhang.com/tutorial/algorithm/401
平衡二叉搜索树又被称为AVL树(有别于AVL算法),且具备如下性质:
也许由于输入值不够随机,也许由于输入顺序的缘由,还或许一些插入、删除操做,会使得二叉搜索树失去平衡,形成搜索效率低落的状况。
好比上面两个树,在平衡树上寻找15就只要2次查找,在非平衡树上却要5次查找方能找到,效率明显降低。
Java:
class TreeNode{ int val; TreeNode left; TreeNode right; pubic TreeNode(int val) { this.val = val; this.left = this.right = null; } }
Python:
class TreeNode: def __init__(self, val): self.val = val self.left, self.right = None, None
TreeSet / TreeMap 是底层运用了红黑树的数据结构
PriorityQueue是基于Heap实现的,它能够保证队头元素是优先级最高的元素,但其他元素是不保证有序的。
好比滑动窗口须要保证有序,那么这时能够用到TreeSet,由于TreeSet是有序的,而且不须要每次移动窗口都从新排序,只须要插入和删除(O(logn))就能够了。
注:在 C++ 中相似的结构为 set / map。在Python中没有内置的TreeSet、TreeMap,须要使用第三方库或者本身实现。
http://www.lintcode.com/problem/consistent-hashing-ii/
练习:链表转平衡排序二叉树
https://www.jiuzhang.com/tutorial/algorithm/33