数据结构之二叉查找树

针对二叉查找树的操作(增删改查)的时间和树的高度成正比,比如都有10个节点的一个树,树高为4和树高为10的操作时间肯定是不同的,这个时间实际上是O(lgn),二叉查找树的期望的树高是lgn,从而基本动态集合的操作平均时间为θ(lgn)。

通常二叉查找树基于链表实现,每个节点保存左,右子节点,如果想更方便的实现前后查找,可以增加个一个父节点属性。由于二叉查找树的特点,采用中序遍历可以按照从小到大的顺序将树的所有元素输出,一般的中序遍历都是通过递归来实现。

下面自己实现一个二叉查找树的插入和中序遍历:

package tree;
/**
 * 二叉搜索树
 * @author Administrator
 *
 */
public class BinarySearchTree {
	
	private Node root;
	
	/**
	 * 将一个新节点插入到二叉搜索树中
	 * @param value
	 */
	public void insert(int value)
	{
		if(null == root)
		{
			root = new Node(null,null,null,value);
		}
		else
		{
			insertNotRoot(value);
		}
	}
	/**
	 * 中序遍历
	 * @param n
	 */
	public void midOrderWalk(Node n)
	{
		if(null == n)
		{
			return;
		}
		midOrderWalk(n.left);
		System.out.println(n.value);
		midOrderWalk(n.right);
	}
	
	private void insertNotRoot(int value) {
		Node cur = root;
		Node parent = root;
		while(null != cur)
		{
			parent = cur;
			//插入左子树
			if(value < cur.value)
			{
				if(null == cur.left)
				{
					Node n = new Node(null,null,parent,value);
					parent.left = n;
					break;
				}
				cur = cur.left;
			}
			//插入右子树
			else
			{
				if(null == cur.right)
				{
					Node n = new Node(null,null,parent,value);
					parent.right = n;
					break;
				}
				cur = cur.right;
			}
		}
	}
	
	public Node getRoot() {
		return root;
	}

	public void setRoot(Node root) {
		this.root = root;
	}

	//节点类,保存左右子孩子和父类节点的索引
	private final class Node
	{
		private Node left;
		
		private Node right;
		
		private Node parent;
		
		private int value;
		
		public Node(Node left, Node right, Node parent, int value) {
			this.left = left;
			this.right = right;
			this.parent = parent;
			this.value = value;
		}
		public Node getLeft() {
			return left;
		}
		public void setLeft(Node left) {
			this.left = left;
		}
		public Node getRight() {
			return right;
		}
		public void setRight(Node right) {
			this.right = right;
		}
		public Node getParent() {
			return parent;
		}
		public void setParent(Node parent) {
			this.parent = parent;
		}
		public int getValue() {
			return value;
		}
		public void setValue(int value) {
			this.value = value;
		}
	}
}

对于二叉查找树来说,删除的动作比插入还要复杂一些,因为删除需要考虑的情况更多些,现在就来实现删除的动作,大概考虑下有哪些情况:

首先肯定如果我们删除一个节点,该节点没有左孩子也没有右孩子,则删除它完全不会影响我们当前的二叉查找树,本来是一棵二叉查找树现在还会是一棵二叉查找树。比如上图中的节点48。

其次,考虑要删除的节点只有左孩子或者只有右孩子,比如节点37只有左孩子,由于只有一个孩子,非左即右,左边的都比该节点小,右边的都不比该节点小,因此大小并没有二义性,取该节点的左孩子或者右孩子节点取代该节点即可。

最麻烦的是有两个孩子节点的情况,比如图中的节点47,针对47的删除,我们可以有下面的两种情况:

              

考虑删掉47我们可以拿哪个节点来替代47从而保证树的连接性,考虑到47左边的子树节点都比它小,如果在左子树找,则肯定要找左子树中最大的节点,这个节点肯定需要沿着左子树一直找右节点,左图就是这样一种情况。同理右图,沿着右子树一直找左节点。

先不急看删除的代码,先看下两个概念,后继和前驱,一个节点的前驱是指小于该节点的最大节点,也就是中序遍历该节点的前一个节点,后继就是指大于该节点的最小节点,也就是中序遍历该节点的后一个节点。比如,左图,其实就是寻找47前驱的过程,右图就是寻找47后继的过程。

先不急写寻找某个节点的前驱和后继的代码,寻找前驱和后继其实就是寻找子树最大和最小节点的过程,先来写寻找一棵树的最大和最小节点的实现,如下:

/**
	 * 寻找一棵树的最大节点
	 * 不断寻找右节点,直到该节点没有右孩子节点
	 * @param root
	 * @return
	 */
	public Node getMaxNode(Node root)
	{
		if(null == root)
		{
			return null;
		}
		else
		{
			Node maxNode = root;
			while(null != maxNode.right)
			{
				maxNode = maxNode.right;
			}
			return maxNode;
		}
	}
	
	public int getMaxValue(Node root)
	{
		return getMaxNode(root).getValue();
	}
	
	/**
	 * 寻找一棵树的最小节点
	 * @param root
	 * @return
	 */
	public Node getMinNode(Node root)
	{
		if(null == root)
		{
			return null;
		}
		else
		{
			Node minNode = root;
			while(null != minNode.left)
			{
				minNode = minNode.left;
			}
			return minNode;
		}
	}
	
	public int getMinValue(Node root)
	{
		return getMinNode(root).getValue();
	}

逻辑不难,就不详细讲解了。有了上面的逻辑,再来看下找出一个节点前驱和后继的代码:

/**
	 * 获取一个节点的后继
	 * @param curNode
	 * @return
	 */
	public Node successor(Node curNode)
	{
		if(null == curNode)
		{
			System.out.println("the curNode is null");
			return null;
		}
		else
		{
			//如果有右子树
			if(null != curNode.right)
			{
				return getMinNode(curNode.right);
			}
			else
			{
				//如果没有右子树,表明该节点是某棵子树除根节点外的最大值,找出该根节点即可,
				//需要向上回溯,如果当前节点是父节点的左节点,则父节点即是后继,否则继续向上回溯。
				Node parent = curNode.parent;
				while(null != parent && curNode == parent.right)
				{
					curNode = parent;
					parent = parent.parent;
				}
				return parent;
			}
		}
	}
	
	/**
	 * 获取一个节点的前驱
	 * @param curNode
	 * @return
	 */
	public Node precessor(Node curNode)
	{
		if(null == curNode)
		{
			System.out.println("the curNode is null");
			return null;
		}
		else
		{
			if(null != curNode.left)
			{
				return getMaxNode(curNode.left);
			}
			else
			{
				//如果没有左子树,表明该节点是某棵子树除根节点外的最小值,找出该根节点即可,
				//需要向上回溯,如果当前节点是父节点的右节点,则父节点即是后继,否则继续向上回溯。
				Node parent = curNode.parent;
				while(null != parent && curNode == parent.left)
				{
					curNode = parent;
					parent = parent.parent;
				}
				return parent;
			}
		}
	}

在写删除逻辑之前再写最后一个方法,根据值获取node节点:

/**
	 * 查找元素
	 * @param value
	 * @return
	 */
	public Node searchNode(int value)
	{
		Node node = root;
		while(null != node && value != node.value)
		{
			if(value < node.value)
			{
				node = node.left;
			}
			else
			{
				node = node.right;
			}
		}
		return node;
	}

好了,做了那么多铺垫,终于可以来完成删除的逻辑了,实际上我们之前的代码,基本完成了一个搜索二叉树提供的功能(不考虑非法数据)。

/**
	 * 删除操作,分三种情况
	 * @param value
	 */
	public void remove(int value)
	{
		Node node = searchNode(value);
		if(null == node)
		{
			System.out.println("there is no item of value");
		}
		else
		{
			//第一种情况,没有子节点
			if(null == node.left && null == node.right)
			{
				 if(node == node.parent.left)
				 {
					 node.parent.left = null;
				 }
				 else
				 {
					 node.parent.right = null;
				 }
			}
			//第二种情况
			else if(null != node.left && null == node.right)
			{
				//如果是根节点
				if(null == node.parent)
				{
					root = node.left;
				}
				else
				{
					if(node == node.left)
					{
						node.parent.left = node.left;
					}
					else
					{
						node.parent.right = node.left;
					}
				}
			}
			//第二种情况
			else if(null == node.left && null != node.right)
			{
				if(null == node.parent)
				{
					root = node.right;
				}
				else
				{
					if(node == node.left)
					{
						node.parent.left = node.right;
					}
					else
					{
						node.parent.right = node.right;
					}
				}
			}
			else
			{
				//两个节点都不为空,则选择查询右子树最小的节点
				Node precessor = precessor(node);
				//继续删除前驱,其实这里可以不用递归,因为删除的动作最多递归两次,某个节点的前驱肯定不会有两个孩子节点。
				remove(precessor.value);
				node.value = precessor.value;
				
			}
		}
	}

删除的代码逻辑分支比较多,仔细想想也不是很难理解,只是需要考虑的场景多一些。我们跟着第一张图来走一遍代码,假设删除的是头结点62,进入删除代码,首先searchNode找出该节点,root左右孩子都有进入最后一个else分支,查找62的前驱,很明显,进入precessor(62),左子树不为空,则进入getMaxNode(58),58没有右节点直接返回58,58就是62的直接前驱,调用remove(58),因为58只有左子树,只需要将62的左孩子指针指向58的左孩子指针即可。此时将58删除了,但是我们要删除的是62头结点,只需要将头结点的value赋值为前驱的节点值即可。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值