浅析数据结构与算法7--二叉查找树 及Java实现

本文详细介绍了二叉查找树的数据结构及其基本操作,包括插入、查找、删除等,并分析了其时间复杂度。

前文介绍了符号表的两种实现,无序链表和有序数组,无序链表在插入的时候具有较高的灵活性,而有序数组在查找时具有较高的效率,本文介绍的二叉查找树(Binary Search Tree,BST)这一数据结构综合了以上两种数据结构的优点。

二叉查找树具有很高的灵活性,对其优化可以生成平衡二叉树,红黑树等高效的查找和插入数据结构,后文会一一介绍。

一 定义

二叉查找树(Binary Search Tree),也称有序二叉树(ordered binary tree),排序二叉树(sorted binary tree),是指一棵空树或者具有下列性质的二叉树:

1. 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

2. 若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;

3. 任意节点的左、右子树也分别为二叉查找树。

4. 没有键值相等的节点(no duplicate nodes)。

如下图,这个是普通的二叉树:

binary tree

在此基础上,加上节点之间的大小关系,就是二叉查找树:

binary search tree

二 实现

在实现中,我们需要定义一个内部类Node,它包含两个分别指向左右节点的Node,一个用于排序的Key,以及该节点包含的值Value,还有一个记录该节点及所有子节点个数的值Number。

public class BinarySearchTreeSymbolTable<TKey, TValue> : SymbolTables<TKey, TValue> where TKey : IComparable<TKey>, IEquatable<TValue>
{
    private Node root;
    private class Node
    {
        public Node Left { get; set; }
        public Node Right { get; set; }
        public int Number { get; set; }
        public TKey Key { get; set; }
        public TValue Value { get; set; }

        public Node(TKey key, TValue value, int number)
        {
            this.Key = key;
            this.Value = value;
            this.Number = number;
        }
    }
...
}

查找

查找操作和二分查找类似,将key和节点的key比较,如果小于,那么就在Left Node节点查找,如果大于,则在Right Node节点查找,如果相等,直接返回Value。

 SearchhitAndSearchMissinBST

该方法实现有迭代和递归两种。

递归的方式实现如下:

public override TValue Get(TKey key)
{
    TValue result = default(TValue);
    Node node = root;
    while (node != null)
    {

        if (key.CompareTo(node.Key) > 0)
        {
            node = node.Right;
        }
        else if (key.CompareTo(node.Key) < 0)
        {
            node = node.Left;
        }
        else
        {
            result = node.Value;
            break;
        }
    }
    return result;
}

迭代的如下:

public TValue Get(TKey key)
{
    return GetValue(root, key);
}

private TValue GetValue(Node root, TKey key)
{
    if (root == null) return default(TValue);
    int cmp = key.CompareTo(root.Key);
    if (cmp > 0) return GetValue(root.Right, key);
    else if (cmp < 0) return GetValue(root.Left, key);
    else return root.Value;
}

插入

插入和查找类似,首先查找有没有和key相同的,如果有,更新;如果没有找到,那么创建新的节点。并更新每个节点的Number值,代码实现如下:

public override void Put(TKey key, TValue value)
{
    root = Put(root, key, value);
}

private Node Put(Node x, TKey key, TValue value)
{
    //如果节点为空,则创建新的节点,并返回
    //否则比较根据大小判断是左节点还是右节点,然后继续查找左子树还是右子树
    //同时更新节点的Number的值
    if (x == null) return new Node(key, value, 1);
    int cmp = key.CompareTo(x.Key);
    if (cmp < 0) x.Left = Put(x.Left, key, value);
    else if (cmp > 0) x.Right = Put(x.Right, key, value);
    else x.Value = value;
    x.Number = Size(x.Left) + Size(x.Right) + 1;
    return x;
}

private int Size(Node node)
{
    if (node == null) return 0;
    else return node.Number;
}

  插入操作图示如下:

insert into BST

下面是插入动画效果:

insert into BST

随机插入形成树的动画如下,可以看到,插入的时候树还是能够保持近似平衡状态:

Insert keys random order in BST

最大最小值

如下图可以看出,二叉查找树的最大最小值是有规律的:

the max and min item in bst

从图中可以看出,二叉查找树中,最左和最右节点即为最小值和最大值,所以我们只需迭代调用即可。

public override TKey GetMax()
{
    TKey maxItem = default(TKey);
    Node s = root;
    while (s.Right != null)
    {
        s = s.Right;
    }
    maxItem = s.Key;
    return maxItem;
}

public override TKey GetMin()
{
    TKey minItem = default(TKey);
    Node s = root;
    while (s.Left != null)
    {
        s = s.Left;
    }
    minItem = s.Key;
    return minItem;
}

以下是递归的版本:

public TKey GetMaxRecursive()
{
    return GetMaxRecursive(root);
}

private TKey GetMaxRecursive(Node root)
{
    if (root.Right == null) return root.Key;
    return GetMaxRecursive(root.Right);
}

public TKey GetMinRecursive()
{
    return GetMinRecursive(root);
}

private TKey GetMinRecursive(Node root)
{
    if (root.Left == null) return root.Key;
    return GetMinRecursive(root.Left);
}

Floor和Ceiling

查找Floor(key)的值就是所有<=key的最大值,相反查找Ceiling的值就是所有>=key的最小值,下图是Floor函数的查找示意图:

floor  function in BST

以查找Floor为例,我们首先将key和root元素比较,如果key比root的key小,则floor值一定在左子树上;如果比root的key大,则有可能在右子树上,当且仅当其右子树有一个节点的key值要小于等于该key;如果和root的key相等,则floor值就是key。根据以上分析,Floor方法的代码如下,Ceiling方法的代码类似,只需要把符号换一下即可:

public TKey Floor(TKey key)
{
    Node x = Floor(root, key);
    if (x != null) return x.Key;
    else return default(TKey);
}

private Node Floor(Node x, TKey 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);
    else
    {
        Node right = Floor(x.Right, key);
        if (right == null) return x;
        else return right;
    }
}

删除

删除元素操作在二叉树的操作中应该是比较复杂的。首先来看下比较简单的删除最大最小值得方法。

以删除最小值为例,我们首先找到最小值,及最左边左子树为空的节点,然后返回其右子树作为新的左子树。操作示意图如下:

delete minimun in BST

代码实现如下:

public void DelMin()
{
    root = DelMin(root);
}

private Node DelMin(Node root)
{
    if (root.Left == null) return root.Right;
    root.Left = DelMin(root.Left);
    root.Number = Size(root.Left) + Size(root.Right) + 1;
    return root;
}

删除最大值也是类似。

现在来分析一般情况,假定我们要删除指定key的某一个节点。这个问题的难点在于:删除最大最小值的操作,删除的节点只有1个子节点或者没有子节点,这样比较简单。但是如果删除任意节点,就有可能出现删除的节点有0个,1 个,2个子节点的情况,现在来逐一分析。

当删除的节点没有子节点时,直接将该父节点指向该节点的link设置为null。

 delete node which has 0 childrens

当删除的节点只有1个子节点时,将该自己点替换为要删除的节点即可。

delete node which has 1 childrens

当删除的节点有2个子节点时,问题就变复杂了。

假设我们删除的节点t具有两个子节点。因为t具有右子节点,所以我们需要找到其右子节点中的最小节点,替换t节点的位置。这里有四个步骤:

1. 保存带删除的节点到临时变量t

2. 将t的右节点的最小节点min(t.right)保存到临时节点x

3. 将x的右节点设置为deleteMin(t.right),该右节点是删除后,所有比x.key最大的节点。

4. 将x的做节点设置为t的左节点。

整个过程如下图:

delete node which has 2 childrens

对应代码如下:

public void Delete(TKey key)
{
    root =Delete(root, key);
        
}

private Node Delete(Node x, TKey key)
{
    int cmp = key.CompareTo(x.Key);
    if (cmp > 0) x.Right = Delete(x.Right, key);
    else if (cmp < 0) x.Left = Delete(x.Left, key);
    else
    {
        if (x.Left == null) return x.Right;
        else if (x.Right == null) return x.Left;
        else
        {
            Node t = x;
            x = GetMinNode(t.Right);
            x.Right = DelMin(t.Right);
            x.Left = t.Left;
        }
    }
    x.Number = Size(x.Left) + Size(x.Right) + 1;
    return x;
}

private Node GetMinNode(Node x)
{
    if (x.Left == null) return x;
    else return GetMinNode(x.Left); 
}

以上二叉查找树的删除节点的算法不是完美的,因为随着删除的进行,二叉树会变得不太平衡,下面是动画演示。

delete node in BST

三 分析

二叉查找树的运行时间和树的形状有关,树的形状又和插入元素的顺序有关。在最好的情况下,节点完全平衡,从根节点到最底层叶子节点只有lgN个节点。在最差的情况下,根节点到最底层叶子节点会有N各节点。在一般情况下,树的形状和最好的情况接近。

BST Tree shape

在分析二叉查找树的时候,我们通常会假设插入的元素顺序是随机的。对BST的分析类似与快速排序中的查找:

BST and quick sort partition

BST中位于顶部的元素就是快速排序中的第一个划分的元素,该元素左边的元素全部小于该元素,右边的元素均大于该元素。

对于N个不同元素,随机插入的二叉查找树来说,其平均查找/插入的时间复杂度大约为2lnN,这个和快速排序的分析一样,具体的证明方法不再赘述,参照快速排序。

 

四 总结

有了前篇文章 二分查找的分析,对二叉查找树的理解应该比较容易。下面是二叉查找树的时间复杂度:

analysis of binary search tree

它和二分查找一样,插入和查找的时间复杂度均为lgN,但是在最坏的情况下仍然会有N的时间复杂度。原因在于插入和删除元素的时候,树没有保持平衡。我们追求的是在最坏的情况下仍然有较好的时间复杂度,这就是后面要讲的平衡查找树的内容了。下文首先讲解平衡查找树的最简单的一种:2-3查找树。

希望本文对您了解二叉查找树有所帮助。


Java实现代码:

/** 
 *  
 * 此程序实现一个二叉查找树的功能,可以进行动态插入、删除关键字; 
 * 查询给定关键字、最小关键字、最大关键字;转换为有序列表(用于排序) 
 *   
 */

import java.util.ArrayList;
import java.util.List;

public class BinarySearchTree {

	// 树的根结点
	private TreeNode root = null;

	// 遍历结点列表
	private List<TreeNode> nodelist = new ArrayList<TreeNode>();

	private class TreeNode {

		private int key;
		private TreeNode leftChild;
		private TreeNode rightChild;
		private TreeNode parent;

		public TreeNode(int key, TreeNode leftChild, TreeNode rightChild,
				TreeNode parent) {
			this.key = key;
			this.leftChild = leftChild;
			this.rightChild = rightChild;
			this.parent = parent;
		}

		public int getKey() {
			return key;
		}

		public String toString() {
			String leftkey = (leftChild == null ? "" : String
					.valueOf(leftChild.key));
			String rightkey = (rightChild == null ? "" : String
					.valueOf(rightChild.key));
			return "(" + leftkey + " , " + key + " , " + rightkey + ")";
		}

	}

	/**
	 * isEmpty: 判断二叉查找树是否为空;若为空,返回 true ,否则返回 false .
	 * 
	 */
	public boolean isEmpty() {
		if (root == null) {
			return true;
		} else {
			return false;
		}
	}

	/**
	 * TreeEmpty: 对于某些二叉查找树操作(比如删除关键字)来说,若树为空,则抛出异常。
	 */
	public void TreeEmpty() throws Exception {
		if (isEmpty()) {
			throw new Exception("树为空!");
		}
	}

	/**
	 * search: 在二叉查找树中查询给定关键字
	 * 
	 * @param key
	 *            给定关键字
	 * @return 匹配给定关键字的树结点
	 */
	public TreeNode search(int key) {
		TreeNode pNode = root;
		while (pNode != null && pNode.key != key) {
			if (key < pNode.key) {
				pNode = pNode.leftChild;
			} else {
				pNode = pNode.rightChild;
			}
		}
		return pNode;
	}

	/**
	 * minElemNode: 获取二叉查找树中的最小关键字结点
	 * 
	 * @return 二叉查找树的最小关键字结点
	 * @throws Exception
	 *             若树为空,则抛出异常
	 */
	public TreeNode minElemNode(TreeNode node) throws Exception {
		if (node == null) {
			throw new Exception("树为空!");
		}
		TreeNode pNode = node;
		while (pNode.leftChild != null) {
			pNode = pNode.leftChild;
		}
		return pNode;
	}

	/**
	 * maxElemNode: 获取二叉查找树中的最大关键字结点
	 * 
	 * @return 二叉查找树的最大关键字结点
	 * @throws Exception
	 *             若树为空,则抛出异常
	 */
	public TreeNode maxElemNode(TreeNode node) throws Exception {
		if (node == null) {
			throw new Exception("树为空!");
		}
		TreeNode pNode = node;
		while (pNode.rightChild != null) {
			pNode = pNode.rightChild;
		}
		return pNode;
	}

	/**
	 * successor: 获取给定结点在中序遍历顺序下的后继结点
	 * 
	 * @param node
	 *            给定树中的结点
	 * @return 若该结点存在中序遍历顺序下的后继结点,则返回其后继结点;否则返回 null
	 * @throws Exception
	 */
	public TreeNode successor(TreeNode node) throws Exception {
		if (node == null) {
			return null;
		}

		// 若该结点的右子树不为空,则其后继结点就是右子树中的最小关键字结点
		if (node.rightChild != null) {
			return minElemNode(node.rightChild);
		}
		// 若该结点右子树为空
		TreeNode parentNode = node.parent;
		while (parentNode != null && node == parentNode.rightChild) {
			node = parentNode;
			parentNode = parentNode.parent;
		}
		return parentNode;
	}

	/**
	 * precessor: 获取给定结点在中序遍历顺序下的前趋结点
	 * 
	 * @param node
	 *            给定树中的结点
	 * @return 若该结点存在中序遍历顺序下的前趋结点,则返回其前趋结点;否则返回 null
	 * @throws Exception
	 */
	public TreeNode precessor(TreeNode node) throws Exception {
		if (node == null) {
			return null;
		}

		// 若该结点的左子树不为空,则其前趋结点就是左子树中的最大关键字结点
		if (node.leftChild != null) {
			return maxElemNode(node.leftChild);
		}
		// 若该结点左子树为空
		TreeNode parentNode = node.parent;
		while (parentNode != null && node == parentNode.leftChild) {
			node = parentNode;
			parentNode = parentNode.parent;
		}
		return parentNode;
	}

	/**
	 * insert: 将给定关键字插入到二叉查找树中
	 * 
	 * @param key
	 *            给定关键字
	 */
	public void insert(int key) {
		TreeNode parentNode = null;
		TreeNode newNode = new TreeNode(key, null, null, null);
		TreeNode pNode = root;
		if (root == null) {
			root = newNode;
			return;
		}
		while (pNode != null) {
			parentNode = pNode;
			if (key < pNode.key) {
				pNode = pNode.leftChild;
			} else if (key > pNode.key) {
				pNode = pNode.rightChild;
			} else {
				// 树中已存在匹配给定关键字的结点,则什么都不做直接返回
				return;
			}
		}
		if (key < parentNode.key) {
			parentNode.leftChild = newNode;
			newNode.parent = parentNode;
		} else {
			parentNode.rightChild = newNode;
			newNode.parent = parentNode;
		}

	}

	/**
	 * insert: 从二叉查找树中删除匹配给定关键字相应的树结点
	 * 
	 * @param key
	 *            给定关键字
	 */
	public void delete(int key) throws Exception {
		TreeNode pNode = search(key);
		if (pNode == null) {
			throw new Exception("树中不存在要删除的关键字!");
		}
		delete(pNode);
	}

	/**
	 * delete: 从二叉查找树中删除给定的结点.
	 * 
	 * @param pNode
	 *            要删除的结点
	 * 
	 *            前置条件: 给定结点在二叉查找树中已经存在
	 * @throws Exception
	 */
	private void delete(TreeNode pNode) throws Exception {
		if (pNode == null) {
			return;
		}
		if (pNode.leftChild == null && pNode.rightChild == null) { // 该结点既无左孩子结点,也无右孩子结点
			TreeNode parentNode = pNode.parent;
			if (pNode == parentNode.leftChild) {
				parentNode.leftChild = null;
			} else {
				parentNode.rightChild = null;
			}
			return;
		}
		if (pNode.leftChild == null && pNode.rightChild != null) { // 该结点左孩子结点为空,右孩子结点非空
			TreeNode parentNode = pNode.parent;
			if (pNode == parentNode.leftChild) {
				parentNode.leftChild = pNode.rightChild;
				pNode.rightChild.parent = parentNode;
			} else {
				parentNode.rightChild = pNode.rightChild;
				pNode.rightChild.parent = parentNode;
			}
			return;
		}
		if (pNode.leftChild != null && pNode.rightChild == null) { // 该结点左孩子结点非空,右孩子结点为空
			TreeNode parentNode = pNode.parent;
			if (pNode == parentNode.leftChild) {
				parentNode.leftChild = pNode.leftChild;
				pNode.rightChild.parent = parentNode;
			} else {
				parentNode.rightChild = pNode.leftChild;
				pNode.rightChild.parent = parentNode;
			}
			return;
		}
		// 该结点左右孩子结点均非空,则删除该结点的后继结点,并用该后继结点取代该结点
		TreeNode successorNode = successor(pNode);
		delete(successorNode);
		pNode.key = successorNode.key;
	}

	/**
	 * inOrderTraverseList: 获得二叉查找树的中序遍历结点列表
	 * 
	 * @return 二叉查找树的中序遍历结点列表
	 */
	public List<TreeNode> inOrderTraverseList() {
		if (nodelist != null) {
			nodelist.clear();
		}
		inOrderTraverse(root);
		return nodelist;
	}

	/**
	 * inOrderTraverse: 对给定二叉查找树进行中序遍历
	 * 
	 * @param root
	 *            给定二叉查找树的根结点
	 */
	private void inOrderTraverse(TreeNode root) {
		if (root != null) {
			inOrderTraverse(root.leftChild);
			nodelist.add(root);
			inOrderTraverse(root.rightChild);
		}
	}

	/**
	 * toStringOfOrderList: 获取二叉查找树中关键字的有序列表
	 * 
	 * @return 二叉查找树中关键字的有序列表
	 */
	public String toStringOfOrderList() {
		StringBuilder sbBuilder = new StringBuilder(" [ ");
		for (TreeNode p : inOrderTraverseList()) {
			sbBuilder.append(p.key);
			sbBuilder.append(" ");
		}
		sbBuilder.append("]");
		return sbBuilder.toString();
	}

	/**
	 * 获取该二叉查找树的字符串表示
	 */
	public String toString() {
		StringBuilder sbBuilder = new StringBuilder(" [ ");
		for (TreeNode p : inOrderTraverseList()) {
			sbBuilder.append(p);
			sbBuilder.append(" ");
		}
		sbBuilder.append("]");
		return sbBuilder.toString();
	}

	public TreeNode getRoot() {
		return root;
	}

	public static void testNode(BinarySearchTree bst, TreeNode pNode)
			throws Exception {
		System.out.println("本结点: " + pNode);
		System.out.println("前趋结点: " + bst.precessor(pNode));
		System.out.println("后继结点: " + bst.successor(pNode));
	}

	public static void testTraverse(BinarySearchTree bst) {
		System.out.println("二叉树遍历:" + bst);
		System.out.println("二叉查找树转换为有序列表: " + bst.toStringOfOrderList());
	}

	public static void main(String[] args) {
		try {
			BinarySearchTree bst = new BinarySearchTree();
			System.out.println("查找树是否为空? " + (bst.isEmpty() ? "是" : "否"));
			int[] keys = new int[] { 15, 6, 18, 3, 7, 13, 20, 2, 9, 4 };
			for (int key : keys) {
				bst.insert(key);
			}
			System.out.println("查找树是否为空? " + (bst.isEmpty() ? "是" : "否"));
			TreeNode minkeyNode = bst.minElemNode(bst.getRoot());
			System.out.println("最小关键字: " + minkeyNode.getKey());
			testNode(bst, minkeyNode);
			TreeNode maxKeyNode = bst.maxElemNode(bst.getRoot());
			System.out.println("最大关键字: " + maxKeyNode.getKey());
			testNode(bst, maxKeyNode);
			System.out.println("根结点关键字: " + bst.getRoot().getKey());
			testNode(bst, bst.getRoot());
			testTraverse(bst);
			System.out.println("****************************** ");
			testTraverse(bst);
		} catch (Exception e) {
			System.out.println(e.getMessage());
			e.printStackTrace();
		}
	}

}



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值