二叉查找树BST原理以及图形化显示实现

二叉查找树是二叉树的一种特殊形式,对于每一个节点来说,其属性Key值可以比较,其Key值比左节点大,比右节点小。即二叉查找树是有序的,中序遍历输出即为有序序列。二叉查找树的查找性能平均情况下为O(logN)对数级别,极端情况下为O(N)。

如下图所示,为一颗二叉查找树,对于每个节点来说,左孩子小于其值,右孩子大于其值,整个二叉树的中序遍历输出为所有数据的有序序列。

                                                        

二叉查找树BST的API:

API作用描述
size()返回输的节点个数
get(Key key)查找某一个key值
put(Key key, Value val)放入某一个节点
max()返回最大元素节点
min()返回最小元素节点
floor(Key key)返回小于等于key的最大节点
ceiling(Key key)返回大于等于key的最小节点
select(int k)查找排名为k的节点
rank(Key key)返回该key对应的排名
delete(Key key)删除某个key值
deleteMin()删除最小值
deleteMax()删除最大值
keys(Key lo, Key hi)返回给定范围内key值集合
printBinaryTree()图形化打印二叉树
height()返回树的高度

二叉查找树的组成

首先构造一个内部类Node来表示二叉树的节点,每个节点对应key-value,以及左右子节点链接,N表示已该节点为根的子树中含有的节点总数。

内部持有一个Node root用来表示二叉树的根节点。

//二叉查找树
public class BST<Key extends Comparable<Key>, Value> {
	
	private class Node{
		private Key key;
		private Value value;
		private Node left, right;
		private int N;
		public Node(Key key, Value value, int N){
			this.key = key;
			this.value = value;
			this.N = N;
		}
//		@Override
//		public String toString() {
//			return "Node [key=" + key + ", value=" + value + ", left=" + left + ", right=" + right + ", N=" + N + "]";
//		}
		@Override
		public String toString() {
			return "Node [key=" + key + "]";
		}
	}
	
	private Node root;
}

下面进行二叉查找树的具体实现,使用递归方式,也可以使用非递归方式来实现,一般情况下,非递归方式效率要高。

Size()方法

size(x)方法返回以x为根的子树含有的节点总数,使用递归,依次将左子树,右子树以及自身相加返回即可。

        public int size(){
		return size(root);
	}
	
	private int size(Node x){
		if(x==null) return 0;
		return size(x.left) + size(x.right) + 1;
	}

get(Key key)方法

对于二叉查找树的查找方法,可以从根节点进行查找

如果相等则直接返回;

如果比根节点小,则待查找节点必定在左子树,则递归到左子树进行查找;

如果比根节点大,则进入到右子树进行查找。

        public Value get(Key key){
		return get(root, key);
	}
	
	private Value get(Node x, Key key){
		if(x == null) return null;
		int cmp = key.compareTo(x.key);
		if(cmp>0)
			return get(x.right, key);
		else if(cmp<0)
			return get(x.left, key);
		else
			return x.value;
	}

put(Key key, Value val)方法

对于插入操作,首先找到待插入节点在二叉树中的位置,从根节点x开始出发比较

如果相等,则说明该节点已经存在,则执行更新操作;

如果小于x,则说明待插入节点合理位置在x的左子树,将x.left指向新建的节点

如果大于x,则说明待插入节点合理位置在x的右子树,将x.right指向新建的节点

        public void put(Key key, Value val){
		root = put(root, key, val);
	}
	
	private Node put(Node x, Key key, Value val) {
		if(x == null)
			return new Node(key, val, 1);
		int cmp = key.compareTo(x.key);
		if(cmp>0)
			x.right = put(x.right, key, val);
		else if(cmp<0)
			x.left = put(x.left, key, val);
		else
			x.value = val;
		x.N = size(x.left) + size(x.right) + 1;
		return x;		
	}

max()与min()

最大值从根节点开始,

如果右子树为空,则说明根节点为最大值;

否则一直往右子树寻找,直到找到某个节点右子树为空,返回该节点即可。

最小值同理,从左子树中不断寻找。

        public Node max(){
		return max(root);
	}
	
	private Node max(Node x){
		if(x == null) 
			return null;
		if(x.right==null)
			return x;
		return max(x.right);
	}
	
	public Node min(){
		return min(root);
	}
	
	private Node min(Node x){
		if(x == null)
			return null;
		if(x.left == null)
			return x;
		return min(x.left);
	}

floor(Key key)与ceiling(Key key)

floor返回小于等于key的最大值节点,从根节点开始寻找,

如果key值等于根节点key,则直接返回根节点即可;

如果key值小于根节点key,则说明该值必在其左子树;

如果key值大于根节点key,则说明该值可能在右子树,如果右子树中最小值比key值大,则说明右子树中不存在该节点,即根节点返回,否则说明右子树中有节点比key值要小,返回最大的值即可。

ceiling方法同理

        //小于等于key的最大值
	public Node floor(Key key){
		if(root == null) 
			return null;
		return floor(root, 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 r = floor(x.right, key);
		if(r!=null) return r;
		else return x;						
	}
	
	//大于等于key的最小值
	public Node ceiling(Key key){
		if(root==null) return null;
		return ceiling(root, key);
	}
	
	private Node ceiling(Node x, Key key){
		if(x==null) return null;
		int cmp = key.compareTo(x.key);
		if(cmp==0) return x;
		if(cmp>0) return ceiling(x.right, key);
		Node l = ceiling(x.left, key);
		if(l!=null) return l;
		else return x;
	}

select(int k)与rank(Key key)

查找排名为k的节点,从根节点开始寻找,利用根节点的左子树的节点总数,

如果根节点的左子树节点总数==k-1,则说明根节点为排名为k的节点,返回根节点即可;

如果左子树总数大于k-1,则说明排名k节点在根节点的左子树中,从左子树中递归查找排名为k的节点

如果大于k-1,则说明排名为k的节点在跟节点的右子树中,在右子树中的排名为k-1-左子树总数;

        //查找排名为k的节点
	public Node select(int k){
		if(root==null) return null;
		return select(root, k);
	}
	
	private Node select(Node x, int k){
		if(x==null) return null;
		int leftnum = size(x.left);
		if(leftnum==k-1) return x;
		if(leftnum>k-1) return select(x.left, k);
		return select(x.right, k-1-leftnum);
	}
	
	//返回该key对应的排名
	public int rank(Key key){
		if(root==null) return -1;
		return rank(root, key);
	}
	
	private int rank(Node x, Key key){
		if(x==null) return -1;
		int leftnum = size(x.left);
		int cmp = key.compareTo(x.key);
		if(cmp==0) return leftnum+1;
		if(cmp<0) return rank(x.left, key);
		return leftnum + 1 + rank(x.right, key);
	}

deleteMin()与deleteMax()

删除最小值,一直往左子树寻找,直到某个节点的左子树为空,将指向该节点的左连接指向该节点的右子树。

                                                

删除最大值,一直往右子树寻找,直到某个节点右子树为空,将指向该节点的有链接指向该节点的左子树。

                                

        public void deleteMin(){
		root = deleteMin(root);
	}
	
	//一直往左子树寻找,直到某个节点的左子树为空,将该节点的右子树指向指向该节点的左连接
	private Node deleteMin(Node x){
		if(x==null) return null;
		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 deleteMax(){
		root = deleteMax(root);
	}
	
	private Node deleteMax(Node x){
		if(x==null) return null;
		if(x.right==null) return x.left;
		x.right = deleteMax(x.right);
		x.N = size(x.left) + size(x.right) + 1;
		return x;
	}

delete()方法

在deleteMin以及deleteMax中是删除的特殊情况,即某个待删除节点的左节点或者右节点为空,如果待删除节点的左右均非空,

可以采用以下步骤来删除该节点,使用该节点的右子树的最小节点来代替该节点。

新建节点t指向待删除节点x

将x指向t右子树最小节点

将左节点指向t左节点

将x右节点指向t右子树删除最小节点后返回的根节点

        public void delete(Key key){
		if(root==null) return;
		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);
		if(cmp>0) x.right = delete(x.right, key);
		if(x.left==null) return x.right;
		if(x.right==null) return x.left;
		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;
	}

keys(Key lo, Key hi)方法

返回给定范围内的key值集合,利用中序遍历以及队列结构,将符合条件的key保存在队列中返回。

中序遍历,从根节点开始比较其与范围左右端点值大小,

如果根节点小于左端点,则说明符合条件值可能在右子树

如果根节点在左右节点范围内部,则将根节点加入到队列中

如果根节点大于右节点,则应该递归遍历其左子树

        public Iterable<Key> keys(){
		return keys(min().key, max().key);
	}
	
	//返回给定范围内的key值集合
	public Iterable<Key> keys(Key lo, Key hi){
		Queue<Key> q = new LinkedList<>();
		keys(root, q, lo, hi);
		return q;
	}
	
	private void keys(Node x, Queue<Key> q, 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.right, q, lo, hi);
		if(cmplo<=0&&cmphi>=0) q.add(x.key);
		if(cmphi>0) keys(x.left, q, lo, hi);	
	}

printBinaryTree()图形化打印二叉树

为了便于形象化展示二叉树,采用控制台来图形化输出二叉树,日后研究一下使用SWT来图形化输出二叉树,使用控制台过于繁琐。

给定一颗二叉树,首选使用层序遍历,将二叉树补全为完全二叉树,树中的空节点使用特殊符号specialChar来代替,层序遍历的结果是将二叉树输出到一个List<List<Node>>中,然后利用坐标变换来计算树中每个节点的位置以及输出绘制。

二叉树转换为完全二叉树的层序遍历

利用队列,求出二叉树的深度,层序遍历二叉树,首先将根节点入队,然后开始循环遍历,每一次循环,代表处理二叉树的每一层,首先计算队列中的节点个数,即该层depth节点个数,将队列中元素依次出队,首先将元素加入到List中,然后将元素的左右节点依次入队,如果子节点为空,则新建一个特殊字符节点来代替,依次遍历每一层,得到最终的层序遍历结果。

        public List<List<Node>> levelOrder2(){
		if(root==null) return null;
		List<List<Node>> ll = new ArrayList<>();
		Queue<Node> q = new LinkedList<>();
		q.add(root);
		int h = height();
		int depth = 1;
		while(q.size()>0){
			int s = q.size();
			List<Node> lk = new ArrayList<>();
			while(s>0){
				Node tmp = q.poll();
				lk.add(tmp);
				if(tmp.left!=null){
					q.add(tmp.left);
				}else{
					q.add(new Node((Key)specialChar, (Value)specialChar, 1));
				}
				if(tmp.right!=null){
					q.add(tmp.right);
				}else{
					q.add(new Node((Key)specialChar, (Value)specialChar, 1));
				}
				s--;
			}
			ll.add(lk);
			if(depth>h-1)
				break;
			depth++;			
		}
		for(List<Node> t : ll){
			for(Node n : t){
				System.out.print(n.key);
			}
			System.out.println();
		}
		return ll;
	}

给定的二叉树以及层序遍历的结果为,即将二叉树的每一行存储到List<List<>>中,List每一个元素代表一行,特殊字符使用#来代替

由于是完全二叉树,深度为h行,节点个数为pow(2,h-1)。

   

最终绘制的效果如下所示:用横线代替箭头

                        

下面分析计算二叉树的每个节点的坐标关系

将二叉树节点以及左右子节点箭头看成一个基本单位,将二叉树往最底层投影,可以得到二叉树节点以及箭头的编号,对于深度为4的完全二叉树来说,最后一层总长度为29个单位,即最后一行总长度为pow(2,h+1)-3

考虑第i行,有:

第i行距离最左边的起始偏移量为pow(2,h-i+1)-2

第i行节点之间的间隔为pow(2,h-i+2)-1

第i行元素距离其左节点或右节点的距离为pow(2,h-i),该距离用于行与行之间的横线绘制

绘制程序如下所示:

遍历层序结果的每一行

对于每一行,首先计算行起始偏移量,以及节点间隔,打印该行的每一个节点

对于该行,绘出下一行横线部分

       横线部分起始偏移地址为pow(2,h-i)-2

       该节点对应的左右节点总距离为2*pow(2,h-i)+1

       横线间隔即为下一行的节点之间的间隔pow(2,h-i+2-1)-1

最终程序如下:

        public void printBinaryTree2(){ 
		List<List<Node>> ll = levelOrder2();
		List<Node> l = null; 
		int h = height();				
		for(int i=1; i<=h; i++){
			//节点行  开始地址
			int lineStart = pow2(h-i+1)-2;
			//每个节点的偏移地址
			int offset = pow2(h-i+2)-1;
			//该节点到左子节点或右子节点的长度
			int brackets = pow2(h-i);
			//System.out.println("linenum = " + i + " start = " + lineStart + " offset = " + offset + " brackets = " + brackets);
			printFormat(lineStart, spaceFormat);
			l = ll.get(i-1);
			for(int j=0; j<l.size(); j++){
				if(!l.get(j).key.equals(specialChar)){
					System.out.print(l.get(j).key);
				}else{
					System.out.print(" ");
				}	
				printFormat(offset, spaceFormat);		
			}
			//最后一行不需要绘出
			if(i>=h)
				break;
			System.out.println();
			//横线部分行开始地址
			int lineCrossStart = pow2(h-i)-2;
			printFormat(lineCrossStart, spaceFormat);
			for(int j=0; j<l.size(); j++){
				//System.out.println(l.get(j).key);
				if(!l.get(j).key.equals(specialChar) && (l.get(j).left!=null || l.get(j).right!=null)){
					//每个元素下一行横线部分长度
					printFormat(brackets*2+1, crossFormat);
					//横线部分 间隔  即下一行的节点的间隔
					printFormat(pow2(h-i+2-1)-1, spaceFormat);
				}else{
					printFormat(brackets*2+1, spaceFormat);
					printFormat(pow2(h-i+2-1)-1, spaceFormat);
				}				
			}
			System.out.println();
		}
		System.out.println();
	}
        public int pow2(int n){
		return (int) Math.pow(2, n);
	}

	public void printFormat(int num, String format){
		StringBuilder sb = new StringBuilder();
		for(int i=0; i<num; i++){
			sb.append(format);
		}
		System.out.print(sb.toString());
	}
	
	public int height(){
		return height(root);
	}
	
	public int height(Node x){
		if(x==null) return 0;
		if(x.left==null&&x.right==null) return 1;
		return 1+Math.max(height(x.left), height(x.right));
	}

演示部分

二叉查找树与快速排序思想类似,二叉查找树的根节点对应快速排序基准值,二叉查找树的最终结果与元素插入顺序有关,如果插入元素顺序为有序的,则最终二叉树会蜕变成线性表,例如依次插入1-5有

                                   

此时执行查找操作,则最终时间复杂度变为O(N)

顺序插入如下数据,删除8节点,效果如下

                BST<Integer, String> bst = new BST<>();	
		int[] arr = new int[]{8,6,10,5,7,9,12,2,4};
		//int[] arr = new int[]{1,2,3,4,5};
		//int[] arr = new int[]{8,6,10,5,7,9,12};
		for(int i=0; i<arr.length; i++){
			bst.put(arr[i], String.valueOf(i));
		}
		bst.printBinaryTree2();
		bst.delete(8);
		bst.printBinaryTree2();

                     

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值