第十章 二叉树及递归问题讲解
10.1 数和二叉树数据结构复习
10.1.1 树(Tree)
树是一种非线性的数据结构,是由n(n >=0)个结点组成的有限集合。
如果n==0,树为空树。如果n>0,树有一个特定的结点,叫做根结点(root)。根结点只有直接后继,没有直接前驱。
除根结点以外的其他结点划分为m(m>=0)个互不相交的有限集合,T0,T1,T2,...,Tm-1,每个集合都是一棵树,称为根结点的子树(sub tree)。
下面是一些其它基本概念:
节点的度:节点拥有的子树个数
叶子节点(leaf):度为0的节点,也就是没有子树的节点
提娶码:o4na
树的高度:树中节点的最大层数,也叫做树的深度
10.1.2 二叉树(Binary Tree)
对于树这种数据结构,使用最频繁的是二叉树。
每个节点最多只有2个子节点的树,叫做二叉树。二叉树中,每个节点的子节点作为根的两个子树,一般叫做节点的左子树和右子树。
(1)二叉树的性质
二叉树有以下性质:
若二叉树的层次从0开始,则在二叉树的第i层至多有2^i个结点(i>=0)
高度为k的二叉树最多有2^(k+1) - 1个结点(k>=-1)(空树的高度为-1)
对任何一棵二叉树,如果其叶子结点(度为0)数为m, 度为2的结点数为n, 则m = n + 1
(2)满二叉树和完全二叉树
满二叉树:除了叶子节点外,每个节点都有两个子节点,每一层都被完全填充。
完全二叉树:除了最后一层外,每一层都被完全填充,并且最后一层所有节点保持向左对齐。
10.1.3 递归(Recursion)
对于树结构的遍历和处理,最为常用的代码结构就是递归(Recursion)。
递归是一种重要的编程技术,该方法用来让一个函数(方法)从其内部调用其自身。一个含直接或间接调用本函数语句的函数,被称之为递归函数。
递归的实现有两个必要条件:
必须定义一个“基准条件”,也就是递归终止的条件。在这种情况下,可以直接返回结果,无需继续递归
在方法中通过调用自身,向着基准情况前进
一个简单示例就是计算阶乘:0 的阶乘被特别地定义为 1;n的阶乘可以通过计算 n-1的阶乘再乘以n来求得的。
代码如下:
// 递归示例:计算阶乘
public static int factorial(int n){
if ( n == 0 ) return 1;
return factorial(n - 1) * n;
}
// 尾递归计算阶乘,需要多一个参数保存“计算状态”
public static int fact(int acc, int n){
if ( n == 0 ) return acc;
return fact( acc * n, n - 1 );
}
上面的第二种实现,把递归调用置于函数的末尾,即正好在return语句之前,这种形式的递归被称为尾递归 (tail recursion),其形式相当于循环。一些语言的编译器对于尾递归可以进行优化,节约递归调用的栈资源。
10.1.4 二叉树的遍历
中序遍历:即左-根-右遍历,对于给定的二叉树根,寻找其左子树;对于其左子树的根,再去寻找其左子树;递归遍历,直到寻找最左边的节点i,其必然为叶子,然后遍历i的父节点,再遍历i的兄弟节点。随着递归的逐渐出栈,最终完成遍历
先序遍历:即根-左-右遍历
后序遍历:即左-右-根遍历
层序遍历:按照从上到下、从左到右的顺序,逐层遍历所有节点。
用递归可以很容易地实现二叉树的先序、中序、后序遍历:
// 遍历二叉树1:先序遍历
public static void printTreePreOrder( TreeNode root ){
if (root == null) return;
System.out.print(root.val + "\t");
printTreePreOrder( root.left );
printTreePreOrder( root.right );
}
// 遍历二叉树2:中序遍历
public static void printTreeInOrder( TreeNode root ){
if (root == null) return;
printTreeInOrder( root.left );
System.out.print(root.val + "\t");
printTreeInOrder( root.right );
}
// 遍历二叉树3:后序遍历
public static void printTreePostOrder( TreeNode root ){
if (root == null) return;
printTreePostOrder( root.left );
printTreePostOrder( root.right );
System.out.print(root.val + "\t");
}
层序遍历,则需要借助一个队列:要访问的节点全部放到队列里。当访问一个节点时,就让它的子节点入队,依次访问。
public static void printTreeLevelOrder( TreeNode root ){
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
while ( !queue.isEmpty() ){
TreeNode curNode = queue.poll();
System.out.print(curNode.val + "\t");
if ( curNode.left != null )
queue.offer(curNode.left);
if ( curNode.right != null )
queue.offer(curNode.right);
}
}
10.1.5 二叉搜索树(Binary Search Tree)
二叉搜索树也称为有序二叉查找树,满足二叉查找树的一般性质,是指一棵空树具有如下性质:
任意节点左子树如果不为空,则左子树中节点的值均小于根节点的值
任意节点右子树如果不为空,则右子树中节点的值均大于根节点的值
任意节点的左右子树,也分别是二叉搜索树
没有键值相等的节点
基于二叉搜索树的这种特点,在查找某个节点的时候,可以采取类似于二分查找的思想,快速找到某个节点。n 个节点的二叉查找树,正常的情况下,查找的时间复杂度为 O(logN)。
二叉搜索树的局限性
一个二叉搜索树是由n个节点随机构成,所以,对于某些情况,二叉查找树会退化成一个有n个节点的线性链表。
10.1.6 平衡二叉搜索树(AVL树)
通过二叉搜索树的分析我们发现,二叉搜索树的节点查询、构造和删除性能,与树的高度相关,如果二叉搜索树能够更“平衡”一些,避免了树结构向线性结构的倾斜,则能够显著降低时间复杂度。
平衡二叉搜索树:简称平衡二叉树。由前苏联的数学家Adelse-Velskil和Landis在1962年提出的高度平衡的二叉树,根据科学家的英文名也称为AVL树。
它具有如下几个性质:
可以是空树
假如不是空树,任何一个结点的左子树与右子树都是平衡二叉树,并且高度之差的绝对值不超过1
平衡的意思,就是向天平一样保持左右水平,即两边的分量大约相同。如定义,假如一棵树的左右子树的高度之差超过1,如左子树的树高为2,右子树的树高为0,子树树高差的绝对值为2就打破了这个平衡。
比如,依次插入1,2,3三个结点后,根结点的右子树树高减去左子树树高为2,树就失去了平衡。我们希望它能够变成更加平衡的样子。
AVL树是带有平衡条件的二叉搜索树,它是严格的平衡二叉树,平衡条件必须满足(所有节点的左右子树高度差不超过1)。不管我们是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡,而旋转是非常耗时的。旋转的目的是为了降低树的高度,使其平衡。
使用场景
AVL树适合用于插入删除次数比较少,但查找多的情况。也在Windows进程地址空间管理中得到了使用。
10.1.7 红黑树(Red-Black Tree)
红黑树是一种特殊的二叉查找树。红黑树的每个节点上都有存储位表示节点的颜色,可以是红(Red)或黑(Black)。
性质:
节点是红色或黑色
根节点是黑色
每个叶子节点都是黑色的空节点(NIL节点)。
每个红色节点的两个子节点都是黑色(从每个叶子到根的所有路径上不能有两个连续的红色节点)
从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点
在插入一个新节点时,默认将它涂为红色(这样可以不违背最后一条规则),然后进行旋转着色等操作,让新的树符合所有规则。
红黑树也是一种自平衡二叉查找树,可以认为是对AVL树的折中优化。
使用场景
红黑树多用于搜索,插入,删除操作多的情况下。红黑树应用比较广泛:
广泛用在各种语言的内置数据结构中。比如C++的STL中,map和set都是用红黑树实现的。Java中的TreeSet,TreeMap也都是用红黑树实现的。
著名的linux进程调度Completely Fair Scheduler,用红黑树管理进程控制块。
epoll在内核中的实现,用红黑树管理事件块
nginx中,用红黑树管理timer等
10.1.8 B树(B-Tree)
B树(B-Tree)是一种自平衡的树,它是一种多路搜索树(并不是二叉的),能够保证数据有序。同时,B树还保证了在查找、插入、删除等操作时性能都能保持在O(logn),为大块数据的读写操作做了优化,同时它也可以用来描述外部存储。
特点:
定义任意非叶子结点最多只有M个儿子;且M>2
根结点的儿子数为[2, M]
除根结点以外的非叶子结点的儿子数为[M/2, M]
每个结点存放至少M/2-1(取上整)和至多M-1个关键字;(至少2个key)
非叶子结点的关键字个数 = 指向儿子的指针个数 – 1
非叶子结点的关键字:K[1], K[2], …, K[M-1];且K[i] < K[i+1]
非叶子结点的指针:P[1], P[2], …, P[M],其中P[1]指向关键字小于K[1]的子树,P[M]指向关键字大于K[M-1]的子树,其它P[i]指向关键字属于(K[i-1], K[i])的子树
所有叶子结点位于同一层
M = 3的B树
10.1.9 B+树
B+树是B-树的变体,也是一种多路搜索树。
B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找。
B+的特性:
所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的
不可能在非叶子结点命中
非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层
更适合文件索引系统
B+ 树的优点:
层级更低,IO 次数更少
每次都需要查询到叶子节点,查询性能稳定
叶子节点形成有序链表,范围查询方便。这使得B+树方便进行“扫库”,也是很多文件系统和数据库底层选用B+树的主要原因。