【数据结构】——二叉排序树

一、定义

二叉排序树也叫二叉查找树,或者是空树,或者是具有下列性质的二叉树:

  • 若它的左子树不空,则左子树上所有的值均小于它的根结点;
  • 若它的右子树不空,则右子树上所有的值均大于它的根结点;
  • 它的左右子树也分别为二叉排序树。

从上面定义可以看出二叉排序树一个很重要的性质:中序遍历可以得到一个结点值严格递增的有序序列。严格递增代表树中没有值相等的结点。(相等也容易实现,只需要在插入的时候修改下查找条件。)

二、存储

采用二叉链表,相应的结点结构

// 结点里还可以添加其它数据信息,为方便只保留关键字。
class TreeNode {
	int key;
	TreeNode lchild;
	TreeNode rchild;

	public TreeNode(int key) {
		this.key = key;
	}
}

三、查找

// 查找成功返回该结点,否则返回null
public static TreeNode search(TreeNode T, int key) {
	while (T != null) {
		if (key == T.key) {
			break;
		} else if (key < T.key) {
			T = T.lchild;
		} else {
			T = T.rchild;
		}
	}
	return T;
}

二叉排序树的查找类似于折半查找,通过不断缩小查找范围查找。但与折半查找不同的是,折半查找每次一定对半缩小范围,而排序树每次缩小的范围取决于排序树的形态,最好情况是完全二叉树形态对应的时间复杂度和折半查找相同为 O ( l o g 2 n ) Ο(log_2n) O(log2n),最坏的情况是斜二叉树平均查找长度为 n + 1 2 \frac {n+1}{2} 2n+1,退化到和顺序查找相同,对应的时间复杂度 O ( n ) Ο(n) O(n)

综合 n n n个结点的排序树的各种形态,平均而言,二叉排序树的查找时间复杂度和折半查找相同为 O ( l o g 2 n ) Ο(log_2n) O(log2n)

四、插入

插入的结点一定是个新的叶子结点,所以插入过程很方便,只需要修改原来叶子结点的左指针或右指针。

// 插入关键字已存在时直接退出,否则插入
public static void insert(TreeNode T, int key) {
	TreeNode pre = null;
	while (T != null) {
		if (key < T.key) {
			pre = T;
			T = T.lchild;
		} else if (key > T.key) {
			pre = T;
			T = T.rchild;
		} else {
			return;
		}
	}
	
	if (key < pre.key) {
		pre.lchild = new TreeNode(key);
	} else {
		pre.rchild = new TreeNode(key);
	}
}

插入的过程即查找的过程,所以插入的时间复杂度也为 O ( l o g 2 n ) Ο(log_2n) O(log2n)

五、创建

二叉排序树的创建过程就是反复插入的过程,也是对无序序列有序的过程。

这里应该注意无序序列中的元素顺序对创建的排序树形态影响很大,比如序列 [ 2, 1, 3 ] 或序列 [ 2, 3, 1 ] 创建后的形态是完全二叉树,而序列 [ 1, 2, 3 ] 和序列 [ 3, 2, 1 ] 创建后分别是右斜二叉树和左斜二叉树。

public static TreeNode creatBinTree(int[] arr) {
	TreeNode T = null;
	if (arr.length > 0) {
		T = new TreeNode(arr[0]);
		for (int i = 1; i < arr.length; i++)
			insert(T, arr[i]);
	}
	return T;
}

n 个结点 n 次插入,所以创建排序树的时间复杂度 O ( n l o g 2 n ) Ο(nlog_2n) O(nlog2n)

六、删除

删除过程同样是查找的过程,通过查找返回对应结点信息并修改相关指针,其中:

  • 找到的结点的左右孩子都为空,那么直接删除该结点即可。
  • 找到的结点的左右孩子有一个为空,那么将不空的那个孩子代替要删除的结点即可。
  • 找到的结点的左右孩子都不为空,那么找到这个结点的右子树中的最小结点(此结点的左孩子一定为空),将最小结点的值赋给要删除的结点,然后删除最小结点。或者将左子树中最大结点(结点右孩子一定为空)的值赋给要删除的结点并删除最大结点。

上面三点始终围绕一点:删除前后其它结点中序遍历的相对顺序不发生改变。

注意在java中删除某个结点是通过断开它的引用来实现,一般需要另设个结点保存其父结点。初写这块时直接想通过node=null来实现删除结点的目的,其实仔细想想这只是将引用名指向空,被引用的结点与父结点间的联系丝毫没受到影响,归根结底还是对java中的引用理解不深刻。

删除部分代码

public static void delete(TreeNode T, int key) {
	TreeNode pre = null;							// 保存删除结点的父结点
	while (T != null) {								// 循坏获取删除结点
		if (key == T.key) {
			break;
		} else if (key < T.key) {
			pre = T;
			T = T.lchild;
		} else {
			pre = T;
			T = T.rchild;
		}
	}

	if (T == null)									// 空树直接返回
		return;

	if (T.lchild == null && T.rchild == null) {		 
		if (pre == null)							// 删除结点左右孩子为空且是根结点直接返回
			return;
		if (T.key < pre.key)						// 否则根据与父结点的大小断开父结点一端的引用
			pre.lchild = null;
		else
			pre.rchild = null;
	} else if (T.lchild == null) {		 
		if (pre == null)							// 删除结点左孩子为空且删除结点是根结点直接返回
			return;
		if (T.key < pre.key)						// 否则根据与父结点的大小将删除结点的右子树嫁接到父结点的一端
			pre.lchild = T.rchild;
		else
			pre.rchild = T.rchild;
	} else if (T.rchild == null) {
		if (pre == null)							// 删除结点右孩子为空且删除结点是根结点直接返回
			return;
		if (T.key < pre.key)						// 否则根据与父结点的大小将删除结点的左子树嫁接到父结点的一端
			pre.lchild = T.lchild;
		else
			pre.rchild = T.lchild;
	} else {										// 删除结点左右孩子均存在
		TreeNode node = null;
		TreeNode minNode = T.rchild;
		while (minNode.lchild != null) {			// 获取右子树最小结点
			node = minNode;
			minNode = minNode.lchild;
		}
		T.key = minNode.key;						// 最小结点的值赋给根结点
		if (node == null) {							// 右子树只有一个结点断开根结点的引用
			T.rchild = null;
		} else {									// 否则断开父结点的引用
			node.lchild = null;
		}	
	}
}

上面对删除结点是根结点的情况没有进行处理,主要是因为根结点没有被任何父结点引用,在这点上java中的引用传递没有C中的地址传递方便,如果想实现可以给树增加一个头结点。具体就不写了~

删除过程同样是查找的过程,时间复杂度仍为 O ( l o g 2 n ) Ο(log_2n) O(log2n)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值