在符号表的初级实现中,我们采用了无序链表(实现顺序查找),有序数组(实现二分查找),但遗憾的是,两者的效果并不突出。我们想要找到查找和插入操作都是对数级别的算法和数据结构。 而,二叉查找树正是实现的关键。
如果想了解有关二叉树,可以看数据结构与算法 & 二叉树工具
一、二叉查找树
1.定义
一个二叉查找树(BST)是一棵二叉树,其中吗,每个结点都含有Comparable的键(以及相关联的值)且每个结点的键都大于其左子树中的任意结点的键而小于右子树的任意结点的键。
2.数据表示
我们使用一个包内类来表示二叉查找树上的一个结点。每个结点都含有一个键、一个值、一个左连接、一个有链接和一个结点计数器。
左连接指向一棵小于该结点所有键组成的二叉查找树,右链接则指向一棵大于该结点所有键组成得二叉查找树。
代码:
class Node{
private Key key;//键
private Value value;//值
private Node left, right;//左、右链接
private int N;//以该结点为根的子树中的结点总数
public Node(Key key, Value value, int N) {
this.key = key;
this.value = value;
this.N = N;
}
}
二、功能实现
1.由键获得值
public Value get(Key key) {
return get(root, key);//从根结点出发
}
private Value get(Node node, Key key) {
if (node == null) {
return null;
}
int cmp = key.compareTo(node.key);
if (cmp < 0) {//获得得键值比当前结点小,进入当前结点的左子树查找
return get(node.left, key);
}else if (cmp > 0) {//获得得键值比当前结点大,进入当前结点的右子树查找
return get(node.right, key);
}
return node.value;
}
- 存储新的键和值
public void put(Key key, Value value) {
root = put(root, key, value);
}
private Node put(Node node, Key key, Value value) {
if (node == null) {
return new Node(key, value, 1);//不存在该键,新生结点
}
int cmp = key.compareTo(node.key);
if (cmp < 0) {
node.left = put(node.left, key, value);
}else if (cmp > 0) {
node.right = put(node.right, key, value);
}else {
node.value = value;//存在键,覆盖值
}
node.N = size(node.left) + size(node.right) +1;
return node;
}
3.获得最小、最大结点
public Key max() {
return max(root).key;
}
private Node max(Node node) {
if (node.right == null) {
return node;
}
return max(node.right);
}
public Key min() {
return min(root).key;
}
private Node min(Node node) {
if (node.left == null) {
return node;
}
return min(node.left);
}
- 向下取整
/**
* 小于等于Key的最大值
* @param key
* @return
*/
public Key floor(Key key) {
Node node = floor(root, key);
if (node == null) {
return null;
}
return node.key;
}
private Node floor(Node node, Key key) {
if (node == null) {
return null;
}
int cmp = key.compareTo(node.key);
if (cmp == 0) {
return node;
}
if (cmp < 0) {
return floor(node.left, key);
}
Node temp = floor(node.right, key);
if (temp != null) {
return temp;
}
return node;
}
- 查找(排名查找)
public Key select(int k) {
return select(root, k).key;
}
private Node select(Node node, int k) {
if (node == null) {
return null;
}
int t = size(node);
if (t > k) {
return select(node.left, k);
}else if (t < k) {
return select(node.right, k - t -1);
}
return node;
}
- 范围(小于某个结点的数量)
public int rank(Key key) {
return rank(root, key);
}
private int rank(Node node, Key key) {
if (node == null) {
return 0;
}
int cmp = key.compareTo(node.key);
if (cmp < 0) {
return rank(node.left, key);
}else if (cmp > 0) {
return rank(node.right, key) + 1 + size(node.left);
}
return size(node.left);
}
- 删除最小值
public void deleteMin() {
root = deleteMin(root);
}
private Node deleteMin(Node node) {
if (node.left == null) {
return node.right;
}
node.left = deleteMin(node.left);
node.N = size(node.left) + size(node.right) +1;//这里因为可能左右子树的大小将会变化
return null;
}
- 删除指定键
public void delete(Key key) {
root = delete(root, key);
}
private Node delete(Node node, Key key) {
if (node == null) {
return null;
}
int cmp = key.compareTo(node.key);
if (cmp < 0) {
node.left = delete(node.left, key);
}else if (cmp > 0) {
node.right = delete(node.right, key);
}else{
Node temp = node;
node = min(temp.right);
node.right = deleteMin(node.right);
node.left = temp.left;
}
node.N = size(node.left) +size(node.right) +1;
return node;
}
等等。
这几项功能实现,都离不开递归的使用。
一般顺序是:
- 判断当前结点的键(或者当前结点的链接)是否存在,不存在返回。
- 比较当前结点的键和查询键的大小关系。如果当前结点键值大,进入其左子树,如果当前结点键值小,进入其右子树。
- 可能需要修改结点属性。
其实实现二叉查找树,也可以用非递归方法实现。不过,递归的实现更容易验证其正确性。
时间,空间复杂度比较如下图:
笔者水平有限,目前只能描述以上问题,如果有其他情况,可以留言,有错误,请指教,有继续优化的,请分享,谢谢!
本篇文章是否有所收获?阅读是否舒服?又什么改进建议?希望可以给我留言或私信,您的的分享,就是我的进步。谢谢。
2020.9.20 网安院2楼