深入理解二叉查找树

二叉查找树是一种能够将链表插入的灵活性和有序数组查找的高效性结合起来的符号表实现。即使用每个结点含有两个链接(链表中每个结点之只含有一个链接)的二叉查找树来高效地实现符号表。

《深入理解二叉查找树》
详解二叉查找树

所使用的数据结构由结点组成,结点包含链接可以为空(null)或者指向其他结点。

一棵二叉查找树(BST)是一棵二叉树,其中每个结点都含有一个Comparable的键(以及相关联的值)且每个结点的键都大于其左子树的任意结点的键而小于右子树的任意结点的键。

数据表示

和链表一样,该数据结构嵌套定义了一个私有类来表示二叉树上的一个结点。

private class Node{
	private Key key;				//键 	private Value val;				//值 	private Node left, right;		        //指向子树的链接 	private int N;					//以该结点为根的子树中的结点总数。 		
	public Node(Key key, Value val, int N) 
	{this.key = key; this.val = val; this.N = N;}
}

算法中实现的size()会将空链表N值当作0,因此就能保证以下公式对于二叉树中的任意结点x总是成立的。

size(x) = size(x.left) + size(x.right) + 1

一棵二叉树代表了一组键(及其相应的值)的集合,而同一个集合可以用多棵二叉树表示。

查找

二叉查找树的递归算法:如果树为空,则查找未命中。否则就递归地在适当的子树中继续查找,如果被查找的键较小就选择左子树,如果被查找的键较大就选择右子树。

基于二叉树的符号表

public class BST <Key extends Comparable<Key>, Value>{
	private Node root;
	
	private class Node{
		private Key key;				//键
		private Value val;				//值
		private Node left, right;		        //指向子树的链接
		private int N;					//以该结点为根的子树中的结点总数。
		
		public Node(Key key, Value val, int N) 
		{	this.key = key; this.val = val; this.N = N;}
	}
	public int size() 
	{ return size(root);}
	
	private int size(Node x) {
		if(x == null)	return 0;
		else			return x.N;
	}
	//二叉查找树的查找和排序方法的实现。
	public Value get(Key key)

	public void put(Key key, Value val) 
	
}

插入

算法中的查找代码几乎和二分查找一样简单,这种简洁性是二叉树的重要特性之一。而二叉查找树的另一个更重要的特征就是插入的实现难度和查找差不多。当查找一个不存在于树中的结点并结束于一条空链接时,我们需要做的是将链接指向一个含有被查找键的新结点。

使用二叉查找树的算法的运行时间取决于树的形状,而树的形状又取决于被插入的先后顺序。在最好的情况下,一棵含有N个结点的树是完全平衡的,每条空连接和根结点的距离都为~lgN。在最坏的情况下,搜素路径上可能又N个结点。

《深入理解二叉查找树》
最好情况
《深入理解二叉查找树》
一般情况
《深入理解二叉查找树》
最坏情况

public Value get(Key key)
	{	return get(root, key);	}
private Value get(Node x, Key key) {
	//在以x为根结点的子树中查找并返回key所对应的值
	//如果找不到则返回null
	if(x == null)	return null;
	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 val) {
	//查找key,找到则更新它的值,否则为他创建一个新的结点
	root = put(root, key, val);
}
private Node put(Node x, Key key, Value val) {
	//如果key存在于以x为根结点的子树中则更新它的值;
	//否则将以key和val为键值对的新结点插入到该子树中
	if(x == null)	return new Node(key, val, 1);
	int cmp = key.compareTo(x.key);
	if		(cmp < 0)	x.left = put(x.left, key, val);
	else if (cmp > 0)	x.right = put(x.right, key, val);
	else	x.val = val;
	x.N = size(x.left) + size(x.right) + 1;
	return x;
}

最大键和最小键

如果根结点的左链接为空,那么一棵二叉查找树中最小键就是根结点;如果左链接非空,那么树中的最小键就是左子树的最小键。最大键类似。

向上取整和向下取整

如果给定的键key小于二叉查找树的根结点的键,那么小于等于key的最大键floor(key)一定在根结点的左子树中;如果给定的键key大于二叉树根结点,那么只有当根结点右子树中存在小于等于key的结点时,小于等于key的最大键才会出现在右子树中,否则根节点就是小于等于key的最大键。

《深入理解二叉查找树》

二叉查找树的其他方法实现

public Key min() {
		return min(root).key;
}
private Node min(Node x) {
	if(x.left == null)	return x;
	return min(x.left);
}
public Key max() {
	return max(root).key;
}
private Node max(Node x) {
	if(x.right == null)	return x;
	return max(x.right);
}
public Key floor(Key key) {
	Node x = floor(root,key);
	if(x == null)	return null;
	return x.key;
}
private Node floor(Node x, Key key) {
	if(x == null)	return null;
	int cmp = key.compareTo(x.key);
	if(cmp == 0)	return x;
	if(cmp < 0)		return floor(x.left, key);
	Node t = floor(x.right, key);
	if(t != null) 	return t;
	else			return x;
}

排名

rank()是select()的逆方法,它会返回给定键的排名。它的实现和select()类似:如果给定的键和根节点的键相等,我们返回左子树中的结点总数t;如果给定的键小于根节点,我们会返回改键在左子树中的排名(递归计算);如果给定的键大于根节点,我们会返回t+1(根节点)加上他在右子树中的排名(递归计算)

//二叉树中select()和rank()方法的实现
public Key select(int k) {
	return select(root, k).key;
}
private Node select(Node x, int k) {
	//返回排名为k的节点
	if	(x == null)		return null;
	int t = size(x.left);
	if		(t > k)		return select(x.left, k);
	else if  	(t > k)		return select(x.right, k);
	else				return x;
}
public int rank(Key key) {
	return rank(key, root);
}
private int rank(Key key, Node x) {
	//返回以x为根节点的子树中小于x.key的键的数量
	if	(x == null)		return 0;
	int cmp = key.compareTo(x.key);
	if		(cmp < 0)	return rank(key, x.left);
	else if	(cmp > 0)	return 1 + rank(key, x.left) + rank(key, x.right);
	else				return size(x.left);
}

删除最大键和最小键

删除可能是二叉树中最难实现的方法了,如何才能删除一个拥有两个子结点的结点?删除之后我们要处理两棵子树,但被删除结点的父结点只有一条空出来的链接。

T.hibbard在1962年提出解决这个难题的第一个方法。在删除结点x后用它的后继结点填补它的位置。因为x有一个右子节点,因此它的后继结点就是其右子树的最小结点。这样的替换仍然能保证树的有序性,因为x.key和它的后继结点的键之间不存在其他的键。我们可以用以下四个步骤完成x的替换

a、将指向将被删除的结点链接保存为t

b、将x指向它的后继结点min(t.right)

c、将x的右节点(原本指向一棵所有结点都大于x.key的二叉查找树)指向deleteMin(t.right),也就是在删除后所有结点仍然都大于x.key的子二叉树;

d、将x的左链接(本为空)设为t.left(其下所有的键都小于被删除的结点和它的后继结点)

public void deleteMin(){	
	root = deleteMin(root);	
}
private Node deleteMin(Node x) {
	if(x.left == null) return x.right;
	x.left = deleteMin(x.left);
	x.N = size(x.left) + size(x.right) + 1;
	return x;
}
public void delete(Key key) {
	root = delete(root, key);
}
private Node delete(Node x, Key key) {
	if	(x == null)		return null;
	int cmp = key.compareTo(x.key);
	if			(cmp < 0) 	x.left = delete(x.left, key);
	else if		        (cmp > 0)	x.right = delete(x.right, key);
	else {
		if	(x.right == null)	return x.left;
		if	(x.left == null)	return x.right;
		Node t = x;
		x = min(t.right);
		x.right = deleteMin(t.right);
		x.left = t.left;
	}
	x.N = size(x.left) + size(x.right) + 1;
	return x;
}

范围查找

//二叉查找树的范围查找操作
public Iterable<Key> keys(){
	return keys(min(), max());
}
public Iterable<Key> keys(Key lo, Key hi){
	Queue<Key> queue = new Queue<Key>();
	keys(root, queue, lo, hi);
	return queue;
}
private void keys(Node x, Queue<Key> queue, Key lo, Key hi) {
	if	(x == null) return;
	int cmplo = lo.compareTo(x.key);
	int cmphi = hi.compareTo(x.key);
	if(cmplo < 0)	                 keys(x.left, queue, lo, hi);
	if(cmplo <= 0 && cmphi >= 0)	queue.enqueue(x.key);
	if(cmphi > 0) 	                 keys(x.right, queue, lo, hi);
}

    原文作者:SeYuFac
    原文地址: https://zhuanlan.zhihu.com/p/61605369
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞