计算机程序的思惟逻辑 (42) - 排序二叉树

本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》(马俊昌著),由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买:京东自营连接 html

40节介绍了HashMap,41节介绍了HashSet,它们的共同实现机制是哈希表,一个共同的限制是没有顺序,咱们提到,它们都有一个能保持顺序的对应类TreeMap和TreeSet,这两个类的共同实现基础是排序二叉树,为了更好的理解TreeMap/TreeSet,本节咱们先来介绍排序二叉树的一些基本概念和算法。算法

基本概念

先来讲树的概念,现实中,树是从下往上长的,树会分叉,在计算机程序中,通常而言,与现实相反,树是从上往下长的,也会分叉,有个根节点,每一个节点能够有一个或多个孩子节点,没有孩子节点的节点通常称为叶子节点。数据库

二叉树是一个树,但,每一个节点最多有两个孩子节点,一左一右,左边的称为左孩子,右边的称为右孩子,咱们看两个例子,以下所示: 编程

这两棵树都是二叉树,左边的根节点为5,除了叶子节点外,每一个节点都有两个孩子节点,右边的根节点为7,有的节点有两个孩子节点,有的只有一个。

树有一个高度或深度的概念,是从根到叶子节点通过的节点个数的最大值,左边树的高度为3,右边的为5。数组

排序二叉树也是二叉树,但,它没有重复元素,并且是有序的二叉树,什么顺序呢?对每一个节点而言:微信

  • 若是左子树不为空,则左子树上的全部节点都小于该节点
  • 若是右子树不为空,则右子树上的全部节点都大于该节点

上面的两颗二叉树都是排序二叉树。好比说左边的树,根节点为5,左边的都小于5,右边的都大于5。再看右边的树,根节点为7,左边的都小于7,右边的都大于7,在以3为根的左子树中,其右子树的值都大于3。数据结构

排序二叉树有什么优势?如何在树中进行基本操做如查找、遍历、插入和删除呢?咱们来看一下基本的算法。性能

基本算法

查找

排序二叉树有一个很好的优势,在其中查找一个元素是很方便、也很高效的,基本步骤为:spa

  1. 首先与根节点比较,若是相同,就找到了
  2. 若是小于根节点,则到左子树中递归查找
  3. 若是大于根节点,则到右子树中递归查找

这个步骤与在数组中进行二分查找或者说折半查找的思路是相似的,若是二叉树是比较平衡的,相似上图中左边的二叉树,则每次比较都能将比较范围缩小一半,效率很高。3d

此外,在排序二叉树中,能够方便的查找最小最大值,最小值即为最左边的节点,从根节点一路查找左孩子便可,最大值即为最右边的节点,从根节点一路查找右孩子便可。

遍历

排序二叉树也能够方便的按序遍历,用递归的方式,用以下算法便可按序遍历:

  1. 访问左子树
  2. 访问当前节点
  3. 访问右子树

好比,遍历访问下面的二叉树:

从根节点开始,但先访问根节点的左子树,一直到最左边的节点,因此第一个访问的是1,1没有右子树,返回上一层,访问3,而后访问3的右子树,4没有左子树,因此访问4,而后是4的右子树6,依次类推,访问顺序就是有序的:1 3 4 6 7 8 9。

不用递归的方式,也能够实现按序遍历,第一个节点为最左边的节点,从第一个节点开始,依次找后继节点。给定一个节点,找其后继节点的算法为:

  • 若是该节点有右孩子,则后继为右子树中最小的节点。
  • 若是该节点没有右孩子,则后继为父节点或某个祖先节点,从当前节点往上找,若是它是父亲节点的右孩子,则继续找父节点,直到它不是右孩子或父节点为空,第一个非右孩子节点的父亲节点就是后继节点,若是找不到这样的祖先节点,则后继为空,遍历结束。

文字描述比较抽象,咱们来看个图,以上图为例,每一个节点的后继以下图绿色箭头所示:

对每一个节点,对照算法,咱们再详细解释下:

  • 第一个节点1没有右孩子,它不是父节点的右孩子,因此它的后继节点就是其父节点3。
    复制代码
  • 3有右孩子,右子树中最小的就是4,因此3的后继节点为4。
  • 4有右孩子,右子树中只有一个节点6,因此4的后继节点为6。
  • 6没有右孩子,往上找父节点,它是父节点4的右孩子,4又是父节点3的右孩子,3不是父节点7的右孩子,因此6的后继节点为3的父节点7。
  • 7有右孩子,右子树中最小的是8,因此7的后继节点为8。
  • 8没有右孩子,往上找父节点,它不是父节点9的右孩子,因此它的后继节点就是其父节点9。
  • 9没有右孩子,往上找父节点,它是父节点7的右孩子,接着往上找,但7已是根节点,父节点为空,因此后继为空。

怎么构建排序二叉树呢?能够在插入、删除元素的过程当中造成和保持。

插入

在排序二叉树中,插入元素首先要找插入位置,即新节点的父节点,怎么找呢?与查找元素相似,从根节点开始往下找,其步骤为:

  1. 与当前节点比较,若是相同,表示已经存在了,不能再插入。
  2. 若是小于当前节点,则到左子树中寻找,若是左子树为空,则当前节点即为要找的父节点。
  3. 若是大于当前节点,则到右子树中寻找,若是右子树为空,则当前节点即为要找的父节点。

找到父节点后,便可插入,若是插入元素小于父节点,则做为左孩子插入,不然做为右孩子插入。

咱们来看个例子,依次插入7, 3, 4, 1, 9, 6, 8的过程,这个过程以下图所示:

删除

从排序二叉树中删除一个节点要复杂一些,有三种状况:

  • 节点为叶子节点
  • 节点只有一个孩子
  • 节点有两个孩子

咱们分别来看下。

若是节点为叶子节点,则很简单,能够直接删掉,修改父节点的对应孩子为空便可。

若是节点只有一个孩子节点,则替换待删节点为孩子节点,或者说,在孩子节点和父节点之间直接创建连接。好比说,在下图中,左边二叉树中删除节点4,就是让4的父节点3与4的孩子节点6直接创建连接。

若是节点有两个孩子,则首先找该节点的后继(根据以前介绍的后继算法,后继为右子树中最小的节点,这个后继必定没有左孩子),找到后继后,替换待删节点为后继的内容,而后再删除后继节点。后继节点没有左孩子,这就将两个孩子的状况转换为了叶子节点或只有一个孩子的状况。

好比说,在下图中,从左边二叉树中删除节点3,3有两个孩子,后继为4,首先替换3的内容为4,而后再删除节点4。

平衡的排序二叉树

从前面的描述中能够看出,排序二叉树的形状与插入和删除的顺序密切相关,极端状况下,排序二叉树可能退化为一个链表,好比说,若是插入顺序为:1 3 4 6 7 8 9,则排序二叉树形状为:

退化为链表后,排序二叉树的优势就都没有了,即便没有退化为链表,若是排序二叉树高度不平衡,效率也会变的很低。

平衡具体定义是什么呢?有一种高度平衡的定义,即任何节点的左右子树的高度差最多为一。知足这个平衡定义的排序二叉树又被称为AVL树,这个名字源于它的发明者G.M. Adelson-Velsky 和 E.M. <Landis,在他们的算法中,在插入和删除节点时,经过一次或屡次旋转操做来从新平衡树。

在TreeMap的实现中,用的并非AVL树,而是红黑树,与AVL树相似,红黑树也是一种平衡的排序二叉树,也是在插入和删除节点时经过旋转操做来平衡的,但它并非高度平衡的,而是大体平衡的,所谓大体是指,它确保,对于任意一条从根到叶子节点的路径,没有任何一条路径的长度会比其余路径长过两倍。红黑树减弱了对平衡的要求,但下降了保持平衡须要的开销,在实际应用中,统计性能高于AVL树。

为何叫红黑树呢?由于它对每一个节点进行着色,颜色或黑或红,并对节点的着色有一些约束,知足这个约束便可以确保树是大体平衡的。

对AVL树和红黑树,它们保持平衡的细节都是比较复杂的,咱们就不介绍了,咱们须要知道的就是,它们都是排序二叉树,都经过在插入和删除时执行开销不大的旋转操做保持了树的高度平衡或大体平衡,从而保证了树的查找效率。

小结

本节介绍了排序二叉树的基本概念和算法。

排序二叉树保持了元素的顺序,并且是一种综合效率很高的数据结构,基本的保存、删除、查找的效率都为O(h),h为树的高度,在树平衡的状况下,h为log2(N),N为节点数,好比,若是N为1024,则log2(N)为10。

基本的排序二叉树不能保证树的平衡,可能退化为一个链表,有不少保持树平衡的算法,AVL树是第一个,能保证树的高度平衡,但红黑树是实际中使用更为普遍的,虽然只能保证大体平衡,但下降了维持树平衡须要的开销,总体统计效果更好。

与哈希表同样,树也是计算机程序中一种重要的数据结构和思惟方式。为了可以快速操做数据,哈希和树是两种基本的思惟方式,不须要顺序,优先考虑哈希,须要顺序,考虑树。除了容器类TreeMap/TreeSet,数据库中的索引结构也是基于树的(不过基于B树,而不是二叉树),而索引是可以在大量数据中快速访问数据的关键。

理解了排序二叉树的基本概念和算法,理解TreeMap和TreeSet就比较容易了,让咱们在接下来的两节中探讨这两个类。


未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),深刻浅出,老马和你一块儿探索Java编程及计算机技术的本质。用心原创,保留全部版权。

相关文章
相关标签/搜索