【数据结构与算法】之二叉查找树 --- 第十三篇

树是一种非线性数据结构,这种数据结构要比线性数据结构复杂的多,因此分为三篇博客进行讲解:

第一篇:树的基本概念及常用操作的Java实现(二叉树为例)

第二篇:二叉查找树

第三篇:红黑树

本文目录

1、二叉查找树的基本概念

2、二叉查找树的查找操作

3、二叉查找树的插入操作

4、二叉查找树的删除操作    —- 重要

4.1  待删除结点没有子结点

4.2  待删除结点存在左子结点或右子结点时

4.3  待删除结点同时存在左右子结点时

4.4  三种情况代码整合   

5、支持重复数据的二叉查找树

6、二叉查找树的时间复杂度分析

7、二叉查找树和散列表的对比

8、二叉查找树的常用操作全代码实现

推荐及参考:

第二篇:二叉查找树BST

1、写在前面:二叉查找树最大的特点就是:支持动态数据集合的快速插入、删除和查找操作

2、文末附:二叉查找树的常用操作。

3、这里需要进行说明下,前面讲二叉查找树的时候,我们默认都是树中结点存储的都是数字。很多时候,在实际的软件开发中,我们在二叉查找树中存储的,是一个包含很多字段的对象。我们利用对象的某个字段作为键值(key)来构建二叉查找树。我们把对象中的其他字段叫做卫星数据。

1、二叉查找树的基本概念

二叉查找树BST(Binary  Sort  Tree):又称为二叉排序树/二叉搜索树。顾名思义,二叉查找树是为了实现快速查找而生的。不过它不仅仅支持快速查找一个数据,还支持快速插入、删除一个数据。

具有以下性质的二叉树称为二叉查找树:

1、若左子树不为空,则左子树上所有结点的值均小于它的根结点的值;

2、若右子树不为空,则右子树上所有的结点的值均大于或等于它的根结点的值;

3、左、右子树也分别为二叉查找树。

《【数据结构与算法】之二叉查找树 --- 第十三篇》

 

从上图可以看出来,二叉查找树中,左子树都比父结点小,右子树都比父结点大,递归定义。

所以,根据这个特点,二叉查找树的中序遍历的结果肯定是有序的,上图中中序遍历结果为:1、3、4、6、7、8、10、13、14。

2、二叉查找树的查找操作

根据二叉查找树的定义,我们可以将二叉查找树的查找操作大概分为以下几个步骤:

1、先比较它与根节点,相等就返回;或者根节点为空,说明树为空,也返回;

2、如果它比根节点小,就从根节点的左子树中进行递归查找;

3、如果它比根节点大,就从根节点的右子树中进行递归查找。

【可以看出来,二叉查找树的查找操作与二分查找十分相似】

《【数据结构与算法】之二叉查找树 --- 第十三篇》

public class BinarySearchTree {

	private Node root;  // 根节点
	
	// 二叉查找树的查找操作
	public Node find(int data){
		Node node = root;
		while(node != null){
			if(data < node.data)       node = node.leftChild;
			else if(data > node.data)  node = node.rightChild;
			else return node;
		}
		return null;
	}
	
	// Node内部类
	public static class Node{
		private int data;
		private Node leftChild;
		private Node rightChild;
		
		public Node(int data){
			this.data = data;
		}
	}
}

3、二叉查找树的插入操作

二叉查找树的插入操作有点类似于查找操作。先查找到合适的位置再插入,新插入的数据一般都在叶子结点上,所以我们只需要从根结点开始,依次比较要插入的数据和结点的大小关系。

二叉查找树的插入操作的关键几个步骤如下:

1、如果要插入的数据比根结点的数据大,并且根结点的右子树为空,就将新数据直接插入到右子结点的位置;如果不为空,就再递归遍历右子树,查找插入位置;

2、同理,如果要插入的数据比根结点数值小,并且结点的左子树为空,就将新数据插入到左子结点的位置;如果不为空,则遍历左子树,查找插入位置。

《【数据结构与算法】之二叉查找树 --- 第十三篇》

// 二叉查找树的插入操作
public void insert(int data){
	if(root == null){
		root = new Node(data);
	}
		
	Node node = root;
	while(node != null){
		if(data > node.data){
			if(node.rightChild == null){
				node.rightChild = new Node(data);  // 右子结点为空,直接将新结点设置为当前结点的右子结点
			}
			node = node.rightChild;  // 右子结点不为空,则继续遍历当前结点的右子树
		}else{   // data < node.data
 			if(node.leftChild == null){
 				node.leftChild = new Node(data);   // 左子结点为空,直接将新结点设置为当前结点的左子结点
 			}
 			node = node.leftChild;   // 左子结点不为空,则继续遍历当前结点的左子树
		}
	}
}

4、二叉查找树的删除操作

二叉查找树的删除操作不是很好理解,该章节出自https://blog.csdn.net/isea533/article/details/80345507

二叉查找树的删除操作相对于它的查找和插入操作来说复杂的多。针对要删除的结点的子结点个数的不同,我们需要分成三种情况来处理:

1、要删除结点没有左右子结点,可以直接删除;

2、存在左结点或者右结点中的一个,删除后需要对子结点移动;

3、同时存在左右子结点时,不能简单地删除,但是可以通过和后继结点交换后转换为前两种情况。

说明:实际上,还有一个特例,就是删除根节点,实现代码中会体现。

下面就正式对删除操作的三种情况进行说明:

一棵查找二叉树的初始状态如下图所示:

《【数据结构与算法】之二叉查找树 --- 第十三篇》

4.1  待删除结点没有子结点

《【数据结构与算法】之二叉查找树 --- 第十三篇》

如图中值为20的结点。这种情况是最简单的,我们只需要删除该结点和它父结点的关系即可。删除的时候需要判断自己和父结点的关系是左侧还是右侧,代码如下:

// 这里忽略了父结点不存在的情况,三种情况整合时会处理这种情况
if(node.parent.left == node){
    node.parent.left = null;    // 如果父结点的左子结点是自己,就将其设置为null
}else{
    node.parent.right = null;   // 如果父结点的右子结点是自己,就将其设置为null
}

4.2  待删除结点存在左子结点或右子结点时

《【数据结构与算法】之二叉查找树 --- 第十三篇》

如图中值为70的结点,删除它时,需要断开两个关系:70与它子结点的关系已经70与它父结点的关系,同时建立70子结点与70父结点之间的父子结点关系。

// 先找到要删除结点的子结点,不需要管他是左还是右
BSTNode<T> child = null;   // 表示待删除结点的子结点
if(node.left != null){
    child = node.left;
}else{
    child = node.right;
}

// 这里忽略了父结点不存在的情况,三种情况整合的时候,会处理
// 将代删除结点的父结点和待删除结点的子结点建立父子结点关系
if(node.parent.left == node){
    // 如果待删除结点node是它父结点的左子结点,则将待删除结点node的子结点设置为node父结点的左子结点,即取代它的位置    
    node.parent.left = child;  
}else{
     // 如果待删除结点node是它父结点的右子结点,则将待删除结点node的子结点设置为node父结点的右子结点,即取代它的位置 
    node.parent.right = child;
}

child.parent = node.parent;  // 建立node的子结点child中的父结点指针

4.3  待删除结点同时存在左右子结点时

《【数据结构与算法】之二叉查找树 --- 第十三篇》

如上图中值为30的结点。如上图中二叉查找树中序遍历的结果,当我们要删除30结点时,整个中序遍历结果从32位都要往前移动一位。而32是30的后继结点(即比30大的结点中最小的结点)。当某个结点存在右结点时,则它的后继结点就是它右子树中最小值,由于左侧结点总比右侧结点和父结点小,所以后继结点一定不会是左子树中的。从这一特点也能想到,待删除结点的后继结点可能存在右结点或者没有任何子结点。30的后继结点还有一个特点就是,他比30的左子树中的结点大,比30的右子树中的结点都小,因此删除30的时候,可以之间将后继结点32转移到30结点上,然后再删除后继结点32。又因为后继结点最多只有一个子结点(最多只有一个比它大的右子结点),因此删除后继结点的时候,就转换成了3种情况的前两种情况。

因此,当待删除结点同时存在左子结点和右子结点时,删除操作被分为了两步:

1、将待删除结点的后继节点“转移”到待删除结点上;

2、删除待删除结点的后继结点。【将删除指定结点转化为了删除它的后继结点】

// 获取当前结点的后继结点
Node<T> successor = successor(ndoe);  // successor()函数的具体实现,后文中会给出

// 1、转移值
node.data = successor.data

// 2、删除后继结点successor
node = successor;

4.4  三种情况代码整合   —- 重要【这样写比较容易理解】

代码说明:为了体现出其他二叉查找树的操作,抽象出很多功能函数,相互配合调调用。后面会贴上二叉查找树的所有常用操作的代码。

public class BSTree<T extends Comparable<T>>{

	BSTNode<T> root;  // 根节点
	
	/**
	 * 删除指定的data结点
	 */
	public void delete(T data){
		// 先查找到要删除的结点
		BSTNode<T> node = search(root, data);
		// 如果存在就删除
		if(node != null){
			delete(node);
		}
	}

	/**
	 * 删除指定的node
	 */
	public BSTNode<T> delete(BSTNode<T> node){
		// 第3种情况,如果同时存在左右子结点
		if(node.left != null && node.right != null){
			// 获取待删除结点的后继结点
			BSTNode<T> successor = successor(node);
			// 转移后继节点值到当前待删除结点上
			node.data = successor.data;
			// 把要删除的当前结点设置为它的后继结点
			node = successor;
		}
		// 经过前面一步的处理,下面只有两种情况,即:待删除结点有一个子结点或者没有子结点
		// 不管有没有子结点,都获取子结点
		BSTNode<T> child = null;
		if(node.left != null){
			child = node.left;   // 待删除结点存在左子结点
		}else{ 
			child = node.right;  // 待删除结点存在右子结点
		}
		
		// 如果child != null,就说明待删除结点存在一个子结点
		if(child != null){
			// 将待删除结点的子结点和待删除结点的父结点关联上
			child.parent = node.parent;
		}
		// 如果当前待删除结点没有父结点,说明要删除的结点是根结点
		if(node.parent == null){
			// 将根节点的子节点设置为根节点,按照前面的逻辑下来,根节点只有一个子结点或者没有子结点
			root = child;
		}
		// 如果待删除结点有父结点,并且待删除结点是它父结点的左结点时
		else if(node == node.parent.left){
			// 将父结点的左子结点设置为child
			node.parent.left = child;
		}
		// 如果待删除结点有父结点,并且待删除结点是它父结点的右结点时
		else{
			// 将父结点的右子结点设置为child
			node.parent.right = child;
		}
		
		// 返回被删除的结点
		return node;
	}
	
	/**
	 * 找到结点node的后继节点。即:查找二叉树中数据大于该结点的最小结点
	 */
	private BSTNode<T> successor(BSTNode<T> node) {
		BSTNode<T> successor = null;
		// 如果存在右子结点,则node的后继结点为以其右子结点为根的右子树中的最小结点
		if(node.right != null){
			while(node.right.left != null){
				successor = node.right.left;  // 最小结点肯定在右子树中的左子结点中
			}
		}
		
		// 如果node没有右子结点,则node有以下两种情况
		BSTNode<T> nodeParent = node.parent;
		// 1.node是“一个左子结点”,则node的后继结点就为node的父结点
		if(nodeParent != null && node == node.parent.left){
			successor = node.parent;
		}
		// 2.node是“一个右子结点”,则查找node的“最低”父结点,并且该父结点要有左子结点,那么这个“最低”的父结点就是node的后继结点
		// 2.1 如果nodeParent是nodeParent.parent的左子结点,则nodeParent.parent就是它的后继节点
		// 2.2如果nodeParent是nodeParent.parent的右子结点,则还需要往上面找,直到node作为它的左子结点时,此时的nodeParent.parent就是它的后继节点
		while(nodeParent != null && node == node.parent.right){
			node = node.parent;
			nodeParent = nodeParent.parent;
			successor = nodeParent;
		}
		
		return successor;
	}

	/**
	 * 递归实现查找以node为根节点的二叉树中值为data的结点
	 */
	private BSTNode<T> search(BSTNode<T> node, T data) {
		
		if(node == null){
			return node;
		}
		
		int cmp = data.compareTo(node.data);
		if(cmp < 0)         return search(node.left, data);   // 在左子树中找
		else if(cmp > 0)    return search(node.left, data);   // 在右子树中找
		else                return node;
	}


	/**
	 * BSTNode:结点内部类
	 */
	public class BSTNode<T extends Comparable<T>>{
		T data;             // 值
		BSTNode<T> left;    // 左子结点
		BSTNode<T> right;   // 右子结点
		BSTNode<T> parent;  // 父结点

		public BSTNode(T key, BSTNode<T> parent, BSTNode<T> left, BSTNode<T> right){
			this.data= data;
			this.parent = parent;
			this.left = left;
			this.right = right;
		}
		
		public T getData(){
			return this.data;
		}
		
		@Override
		public String toString(){
			return "data:" + data;
		}
	}
}

5、支持重复数据的二叉查找树

前面我们讲的二叉查找树的操作,针对的都是不存在键值相同的情况。如果存在存储两个对象的键值相同的情况,我们可以有两种解决办法:

方法1:通过链表和支持动态扩容的数组等数据结构实现二叉树中的结点,把值相同的数据都存储在同一个结点上;

方法2:每个结点仍然只存储一个数据。在查找插入位置的过程中,如果碰到一个结点的值与要插入数据的值相同,那么就将这个要插入的数据放到这个结点的右子树中,也就是说把这个新插入的数据当作大于这个结点的值来处理。

第1种方法比较容易理解,下面对第2种方法,进行下说明:

《【数据结构与算法】之二叉查找树 --- 第十三篇》

当我们查找数据的时候,遇到值相同的结点,我们并不停止查找操作,而是继续在右子树中查找,直到遇到叶子节点,才停止。这样就可以把键值等于要查找值得所有结点都找出来了。

对于删除操作,我们也需要先查找到每个要删除得结点,然后再按照前面讲得删除操作的方法,依次删除。

《【数据结构与算法】之二叉查找树 --- 第十三篇》

6、二叉查找树的时间复杂度分析

此章节内容出自于:https://time.geekbang.org/column/article/68334

二叉查找树的形态多种多样。比如下图中,对于同一组数据,我们构造了三种二叉查找树。它们的查找、插入和删除操作的执行效率都不一样的。图中第一种二叉查找树,根节点的左右子树极不平衡,已经退化成了链表,所以查找的时间复杂度为O(n)。这是最糟糕的情况。

《【数据结构与算法】之二叉查找树 --- 第十三篇》

上面分析了最糟糕的情况,下面来分析下最理想的情况,即二叉查找树是一棵满二叉树。

其实从前面的讲解中,我们不难发现,不管操作是查找、插入还是删除,时间复杂度都跟树的高度成正比,也就是O(height)。所以,求解二叉查找树操作的时间复杂度就转化为了如何求一棵包含n个结点的完全二叉树的高度。

树的高度就等于最大层数减1,为了方便计算,我们转换为层来表示。从图中可以看出,包含n个结点的完全二叉树中,下面一层的结点个数是上面一层的2倍,即第k层包含的结点个数就是2^(k – 1)。不过对于完全二叉树的最后一层的结点个数就没有规律了,它包含的结点个数在1~2^(L – 1)个之间(假设最大层数为L)。如果我们把每一层的结点个数加起来就是总的结点个数n。那么可以得出如下关系:

1 + 2 + 4 + 8 + … + 2^(L-2) + 1  <= n <= 1 + 2 + 4 + 8 + … + 2^(L-2) + 2^(L-1)    // L-2:不包含最后一层

借助等比数列的求和公式,我们可以计算出L的范围是[《【数据结构与算法】之二叉查找树 --- 第十三篇》 , 《【数据结构与算法】之二叉查找树 --- 第十三篇》]。完全二叉树的层数小于等于 《【数据结构与算法】之二叉查找树 --- 第十三篇》,也就是说,完全二叉树的高度小于等于《【数据结构与算法】之二叉查找树 --- 第十三篇》。即理想状态下的完全二叉树的查找、插入和删除操作的时间复杂度都是O(logn)。

从上面的最坏和最好的二叉查找树的时间复杂度分析来看,极度不平衡的二叉查找树的查找性能肯定不能满足我们的需求。我们需要构建出一种不管怎么删除、插入和查找数据都能保持任意结点的左、右子树比较平衡的二叉查找树,这就是下一篇文章要讲的一种特殊的二叉查找树,即平衡二叉查找树(红黑树)。平衡二叉查找树的高度接近logn,所以它的查找、插入和删除操作的时间复杂度都比较稳定,是O(logn)。

7、二叉查找树和散列表的对比

散列表的讲解:https://blog.csdn.net/pcwl1206/article/details/83582986

在前面的散列表文章中讲过,散列表的查找、插入和删除操作的时间复杂度都可以做到常量级的O(1),非常的高效。那么二叉查找树在比较平衡的情况下,查找、插入和删除操作的时间复杂度才是O(logn),相对散列表好像没有什么优势,那么我们为什么还要用二叉查找树呢?

主要原因有以下几个方面:

1、散列表中的数据是无序存储的,如果要输出有序的数据,需要先进行排序。对于二叉查找树来说,只需要中序遍历,就可以在O(n)的时间复杂度内输出所有的有序数据;

2、散列表扩容耗时很多,而且当遇到散列冲突的时候,性能不稳定。在工程中,我们常用的平衡二叉查找树的性能非常稳定,时间复杂度稳定在O(logn);

3、笼统的来说,尽管散列表的查找等操作的时间复杂度是常量级的,但是因为哈希冲突的存在,这个常量不一定比logn小,所以实际的查找速度可能不一定比O(logn)快。再加上哈希函数的耗时,也不一定比平衡二叉查找树的效率高;

4、散列表的构造要比二叉查找树要复杂的多,需要考虑的东西很多。比如散列函数的设计、冲突解决办法、扩容、缩容等。平衡二叉查找树只需要考虑平衡性这一个问题,而且这个问题的解决方案比较成熟、固定。

5、散列表为了避免过多的散列冲突,散列表装载因子不能太大,特别是基于开放寻址法解决散列冲突的散列表,不然会浪费一定的存储空间。

综合以上几点:平衡二叉查找树在某些方面还是由于散列表的,所以,这两者之间并不存在冲突。我们在实际的开发中,需要结合实际的需求再来选择使用哪一个。

8、二叉查找树的常用操作全代码实现

package com.zju.tree;
/** 
    * @author    作者 pcwl
    * @date      创建时间:2018年11月17日 上午9:04:51 
    * @version   1.0 
    * @comment   二叉查找树的操作
*/
public class BSTree<T extends Comparable<T>>{

	BSTNode<T> root;  // 根节点
	
	public BSTree(){
		root = null;
	}
	
	/**
	 * BSTNode:结点内部类
	 */
	public class BSTNode<T extends Comparable<T>>{
		T data;             // 值
		BSTNode<T> left;    // 左子结点
		BSTNode<T> right;   // 右子结点
		BSTNode<T> parent;  // 父结点

		public BSTNode(T key, BSTNode<T> parent, BSTNode<T> left, BSTNode<T> right){
			this.data= data;
			this.parent = parent;
			this.left = left;
			this.right = right;
		}
		
		public T getData(){
			return this.data;
		}
		
		@Override
		public String toString(){
			return "data:" + data;
		}
	}
	
	/**
	 * 前序遍历指定结点下的所有结点
	 */
	public void preOrder(BSTNode<T> node){
		if(node != null){
			System.out.println(root.data + " ");
			preOrder(node.left);
			preOrder(node.right);
		}
	}
	
	/**
	 * 前序遍历二叉查找树下的所有结点
	 */
	public void preOrder(){
		preOrder(root);
	}
	
	/**
	 * 中序遍历指定结点下的所有结点
	 */
	public void inOrder(BSTNode<T> node){
		if(node != null){
			inOrder(node.left);
			System.out.println(node.data + " ");
			inOrder(node.right);
		}
	}
	
	/**
	 * 中序遍历二叉查找树下的所有结点
	 */
	public void inOrder(){
		inOrder(root);
	}
	
	/**
	 * 后序遍历指定结点下的所有结点
	 */
	public void lastOrder(BSTNode<T> node){
		if(node != null){
			lastOrder(node.left);
			lastOrder(node.right);
			System.out.println(node.data + " ");
		}
	}
	
	/**
	 * 后序遍历二叉查找树下的所有结点
	 */
	public void lastOrder(){
		lastOrder(root);
	}
	
	
	/**
	 * 递归实现查找以node为根节点的二叉树中值为data的结点
	 */
	private BSTNode<T> search(BSTNode<T> node, T data) {
		
		if(node == null){
			return node;
		}
		
		int cmp = data.compareTo(node.data);
		if(cmp < 0)         return search(node.left, data);   // 在左子树中找
		else if(cmp > 0)    return search(node.left, data);   // 在右子树中找
		else                return node;
	}
	
	/**
	 * 递归实现查找根节点下值为data的结点
	 */
	public BSTNode<T> search(T data){
		return search(root, data);
	}
	
	/**
	 * 删除指定的data结点
	 */
	public void delete(T data){
		// 先查找到要删除的结点
		BSTNode<T> node = search(root, data);
		// 如果存在就删除
		if(node != null){
			delete(node);
		}
	}

	/**
	 * 删除指定的node
	 */
	public BSTNode<T> delete(BSTNode<T> node){
		// 第3种情况,如果同时存在左右子结点
		if(node.left != null && node.right != null){
			// 获取待删除结点的后继结点
			BSTNode<T> successor = successor(node);
			// 转移后继节点值到当前待删除结点上
			node.data = successor.data;
			// 把要删除的当前结点设置为它的后继结点
			node = successor;
		}
		// 经过前面一步的处理,下面只有两种情况,即:待删除结点有一个子结点或者没有子结点
		// 不管有没有子结点,都获取子结点
		BSTNode<T> child = null;
		if(node.left != null){
			child = node.left;   // 待删除结点存在左子结点
		}else{ 
			child = node.right;  // 待删除结点存在右子结点
		}
		
		// 如果child != null,就说明待删除结点存在一个子结点
		if(child != null){
			// 将待删除结点的子结点和待删除结点的父结点关联上
			child.parent = node.parent;
		}
		// 如果当前待删除结点没有父结点,说明要删除的结点是根结点
		if(node.parent == null){
			// 将根节点的子节点设置为根节点,按照前面的逻辑下来,根节点只有一个子结点或者没有子结点
			root = child;
		}
		// 如果待删除结点有父结点,并且待删除结点是它父结点的左结点时
		else if(node == node.parent.left){
			// 将父结点的左子结点设置为child
			node.parent.left = child;
		}
		// 如果待删除结点有父结点,并且待删除结点是它父结点的右结点时
		else{
			// 将父结点的右子结点设置为child
			node.parent.right = child;
		}
		
		// 返回被删除的结点
		return node;
	}

	/**
	 * 查找以node为根节点下的最小结点
	 */
	public BSTNode<T> minNode(BSTNode<T> node){
		if(node == null){
			return null;
		}
		
		// 其实就是找ndoe下的最后一个左子结点
		while(node.left != null){
			node = node.left;
		}
		return node;
	}

	/**
	 * 查找整棵二叉查找树下的最小结点
	 */
	public BSTNode<T> minNode(){
		return minNode(root);
	}
	
	/**
	 * 查找以node为根节点下的最大结点
	 */
	public BSTNode<T> maxNode(BSTNode<T> node){
		if(node == null){
			return null;
		}
		
		// 其实就是找ndoe下的最后一个右子结点
		while(node.right != null){
			node = node.right;
		}
		return node;
	}
	 
	/**
	 * 查找整棵二叉查找树下的最大结点
	 */
	public BSTNode<T> maxNode(){
		return maxNode(root);
	}
	
	/**
	 * 找到结点node的后继节点。即:查找二叉树中数据大于该结点的最小结点
	 */
	private BSTNode<T> successor(BSTNode<T> node) {
		BSTNode<T> successor = null;
		// 如果存在右子结点,则node的后继结点为以其右子结点为根的右子树中的最小结点
		if(node.right != null){
			while(node.right.left != null){
				successor = node.right.left;  // 最小结点肯定在右子树中的左子结点中
			}
		}
		
		// 如果node没有右子结点,则node有以下两种情况
		BSTNode<T> nodeParent = node.parent;
		// 1.node是“一个左子结点”,则node的后继结点就为node的父结点
		if(nodeParent != null && node == node.parent.left){
			successor = node.parent;
		}
		// 2.node是“一个右子结点”,则查找node的“最低”父结点,并且该父结点要有左子结点,那么这个“最低”的父结点就是node的后继结点
		// 2.1 如果nodeParent是nodeParent.parent的左子结点,则nodeParent.parent就是它的后继节点
		// 2.2如果nodeParent是nodeParent.parent的右子结点,则还需要往上面找,直到node作为它的左子结点时,此时的nodeParent.parent就是它的后继节点
		while(nodeParent != null && node == node.parent.right){
			node = node.parent;
			nodeParent = nodeParent.parent;
			successor = nodeParent;
		}
		
		return successor;
	}
	
	/**
	 * 找到结点node的前驱节点。即:查找二叉树中数据小于该结点的最小结点
	 */
	private BSTNode<T> predecessor(BSTNode<T> node) {
		BSTNode<T> predecessor = null;
		// 如果存在左子结点,则node的前驱结点为以其左子结点为根的左子树中的最大结点
		if(node.left != null){
			while(node.left.right != null){
				predecessor = node.left.right;  // 最大结点肯定在左子树中的右子结点中
			}
		}
		
		// 如果node没有左子结点,则node有以下两种情况
		BSTNode<T> nodeParent = node.parent;
		// 1.node是“一个右子结点”,则node的前驱结点就为node的父结点
		if(nodeParent != null && node == node.parent.left){
			predecessor = node.parent;
		}
		// 2.node是“一个左子结点”,则查找node的“最低”父结点,并且该父结点要有右子结点,那么这个“最低”的父结点就是node的前驱结点
		// 2.1 如果nodeParent是nodeParent.parent的右子结点,则nodeParent.parent就是它的前驱节点
		// 2.2如果nodeParent是nodeParent.parent的左子结点,则还需要往上面找,直到node作为它的右子结点时,此时的nodeParent.parent就是它的前驱节点
		while(nodeParent != null && node == node.parent.left){
			node = node.parent;
			nodeParent = nodeParent.parent;
			predecessor = nodeParent;
		}
		
		return predecessor;
	}
	
	/**
	 * 将结点插入到查找二叉树中
	 */
	public void insert(BSTree<T> bst, BSTNode<T> node){
		int cmp;
		BSTNode<T> node1 = null;
		BSTNode<T> root = bst.root;
		
		// 先查找node的插入位置
		while(root != null){
			node1 = root;
			cmp = node.data.compareTo(root.data);
			if(cmp < 0){
				root = root.left; 
			}else{
				root = root.right;
			}
		}
		
		node.parent = node1;
		if(node1 == null){
			bst.root = node;
		}else{
			cmp = node.data.compareTo(node1.data);
			if(cmp < 0){
				node1.left = node;
			}else{
				node1.right = node;
			}
		}
	}
	
	/**
	 * 销毁二叉树
	 */
	public void destroy(BSTNode<T> node){
		if(node == null){
			return ;
		}
		if(node.left != null){
			destroy(node.left);
		}
		if(node.right != null){
			destroy(node.right);
		}
		node = null;
	}
	
	/**
	 * 打印node作为根节点的二叉树
	 * direction:  0:表示该结点是根结点
	 *            -1:表示该结点是它父结点的左子结点
	 *             1:表示该结点是它父结点的右子结点
	 */
	public void print(BSTNode<T> node, T data, int direction){
		if(node != null){
			if(direction == 0){
				System.out.printf("%2d is root\n", node.data);
			}else{
				System.out.printf("%2d is %2d's %6s child\n", node.data, data, direction == 1 ? "right" : "left");
			}
			print(node.left, node.data, -1);
			print(node.right, node.data, 1);
		}
	}
	
	/**
	 * 打印二叉树
	 */
	public void print(){
		if(root != null){
			print(root, root.data, 0);
		}
	}
}

推荐及参考:

 

1、重温数据结构:二叉排序树的查找、插入、删除:https://blog.csdn.net/u011240877/article/details/53242179

2、《数据结构与算法之美》:https://time.geekbang.org/column/article/68334

3、二叉树的删除操作详解:https://blog.csdn.net/isea533/article/details/80345507【推荐阅读】

    原文作者:二叉查找树
    原文地址: https://blog.csdn.net/pcwl1206/article/details/84146254
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞