本章继续承接上章的内容,具体实现TreeMap
中的方
1.简单的TreeMap
这里比较核心的一个方法是findNode
,用来寻找与键值相当的节点,下面是它的实现:
private Node findNode(Object target) {
if (target == null) {
throw new IllegalArgumentException();
}
@SuppressWarnings("unchecked")
Comparable<?super K> k = (Comparable<?super K>)target;
Node node = root;
while (node != null) {
int cmp = k.compareTo(node.key);
if (cmp <0)
node = node.left;
else if (cmp>0)
node = node.right;
else
return node;
}
return null;
}
- 在这个实现中,
null
不是键的合法值。 - 在我们可以在
target
上调用compareTo
之前,我们必须把它强制转换为某种形式的Comparable
。这里使用的“类型通配符”会尽可能允许;也就是说,它适用于任何实现Comparable
类型,并且它的compareTo
接受K
或者任和K
的超类(可以同任何类型做比较)。
之后,实际搜索比较简单。我们初始化一个循环变量node
来引用根节点。每次循环中,我们将目标与node.key
比较。如果目标小于当前键,我们移动到左子树。如果它更大,我们移动到右子树。如果相等,我们返回当前节点(这里用的是迭代,不断赋值)。
2.搜索值
findNode
运行时间与树的高度成正比,而不是节点的数量,因为我们不必搜索整个树。但是对于containsValue
,我们必须搜索值,而不是键;BST 的特性不适用于值,因此我们必须搜索整个树。
下面是containsValue
方法,这里用递归实现:
public boolean containsValue(Object target) {
return containsValueHelper(root, target);
}
private boolean containsValueHelper(Node node, Object target) {
if (node == null) {
return false;
}
if (equals(target, node.value)) {
return true;
}
if (containsValueHelper(node.left, target)) {
return true;
}
if (containsValueHelper(node.right, target)) {
return true;
}
return false;
}
这是containsValueHelper
的工作原理:
- 第一个
if
语句检查递归的边界情况。如果node
是null
,那意味着我们已经递归到树的底部,没有找到target
,所以我们应该返回false
。请注意,这只意味着目标没有出现在树的一条路径上;它仍然可能会在另一条路径上被发现。 - 第二种情况检查我们是否找到了我们正在寻找的东西。如果是这样,我们返回
true
。否则,我们必须继续。 - 第三种情况是执行递归调用,在左子树中搜索
target
。如果我们找到它,我们可以立即返回true
,而不搜索右子树。否则我们继续。 - 第四种情况是搜索右子树。同样,如果我们找到我们正在寻找的东西,我们返回
true
。否则,我们搜索完了整棵树,返回false
。
该方法“访问”了树中的每个节点,所以它的所需时间与节点数成正比。
3.实现put
put
方法比起get
要复杂一些,因为要处理两种情况:
- 如果给定的键已经在树中,则替换并返回旧值;
- 否则必须在树中添加一个新的节点,在正确的地方。
public V put(K key, V value) {
if (key == null) {
throw new IllegalArgumentException();
}
if (root == null) {
root = new Node(key, value);
size++;
return null;
}
return putHelper(root, key, value);
}
private V putHelper(Node node, K key, V value) {
Comparable<? super K> k = (Comparable<? super K>) key;
int cmp = k.compareTo(node.key);
if (cmp < 0) {
if (node.left == null) {
node.left = new Node(key, value);
size++;
return null;
} else {
return putHelper(node.left, key, value);
}
}
if (cmp > 0) {
if (node.right == null) {
node.right = new Node(key, value);
size++;
return null;
} else {
return putHelper(node.right, key, value);
}
}
V oldValue = node.value;
node.value = value;
return oldValue;
}
第一个参数node
最初是树的根,但是每次我们执行递归调用,它指向了不同的子树。就像get
一样,我们用compareTo
方法来弄清楚,跟随哪一条树的路径。如果cmp < 0
,我们添加的键小于node.key
,那么我们要走左子树。有两种情况:
- 如果左子树为空,那就是,如果
node.left
是null
,我们已经到达树的底部而没有找到key
。这个时候,我们知道key
不在树上,我们知道它应该放在哪里。所以我们创建一个新节点,并将它添加为node
的左子树。 - 否则我们进行递归调用来搜索左子树。
如果cmp > 0
,我们添加的键大于node.key
,那么我们要走右子树。我们处理的两个案例与上一个分支相同。最后,如果cmp == 0
,我们在树中找到了键,那么我们更改它并返回旧的值。
4.中序遍历
这里我们还剩最后一个方法KeySet
,它返回一个Set
,按升序包含树中的键。在其他Map
实现中,keySet
返回的键没有特定的顺序,但是树形实现的一个功能是,对键进行简单而有效的排序。下面是如何实现它的:
public Set<K> keySet() {
Set<K> set = new LinkedHashSet<K>();
addInOrder(root, set);
return set;
}
private void addInOrder(Node node, Set<K> set) {
if (node == null) return;
addInOrder(node.left, set);
set.add(node.key);
addInOrder(node.right, set);
}
在keySet
中,我们创建一个LinkedHashSet
,这是一个Set
实现,使元素保持有序,第一个参数node
最初是树的根,但正如你的期望,我们用它来递归地遍历树。addInOrder
对树执行经典的“中序遍历”。
- 按顺序遍历左子树。
- 添加
node.key
。 - 按顺序遍历右子树。
5.二叉搜索树的问题
我们获取最有查询效率时,一般是O(log(n)),这种情况会在所搜索的树为平衡二叉树时出现,若不是平衡二叉树,搜索效率则会很低。
如果你思考put
如何工作,你可以弄清楚发生了什么。每次添加一个新的键时,它都大于树中的所有键,所以我们总是选择右子树,并且总是将新节点添加为,最右边的节点的右子节点。结果是一个“不平衡”的树,只包含右子节点。
这种树的高度正比于n
,不是logn
,所以get
和put
的性能是线性的,不是对数的
6.自平衡树
这个问题有两种可能的解决方案:
- 你可以避免向
Map
按顺序添加键。但这并不总是可能的。 你可以制作一棵树,如果碰巧按顺序处理键,那么它会更好地处理键。(按顺序添加会导致这是一个极不平衡的树) - 第二个解决方案是更好的,有几种方法可以做到。最常见的是修改
put
,以便它检测树何时开始变得不平衡,如果是,则重新排列节点。具有这种能力的树被称为“自平衡树”。普通的自平衡树包括 AVL 树(“AVL”是发明者的缩写),以及红黑树,这是 JavaTreeMap
所使用的。
总而言之,二叉搜索树可以以对数时间实现get
和put
,但是只能按照使得树足够平衡的顺序添加键。自平衡树通过每次添加新键时,进行一些额外的工作来避免这个问题。
7.二叉搜索树的删除
实现思路:
- 删除叶子节点
- 删除只有左孩子的节点
- 删除只有右孩子的节点
- 删除左右孩子都有的节点,这里我们需要找出待删除节点的右子树中的最小节点,并放到待删除位置上
下面是实现的具体方案:
public V remove(Object k) {
// 一会儿我来填这个坑
Comparable<?super K> key = (Comparable<?super K>)k;
Node currentNode = this.root; //用来保存待删除节点
Node parentNode = this.root; //保存待删除节点的父节点
boolean isLeftNode = true; //左右节点判断
V oldValue = null;
// 寻找需要的节点(同时记录它的位置)
while ( (currentNode != null) && (currentNode.key != key) ) {
parentNode = currentNode;
int cmp = key.compareTo(currentNode.key);
if (cmp < 0) {
currentNode = currentNode.left;
isLeftNode = true;
} else {
currentNode = currentNode.right;
isLeftNode = false;
}
}
// 空树
if (currentNode == null) {
return null;
}
// 删除的节点是叶子节点
if ( (currentNode.left == null) && (currentNode.right == null) ) {
if (currentNode == this.root) {
oldValue = root.value;
root = null;
} else if (isLeftNode) {
oldValue = parentNode.left.value;
parentNode.left = null;
} else {
oldValue = parentNode.right.value;
parentNode.right = null;
}
} else if ( (currentNode.right == null) && (currentNode.left != null) ) { //删除节点只有左孩子(分情况看它挂到哪)
if (currentNode == this.root) {
oldValue = root.value;
root = currentNode.left;
} else if (isLeftNode) {
oldValue = currentNode.left.value;
parentNode.left = currentNode.left;
} else {
oldValue = currentNode.right.value;
parentNode.right = currentNode.left;
}
} else if ( (currentNode.right != null) && (currentNode.left == null) ) { //删除节点只有右孩子
if (currentNode == root) {
oldValue = root.value;
root = currentNode.right;
} else if (isLeftNode) {
oldValue = parentNode.left.value;
parentNode.left = currentNode.right;
} else {
oldValue = parentNode.right.value;
parentNode.right = currentNode.right;
}
} else { //待删除节点既有左子树,又有右子树(思路:将待删除节点右子树的最小节点赋值给待删除节点)
Node directPostNode = getDirectPostNode(currentNode);
oldValue = directPostNode.value;
currentNode.key = directPostNode.key;
currentNode.value = directPostNode.value;
}
size--;
return oldValue;
}
得到待删除节点的直接后继节点:
private Node getDirectPostNode(Node delNode) {
Node parentNode = delNode; //用来保存待删除节点的(直接后继节点的父亲节点)
Node directNode = delNode; //用来保存待删除节点的(直接后继节点)
Node currentNode = delNode.right; // 待删除节点右子树
while (currentNode != null) {
parentNode = directNode;
directNode = currentNode;
currentNode = currentNode.left;
}
// 直接删除此后继节点(因为不是直接相连的,最小的肯定是叶子节点或者没有左子树)
if (directNode != delNode.right) {
parentNode.left = directNode.right;
directNode.right = null;
}
return directNode;
}
具体的可以看图: