前面咱们学习了基于线性表的数据结构,如数组,链表,队列,栈等。如今咱们要开始学习一种非线性的数据结构--树(tree),是否是很兴奋呢!让咱们开始新的系列吧!html
先让咱们回忆一下线性表的查找,首先最暴力的方法就是作一个线性扫描,一一对比是否是要找的值。这么作的时间复杂度显而易见的是 O(N),如表格第一行;更机智一点,咱们采用二分法,首先将线性表排好顺序,而后每次对比中间的值就行了,这样作的时间复杂度就是 O(logN),如表格第二行。但上面的作法都是利用的线性数据结构,而它有致命的缺点;那就是进行动态的操做时,好比插入,删除;没法同时实现迅速的查找,只能等从新排序之后再查,效率就低了不少,没法知足平常需求(以下表)。这个时候咱们的主角就闪亮登场了——二叉查找树。java
图源node
首先我放几张图说明一下什么是二叉树,树的高度,深度等等,详细的介绍我已经放在这里,有兴趣的话也能够看看别人的博客。数组
图源 转载学习,如侵权则联系我删除!数据结构
废话很少说咱们开始实现一颗二叉查找树(BST)吧!
[注] 为了方便理解大部分代码都提供了递归实现!性能
public class BST<Key extends Comparable<Key>, Value> {
private Node root; // root of BST
private class Node {
private Key key; // sorted by key
private Value val; // associated data
private Node left, right; // left and right subtrees
private int size; // number of nodes in subtree
public Node(Key key, Value val, int size) {
this.key = key;
this.val = val;
this.size = size;
}
}
/** * Initializes an empty symbol table. */
public BST() {}
/** * Returns the number of key-value pairs in this symbol table. * @return the number of key-value pairs in this symbol table */
public int size() {
return size(root);
}
// return number of key-value pairs in BST rooted at x
private int size(Node x) {
if (x == null) return 0;
else return x.size;
}
}
咱们知道二叉查找树的任一个节点,他的左子结点比他小,右子节点比他大,哈,那么咱们只要进行一波中序遍历就能够完成数据的排序啦!学习
/*************************************************************************** * 中序遍历,非递归版本 ***************************************************************************/
public Iterable<Key> keys() {
Stack<Node> stack = new Stack<Node>();
Queue<Key> queue = new Queue<Key>();
Node x = root;
while (x != null || !stack.isEmpty()) {
if (x != null) {
stack.push(x);
x = x.left;
} else {
x = stack.pop();
queue.enqueue(x.key);
x = x.right;
}
}
return queue;
}
/************************************************************************ * 中序遍历,递归打印 ************************************************************************/
public void inOrder(Node* root) {
if (root == null) return;
inOrder(root.left);
print root // 此处为伪代码,表示打印 root 节点
inOrder(root.right);
}
/************************************************************************ * 非递归 ************************************************************************/
Value get(Key key){
Node x = root;
while(x != null){
int cmp = key.compareTo(x.key);
if(cmp<0)
x = x.left;
else if(cmp>0)
x = x.right;
else return
x.value;
}
return null;
}
/************************************************************************ * 递归 ************************************************************************/
public Value get(Key key) {
return get(root, key);
}
private Value get(Node x, Key key) {
if (x == null) return null;
int cmp = key.compareTo(x.key);
if (cmp < 0) return get(x.left, key);
else if (cmp > 0) return get(x.right, key);
else return x.val;
}
/************************************************************************ * 非递归 ************************************************************************/
public void put(Key key, Value val) {
Node z = new Node(key, val);
if (root == null) {
root = z;
return;
}
Node parent = null, x = root;
while (x != null) {
parent = x;
int cmp = key.compareTo(x.key);
if (cmp < 0)
x = x.left;
else if (cmp > 0)
x = x.right;
else {
x.val = val;
return;
}
}
int cmp = key.compareTo(parent.key);
if (cmp < 0)
parent.left = z;
else
parent.right = z;
}
/************************************************************************ * 递归版本 ************************************************************************/
public void put(Key key, Value value) {
root = put(root, key, value);
}
private Node put(Node x, Key key, Value value) {
if (x == null)
return new Node(key, value, 1);
int cmp = key.compareTo(x.key);
if (cmp < 0)
x.left = put(x.left, key, value);
else if (cmp > 0)
x.right = put(x.right, key, value);
else
x.value = value;
x.size = 1 + size(x.left) + size(x.right);
return x;
}
删除有两种方式,一种是合并删除
,另外一种是复制删除
,这里我主要讲第二种,想了解第一种能够点这里ui
在正式的删除以前让咱们先热身一下,看看怎么删除一棵树的最小值(如图)。this
步骤spa
public void deleteMin(){
root = deleteMin(root);
}
private Node deleteMin(Node x){
if (x.left == null) return x.right;
x.left = deleteMin(x.left);
x.N = size(x.left) + size(x.right) + 1;
return x;
}
在说复制删除以前,咱们须要先熟悉二叉查找树的前驱和后继(根据中序遍历衍生出来的概念)。
上图是复制删除的原理,咱们既能够用前驱节点 14 代替,又能够用后继节点 18 代替。
如图所示,咱们分为四个步骤
min(t.right)
;deleteMin(t.right)
,也就是在删除后全部节点仍然都大于 x.key 的子二叉查找树。public void delete(Key key){
root = delete(root,key);
}
private Node min(Node x){
if(x.left == null) return x;
else return min(x.left);
}
private Node delete(Node x, Key key){
if(x==null) return null;
int cmp = key.compareTo(x.key);
if(cmp < 0) x.left = delete(x.left, key);
else if(cmp > 0) x.right = delete(x.right, key);
else{
if(x.right == null) return x.left;
if(x.left == null) return x.right;
Node t = x;
x = min(t.right);
x.right = deleteMin(t.right);
x.left = t.left;
}
x.N = size(x.left) + size(x.right) + 1;
return x;
}
在前面的代码中,咱们老是删除node中的后继结点,这样必然会下降右子树的高度,在前面中咱们知道,咱们也可使用前驱结点来代替被删除的结点。因此咱们能够交替的使用前驱和后继来代替被删除的结点。
J.Culberson从理论证明了使用非对称删除, IPL(内部路径长度)的指望值是 O(n√n), 平均查找时间为 O(√n),而使用对称删除, IPL的指望值为 O(nlgn),平均查找时间为 O(lgn)。
查找节点 x 的排名
public int rank(Key key){
return rank(key, root);
}
private int rank(Key key, Node x){
// 返回以 x 为根节点的子树中小于x.key的数量
if(x == null) return 0;
int cmp = key.compareTo(x.key);
if(cmp<0) return rank(key,x.left);
else if(cmp>0) return 1 + size(x.left) + rank(key,x.right);
else return size(x.left);
}
经过前面的分析咱们知道,通常状况下二叉查找树的查找,插入,删除都是 O(lgn)的时间复杂度,可是二叉查找树的时间复杂度是和树的高度是密切相关的,若是咱们以升序的元素进行二叉树的插入,咱们会发现,此时的二叉树已经退化成链表了,查找的时间复杂度变成了 O(n),这在性能上是不可容忍的退化!那么咱们该怎么解决这个问题呢?
答案相信你们都知道了,那就是构建一颗始终平衡的二叉查找树。那么有哪些平衡二叉查找树呢?如何实现?
这些咱们留到下节再讲。
这一节咱们学会了使用非线性的数据结构--二叉查找树来高效的实现查找,插入,删除操做。分析了它的性能,在随机插入的状况下,二叉查找树的高度趋近于 2.99lgN
,平均查找时间复杂度为 1.39lgN(2lnN)
,并且在升序插入的状况下,树会退化成链表。这些知识为咱们后面学习2-3查找树和红黑树打下了基础。