小小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比较大的文件还是很有效的。

原文地址:https://www.cnblogs.com/CSharpSPF/p/3181114.html