问题:既然有了这么高效的散列表,使用二叉树的地方是不是都可以替换成散列表呢?有没有哪些地方是散列表做不了,必须要用二叉树来做的呢?
1. 二叉查找树(Binary Search Tree)
又名:二叉搜索树,特点:左子树都小于节点值,右子树都大于节点值。
1.1 查找
从根节点开始,等于直接返回,比它小查左子树,比它大查右子树。以此类推。
public class BinarySearchTree {
private Node tree;
public Node find(int data) {
Node p = tree;
while (p != null) {
if (data < p.data) p = p.left;
else if (data > p.data) p = p.right;
else return p;
}
return null;
}
public static class Node {
private int data;
private Node left;
private Node right;
public Node(int data) {
this.data = data;
}
}
}
1.2 插入
public void insert(int data) {
if (tree == null) {
tree = new Node(data);
return;
}
Node p = tree;
while (p != null) {
if (data > p.data) {
if (p.right == null) {
p.right = new Node(data);
return;
}
p = p.right;
} else {
if (p.left == null) {
p.left = new Node(data);
return;
}
p = p.left;
}
}
}
1.3 删除
三种情况:
public void delete(int data) {
Node p = tree;
Node pp = null;
while (p != null && p.data != data) {
pp = p;
if (data > p.data) p = p.right;
else p = p.left;
}
if (p == null) return;
if (p.left != null && p.right != null) {
Node minP = p.right;
Node minPP = p;
while (minP.left != null) {
minPP = minP;
minP = minP.left;
}
p.data = minP.data;
p = minP;
pp = minPP;
}
Node child;
if (p.left != null) child = p.left;
else if (p.right != null) child = p.right;
else child = null;
if (pp == null) tree = child;
else if (pp.left == p) pp.left = child;
else pp.right = child;
}
1.4 二叉查找树的其他操作
快速地查找最大节点和最小节点、前驱节点和后继节点.
重要特性:中序遍历二叉查找树,可以输出有序的数据序列,时间复杂度是O(n),非常高效。
2.支持重复数据的二叉查找树
两种处理方法:
- 二叉查找树中每一个节点不仅会存储一个数据,因此我们通过链表和支持动态扩容的数组等数据结构,把值相同的数据都存储在同一个节点上。
- 每个节点仍然只存储一个数据,如果碰到一个节点的值,与要插入数据的值相同,我们就将这个要插入的数据放到这个节点的右子树。
3. 二叉查找树的时间复杂度分析
- 平衡二叉查找树的高度接近logn,所以插入、删除、查找操作的时间复杂度也比较稳定,是O(logn)。
- 如果退化成链表:O(n).
4. 解答开篇
- 散列表中的数据是无序存储的,对于二叉查找树,只需中序遍历,就可以在O(n)的时间复杂度内,输出有序的数据序列;
- 散列表扩容耗时很多,而且当遇到散列冲突时,性能不稳定;最常用的平衡二叉查找树的性能非常稳定,时间复杂度稳定在O(logn);
- 尽管散列表的查找等操作的时间复杂度是常量级的,但因为哈希冲突的存在,这个常量不一定比logn小,所以实际的查找速度可能不一定比O(logn)快。加上哈希函数的耗时,也不一定就比平衡二叉查找树的效率高。
- 为了避免过多的散列冲突,散列表装载因子不能太大,特别是基于开放寻址法解决冲突的散列表,不然会浪费一定的存储空间。
|