参考算法导论第三版第十二章: 二叉搜索树。实现代码:百度网盘, 提取密码:duri。
对二叉树进行这样的限制条件:对任意结点x,其左子树中所有关键字均小于x.key,其右子树中所有关键字均大于(等于)x.key。这样的二叉树被称为二叉搜索树。如下图:
可以看出,相同的关键字集合,能够构建出不同结构的二叉搜索树,而任意结构都满足二叉搜索树的性质。由于二叉搜索树关键字组织特性,树的中序遍历即可获得关键字的按序排列。二叉搜索树中,从结点x依次向左查找,最后一个不为空的结点就是以x树为根的子树中关键字最小的结点,而向右查找,最后一个不为空的结点就是该子树中关键字最大的结点。
private TreeNode<K, V> min(TreeNode<K, V> node) {
if (node == null)
return null;
while (node.left != null)
node = node.left;
return node;
}
private TreeNode<K, V> max(TreeNode<K, V> node) {
if (node == null)
return null;
while (node.right != null)
node = node.right;
return node;
}
中序遍历:
在二叉树的中序遍历过程中,当访问某个结点x前,先遍历结点x的左子树,然后访问结点x,最后遍历结点x的右子树。按照递归思想很容易实现中序遍历,也可以利用栈的LIFO特性实现非递归遍历:
/**
* 对以node为根的子树进行中序遍历,递归实现
*
* @param node 树中某个结点,如果是root,就是对整棵树的遍历
*/
private void inOrderTraversal(TreeNode<K, V> node) {
if (node != null) {
inOrderTraversal(node.left);
System.out.print(node);
inOrderTraversal(node.right);
}
}
/**
* 中序遍历,非递归实现,借助栈的LIFO特性
* @param node 树中某个结点,如果是root,就是对整棵树的遍历
*/
private void inOrder(TreeNode<K, V> node) {
if (node != null) {
TreeNode<K, V> r = node;
Deque<TreeNode<K, V>> stack = new LinkedList<>();
// 所有结点访问前均入栈,当某个结点访问完毕,说明其左子树中所有结点也访问完毕,
// 则对其右子树进行遍历
while (r != null || stack.size() > 0) {
if (r != null) {
stack.push(r); // 访问某个结点前将其入栈
r = r.left; // 然后遍历其左子树
} else {
r = stack.pop(); // 出栈操作,得到栈顶结点
System.out.print(r); // 从栈中取出的结点访问完毕,表明此结点的左子树中所有结点也访问完毕
r = r.right; // 然后对其右子树进行遍历
}
}
}
}
前驱和后继:
根据二叉搜索树的特点,某个结点x按中序遍历的前驱结点:如果x存在左子树,则前驱结点应该为结点x左子树中最大的结点。否则应该从结点x向上查找到第一个键值小于结点x的祖先结点,这个结点就是结点x的前驱结点。若不存在,则结点x不存在前驱结点。后继结点同理。
/**
* 查找node的前驱结点(中序)
*
* @param node 树中某个结点
* @return node的前驱结点,可能返回null
*/
private TreeNode<K, V> predecessor(TreeNode<K, V> node) {
if (node.left != null) // 如果结点node存在左子树,则该结点前驱结点为左子树中最大结点
return max(node.left);
TreeNode<K, V> p = node.parent;
while (p != null && node == p.left) { // 向上查找,找到第一个小于结点node的祖先结点
node = p;
p = node.parent;
}
return p;
}
/**
* 查找node的后继结点(中序)
*
* @param node 树中某个结点
* @return node的后继结点,可能返回null
*/
private TreeNode<K, V> successor(TreeNode<K, V> node) {
if (node.right != null) // 如果结点node存在右子树,则该结点后继结点为右子树中最小结点
return min(node.right);
TreeNode<K, V> p = node.parent;
while (p != null && node == p.right) { // 向上查找,找到第一个大于结点node的祖先结点
node = p;
p = node.parent;
}
return p;
}
搜索操作:
根据二叉搜索树特点,很容易对关键字key进行查找。当某个结点x.key=key时,x就是要找的结点。当结点key<x.key时,则在结点x的左子树中进行查找,当key>x.key时,则在结点x的右子树中进行查找。
/**
* 查找关键字为key的结点
*
* @param key 要查找的关键字
* @return 关键字为key的结点,查找失败返回null
*/
@SuppressWarnings("unchecked")
private TreeNode<K, V> treeNode(K key) {
if (key == null)
throw new NullPointerException("Key can not be null");
TreeNode<K, V> r = root; // 从根结点开始查找
while (r != null && key.compareTo(r.key) != 0) { // 直到r为null,或者r就是要找的结点,结束循环
if (key.compareTo(r.key) < 0) // 当key<r.key,则在r的左子树中继续查找
r = r.left;
else // 当key>r.key,则在r的右子树中继续擦找
r = r.right;
}
return r;
}
插入操作:
插入操作和查找操作类似,都是根据关键字key的比较找到正确的位置。插入操作通过比较直到找到某个结点的空孩子处,在此处创建新的结点。比较过程中若发现相等的关键字,可以进行替换value操作。
/**
* 向树种插入结点
*
* @param key 关键字
* @param value 对应的值
* @param putIfAbsent true:不存在相同的key时进行插入,false:存在相同的key时,替换value
*/
@SuppressWarnings("unchecked")
private void insert(K key, V value, boolean putIfAbsent) {
if (root == null) // 当前树为空,创建根结点
root = newTreeNode(key, value, null);
else {
TreeNode<K, V> r = root, p;
do {
p = r;
if (key.compareTo(r.key) == 0) { // 如果查找到相同的key,则根据putIfAbsent和当前value值决定是否替换
if (!putIfAbsent || r.value == null)
r.value = value;
return;
} else if (key.compareTo(r.key) < 0) // key小于当前结点关键字,则到左子树中进行定位
r = r.left;
else // key大于当前结点关键字,则到右子树中进行定位
r = r.right;
} while (r != null);
// 循环结束时,p至多有1个非空孩子结点(r为其一个孩子,r=null),通过key和p.key的比较确定新结点位置
r = newTreeNode(key, value, p); // 以p结点为父结点构建新插入的结点
if (r.key.compareTo(p.key) < 0) // 如果key小于p.key,则r作为p的左孩子,否则作为右孩子
p.left = r;
else
p.right = r;
}
size++;
}
删除操作:
删除操作较插入操作要复杂一点。当删除叶子结点时,直接删除即可。而当删除某个非叶子结点z时,存在三种情况:
一、结点z只有左子树,则将其左子树代替其位置。
二、结点z只有右子树,则将其右子树代替其位置。
三、结点z既有左子树又有右子树,则有两种策略:第一种是在结点z的左子树中选最大结点代替它,第二种是在结点z右子树中选取最小结点代替它,无论哪种策略都能够保证删除后,树依然保持二叉搜索树特性。
二、结点z只有右子树,则将其右子树代替其位置。
三、结点z既有左子树又有右子树,则有两种策略:第一种是在结点z的左子树中选最大结点代替它,第二种是在结点z右子树中选取最小结点代替它,无论哪种策略都能够保证删除后,树依然保持二叉搜索树特性。
而在第三种情况中,有存在两种情况:
1、右子树中最小结点y是结点z的右孩子,则直接用右子树代替结点z。
2、右子树中最小结点y不是结点z的右孩子(结点y一定没有左孩子,否则结点y就不是右子树中最小结点),则先用结点y的右子树代替结点y,然后用结点y取代结点z。
/**
* 从树中删除关键字为key的结点
* @param key 目标关键字
* @return 被删除结点的value,若不存在关键字为key的结点,则返回null
*/
public V remove(K key) { // node就是结点z,t就是结点y
TreeNode<K, V> node = treeNode(key); // 先找到要删除的目标结点
if (node == null)
return null;
if (node.left == null) // 若目标结点不存在左子树,则用其右子树代替目标结点(情况二),右子树空相当于直接删除
transplant(node, node.right);
else if (node.right == null) // 若目标结点不存在右子树,则用其左子树代替目标结点(情况一)
transplant(node, node.left);
else { // 左右子树均存在(情况三)
TreeNode<K, V> t = min(node.right); // 找到右子树中最小结点t
if (t.parent != node) { // 如果结点结点t不是目标结点的右孩子(情况三.2),先用结点t的右子树代替结点t
transplant(t, t.right);
t.right = node.right;
t.right.parent = t;
}
transplant(node, t); // 情况三.1,也是情况三.2中的一部分,用结点t代替目标结点
t.left = node.left;
t.left.parent = t;
}
size--;
return node.value;
}
/**
* 子树替换操作,在二叉搜索树中,用以结点v为根结点的子树代替以结点u为根结点的子树
* @param u 某个结点u
* @param v 某个结点v
*/
private void transplant(TreeNode<K, V> u, TreeNode<K, V> v) {
if (u.parent == null) // u是根结点
root = v;
else if (u == u.parent.left) // 修改结点u父结点的子结点
u.parent.left = v;
else
u.parent.right = v;
if (v != null) // 修改结点v的父结点
v.parent = u.parent;
}