【算法】排序 (三):二叉树排序&基于散列排序(C++实现)

一. 二叉树排序

  • 对比前面所述的一些排序算法,之前通常使用数组或者链表之类的初级数据结构,二叉树排序使用的是高级数据结构——树。实际上是使用二叉搜索树的机制,对二叉搜索树中序遍历即可以得到排序数组。代码可以直接参考之前的文章[1]。

  • 时间复杂度分析
    二叉树排序主要耗时的两个环节为:构建二叉树和中序遍历。
    构建二叉树的主要耗时是在比较和移动指针,而比较和树的层数有关,最好情况是变成满二叉树,此时层数最少,为 log2n 层,时间复杂度为 O(nlog2n) 。如果是最坏情况,树变成了一条长链(则数串一开始就是顺序的或者是倒序的)。最后形成的树有n层,时间复杂度为 O(n2) 。对于平均情况,我们将每个节点向左扩展或者向右扩展看成等可能,则可以列出递归式,如下所示

    T(n)=1n[2k=0n1T(k)]

    中序遍历每次会遍历一个元素,对于一个长度为n的数串来说,遍历整棵树一次即可得到结果,其时间复杂度为 O(n)
    通过不断迭代可以大致估算出
    T(n)=1.39nlog2n+O(n)

    因此总的时间复杂度为 O(nlog2n) ,由此可以得到一个推论(正确性可证),二叉树排序法和固定先导元素的快速排序在时间复杂度上等价。

  • 空间复杂度分析
    空间复杂度取决于树的层数,综上最好情况下,递归层数约为 log2n 层,最坏情况下递归层数为n层,平均情况下递归层数为 klog2n 层,因此空间复杂度也和固定先导元素的快速排序同样为 O(log2n)

  • 优劣及稳定性
    这个算法的操作比较复杂,对于小数据来说同样操作链表会比数组随机读取慢,但是好处就是空间可以开很大。
    对于排序后需要搜索的情况首选基于二叉树的排序算法。
    但是该算法存在的问题也不少,虽然说时间空间复杂度上与固定先导元素的快速排序等价。然而这种等价只是在级数上等价,而不是完全等于。由于算法设计大量的指针操作和内存分配,因此算法的时间消耗会大不少。其次在遍历时涉及到大量的递归,每次递归都需要传入大量数据,不仅在空间上消耗大,在时间上消耗也大。
    另外,二叉树排序算法不是稳定排序。

二. 基于散列排序

  • 注意到前两篇文章说到存在一些排序算法能够突破基于比较的排序算法的时间复杂度下界 O(nlog2n) 。比如计数排序等。但是这些算法存在的一个致命缺点就是空间冗余过大,这对降低时间复杂度也起到了反作用。所以我们考虑不要使用过大的空间来存储,此时的问题就是不同的元素会占用一个空间的情况。所以考虑使用高级存储结构——散列来存储数据。
    散列定义了某种映射是输入数据能够映射到散列表中的某一块区域来进行存放。这种存储结构对于那种大输入集但实际输入数量又不大是非常有利的。散列解决冲突的方法有探查法(就是如果起冲突就以一定的规则去看其他位置是否有空位可以放这个元素)。探查法有三种,线性探查、二次探查和随机探查。线性探查从当前节点不断往后直到找到第一个空位就可以存放,这样的好处能够最大化的利用空间,坏处在于很容易造成数据堆积。二次探查是以 d,d+12,d+22,d+32... 的顺序进行。这样的好处在于数据不容易堆积,坏处在于有一些位置到不了。随机探查利用的是随机函数(注意:不能使用系统时间这样变化的参数作为随机种子,要保持随机种子的一致性)。随机探查则以d, random(d), random(random(d))的方式进行。这样的好处是数据进一步分散,缺点是如果剩余空间不大就比较难找到空节点。这些方法在基于数组的情况是好实现的,但是基于链表就很难实现。
    此时基于散列的排序算法就可以很好的解决冲突问题。基于散列来达到线性时间排序的方法统称为桶排序,基数排序是桶排序的特例。
    使用散列结构的关键,一是设定什么规则来对元素进行映射,二是如何处理好冲突。
    对于第一个关键点,我们需要使数据尽量分散,减少每个桶的数据量,但是又不能让桶的数量过于庞大。另外,我们最终的目的是为了排序,所以最好桶本身就是有序的。所以我们很容易想到一种方法就是以最高有效位的值作为地址,凡是最高有效位的值相同就会被扔到同一个桶里。
    对于第二个关键点,可以直接使用链式结构实现。桶内数据使用其他排序,如插入排序、分治排序等得到有序数列。

  • 时间复杂度分析
    桶排序两个主要耗时的环节为元素分配到桶里,二是对桶中的元素进行排序。分配一个元素到桶里的时间为O(1),因此前者的时间复杂度为O(n)。对于后者使用基于比较的排序,则时间复杂度为 O(nlog2n) 。对于最好情况,则每个桶里最多只有一个元素,则桶内排序几乎不需要时间,则总体时间复杂度为O(n)。最坏情况则将全部元素扔到同一个桶里,则时间复杂度为 O(nlog2n)+O(n) ,因此是 O(nlog2n) 的总体时间复杂度。对于平均情况,可以做如下分析。令T(n)为总时间复杂度,桶排序的时间复杂度表达式为

    T(n)=O(n)+i=0n1O(nilog2ni)

    对式子进行适当的两边放大为
    T(n)=O(n)+i=0n1O(n2i)

    通过对两边求期望得到
    E(T(n))=E[O(n)+i=0n1O(n2i)]

    利用期望的线性性质有
    E(T(n))=O(n)+i=0n1E[O(n2i)]

    上式可以化为
    E(T(n))=O(n)+i=0n1O(E[n2i])

    接下来分析可知道 E[n2i]=21n ,代入原式,可得到
    T(n)=O(n)+i=0n1O(n2i)=O(n)+n×O(21n)=O(n)

    则桶排序的平均时间复杂度为O(n)。

  • 空间复杂度分析
    空间复杂度可以认为是O(1),只是前面系数一定大于n。

  • 优劣及稳定性
    注意制定的那个映射规则一定要想办法将元素全部分散开来,不然时间复杂度会接近于最坏的时间复杂度,而且由于开散列表(创建桶的过程当中需要大量时间)也需要大量的时间,所以实际运行时占用时间会很长。
    桶排序的优势在于它确实突破了基于比较的排序算法的下界,达到了线性水平,同时也通过散列,弥补了计数排序算法上的一些不足。
    桶排序的问题在于未知输入数分布的情况下很难设计出一个很科学的映射规则使得数据尽可能分散开,同时占用空间虽然在空间复杂度上看不出来很大,但是实际运行时前面的系数非常大,也很耗费空间。

参考及代码

[1] 【数据结构】树(二):二叉树&二叉搜索树&平衡二叉树(C++实现)
[2] 桶排序实现(bucket_sort.cpp)

你可能感兴趣的:(课程,算法)