BST 二叉查找树
简介
二叉查找树,就是一颗有序的二叉树,使用中序遍历可以得到一个升序的序列。
二叉查找树主要是为了解决有序数组中插入和删除以及无序链表中查找的时间复杂度提出的。即将二分查找的效率和链表的灵活性结合起来。
缺点是二叉查找树并不稳定,在特殊的情况下能让查找和插入的时间复杂度依然是O(N)级别的。
实现
一个新的数据结构的提出总是为了解决在某种场景下的问题,最先实现的操作大多数都是增删改查。BST在增删的时候只要保证中序遍历这棵树得到的是一个升序的序列就行。
基本定义
定义BST的节点和一些基本操作
在定义该节点是使用了一个变量N来保存以该节点为根的树的节点数量,在增删的过程中维护这个变量需要额外的代码,这个变量主要是让size的实现是常数级别的,这在实现排名的代码中十分重要,如果你没这个需求可以去掉。
public class BST<K extends Comparable<K>, V> {
/*
1. 定义节点和持有一个根节点
2. 编写put 和 get 方法
3. 节点的定义是
左子树
右子树
该节点的键和值
以该节点为根的子树的节点数
*/
private class Node {
K key;
V value;
Node left;
Node right;
int n;//表示该节点为根的节点的数量
public Node(K key, V value, Node left, Node right, int n) {
this.key = key;
this.value = value;
this.left = left;
this.right = right;
this.n = n;
}
@Override
public String toString() {
return "Node{" +
"key=" + key +
", value=" + value +
", left=" + left +
", right=" + right +
", n=" + n +
'}';
}
}
private Node root;//根节点
/**
* @return 树中节点的数量
*/
public int size() {
return size(root);
}
/**
* 返回指定节点为根节点的树的节点数
* @param x 指定节点
* @return
*/
private int size(Node x) {
if (x == null) return 0;
return x.n;
}
//增删改查
}
增改
使用put方法进行增和改,没有这个键的时候就是增,有这个键的时候就是修改这个键对应的值。
public void put(K key, V value) {
root = put(root, key, value);
}
/*
递归终止条件 x == null 返回新节点
递归步进段
1. 使用key和当前节点的key比较 得出值 cmp
2. cmp 大于O 往右子树上去找合适的位置插入
3. cmp 小于0 往左子树上去找合适的位置插入
4. cmp 等于O 更新该节点的值
更新该节点的节点数
*/
private Node put(Node x, K key, V value) {
if (x == null)
return new Node(key, value, null, null, 1);
int cmp = key.compareTo(x.key);
if (cmp < 0) x.left = put(x.left, key, value);
if (cmp > 0) x.right = put(x.right, key, value);
else x.value = value;
x.n = size(x.left) + size(x.right) + 1; //更新该节点的节点数
return x;
}
查找
public V get(K key) {
return get(root, key);
}
/*
递归的终止条件 找到了与他相等的node,或者节点为null
递归步进段
1. 使用key和当前节点的key比较 得出值 cmp
2. cmp 大于O 往右子树上去找
3. cmp 小于0 往左子树上去找
4. cmp 等于O 返回结果
*/
private V get(Node x, K key) {
if (x == null)
return null;
int cmp = key.compareTo(x.key);
if (cmp < 0) return get(x.left, key);
if (cmp > 0) return get(x.right, key);
else return x.value;
}
删除
删除相对于增改查来说复杂一点,删除最大键和最小键相对来说简单一点,但是删除中间节点就比较复杂了。
删除最小节点
最小节点一定是叶子节点或者带有右子树的节点,这两种情况都可以使用一个步骤来解决,即把指向最小节点的指针改成指向最小节点右子树
实现
/**
* 删除最小的那个节点
*/
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;
}
删除最大节点和删除最小节点类似。
删除中间节点
如果删除的是中间节点,如果这个中间节点只有一个子节点,这个操作和删除最小节点就是类似的,如果删除的节点有两个子节点,那么情况就要复杂些,可以使用他的后继节点来代替该节点,具体步骤如下
- 将指向即将被删除的结点的链接保存为t
- 将x指向它的后继结点min(t.right);
- 将x的右链接(原本指向一棵所有结点都大于x.key的二叉査找树)指向 deletemin(t.right),也就是在删除后所有结点仍然都大于x.key的子二又査找树;
- 将x的左链接(本为空)设为t.left(其下所有的键都小于被删除的结点和它的后继结点)
删除节点实现
public void delete(K key) {
root = delete(root, key);
}
private Node delete(Node x, K key) {
if (x == null)
return null;
int cmp = key.compareTo(x.key);
if (cmp > 0) x.right = delete(x.right, key);//往右子树找
else if (cmp < 0) x.left = delete(x.left, key);//往左子树找
else {
if (x.right == null) return x.left; //找到的节点只有左子树
if (x.left == null) return x.right; //找到的节点只有右子树
//找到的节点x有两个子节点
/*
1. 保存该节点的指针
2. 找到该节点的后继节点,即右子树的最小节点并代替x
3. 删除那个后继节点deleteMin
4. 更新指针指向,这样任然是一颗二叉查找树
*/
Node t = x;
x = min(x.right);//节点代替
x.right = deleteMin(t.right);
x.left = t.left;
}
x.n = size(x.left) + size(x.right) + 1;
return x;//返回自身
}
其他查找实现
查找最小值和最大值
/**
* @return 返回该树的最小值,即最左侧节点
*/
public K min() {
return min(root).key;
}
private Node min(Node x) {
if (x.left == null)
return x;
return min(x.left);
}
/**
* @return 返回树的最大值。即最右侧节点
*/
public K max() {
return max(root).key;
}
private Node max(Node x) {
if (x.right == null)
return x;
return min(x.right);
}
范围查找
public Iterable<K> keys() {
return keys(min(),max());
}
public Iterable<K> keys(K lo, K hi) {
Deque<K> queue = new LinkedList<>();
keys(queue,root, lo, hi);
return queue;
}
/**
* 使用中序遍历的变形求解这道题
* @param queue 符合 lo <= x.key <=hi 所有key的集合
* @param x 当前节点
* @param lo 下界
* @param hi 上界
*/
private void keys(Deque<K> queue,Node x, K lo, K hi) {
if (x == null)
return;
int loCmp = lo.compareTo(x.key);
int hiCmp = hi.compareTo(x.key);
if (loCmp < 0) keys(queue,x.left,lo,hi);//lo < x.key 才能往左找,如果大于就没必要找了。
if (loCmp <= 0 && hiCmp >= 0) queue.offer(x.key);//lo <= x.key <=hi 说明该节点符合条件
if (hiCmp > 0) keys(queue,x.right,lo,hi);//hi > x.key 才能往左找,如果小于就没必要找了。
}
向下取整和向下取整
/**
* @param key 指定的key
* @return 小于等于指定Key的最大值
*/
public K floor(K key) {
Node x = floor(root, key);
if (x == null) return null;
return x.key;
}
/*
算法思想:
因为是找比他小的值,先判断根节点是不是等于key,不是再往左找,左边找不到再往右找,右边找不到就是往右找的节点。
*/
private Node floor(Node x, K key) {
if (x == null)
return null;
int cmp = key.compareTo(x.key);
if (cmp == 0)
return x;
if (cmp < 0)
return floor(x.left, key);
Node r = floor(x.right, key);
if (r != null)
return r;
else
return x;
}
向上取整和向下取整类似
返回一个键的排名
一个键的排名也就是小于该键的数量
/**
* 返回小于指定key的数量
*
* @param key 指定的key
* @return 小于指定key的数量
*/
public int rank(K key) {
return rank(root, key);
}
/*
从根节点开始找,如果根节点和key相等,返回他左子树的数量
根节点小于key在左子树中找
根节点大于key在右子树中找到的数量加上左子树的数量
*/
private int rank(Node x, K key) {
if (x == null)
return 0;
int cmp = key.compareTo(x.key);
if (cmp < 0)
return rank(x.left, key);
else if (cmp > 0) {
int l = size(x.left);
return l + 1 + rank(x.right, key);
} else
return size(x.left);
}
返回一个排名为k的键
/**
* 返回排名为k的键
*
* @param k 指定排名
* @return 返回排名为k的键
*/
public K select(int k) {
return select(root, k).key;
}
/*
判断该节点左子树的数量,如果大于k 往左子树找以左子树中的节点为根的左子树节点数量为t的子树,如果小于k往右子树找一颗节点数量为k - t - 1的子树,相等返回该节点
*/
private Node select(Node x, int k) {
if (x == null)
return null;
int t = size(x.left);
if (t > k)
return select(x.left, k);
else if (t < k)
return select(x.right, k - (t + 1));
else
return x;
}
总结
在BST中所有的操作的时间复杂度都和树高有关,如果这颗二叉查找树只有左子树而没有右子树的话,那么他增加和删除的时间复杂度都会是O(N)。这也是后面为了避免这种情况的出现提出2-3树和红黑树的原因。