目录

数据结构 - 二叉树

Binary Tree

在计算机科学中,二叉树(英语:Binary tree)是每个节点最多只有两个分支(即不存在分支度大于2的节点)的树结构。通常分支被称作“左子树”或“右子树”。二叉树的分支具有左右次序,不能随意颠倒。

二叉树的性质

二叉树是一个有根树,并且每个节点最多有2个子节点。非空的二叉树,若树叶总数为 n0,分支度为2的总数为 n2,则 n0 = n2 + 1。

/images/data-structure/binary-tree/BinaryTree_leaf.jpg
分支度计算关系

二叉树的第 i 层至多拥有 $2^{i-1}$ 个节点;

深度为h的二叉树最多有2^h-1个结点(h>=1),最少有h个结点;

特殊类型

/images/data-structure/binary-tree/FullBT_CompleteBT.jpg
特殊类型

完美二叉树 Perfect Binary Tree

一棵深度为k,且有 ${\displaystyle 2^{\begin{aligned}k\end{aligned}}-1}$ 个节点的二叉树,称为完美二叉树(Perfect Binary Tree)。(也称满二叉树 Full Binary Tree) 这种树的特点是每一层上的节点数都是最大节点数。

完全二叉树 Complete Binary Tree

在一颗二叉树中,若除最后一层外的其余层都是满的,并且最后一层要么是满的,要么在右边缺少连续若干节点,则此二叉树为完全二叉树(Complete Binary Tree)。具有n个节点的完全二叉树的深度为 $log_2n+1$ 。深度为k的完全二叉树,至少有 ${\displaystyle 2^{\begin{aligned}k-1\end{aligned}}}$ 个节点,至多有 ${\displaystyle 2^{\begin{aligned}k\end{aligned}}-1}$ 个节点。

存储

顺序存储表示

二叉树可以用数组或链接串列来存储,若是满二叉树就能紧凑排列而不浪费空间。如果某个节点的索引为i,(假设根节点的索引为0)则在它左子节点的索引会是 2i+1,以及右子节点会是 2i+2;而它的父节点(如果有)索引则为 ${\displaystyle \left\lfloor {\frac {i-1}{2}}\right\rfloor }$。这种方法更有利于紧凑存储和更好的访问的局部性,特别是在前序遍历中。然而,它需要连续的存储空间,这样在存储高度为h的n个节点所组成的一般树时,将浪费很多空间。在最糟糕的情况下,如果深度为h的二叉树其每个节点都只有右孩子,则该存储结构需要占用 $2^h-1$ 的空间,实际上却有h个节点,浪费了不少空间,是顺序存储结构的一大缺点。

二叉链表存储表示

在使用记录或存储器地址指针的程序设计语言中,二叉树通常用树结点结构来存储。有时也包含指向唯一的父节点的指针。如果一个结点的子结点个数小于2,一些子结点指针可能为空值,或者为特殊的哨兵结点。 使用链表能避免顺序存储浪费空间的问题,算法和结构相对简单,但使用二叉链表,由于缺乏父链的指引,在找回父节点时需要重新扫描树得知父节点的节点地址。

三叉链表存储表示

改进于二叉链表,增加父节点的指引,能更好地实现节点间的访问,不过算法相对复杂。 当二叉树用三叉链表表示时,有N个结点,就会有N+2个空指针。

遍历

深度优先遍历

在深度优先级中,我们希望从根结点访问最远的结点。和图的深度优先搜索不同的是,不需记住访问过的每一个结点,因为树中不会有环。前序,中序和后序遍历都是深度优先遍历的特例。

(1)前序遍历(DLR),首先访问根结点,然后遍历左子树,最后遍历右子树。简记根-左-右。

1
2
3
4
visit(node)
    print node.value
    if node.left  != null then visit(node.left)
    if node.right != null then visit(node.right)

(2)中序遍历(LDR),首先遍历左子树,然后访问根结点,最后遍历右子树。简记左-根-右。这在遍历二叉搜索树时很常用,因为它能用递增的顺序来遍历所有的值。

(3)后序遍历(LRD),首先遍历左子树,然后遍历右子树,最后访问根结点。简记左-右-根。

/images/data-structure/binary-tree/BinaryTree.png
BinaryTree

  • 前序遍历的结果:M,G,D,B,A,C,F,E,J,H,I,K,L,S,P,O,N,Q,R,W,U,T,V,X,Z,Y
  • 后序遍历的结果:A,C,B,E,F,D,I,H,L,K,J,G,N,O,R,Q,P,T,V,U,Y,Z,X,W,S,M
  • 中序遍历的结果:A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z

广度优先遍历

和深度优先遍历不同,广度优先遍历会先访问离根节点最近的节点。二叉树的广度优先遍历又称按层次遍历。算法借助队列实现。

  • 深度优先遍历用栈(stack)来实现
  • 广度优先遍历使用队列(queue)来实现

二叉查找树 Binary Search Tree

二叉查找树(英语:Binary Search Tree),也称为二叉搜索树、有序二叉树(ordered binary tree)或排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:

  • 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值;
  • 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值;
  • 任意节点的左、右子树也分别为二叉查找树;

二叉查找树相比于其他数据结构的优势在于查找、插入的时间复杂度较低。为 $O(\log n)$。

二叉查找树的查找过程和次优二叉树类似,通常采取二叉链表作为二叉查找树的存储结构。中序遍历二叉查找树可得到一个关键字的有序序列,一个无序序列可以透过建构一棵二叉查找树变成一个有序序列,建构树的过程即为对无序序列进行查找的过程。

二叉堆(英语:binary heap)是一种特殊的堆,二叉堆是完全二叉树或者是近似完全二叉树。二叉堆满足堆特性:父节点的键值总是保持固定的序关系于任何一个子节点的键值,且每个节点的左子树和右子树都是一个二叉堆。

当父节点的键值总是大于或等于任何一个子节点的键值时为“最大堆”。当父节点的键值总是小于或等于任何一个子节点的键值时为“最小堆”。

堆的实现通过构造二叉堆(binary heap),实为二叉树的一种;由于其应用的普遍性,当不加限定时,均指该数据结构的这种实现。这种数据结构具有以下性质。

  • 任意节点小于(或大于)它的所有后裔,最小元(或最大元)在堆的根上(堆序性)。
  • 堆总是一棵完全树。即除了最底层,其他层的节点都被元素填满,且最底层尽可能地从左到右填入。

将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

堆有许多种高级类型包含了适合制作双端队列的最大—最小堆及制作优先权队列的斐波那契堆等。

平衡二叉搜索树 Balanced Binary Search Tree

平衡二叉搜索树(英语:Balanced Binary Search Tree)是一种结构平衡的二叉搜索树,它是一种每个节点的左右两子树高度差都不超过一的二叉树。它能在 $O({\displaystyle \log n}\log n)$ 内完成插入、查找和删除操作,最早被发明的平衡二叉搜索树为AVL树。

  • AVL 树是每个节点包含平衡因子, 等于左高-右高
  • 红黑树 每个节点是红色或者黑色,颜色隔代遗传
  • Treap (树堆)是每个节点都带个随机的 priority number, parent priority >= child priority
  • Splay tree (伸展树)是每个节点带个父节点的指针

AVL 树

AVL树(Adelson-Velsky and Landis Tree)是计算机科学中最早被发明的自平衡二叉查找树。在AVL树中,任一节点对应的两棵子树的最大高度差为1,因此它也被称为高度平衡树。查找、插入和删除在平均和最坏情况下的时间复杂度都是 $O(\log{n})$。增加和删除元素的操作则可能需要借由一次或多次树旋转,以实现树的重新平衡。

节点的平衡因子是它的左子树的高度减去它的右子树的高度(有时相反)。带有平衡因子1、0或 -1的节点被认为是平衡的。带有平衡因子 -2或2的节点被认为是不平衡的,并需要重新平衡这个树。平衡因子可以直接存储在每个节点中,或从可能存储在节点中的子树高度计算出来。

  • 要么是棵空树,要么其根节点左右子树的深度之差的绝对值不超过1;
  • 其左右子树也都是平衡二叉树;
  • 二叉树节点的平衡因子定义为该节点的左子树的深度减去右子树的深度。则平衡二叉树的所有节点的平衡因子只可能是-1,0,1。

操作

AVL树的基本操作一般涉及运作同在不平衡的二叉查找树所运作的同样的算法。但是要进行预先或随后做一次或多次所谓的"AVL旋转"。

以下图表以四列表示四种情况,每行表示在该种情况下要进行的操作。在左左和右右的情况下,只需要进行一次旋转操作;在左右和右左的情况下,需要进行两次旋转操作。

/images/data-structure/binary-tree/Tree_Rebalancing.png
AVL 旋转

删除

从AVL树中删除,可以通过把要删除的节点向下旋转成一个叶子节点,接着直接移除这个叶子节点来完成。因为在旋转成叶子节点期间最多有 $log n$ 个节点被旋转,而每次AVL旋转耗费固定的时间,所以删除处理在整体上耗费 $O(log n)$ 时间。

搜索

可以像普通二叉查找树一样的进行,所以耗费 $O(log n)$ 时间,因为AVL树总是保持平衡的。不需要特殊的准备,树的结构不会由于查找而改变。(这是与伸展树搜索相对立的,它会因为搜索而变更树结构。)

红黑树 Red–black tree

红黑树(英语:Red–black tree)是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型用途是实现关联数组。它在1972年由鲁道夫·贝尔发明,被称为"对称二叉B树",它现代的名字源于Leo J. Guibas和罗伯特·塞奇威克于1978年写的一篇论文。红黑树的结构复杂,但它的操作有着良好的最坏情况运行时间,并且在实践中高效:它可以在 ${\displaystyle {\text{O}}(\log n)}$ 时间内完成查找、插入和删除,这里的 n 是树中元素的数目。

性质

红黑树是每个节点都带有颜色属性的二叉查找树,颜色为红色或黑色。在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

  • 节点是红色或黑色。
  • 根是黑色。
  • 所有叶子都是黑色(叶子是NIL节点)。
  • 每个红色节点必须有两个黑色的子节点。(或者说从每个叶子到根的所有路径上不能有两个连续的红色节点。)(或者说不存在两个相邻的红色节点,相邻指两个节点是父子关系。)(或者说红色节点的父节点和子节点均是黑色的。)
  • 从任一节点到其每个叶子的所有简单路径都包含相同数目的黑色节点。

/images/data-structure/binary-tree/Red-black_tree_example.svg
红黑树的图例

树堆 Treap

树堆(英语:Treap),是计算机科学中术语。是有一个随机附加域满足堆的性质的二叉搜索树,其结构相当于以随机数据插入的二叉搜索树。其基本操作的期望时间复杂度为 $O(\log{n})$。相对于其他的平衡二叉搜索树,Treap的特点是实现简单,且能基本实现随机平衡的结构。属于弱平衡树。

Treap一词由Tree和Heap二词合成而来。其本身是一棵二叉搜索树,它的左子树和右子树也分别是一个Treap,和一般的二叉搜索树不同的是,Treap为每个节点记录优先级。Treap在以关键码构成二叉搜索树的同时,其节点优先级还满足堆的性质。Treap维护堆性质的方法用到了旋转,且只需要进行两种旋转操作,因此编程复杂度较Splay要小一些。

伸展树 Splay Tree

伸展树(英语:Splay Tree)是一种能够自我平衡的二叉查找树,它能在均摊 $O(\log n)$ 的时间内完成基于伸展(Splay)操作的插入、查找、修改和删除操作。它是由丹尼尔·斯立特(Daniel Sleator)和罗伯特·塔扬在1985年发明的

在伸展树上的一般操作都基于伸展操作:假设想要对一个二叉查找树执行一系列的查找操作,为了使整个查找时间更小,被查频率高的那些条目就应当经常处于靠近树根的位置。于是想到设计一个简单方法,在每次查找之后对树进行调整,把被查找的条目搬移到离树根近一些的地方。伸展树应运而生。伸展树是一种自调整形式的二叉查找树,它会沿着从某个节点到树根之间的路径,通过一系列的旋转把这个节点搬移到树根去。

它的优势在于不需要记录用于平衡树的冗余信息。

哈夫曼树

带权路径最短的二叉树称为哈夫曼树或最优二叉树。

给定N个权值作为N个叶子结点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(Huffman Tree)。哈夫曼树是带权路径长度最短的树,权值较大的结点离根较近。

笛卡尔树

是一种特定的二叉树数据结构,可由数列构造,在范围最值查询、范围top k查询(range top k queries)等问题上有广泛应用。它具有堆的有序性,中序遍历可以输出原数列。笛卡尔树结构由Vuillmin(1980)在解决范围搜索的几何数据结构问题时提出。从数列中构造一棵笛卡尔树可以线性时间完成,需要采用基于栈的算法来找到在该数列中的所有最近小数。

附录