二叉查找树的查找、插入和删除 - Java实现

http://www.cnblogs.com/yangecnu/p/Introduce-Binary-Search-Tree.html

作者: yangecnuyangecnu‘s Blog on 博客园) 
出处:http://www.cnblogs.com/yangecnu/

英文原文的出处:
http://algs4.cs.princeton.edu/32bst/

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

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

一 定义

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

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

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

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

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

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

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

二 实现

在实现中,我们需要定义一个内部类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。

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

递归的方式实现如下:

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;
}

插入操作图示如下:

下面是插入动画效果:

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

最大最小值

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

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

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为例,我们首先将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;
    }
}

删除

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

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

代码实现如下:

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。

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

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

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

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

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

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

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

整个过程如下图:

对应代码如下:

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);
}

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

三 分析

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

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

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

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

四 总结

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

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

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

时间: 2024-10-16 14:46:24

二叉查找树的查找、插入和删除 - Java实现的相关文章

重温数据结构:二叉排序树的查找、插入、删除

读完本文你将了解到: 什么是二叉排序树 Binary Sort Tree BST 二叉排序树的关键操作 查找 插入 删除 运行代码测试 一道面试题 总结 Thanks 我们知道,二分查找可以缩短查找的时间,但是有个要求就是 查找的数据必须是有序的.每次查找.操作时都要维护一个有序的数据集,于是有了二叉排序树这个概念. 上篇文章 我们介绍了 二叉树 的概念,二叉树有左右子树之分,想必在区分左右子树时有一定的规则. 现在我们来介绍二叉树的一种特殊形式 - 二叉排序树,了解它的区分策略及常用操作. 什

数据结构Java实现03----单向链表的插入和删除

数据结构Java实现03----单向链表的插入和删除 文本主要内容: 链表结构 单链表代码实现 单链表的效率分析 一.链表结构:            概念: 链式存储结构是基于指针实现的.我们把一个数据元素和一个指针称为结点.   数据域:存数数据元素信息的域. 指针域:存储直接后继位置的域. 链式存储结构是用指针把相互直接关联的结点(即直接前驱结点或直接后继结点)链接起来.链式存储结构的线性表称为链表. 链表类型: 根据链表的构造方式的不同可以分为: 单向链表 单向循环链表 双向循环链表 二

二叉查找树(二叉排序树)创建,插入,删除操作。

二叉排序树 二叉排序树是一个基础的树的数据结构.应用非常多. 它的特性就是,左孩子小于parent,右孩子大于parent. 寻找节点 寻找节点就是直接根据数值的大小,从root节点开始遍历,大于当前遍历节点就向它的右子树查找,相反则查找它的左子树.然后返回. 查找最大最小节点 直接根据root节点,遍历到最右就是最大节点,遍历到最左,就是最小节点. 插入节点 插入节点我这里插入的节点都会成为叶子节点.根据大小的关系向下遍历,遍历到最后的节点,然后插入就可以了. 删除节点 这里删除节点是相对麻烦

二叉排序树的查找、插入和删除

1.      二叉排序树 二叉排序树(Binary Sort Tree)或者是一棵空树,或者是具有下列性质的二叉树: (1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值: (2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值: (3)左.右子树也分别为二叉排序树: (4)没有结点值相同的结点. 二叉排序树又称二叉查找树(Binary Search Tree),亦称二叉搜索树.通常采用二叉链表作为二叉排序树的存储结构.中序遍历二叉排序树可以得到关键字有序的序列,即一个无序序

闭散列表的查找、插入和删除操作的完整C代码

/*闭散列表的建立.查找.插入.删除*/ #include <stdio.h> #define NIL -1 //假设关键字为非负整数 #define DEL -2 typedef int KeyType; KeyType HashTable[13]; //便于验证算法,关键字个数假定为不超过13,哈希表长定为13 //关键字插入函数 void InsertHashTable(KeyType k) { for(int i=0; i<13; i++) if( NIL == HashTabl

支持泛型AVL Tree的简单实现,并和STL map比较了插入,删除,查找的性能

1.问题描述: 1)AVL tree是一种自平衡树.它通过左右子树的高度差来控制树的平衡,当高度差是不大于1的时候,认为树是平衡的.树的平衡保证了树在极端情况下 (输入序列不够随机)的性能.很显然当左右子树高度平衡,保证了任何插入,删除,查找操作平均性能呢个,当不平衡时(有的子树很高),当 要操作的元素在这个子树时,性能会很差: 2)AVL tree 和Red black tree 都是一种平衡树,它的操作的时间复杂度是:O(lgN) ,N是树的节点的数目: 3)本文实现了AVL Tree, 并

二叉排序树(BST):创建、查找、插入与删除

删除结点的相关操作(左右子树均为非空的删除结点的方法): 算法分析: 下面以实例来说明二叉排序树的创建.查找.插入和删除等相关操作: 如输入关键字序列(45,24,37,12,54,93),然后对其进行相应的操作,程序如下: #include <iostream> #include <stdio.h> #include <stdlib.h> using namespace std; typedef struct BiTNode { int value; struct B

Windows下Java程序实现对Mysql数据库的插入,删除,修改和查询

运行环境:Windows7,Eclipse,MySql,JDK1.7,mysql-connector-java-5.1.17-bin.jar 1 //import java.io.*; 2 import java.util.*; 3 import java.sql.*; 4 5 /** 6 * 7 * @date 2014-05-20 8 * @author asif 9 * @作用 java实现对mysql数据库的增减删查 10 * @bug 插入,删除,更新的时候异常停止 11 * 12 *

Java 集合与队列的插入、删除在并发下的性能比较

这两天在写一个java多线程的爬虫,以广度优先爬取网页,设置两个缓存: 一个保存已经访问过的URL:vistedUrls 一个保存没有访问过的URL:unVistedUrls 需要爬取的数据量不大,对URL压缩后,可以把这两个数据结构都放入内存,vistedUrls很显然用HashSet<String>实现,因为已经访问的URL只会添加,不会删除和修改,使用HashSet可以高效判断一个URL是否已经访问. 纠结unVistedUrls该用什么数据结构,如果用队列的话,并发情况下,队列中可能会