简介
健值对在现代计算机和网络系统应用广泛(常见如数据库系统、搜索引擎等),如果不能快速完成相关操作,大规模应用将无从谈起。 下面列举了健值对部分基础操作:
- 查找
Value get(Key key) - 插入
void put(Key key, Value value) - 删除
void delete(Key key) - 包含检测
boolean contains(Key key) - 空检测
boolean isEmpty() - 大小
int size()本文尝试找着一种实现方式,使得所有的操作都具备对数级别的时间复杂度,本文只关注查找和插入操作(因为其他接口差不多是这两个的封装)。
链表
链表实现是比较容易想到和理解的方式:将所有元素连接成链表:
查找和插入
因为是链表,所以就是遍历:
public Value get(Key key) {
for (Node x = first; x != null; x = x.next) {
if (key.equals(x.key))
return x.val;
}
return null;
}
public void put(Key key, Value val) {
for (Node x = first; x != null; x = x.next) {
if (key.equals(x.key)) {
x.val = val;
return;
}
}
first = new Node(key, val, first);
n++;
}
优缺点
- 优点:新增一个节点的时间是固定的
- 缺点:插入、查找操作都是线性级别的时间复杂度,运气不好的话需要遍历整个链表
有序数组
针对链表查找慢的缺点,我们想到使用二分查找去解决,即使用一对平行数组:一个存储健,一个存储值,保证存放健的数组是有序的,再通过数组的索引去获取、更新值的数组:
查找和插入
因为是有序数组,所以使用二分搜索确定位置,插入新元素的同时,后移后面的元素,并适时增加数组长度:
public Value get(Key key) {
if (isEmpty()) return null;
int i = rank(key);
if (i < n && keys[i].compareTo(key) == 0)
return vals[i];
return null;
}
public void put(Key key, Value val) {
int i = rank(key);
if (i < n && keys[i].compareTo(key) == 0) {
vals[i] = val;
return;
}
if (n == keys.length) resize(2*keys.length);
// !!!后移健值
for (int j = n; j > i; j--) {
keys[j] = keys[j-1];
vals[j] = vals[j-1];
}
keys[i] = key;
vals[i] = val;
n++;
}
// 二分搜索找到key在数组中的index
public int rank(Key key) {
int lo = 0, hi = n-1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
int cmp = key.compareTo(keys[mid]);
if (cmp < 0) hi = mid - 1;
else if (cmp > 0) lo = mid + 1;
else return mid;
}
return lo;
}
优缺点
- 优点:检索复杂度达到对数级别。
- 缺点:插入操作需要线性级别的时间复杂度,运气不好的话需要移动整个数组。
二叉查找树
本节我们介绍二叉查找树,一种结合链表和有序数组优点的数据结构:
查找与插入
因为节点是有序的,所以只需要比大小就能判断是当前节点还是在左右子树中,递归即可:
private Value get(Node x, Key key) {
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 value) {
root = put(root, key, value);
}
private Node put(Node curr, Key key, Value value) {
// 遇到空节点,返回一个新建节点
if (curr == null)
return new Node(key, value);
int r = key.compareTo(curr.key);
if (r < 0)
curr.left = put(curr.left, key, value);
else if (r > 0)
curr.right = put(curr.right, key, value);
else
curr.value = value;
return curr;
}
优缺点
由代码可知:二叉树形状受节点的插入顺序相关,下图分别展示了最优情况、一般情况以及最差情况:
总结
文本层层递进,先后学习使用链表、有序数组和二叉树3种实现方式,但都不能保证最差情况下对数级别时间复杂度的要求,下篇文章 我们将在二叉树的基础上继续探索。