数据结构基础温故-6.查找(上):基本查找与树表查找

只要你打开电脑,就会涉及到查找技术。如炒股软件中查股票信息、硬盘文件中找照片、在光盘中搜DVD,甚至玩游戏时在内存中查找攻击力、魅力值等数据修改用来作弊等,都要涉及到查找。当然,在互联网上查找信息就更加是家常便饭。查找是计算机应用中最常用的操作之一,也是许多程序中最耗时的一部分,查找方法的优劣对于系统的运行效率影响极大。因此,本篇讨论一些查找方法。

一、顺序查找

1.1 基本思想

  顺序查找(Sequential Search)又叫线性查找,是最基本的查找技术,它的查找过程是:从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较,若某个记录的关键字和给定值相等,则查找成功,找到所查的记录;如果直到最后一个(或第一个)记录,其关键字和给定值比较都不等时,则表中没有所查的记录,查找不成功。

顺序查找所用时间与查找关键字Key在线性表中的位置有关,其时间复杂度为O(n)。顺序查找的优点在于:算法简单易行,且对表的结构无任何要求(无论是顺序表还是链表,也无论是按关键字有序还是无序存放)。当然,其缺点也比较明显:算法效率较低,在较大规模的数据集合中进行查找时,不宜采用顺序查找

1.2 代码实现

        static void SequenceSearchDemo()
        {
            int[] seqList = { 2, 8, 10, 13, 21, 36, 51, 57, 62, 69 };

            Console.WriteLine("-------------基本顺序查找-------------");
            Console.WriteLine("查找51:{0}", SequenceSearch(seqList, 51));
            Console.WriteLine("查找8:{0}", SequenceSearch(seqList, 8));
            Console.WriteLine("查找15:{0}", SequenceSearch(seqList, 15));
        }

        static int SequenceSearch(int[] seqList, int key)
        {
            int index = -1;
            for (int i = 0; i < seqList.Length; i++)
            {
                if (seqList[i] == key)
                {
                    index = i;
                    break;
                }
            }

            return index;
        }

  运行结果为:

二、二分查找

2.1 基本思想

  折半查找(Binary Search)技术,又称为二分查找。它的前提是线性表中的记录必须是关键码有序(通常从小到大有序),线性表必须采用顺序存储,其时间复杂度为O(logn)。

折半查找的基本思想是:在有序表中,取中间记录作为比较对象,若给定值与中间记录的关键字相等,则查找成功;若给定值小于中间记录的关键字,则在中间记录的左半区继续查找;若给定值大于中间记录的关键字,则在中间记录的右半区继续查找。不断重复上述过程,直到查找成功,或所有查找区域无记录,查找失败为止。

2.2 代码实现

        static void SeqSearchDemo()
        {
            int[] seqList = { 2, 8, 10, 13, 21, 36, 51, 57, 62, 69 };

            Console.WriteLine("-------------基本二分查找-------------");
            Console.WriteLine("查找51:{0}", SeqSearch(seqList, 51));
            Console.WriteLine("查找8:{0}", SeqSearch(seqList, 8));
            Console.WriteLine("查找15:{0}", SeqSearch(seqList, 15));
        }

        static int SeqSearch(int[] seqList, int key)
        {
            int low = 0;
            int high = seqList.Length - 1;
            int mid;

            while (low <= high)
            {
                mid = (low + high) / 2;
                if (seqList[mid] == key)
                {
                    return mid;
                }
                else if (seqList[mid] < key)
                {
                    low = mid + 1;
                }
                else
                {
                    high = mid - 1;
                }
            }

            return -1;
        }

  运行结果为:

2.3 Array.BinarySearch方法

  在.NET中的数组类Array中,内置了一个二分查找的方法—Array.BinarySearch,它是一个静态方法。需要注意的是:在调用这个方法前,需要确保作为参数的查找表内的关键字已经有序,否则就需要手动调用Array.Sort()方法进行排序。

    int[] seqList = { 32, 25, 8, 10, 13, 21, 36, 51, 57, 62, 69 };
    Console.WriteLine("-------------Array.BinarySearch-------------");
    Array.Sort(seqList);
    Console.WriteLine("查找51:{0}", Array.BinarySearch(seqList, 51));
    Console.WriteLine("查找69:{0}", Array.BinarySearch(seqList, 69));
    Console.WriteLine("查找15:{0}", Array.BinarySearch(seqList, 15));

  在Array.BinarySearch()方法内部的求mid值的公式为:mid=low+((high-low)>>1),这是因为整数右移一位相当于整数除2操作,但位移运算的速度快于除法运算

2.4 System.Collections.SortedList类

  在.NET中的System.Collections命名空间下,SortedListSortedList<TKey,TValue>两个类是用于存放键值对的集合类,它们的元素存储于线性表中,并按键值进行排序。其中SortedList使用了两个数组来分别存放key和value,并巧妙地运用了二分查找使得它的各项性能与ArrayList十分近似。

    SortedList<string, string> studentList = new SortedList<string, string>();
    studentList.Add("005", "张三");
    studentList.Add("004", "李四");
    studentList.Add("006", "王五");
    studentList.Add("012", "马六");
    studentList.Add("002", "钱七");
    studentList.Add("009", "刘八");

    foreach (var item in studentList)
    {
       Console.WriteLine("{0}:{1}", item.Key, item.Value);
    }

  运行结果为:

  回过头来,我们看看SortedList类的Add方法,从中可以发现,它借助了Array.BinarySearch方法获取存储位置,也就是说它也使用了二分查找方法。

三、查找树方法

  前面讨论的几种查找方法中,二分查找效率最高,但其要求表中记录按照关键字有序,且只能在顺序表上实现,从而需要在插入和删除操作时移动很多的元素。如果不希望表中记录按关键字有序,而又希望得到较高的插入和删除效率,可以考虑使用几种特殊的二叉树或树作为表的组织形式。

3.1 二叉查找树

(1)基本概念

  二叉查找树(Binary Search Tree,BST)又称二叉排序树,它是满足如下性质的二叉树:

  •  若它的左子树非空,则左子树上所有记录的值均小于根记录的值;
  •  若它的右子树非空,则右子树上所有记录的值均大于根记录的值;
  •  左、右子树又各是一棵二叉查找树。

  假如有一个序列{62,88,58,47,35,73,51,99,37,93},那么构造出来的二叉查找树如下图所示:

  二叉查找树是递归定义的,其一般理解是:二叉查找树中任一节点,其值为k,只要该节点有左孩子,则左孩子的值必小于k,只要有右孩子,则右孩子的值必大于k。二叉查找树的一个重要的性质是:中序遍历该树得到的序列是一个递增有序的序列

(2)二叉查找树的新增操作

(3)二叉查找树的删除操作

(4)二叉查找树的代码实现

  有关二叉查找树的新增和删除节点如何实现,可以阅读《数据结构基础温故—4.树(中)》一文,该文使用C#实现了二叉查找树。

注意:对于二叉查找树最糟糕的情况是插入一个有序序列,使得具有N个元素的集合生成了高度为N的单枝二叉树,从而使其退化了一个单链表,其查找效率也会会由O(logn)变为O(n)。

3.2 平衡二叉树

  刚刚提到在二叉查找树中,如果插入元素的顺序接近有序,那么二叉查找树将退化为链表,从而导致二叉查找树的查找效率大大降低。前苏联两位科学家G.M. Adelson-Velskii和E.M. Landis在1962年的一篇论文中提出了一种自平衡二叉查找树。这种二叉查找树在插入和删除操作中,可以通过一系列的旋转操作来保持平衡,从而保证了二叉查找树的查找效率。最终这种二叉查找树被命名为AVL-Tree,也被称为平衡二叉树。

(1)基本概念

  平衡二叉树定义(AVL):它或者是一颗空树,或者具有以下性质的二叉树:它的左子树和右子树的深度之差的绝对值不超过1,且它的左子树和右子树都是一颗平衡二叉树。

  平衡因子(BF):结点的左子树的深度减去右子树的深度,那么显然-1<=bf<=1;

PS:平衡二叉树上所有结点的平衡因子只可能是-1、0和1。只要二叉树上有一个结点的平衡因子的绝对值大于1,则该二叉树就是不平衡的

(2)平衡二叉树的操作

  假设我们已经有棵平衡二叉树,现在让我们来看看插入节点后,原来节点失去平衡后,平衡二叉树会进行不同类型(RR、LL、LR以及RL)的旋转来保持平衡。

3.3 System.Collections.Generic.SortedDictionary类

  另一种与平衡二叉树类似的是红黑树,红黑树和AVL树的区别在于它使用颜色来标识节点的高度,它所追求的是局部平衡而不是AVL树中的非常严格的平衡。在.NET中的System.Collections.Generic命名空间下,SortedDictionary类就是使用红黑树实现的。红黑树和AVL树的原理非常接近,但是复杂度却远胜于AVL树,这里也就不做讨论。园子里也已经有了不少关于红黑树的比较好的介绍的文章,有兴趣的可以去阅读阅读。

  在代码中,我们可以模拟100000个数字进行添加:

            Random random = new Random();
            int array_count = 100000;
            List<int> intList = new List<int>();
            for (int i = 0; i <= array_count; i++)
            {
                int ran = random.Next();
                intList.Add(ran);
            }

            SortedDictionary<int, int> dic_int = new SortedDictionary<int, int>();
            foreach (var item in intList)
            {
                if (dic_int.ContainsKey(item) == false)
                {
                    dic_int.Add(item, item);
                }
            }

  当然,还可以与SortedList(SortedList内部是Array,而SortedDictionary内部是红黑树)进行一下对比,这里使用了老赵的CodeTimer类

  (1)新增操作对比

  由于SortedList用Array数组保存,每次进行插入操作时,首先用二分查找法找到相应的位置,得到位置以后,SortedList会把该位置以后的值依次往后移一个位置,空出当前位,再把值插入,这个过程中用到了Array.Copy方法,而调用该方法是比较损耗性能的,该代码如下:

private void  Insert(int  index, TKey key, TValue value)
{
    ......

    if  (index < this._size)
    {
        Array.Copy(this.keys, index, this.keys, index + 1, this._size - index);
        Array.Copy(this.values, index, this.values, index + 1, this._size - index);
    }

    ......
}

  SortedDictionary在添加操作时,只会根据红黑树的特性,旋转节点,保持平衡,并没有对Array.Copy的调用。下面我们就用数据测试一下:循环一个int型、容量为100000的随机数组,分别用SortedList和SortedDictionary添加,看看效率如何:

        static void SortedAddInTest()
        {
            Random random = new Random();
            int array_count = 100000;
            List<int> intList = new List<int>();
            for (int i = 0; i <= array_count; i++)
            {
                int ran = random.Next();
                intList.Add(ran);
            }

            SortedList<int, int> sortedlist_int = new SortedList<int, int>();
            SortedDictionary<int, int> dic_int = new SortedDictionary<int, int>();
            CodeTimer.Time("sortedList_Add_int", 1, () =>
            {
                foreach (var item in intList)
                {
                    if (sortedlist_int.ContainsKey(item) == false)
                        sortedlist_int.Add(item, item);
                }
            });
            CodeTimer.Time("sortedDictionary_Add_int", 1, () =>
            {
                foreach (var item in intList)
                {
                    if (dic_int.ContainsKey(item) == false)
                        dic_int.Add(item, item);
                }
            });
        }

  运行结果如下图所示:

  从上图可以看出:在大量添加操作的情况下,SortedDictionary性能优于SortedList。

  (2)查询操作对比

  两者的查询操作中,时间复杂度都为O(logn),且源码中也没有额外的操作造成性能损失,那么他们在查询操作中性能如何?继续上面一个例子进行测试。

        static void SortedQueryInTest()
        {
            Random random = new Random();
            int array_count = 100000;
            List<int> intList = new List<int>();
            for (int i = 0; i <= array_count; i++)
            {
                int ran = random.Next();
                intList.Add(ran);
            }

            SortedList<int, int> sortedlist_int = new SortedList<int, int>();
            SortedDictionary<int, int> dic_int = new SortedDictionary<int, int>();

            CodeTimer.Time("sortedList_Search_int", 1, () =>
            {
                foreach (var item in intList)
                {
                    sortedlist_int.ContainsKey(item);
                }
            });

            CodeTimer.Time("sortedDictionary_Search_int", 1, () =>
            {
                foreach (var item in intList)
                {
                    dic_int.ContainsKey(item);
                }
            });
        }

  运行结果如下图所示:

  从上图可以看出:两者在循环10w次的情况下,查询操作仅仅相差几十毫秒,可以得出两者的查询操作性能相差不大。

  (3)删除操作对比

  从添加操作例子可以看出,由于SortedList内部使用Array数组进行存储数据,而数组本身的局限性使得SortedList大部分的添加操作都要调用Array.Copy方法,从而导致了性能的损失,这种情况同样存在于删除操作中。

  SortedList每次删除操作都会将删除位置后的值往前挪动一位,以填补删除位置的空白,这个过程刚好跟添加操作反过来,同样也需要调用Array.Copy方法,相关代码如下:

public void RemoveAt(int index)
{
    ......

    if (index < this._size)
    {
        Array.Copy(this.keys, index + 1, this.keys, index, this._size - index);
        Array.Copy(this.values, index + 1, this.values, index, this._size - index);
    }

    ......
}

  而SortedDictionary使用红黑树结构存储元素,红黑树本身是一棵二叉查找树,它的删除和二叉查找树的删除类似。首先要找到真正的删除点,当被删除结点n存在左右孩子时,真正的删除点应该是n的中序遍历的前驱,关于这一点请参考二叉查找树的删除。如下图所示,当删除结点20时,实际被删除的结点应该为18,结点20的数据变为18。

  这里,我们仍然选择上面的例子来进行一个简单的对比测试,不过这里我们将随机数的数量改为1000000。

        static void SortedDeleteInTest()
        {
            Random random = new Random();
            int array_count = 1000000;
            List<int> intList = new List<int>();
            for (int i = 0; i <= array_count; i++)
            {
                int ran = random.Next();
                intList.Add(ran);
            }

            SortedList<int, int> sortedlist_int = new SortedList<int, int>();
            SortedDictionary<int, int> dic_int = new SortedDictionary<int, int>();

            CodeTimer.Time("sortedList_Delete_String", 1, () =>
            {
                foreach (var item in intList)
                {
                    sortedlist_int.Remove(item);
                }
            });

            CodeTimer.Time("sortedDictionary_Delete_String", 1, () =>
            {
                foreach (var item in intList)
                {
                    dic_int.Remove(item);
                }
            });
        }

  运行结果如下图所示:

  从上图也可以看出:在1000000次的删除操作中,SortedDictionary的处理速度和性能消耗仅为SortedList的一半左右

总结:

①SortedList用数组存储数据,所以对GC比较友好一点,而且对于相对比较有序的输入源而言,操作较少(eg:List<int> intList = Enumerable.Range(0, array_count).ToList())。
②SortedDictionary用节点链存储数据,所以对GC而言,相对比较复杂。所以当可以预见到集合中的元素比较少的时候或者数据本身相对比较有序时,应该倾向于使用SortedList。

参考资料

(1)程杰,《大话数据结构》

(2)陈广,《数据结构(C#语言描述)》

(3)段恩泽,《数据结构(C#语言版)》

(4)许两会,《.NET集合类的研究—有序集合(SortedDictionary)

特别感谢

  太原理工大学,数据结构算法演示网站

作者:周旭龙

出处:http://edisonchou.cnblogs.com

本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。

时间: 2024-10-05 12:28:17

数据结构基础温故-6.查找(上):基本查找与树表查找的相关文章

查找算法(5)--Tree table lookup--树表查找

1.树表查找 (1) 最简单的树表查找算法——二叉树查找算法. [1]基本思想:二叉查找树是先对待查找的数据进行生成树,确保树的左分支的值小于右分支的值,然后在就行和每个节点的父节点比较大小,查找最适合的范围. 这个算法的查找效率很高,但是如果使用这种查找方法要首先创建树. [2]二叉查找树(BinarySearch Tree,也叫二叉搜索树,或称二叉排序树Binary Sort Tree)或者是一棵空树,或者是具有下列性质的二叉树: 1)若任意节点的左子树不空,则左子树上所有结点的值均小于它的

数据结构基础温故-6.查找(下):哈希表

哈希(散列)技术既是一种存储方法,也是一种查找方法.然而它与线性表.树.图等结构不同的是,前面几种结构,数据元素之间都存在某种逻辑关系,可以用连线图示表示出来,而哈希技术的记录之间不存在什么逻辑关系,它只与关键字有关联.因此,哈希主要是面向查找的存储结构.哈希技术最适合的求解问题是查找与给定值相等的记录. 一.基本概念及原理 1.1 哈希定义的引入 这里首先看一个场景:在大多数情况下,数组中的索引并不具有实际的意义,它仅仅表示一个元素在数组中的位置而已,当需要查找某个元素时,往往会使用有实际意义

数据结构基础温故-1.线性表(上)

开篇:线性表是最简单也是在编程当中使用最多的一种数据结构.例如,英文字母表(A,B,C,D...,Z)就是一个线性表,表中的每一个英文字母都是一个数据元素:又如,成绩单也是一个线性表,表中的每一行是一个数据元素,每个数据元素又由学号.姓名.成绩等数据项组成.顺序表和链表作为线性表的两种重要的存在形式,它们是堆栈.队列.树.图等数据结构的实现基础. 一.线性表基础 1.1 线性表的基本定义 线性表:零个或多个数据元素的有限序列.线性表中的元素在位置上是有序的,类似于储户去银行排队取钱,人们依次排着

数据结构基础温故-5.图(上)

前面几篇已经介绍了线性表和树两类数据结构,线性表中的元素是“一对一”的关系,树中的元素是“一对多”的关系,本章所述的图结构中的元素则是“多对多”的关系.图(Graph)是一种复杂的非线性结构,在图结构中,每个元素都可以有零个或多个前驱,也可以有零个或多个后继,也就是说,元素之间的关系是任意的.现实生活中的很多事物都可以抽象为图,例如世界各地接入Internet的计算机通过网线连接在一起,各个城市和城市之间的铁轨等等. 一.图的基本概念 1.1 多对多的复杂关系 现实中人与人之间关系非常复杂,比如

数据结构基础温故-4.树与二叉树(上)

前面所讨论的线性表元素之间都是一对一的关系,今天我们所看到的结构各元素之间却是一对多的关系.树在计算机中有着广泛的应用,甚至在计算机的日常使用中,也可以看到树形结构的身影,如下图所示的Windows资源管理器和应用程序的菜单都属于树形结构.树形结构是一种典型的非线性结构,除了用于表示相邻关系外,还可以表示层次关系.本文重点讨论树与二叉树的基本结构和遍历算法等内容. 一.好大一棵树,绿色的祝福 1.1 树的基本概念 Defination:树(Tree)是 n(n≥0)个结点的有限集.n=0时,该树

数据结构基础温故-1.线性表(下)

在上一篇中,我们了解了单链表与双链表,本次将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表(circular linked list). 一.循环链表基础 1.1 循环链表节点结构 循环链表和单链表的主要差异就在于循环的判断条件上,原来是判断p.next是否为空,现在则是p.next不等于头结点,则循环未结束. 1.2 循环链表的O(1)访问时间 在单链表中,有了头结点,我们可以在O(1)时间访问到第一个节点,但如果要访

数据结构基础温故-7.排序

排序(Sorting)是计算机内经常进行的一种操作,其目的是将一组“无序”的记录序列调整为按关键字“有序”的记录序列.如何进行排序,特别是高效率地进行排序时计算机工作者学习和研究的重要课题之一.排序有内部排序和外部排序之分,若整个排序过程不需要访问外存便能完成,则称此类排序为内部排序,反之则为外部排序.本篇主要介绍插入排序.交换排序.选择排序和归并排序这几种内部排序方法. 首先,我们今天的目标就是编写一个SortingHelper类,它是一个提供了多种排序方法的帮助类,后面我们的目标就是实现其中

数据结构基础温故-4.树与二叉树(中)

在上一篇中,我们了解了树的基本概念以及二叉树的基本特点和代码实现,还用递归的方式对二叉树的三种遍历算法进行了代码实现.但是,由于递归需要系统堆栈,所以空间消耗要比非递归代码要大很多.而且,如果递归深度太大,可能系统撑不住.因此,我们使用非递归(这里主要是循环,循环方法比递归方法快, 因为循环避免了一系列函数调用和返回中所涉及到的参数传递和返回值的额外开销)来重新实现一遍各种遍历算法,再对二叉树的另外一种特殊的遍历—层次遍历进行实现,最后再了解一下特殊的二叉树—二叉查找树. 一.递归与循环的区别及

数据结构基础温故-1.线性表(中)

在上一篇中,我们学习了线性表最基础的表现形式-顺序表,但是其存在一定缺点:必须占用一整块事先分配好的存储空间,在插入和删除操作上需要移动大量元素(即操作不方便),于是不受固定存储空间限制并且可以进行比较快捷地插入和删除操作的链表横空出世,所以我们就来复习一下链表. 一.单链表基础 1.1 单链表的节点结构 在链表中,每个节点由两部分组成:数据域和指针域. 1.2 单链表的总体结构 链表就是由N个节点链接而成的线性表,如果其中每个节点只包含一个指针域那么就称为单链表,如果含有两个指针域那么就称为双