堆排序

堆排序

堆的定义

  堆是一种特殊的树形数据结构,每个结点都有一个值.通常我们所说的堆的数据结构,是指二叉堆.堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。堆排序是选择排序的一种,可以利用数组的特点快速定位指定索引的元素。堆分为大根堆和小根堆,是完全二叉树当父结点的键值总是大于或等于任何一个子节点的键值时为最大堆。当父结点的键值总是小于或等于任何一个子节点的键值时为最小堆。

堆的存储

  一般都用数组来表示堆,我们用长度为N+1的数组来表示一个大小为N的堆,我们不会使用数组的第一个位置,这么做的目的是为了便于定位指定位置结点的父节点与子结点的位置,这样我们可以很容易的知道在一个堆中,位置K的结点的父节点的位置为K/2,而它的两个子节点的位置则分别为2K和2K+1。

堆排序的实现

  我们可以利用根结点为最大(或最小)的性质,来实现堆排序,每次取出根结点后,对剩下的数据重新构造堆,一直到最后一个元素,所以我们需要解决两个问题
  1.如果将一个序列构建成堆
  2.输出堆顶元素之后,如果将剩下的元素构建成一个新堆
  我们需要先建立一个堆,而建堆的核心内容是调整堆,使二叉树满足堆的定义,调堆的过程应该从最后一个非叶子节点开始,假设有数组A = {2, 3, 8, 5, 7, 1, 6, 4, 9}
调堆的过程应该从最后一个非叶子节点开始根据 ,我们知道最后一个非叶子结点为N/2向下取整,假设数组下标从1开始 最后一个非叶子结点为 A[4]=5,分别与左孩子和右孩子比较大小,如果为最小则不用调整,否则和最小的叶子结点进行交换,我们知道位置K的结点的父节点的位置为K/2,而它的两个子节点的位置则分别为2K和2K+1 两个叶子结点分别为 A[8]=4,A[9]=9, 由于A[8]<A[4]<A[9] 所以A[4]和A[8]  建堆的过程如下图

  堆创建好之后,就可以进行第二步操作了,堆排序是一种选择排序,排序开始,首先输出堆顶元素,将堆顶元素和最后一个元素交换,这样,第n个位置(即最后一个位置)作为有序区,前n-1个位置仍是无序区,对无序区进行调整,得到堆之后,再交换堆顶和最后一个元素,这样有序区长度变为2。。。不断进行此操作,将剩下的元素重新调整为堆,然后输出堆顶元素到有序区。每次交换都导致无序区-1,有序区+1。不断重复此过程直到有序区长度增长为n-1,排序完成。

代码如下

        /// <summary>
        /// 构建堆
        /// </summary>
        /// <param name="array"></param>
        /// <param name="hLen"></param>
        void BuildHeap(int[] array, int hLen)
        {
            int i;
            int begin = hLen / 2 - 1 ;  //最后一个非叶子节点
            for (i = begin; i >= 0; i--)
            {
                AdjustHeap(array, hLen, i);
            }
        }

        /// <summary>
        /// 排序
        /// </summary>
        /// <param name="array"></param>
        void HeapSort(int[] array)
        {
            int hLen = array.Length;
            int temp;
            BuildHeap(array, hLen);      //建堆
            while (hLen > 1)
            {
                temp = array[hLen - 1];    //交换堆的第一个元素和堆的最后一个元素
                array[hLen - 1] = array[0];
                array[0] = temp;
                hLen--;        //堆的大小减一
                AdjustHeap(array, hLen, 0);  //调堆
            }
        }

        /// <summary>
        /// 调整堆
        /// </summary>
        /// <param name="array"></param>
        /// <param name="hLen">堆的长度</param>
        /// <param name="i">需要调整的节点</param>
        void AdjustHeap(int[] array, int hLen, int i)
        {
            int left = 2 * i + 1;  //节点i的左孩子
            int right = left + 1; //节点i的右孩子节点
            int min = i;
            int temp;
            while (left < hLen || right < hLen)
            {
                if (left < hLen && array[min] > array[left])
                {
                    min = left;
                }
                if (right < hLen && array[min] > array[right])
                {
                    min = right;
                }
                if (i != min) //最小结点发生变化 进行交换
                {
                    temp = array[min];
                    array[min] = array[i];
                    array[i] = temp;
                    i = min;          //调整交换后的节点 
                    left = 2 * i +1 ;
                    right = left + 1;
                }
                else
                {
                    break;
                }
            }
        }

堆排序分析

  再来回顾下推排序的过程
  第一步建堆,建堆是不断调整堆的过程,从len/2处开始调整,一直到第一个节点,建堆的过程是线性的过程,从len/2到1处一直调用调整堆的过程,相当于o(h1)+o(h2)…+o(hlen/2) 其中h表示节点的深度,len/2表示节点的个数,这是一个求和的过程,结果是线性的O(n)。
  第二步调整堆:调整堆在构建堆的过程中会用到,而且在堆排序过程中也会用到。利用的思想是比较节点i和它的孩子节点left(i),right(i),选出三者最大(或者最小)者,如果最大(小)值不是节点i而是它的一个孩子节点,则交换节点i和该节点,然后再调用调整堆过程,这是一个递归的过程。调整堆的过程时间复杂度与堆的深度有关系,是lgn的操作,因为是沿着深度方向进行调整的。
  第三步堆排序:堆排序是利用上面的两个过程来进行的。首先是根据元素构建堆。然后将堆的根节点取出(一般是与最后一个节点进行交换),将前面len-1个节点继续进行堆调整的过程,然后再将根节点取出,这样一直到所有节点都取出。堆排序过程的时间复杂度是O(nlogn)。因为建堆的时间复杂度是O(n)(调用一次);调整堆的时间复杂度是lgn,调用了n-1次,所以堆排序的时间复杂度是 O(nlogn)
  堆排序方法对记录数较少的文件并不值得提倡,但对n较大的文件还是很有效的。因为其运行时间主要耗费在建初始堆和调整建新堆时进行的反复“筛选”上,堆排序在最坏的情况下,其时间复杂度也为O(nlogn)。相对于快速排序来说,这是堆排序的最大优点。此外,堆排序仅需一个记录大小的供交换用的辅助存储空间

原文地址:https://www.cnblogs.com/zhaodayou/p/6895000.html