二叉查找树 (树ADT连载 3/13)
可在作者的 github仓库 中获取本文和其他文章的 markdown 源文件及相关代码。
欢迎评论或仓库 PR 指出文章错漏或与我讨论相关问题,我将长期维护所有文章。
所有文章均用 Typora 完成写作,可使用 Typora 打开文章 md 文件,以获得最佳阅读体验。
🌲🌲🌲 一起手写一棵完整的二叉查找树。
❗️ 【NEW】 ❗️
这是小白 yuki 推出的「树ADT」系列文章的第 3 篇 (3/13) 。
实际上本文是为了后续讲解多种 平衡二叉树 的前置文章,最终目的是讲解 「红黑树」 (已推出,红黑树从入门到看开 )。
本文重点在于展现如何设计一个 BST 类,分析主要方法的代码实现,并给出该类的完整实现代码。读者学习之后 可以自己写出一个方法较为完备的基本 BST 类 。
yuki的其他文章如下,欢迎阅读指正!
如下所有文章同时也在我的 github 仓库 中维护。
[TOC]
二叉查找树
二叉查找树 / 二叉搜索树 / 二叉排序树 (Binary Search Tree / Binary Sort Tree, BST) : 对于一棵二叉树,每个结点存有一个可用于比较的数据项,规定结点 x x x 的左子树中所有结点的数据项小于 x x x 的数据项,而 x x x 的右子树中所有结点的数据项大于 x x x 的数据项,这样的二叉树即为二叉查找树。对于有 n n n 个元素的二叉树,假设树高平衡,则根据前面的性质可知其高度为 l o g n logn l o g n ,我们将能够通过其结点数据项有序的特点,以类似二分查找的树上的 二分操作 实现 O ( l o g n ) O(logn) O ( l o g n ) 平均时间复杂度的插入,查找和删除等操作。BST 内容非常丰富,下面我们先介绍非自平衡的 基本 BST ,由于基本BST树高不保证平衡,树上操作的复杂度无法保证为 O ( l o g n ) O(logn) O ( l o g n ) ,因此引入 平衡 BST 。我们将会详细几种不同的平衡 BST 实现。
根据定义可知下图中只有左侧的树是 BST ,右侧的树中结点 7 与 6 不满足 BST 的结点顺序性质,若结点 7 是结点 8 的左子结点,则为 BST。
※ 应用二叉查找树实现的数据结构一般为 s e t set se t 或 m a p map ma p ,若为 s e t set se t (例如 Java 中的 T r e e S e t TreeSet T ree S e t ),则不会存储相同数据项,若为 m a p map ma p (例如 Java 中的 T r e e M a p TreeMap T ree M a p ),则不会存储相同 k e y key k ey 值。若要求保存相同值,可以采用链表或将相同值看作小于或大于来处理,本文以及后续讲解的所有二叉查找树均不保存相同值。
根据 Wiki 词条,此数据结构在 1960 年代由多人独立提出。
The binary search tree algorithm was discovered independently by several researchers, including P.F. Windley, Andrew Donald Booth , Andrew Colin , Thomas N. Hibbard .
基本BST
基本 BST 即只保持 BST 结点数据项大小关系性质,而不维护树高平衡的 非平衡 BST 。所谓「平衡」,指树的形态类似等腰三角形,从根结点到任意叶子结点的路径长度都是稳定的 O ( l o g n ) O(logn) O ( l o g n ) ,更严格的定义是 「任意结点的左右子树高差不超过 1」 ,如此即能保证对结点的增删改查的时间复杂度均为 O ( l o g n ) O(logn) O ( l o g n ) 。
基本BST的定义十分简单,我们直接给出它的类的实现架构,并介绍其中的主要方法,然后再分析主要操作的时空复杂度。
本节内容为 Mark Allen Weiss 所著 数据结构与算法分析:Java语言描述 相关章节的整理和总结。代码亦来自该书,略作改动。
基本BST类架构
以下是基本 BST 类 (M y T r e e S e t MyTreeSet M y T ree S e t ) 架构。
类成员/方法
描述
private Node<E> root
唯一字段,根结点
public MyTreeSet()
无参构造器
public void clear()
树置空
public boolean isEmpty()
树判空
public void add(E e)
插入结点驱动方法
public void remove(E e)
删除结点驱动方法
public E first()
查找最小结点驱动方法
public E last()
查找最大结点驱动方法
public E floor(E e)
查找小于等于 e e e 的最大元素驱动方法
public E ceiling(E e)
查找大于等于 e e e 的最小元素驱动方法
public E lower(E e)
查找严格小于 e e e 的最大元素驱动方法
public E higher(E e)
查找严格大于 e e e 的最小元素驱动方法
public boolean contains(E e)
结点存在判定驱动方法
public void printTree()
中序遍历打印树的驱动方法
public int size()
求树大小驱动方法
public int height()
求树高驱动方法
private Node<E> add(E e, Node<E> t)
插入结点
private Node<E> remove(E e, Node<E> t)
删除结点(懒惰删除)
private Node<E> first(Node<E> t)
返回树的最小结点
private Node<E> last(Node<E> t)
返回树的最大结点
private Node floor(Node<E> x, E e)
查找小于等于 e e e 的最大元素
private Node<E> ceiling(Node<E> x, E e)
查找大于等于 e e e 的最小元素
private Node lower(Node<E> x, E e)
查找严格小于 e e e 的最大元素
private Node<E> higher(Node<E> x, E e)
查找严格大于 e e e 的最小元素
private boolean contains(E e, Node<E> t)
判断树中是否有指定元素的结点
private void printTree(Node<E> t)
中序遍历打印树
private int height(Node<E> t)
求以 t t t 为根结点的树高
private int size(Node<E> t)
求以 t t t 为根结点的树大小
以下是二叉树结点嵌套类 N o d e Node N o d e 的架构。
类成员/方法
描述
public E element
结点数据
public Node<E> left
左子结点
public Node<E> right
右子结点
public Node(E element)
构造器
public Node(E element, Node<E> left, Node<E> right)
构造器
主要方法
在类架构表格中,我们看到 BST 实现的 s e t set se t 类 (M y T r e e S e t MyTreeSet M y T ree S e t ) 主要应该包含哪些方法,接下来我们讲解这其中的主要方法。其他实现简单的方法在「类的实现代码」中呈现。
add
插入结点操作由插入驱动方和具体插入方法完成,将值为 e e e 的结点插入到当前二叉树中并使得插入后仍保持查找树性质。递归地比较 e e e 和当前结点值的大小,最后 e e e 总能 作为一个叶子结点 被插入到满足查找树性质的位置。
对于具体插入方法,当原树为 n u l l null n u ll 时返回 Node<>(e,null, null)
给 r o o t root roo t ,即数据为 e e e 的结点作为根结点插入。当原树不为 n u l l null n u ll 时, 插入后返回原根 。
1 2 3 4 5 6 7 8 9 10 public void add (E e) { root = add(e, root); } private Node<E> add (E e, Node<E> t) { if (t == null ) return new Node <>(e,null , null ); int cmp = e.compareTo(t.element); if (cmp < 0 ) t.left = add(e, t.left); else if (cmp > 0 ) t.right = add(e, t.right); return t; }
remove
删除结点操作由删除驱动方法和具体删除方法完成,将数据项为 e e e 的结点删除并使得删除后仍保持查找树性质。
首先,在驱动方法中执行 c o n t a i n s ( e ) contains(e) co n t ain s ( e ) ,若不存在直接返回,否则删除目标存在,调用具体删除方法,递归地寻找 e l e m e n t element e l e m e n t 为 e e e 的结点 t t t ,对其执行 懒惰删除 。删除相比其他操作要复杂一些,以下为找到时结点 t t t 的两种情形。
情形1: t t t 有左右两个子结点。
情形2: t t t 有一个或没有子结点。
情形1时,找到 t t t 的右子树中最小结点 m i n min min ,令 t.element = min.element
,即相当于用 m i n min min 替换掉要删除的 t t t ,然后对右子树执行删除 m i n min min 的操作(由于是最小值,故此删除必属于情形 2)。
情形2时,根据是否有左子结点,执行此语句 t = (t.left != null) ? t.left : t.right
。有左子结点时 t = t.left
,即用这个左子结点取代 t t t 。没有左子结点时 t = t.right
,表示以右子结点取代 t t t (包含右子结点也为空的情形,则此时 t = null
)。
对于具体删除方法,当原树为 n u l l null n u ll 时返回 t t t (即 r o o t root roo t ) 给 r o o t root roo t 。当原树不为 n u l l null n u ll 时,在删除后递归地返回到当前根给 r o o t root roo t (例如当树只有根结点,且删除的就是该根时,root = null
)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void remove (E e) { if (e == null ) throw new IllegalArgumentException ("argument to remove() is null" ); if (!contains(e)) return ; root = remove(e, root); } private Node<E> remove (E e, Node<E> t) { if (t == null ) return null ; int cmp = e.compareTo(t.element); if (cmp < 0 ) t.left = remove(e, t.left); else if (cmp > 0 ) t.right = remove(e, t.right); else if (t.left != null && t.right != null ) { t.element = first(t.right).element; t.right = remove(t.element, t.right); } else t = (t.left != null ) ? t.left : t.right; return t; }
first/last
查找具有最小/最大数据的结点的操作由驱动方法和具体方法实现。根据 BST 性质一直向左/向右寻找即可找到最小/最大值。以下以递归方式实现 f i r s t first f i rs t ,以迭代方式实现 l a s t last l a s t 。这两个方法的实现容易理解,不赘述。如下展示 f i r s t first f i rs t 的实现,l a s t last l a s t 的实现请看「类的实现代码」。
1 2 3 4 5 6 7 8 public E first () { if (isEmpty()) throw new NoSuchElementException (); return first(root).element; } private Node<E> first (Node<E> t) { if (t.left == null ) return t; return first(t.left); }
floor/ceiling
f l o o r floor f l oor 方法用于查找小于等于指定元素中的最大者,c e i l i n g ceiling ce i l in g 方法用于查找大于等于指定元素中的最小者,均由驱动方法和具体方法实现。其实现方式也是根据 BST 性质向左/向右寻找目标。
以 f l o o r floor f l oor 为例,说明如下。
if (cmp == 0) return x;
若当前元素 x x x 与目标元素相等,则 x x x 即为所求直接返回。
if (cmp < 0) return floor(x.left, e);
若 e e e 小于当前元素 x x x ,说明目标元素在 x x x 的左子树中,传入 x . l e f t x.left x . l e f t 递归调用 f l o o r floor f l oor 。
Node<E> t = floor(x.right, e);
否则此时 e e e 大于 x x x ,在 x x x 的右子树中找最大者,即为所求。
如下展示 f l o o r floor f l oor 的实现,c e i l i n g ceiling ce i l in g 的实现请看「类的实现代码」。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public E floor (E e) { if (e == null ) throw new IllegalArgumentException ("argument to floor() is null" ); if (isEmpty()) throw new NoSuchElementException ("calls floor() with empty symbol table" ); Node<E> x = floor(root, e); if (x == null ) throw new NoSuchElementException ("argument to floor() is too small" ); else return x.element; } private Node floor (Node<E> x, E e) { if (x == null ) return null ; int cmp = e.compareTo(x.element); if (cmp == 0 ) return x; if (cmp < 0 ) return floor(x.left, e); Node<E> t = floor(x.right, e); if (t != null ) return t; else return x; }
类似地,l o w e r lower l o w er 方法用于查找严格小于指定元素中的最大者,h i g h e r higher hi g h er 方法用于查找严格大于指定元素的最小者。二者的实现方式与 f l o o r / c e i l i n g floor/ceiling f l oor / ce i l in g 非常类似,只需要将等于的情形与需要递归的情形合写即可,具体请看「类的实现代码」。
contains
寻找树中是否包含数据项为 e e e 的结点的操作由相应的驱动方法和具体方法实现。根据二叉查找树的性质以递归方式一直向左/向右寻找。找到返回 t r u e true t r u e ,否则返回 f a l s e false f a l se 。
1 2 3 4 5 6 7 8 9 10 public boolean contains (E e) { return contains(e, root); } private boolean contains (E e, Node<E> t) { if (t == null ) return false ; int cmp = e.compareTo(t.element); if (cmp < 0 ) return contains(e, t.left); else if (cmp > 0 ) return contains(e, t.right); else return true ; }
时空复杂度
二叉查找树的平均深度为 O ( l o g n ) O(logn) O ( l o g n ) ,因此上述介绍的主要方法 平均时间复杂度 均为 O ( l o g n ) O(logn) O ( l o g n ) 。当树为链状时,退化为链表形式,达到最坏时间复杂度 O ( n ) O(n) O ( n ) 。若方法以递归实现,则空间复杂度取决于递归深度 O ( l o g n ) O(logn) O ( l o g n ) ,若以迭代方式实现则空间复杂度为 O ( 1 ) O(1) O ( 1 ) 。
方法
平均时间
最坏时间
add/remove/first/last/floor/ceiling/lower/higher/contains
O ( l o g n ) O(logn) O ( l o g n )
O ( n ) O(n) O ( n )
BST 平均深度为 O ( l o g n ) O(logn) O ( l o g n ) 的证明见此论文 。
类的实现代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 class MyTreeSet <E extends Comparable <? super E>>{ private Node<E> root; public MyTreeSet () {} public void clear () { root = null ; } public boolean isEmpty () { return root == null ; } public void add (E e) { root = add(e, root); } public void remove (E e) { if (e == null ) throw new IllegalArgumentException ("argument to remove() is null" ); if (!contains(e)) return ; root = remove(e, root); } public E first () { if (isEmpty()) throw new NoSuchElementException (); return first(root).element; } public E last () { if (isEmpty()) throw new NoSuchElementException (); return last(root).element; } public E floor (E e) { if (e == null ) throw new IllegalArgumentException ("argument to floor() is null" ); if (isEmpty()) throw new NoSuchElementException ("calls floor() with empty symbol table" ); Node<E> x = floor(root, e); if (x == null ) throw new NoSuchElementException ("argument to floor() is too small" ); else return x.element; } public E ceiling (E e) { if (e == null ) throw new IllegalArgumentException ("argument to ceiling() is null" ); if (isEmpty()) throw new NoSuchElementException ("calls ceiling() with empty symbol table" ); Node<E> x = ceiling(root, e); if (x == null ) throw new NoSuchElementException ("argument to ceiling() is too large" ); else return x.element; } public E lower (E e) { if (e == null ) throw new IllegalArgumentException ("argument to lower() is null" ); if (isEmpty()) throw new NoSuchElementException ("calls lower() with empty symbol table" ); Node<E> x = lower(root, e); if (x == null ) throw new NoSuchElementException ("argument to lower() is too small" ); else return x.element; } public E higher (E e) { if (e == null ) throw new IllegalArgumentException ("argument to higher() is null" ); if (isEmpty()) throw new NoSuchElementException ("calls higher() with empty symbol table" ); Node<E> x = higher(root, e); if (x == null ) throw new NoSuchElementException ("argument to higher() is too large" ); else return x.element; } public boolean contains (E e) { return contains(e, root); } public void printTree () { if (isEmpty()) System.out.println("Empty tree" ); else printTree(root); } public int size () { return size(root); } public int height () { return height(root); } private Node<E> add (E e, Node<E> t) { if (t == null ) return new Node <>(e,null , null ); int cmp = e.compareTo(t.element); if (cmp < 0 ) t.left = add(e, t.left); else if (cmp > 0 ) t.right = add(e, t.right); return t; } private Node<E> remove (E e, Node<E> t) { if (t == null ) return null ; int cmp = e.compareTo(t.element); if (cmp < 0 ) t.left = remove(e, t.left); else if (cmp > 0 ) t.right = remove(e, t.right); else if (t.left != null && t.right != null ) { t.element = first(t.right).element; t.right = remove(t.element, t.right); } else t = (t.left != null ) ? t.left : t.right; return t; } private Node<E> first (Node<E> t) { if (t.left == null ) return t; return first(t.left); } private Node<E> last (Node<E> t) { while (t.right != null ) t = t.right; return t; } private Node floor (Node<E> x, E e) { if (x == null ) return null ; int cmp = e.compareTo(x.element); if (cmp == 0 ) return x; if (cmp < 0 ) return floor(x.left, e); Node<E> t = floor(x.right, e); if (t != null ) return t; else return x; } private Node<E> ceiling (Node<E> x, E e) { if (x == null ) return null ; int cmp = e.compareTo(x.element); if (cmp == 0 ) return x; if (cmp > 0 ) return ceiling(x.right, e); Node<E> t = ceiling(x.left, e); if (t != null ) return t; else return x; } private Node lower (Node<E> x, E e) { if (x == null ) return null ; int cmp = e.compareTo(x.element); if (cmp <= 0 ) return lower(x.left, e); Node<E> t = lower(x.right, e); if (t != null ) return t; else return x; } private Node<E> higher (Node<E> x, E e) { if (x == null ) return null ; int cmp = e.compareTo(x.element); if (cmp >= 0 ) return higher(x.right, e); Node<E> t = higher(x.left, e); if (t != null ) return t; else return x; } private boolean contains (E e, Node<E> t) { if (t == null ) return false ; int cmp = e.compareTo(t.element); if (cmp < 0 ) return contains(e, t.left); else if (cmp > 0 ) return contains(e, t.right); else return true ; } private void printTree (Node<E> t) { if (t != null ) { printTree(t.left); System.out.println(t.element); printTree(t.right); } } private int height (Node<E> t) { if (t == null ) return -1 ; else return 1 + Math.max(height(t.left), height(t.right)); } private int size (Node<E> t) { if (t != null ) { if (t.left != null && t.right != null ) { return 1 + size(t.left) + size(t.right); } else { return t.left != null ? 1 + size(t.left) : 1 + size(t.right); } } return 0 ; } private static class Node <E>{ public E element; public Node<E> left, right; public Node (E element, Node<E> left, Node<E> right) { this .element = element; this .left = left; this .right = right; } } }
测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 package com.yukiyama;import java.util.NoSuchElementException;public class MyTreeSetDemo { public static void main (String[] args) { MyTreeSet<Integer> t = new MyTreeSet <>( ); int [] elements = {10 ,98 ,61 ,13 ,8 ,20 ,48 ,29 ,47 ,74 }; for (int e : elements) t.add(e); t.add(10 ); t.printTree(); System.out.printf("size: %d, height: %d, min: %d, max: %d\n" , t.size(), t.height(), t.first(), t.last()); System.out.printf("has 47? %s, has 100? %s\n" , t.contains(47 ), t.contains(100 )); System.out.printf("floor(9): %s, floor(10): %s, ceiling(73): %s, ceiling(98): %s\n" , t.floor(9 ), t.floor(10 ), t.ceiling(73 ), t.ceiling(98 )); System.out.printf("lower(9): %s, lower(10): %s, higher(73): %s, higher(61): %s\n" , t.lower(9 ), t.lower(10 ), t.higher(73 ), t.higher(61 )); t.remove(47 ); t.remove(48 ); t.remove(61 ); t.printTree(); System.out.println("is empty: " + t.isEmpty()); t.clear(); System.out.println("is empty: " + t.isEmpty()); System.out.printf("size: %d, height: %d\n" , t.size(), t.height()); } }