zoukankan      html  css  js  c++  java
  • 小小c#算法题

    在讨论堆排序之前,我们先来讨论一下另外一种排序算法——插入排序。插入排序的逻辑相当简单,先遍历一遍数组找到最小值,然后将这个最小值跟第一个元素交换。然后遍历第一个元素之后的n-1个元素,得到这n-1个元素中的最小值,即整个序列的次小值,将其跟第二个元素交换。接下来对后n-2个元素进行相同的操作,直到得到有序序列。

    很显然,插入排序的时间复杂度是O(n2)。在n个关键字中选出最小值,至少进行n-1次比较,然而,继续在剩余的n-1个关键字中选择次小值就并非一定要进行n-2次比较,若能利用前n-1次比较所得信息,则可以减少以后各趟选择排序中所用的比较次数。堆排序正是利用了之前比较信息的一种排序算法,从而提高了效率。

    鉴于篇幅和编辑的难度,这里不会非常详细的介绍堆排序的细节,如果想了解更多的话,可以看看数据结构的书籍或其他文章。

    堆排序是利用堆这种数据结构进行排序的一种算法。堆的定义如下:n个元素序列{k1,k2,...,kn}当且仅当满足以下关系时,称之为堆

    情况1:ki <= k(2i) && ki <= K(2i+1) 

    情况2:ki >= k(2i) && ki >= k(2i+1)

    满足情况1的我们称之为小顶堆,满足情况2的我们称之为大顶堆。

    若将此序列对应的一维数组(即以一维数组作此序列的存储结构)看成一个完全二叉树,则堆的定义表明,完全二叉树中所有终端结点的值均不大于(或不小于)其左右孩子结点的值。由此,堆顶元素必为n个元素序列中的最小值或最大值。

    若在输出堆顶的最小值(或最大值)之后,使得剩余n-1个元素的序列又建成一个堆,则得到n个元素中的次小值(或次大值)。如此反复执行,便能得到一个有序序列,这个过程称之为堆排序。

    那么现在就需要处理两个问题了:

    1. 怎么样由无序序列建成一个堆?

    2. 如何在输出堆顶元素之后,调整剩余元素成为一个新的堆?

    我们假设我们在使用大顶堆的情况。

    首先,我们先考虑问题2,通常把堆顶元素跟最后一个元素交换,这样就把最大值放到了序列的最后。现在我们要调整前n-1个元素成为一个新的堆。现在堆顶元素的左右子树都为堆,则仅需自上至下进行调整即可。首先以堆顶元素和其左、右子树根结点进行比较,将三者中最大的放到堆项:(a)如果堆顶本来就最大,不用交换,并且现在已经是一个堆了,因为左、右子树都是堆,调整可以退出了。(b)如果三者中最大的元素为左右子树根结点中的一个,则要和堆顶结点交换,被交换的子树根结点所在子树被破坏,不再是堆,所以又要进行相同过程的调整,如此往复,直至(a)的情形或叶子结点。我们称这个自堆顶至叶子的调整过程为“筛选”。如果有完全二叉树的图结合着看的话,效果会比较好。

    然后问题1,从一个无序序列建堆的过程就是一个反复“筛选”的过程。若将些序列看成是一个完全二叉树,则最后一个非终端结点是第n/2(下取整)个元素,由些“筛选”只需从第n/2(下取整)个元素开始,一直到要第一个元素,即树根,堆顶。

    下面是筛选的代码,即调整堆的过程:(注意在由于数组下标从0开始的,所以计算下标的时候要注意一下)

            static void HeapAdjust(int[] numbers, int index, int length)
            {
                for (int childIndex = 2 * index + 1; childIndex <= length; childIndex *= 2)
                {
                    if (childIndex < length && numbers[childIndex] < numbers[childIndex + 1])
                    {
                        // childIndex为两棵子树的根结点中较大的那个的下标
                        childIndex++;
                    }
    
                    if (numbers[index] >= numbers[childIndex])
                    {
                        // 如果堆顶已经为三者(目前堆顶元素,堆顶元素左子树的根结点,堆顶元素右子树的根结点)最大值,
                        // 则堆已调整好,可以结束了。
                        break;
                    }
    
                    // 如果堆顶不是三者(目前堆顶元素,堆顶元素左子树的根结点,堆顶元素右子树的根结点)最大值
                    // 则要进行交换
                    Switch<int>(ref numbers[index], ref numbers[childIndex]);
                }
            }
    
            static void Switch<T>(ref T a, ref T b)
            {
                T temp;
                temp = a;
                a = b;
                b = temp;
            }

    上面的方法中有一个可以优化的地方,即直到最后调整成为一个新堆的时候,才能确定原先的堆顶元素所在新的位置。比如,堆顶元素r和其一个孩子结点交换后,孩子结点所在的子树要进行新的调整,此时孩子结点所在的子树的根结点是r,r 又跟其一个孩子结点交换之后,新的堆构成了。那么,r的第一次交换其实就是可以优化的,只要事先保存了r的值,就不用交换了,只需为堆顶元素赋值即可,而不用把r的值再赋给其本来要交换的那个结点了。这个自己理解吧,和快速排序中的交换优化一模一样。所以代码可以优化为:

            static void HeapAdjust(int[] numbers, int index, int length)
            {
                int temp = numbers[index];
                for (int childIndex = 2 * index + 1; childIndex <= length; childIndex *= 2)
                {
                    if (childIndex < length && numbers[childIndex] < numbers[childIndex + 1])
                    {
                        childIndex++;
                    }
    
                    if (temp >= numbers[childIndex])
                    {
                        break;
                    }
    
                    numbers[index] = numbers[childIndex];
                    index = childIndex;
                }
    
                numbers[index] = temp;
            }


    调整的代码完成之后,下面是堆排序的代码,这里我没有把由无序序列构建堆的过程封装到另一个方法里面,而是直接写了,你如果想另写一个方法的话,当然可以了。

            static void HeapSort(int[] numbers)
            {
                // 得到大顶堆
                for (int i = numbers.Length / 2 - 1; i >= 0; i--)
                {
                    HeapAdjust(numbers, i, numbers.Length - 1);
                }
    
                // 开始堆排序
                // 1. 即将堆顶元素(最大值)跟最后一个元素交换,此时最大元素已经就绪,放到了最后
                // 2. 现在只需要关注前n-1个结点就可了,由于上一步将取后一个元素放到了根结点,所以前n-1个结点不再是大顶堆了,
                //    所以现在要调整堆为一个大顶堆,即筛选
                // 3. 一次筛选完成之后把堆顶元素再和最后一个交换,次大数就绪
                // 4. 循环这个过程,最终得到有序序列
                int temp;
                for (int i = numbers.Length - 1; i > 0; )
                {
                    temp = numbers[i];
                    numbers[i] = numbers[0];
                    numbers[0] = temp;
                    i--;
                    HeapAdjust(numbers, 0, i);
                }
            }

    下面是一个调用堆排序并输出排序结果的例子:

            static void Main(string[] args)
            {
                int[] numbers = { 49, 38, 65, 97, 76, 13, 27, 49 };
                HeapSort(numbers);
                
                foreach (int i in numbers)
                {
                    Console.Write(i.ToString() + " ");
                }
    
    Console.Read(); }

    最后,堆排序是一种不稳定的排序算法。时间复杂度为O(n*logn),只需一个记录大小的辅助空间,即空间复杂度为O(1)。堆排序方法对记录数较少的文件并不值得提倡,但对于n比较大的文件还是很有效的。

  • 相关阅读:
    并发之线程封闭与ThreadLocal解析
    并发之不可变对象
    开发者
    并发之atomicInteger与CAS机制
    并发之synchronized关键字的应用
    并发之volatile关键字
    并发研究之可见性、有序性、原子性
    并发研究之Java内存模型(Java Memory Model)
    并发研究之CPU缓存一致性协议(MESI)
    线程安全的日期处理
  • 原文地址:https://www.cnblogs.com/CSharpSPF/p/3181114.html
Copyright © 2011-2022 走看看