堆排序

(1)堆排序(heap sort)是简单选择排序的一种改进,改进的着眼点是:如何减少关键码的比较次数。简单选择排序在一趟排序中仅选出最小关键码,没有把一趟比较结果保存下来,因而记录的比较次数较多。堆排序在选出最小关键码的同时,也找出较小关键码,减少了在后面的选择中的比较次数,从而提高了整个排序的效率。堆排序为不稳定排序,不适合记录较少的排序。

(2)堆是一种特殊的树形数据结构,其每个节点都有一个值,通常提到的堆都是指一颗完全二叉树,根节点的值小于等于(或大于等于)两个子节点的值,同时根节点的两个子树也分别是一个堆。(二叉堆,一般简称为堆)

(3)堆的具体定义如下:
第一种:如果将堆按层序从1开始编号
n个元素的序列{k1,k2,…,kn}当且仅当满足下列关系之一时,称之为堆。
情形1:ki <= k2i 且ki <= k(2i+1) (小顶堆)
情形2:ki >= k2i 且ki >= k(2i+1) (大顶堆)
其中i=1, 2, …, n/2 向下取整; (i结点的父结点下标为 i / 2)

第二种:如果将堆按层序从0开始编号
n个元素的序列{k0, k1, …, k(n-1)}当且仅当满足下列关系之一时,称之为堆。
情形1:ki <= k(2i+1) 且ki <= k(2i+2) (小顶堆)
情形2:ki >= k(2i+1) 且ki >= k(2i+2) (大顶堆)
其中i=0,1,2,…, (n-2)/2向下取整; (i结点的父结点下标为 (i – 1) / 2)

(4)堆排序是利用堆的性质进行的一种选择排序,在排序过程中,将 K[1, … , N]看成是一颗完全二叉树的顺序存储结构,一般用数组来表示堆,若根结点存在序号0处, i结点的父结点下标就为(i-1)/2。i结点的左右子结点下标分别为2 * i + 1和2 * i + 2。如第0个结点左右子结点下标分别为1和2。(注:如果根结点是从1开始,则左右孩子结点分别是2i和2i+1。)

(5)堆排序的思想(以大顶堆为例)
<1> 将初始待排序关键字序列(K0,K1….K(n-1))构建成大顶堆,此堆为初始的无序区;
<2> 将堆顶元素K[0]与最后一个元素K[n-1]交换,此时得到新的无序区(K0,K1,……Kn-2)和新的有序区(K(n-1)),且满足K[0,1…n-2] <= K[n-1];
<3> 由于交换后新的堆顶K[0]可能违反堆的性质,因此需要对当前无序区(K0,K1,……Kn-2)调整为新堆,然后再次将K[0]与无序区最后一个元素交换,得到新的无序区(K0,K1….Kn-3)和新的有序区(K(n-2), K(n-1))。不断重复此过程直到有序区的元素个数为n-1(即无序区只剩一个元素时),则整个排序过程完成。

操作过程如下:
<1> 初始化堆:将K[0… (n-1)]构造为堆;
<2> 将当前无序区的堆顶元素K[0]同该区间的最后一个记录交换,然后将新的无序区调整为新的堆。
因此对于堆排序,最重要的两个操作就是构造初始堆调整堆,其实构造初始堆事实上也是调整堆的过程,只不过构造初始堆是对所有的非叶节点都进行调整。

(6)如何在输出堆顶元素到有序区后,调整剩余元素成为一个新的堆?
调整堆的过程中,总是将根节点(即被调整节点)与左右孩子节点进行比较。若不满足堆的条件,则将根节点与左右孩子节点的较大者进行交换,这个调整过程一直进行到所有子树均为堆或将被调整节点交换到叶子节点为止。这个自堆顶至叶子节点的调整过程称为“筛选”。

(7)如何构造初始堆
从一个无序序列建堆的过程就是一个反复“筛选”的过程。首先将待排序数组元素构建一个完全二叉树(即把无序数组元素看成是一个完全二叉树的顺序存储),则所有的叶子节点都已经是堆,所以从最后一个非叶节点开始调整,执行上述“筛选”过程,直到根节点。(初始化堆的过程就是对所有的非叶子节点进行筛选的过程)
备注:
<1> 如果将堆按层序从0开始编号,最后一个非叶节点的下标是(n-2)/2(向下取整)。
<2> 如果将堆按层序从1开始编号,最后一个非叶节点的下标是n/2(向下取整)。

具体实现如下,代码中有详细注释:

#include <iostream>

void swap(int &a, int &b)
{
    int temp;
    temp = a;
    a = b;
    b = temp;
}


/**
 *  该函数用于“调整堆”,”初始构建堆“时也用到该函数;
 *
 *  @param arr 待排序的数组;
 *  @param pos 当前要调整位置的节点在数组中的下标,pos位置对应的节点一定是非叶节点;
 *  @param len 当前未排序的序列中,最后一个元素所对应的数组下标,注意,len 不是当前未排序的数组长度;
 */
void AdjustMaxHeap(int arr[], int pos, int len)
{
    int temp; // 存储当前待调整的节点(首先,该待调整的节点必须是非叶节点)
    int child;
    for (temp = arr[pos]; 2 * pos + 1 <= len; pos = child) // 如果当前节点的pos满足 2*pos+1 <= len,说明当前pos位置对应的节点是非叶子节点,这也是“筛选”的前提;
    {
        child = 2 * pos + 1; // 取出“当前pos位置对应节点”的“左孩子节点的下标”;
        if (child < len && arr[child] < arr[child + 1]) // 若child<len说明了当前节点的左/右孩子节点同时存在(因为未排序数组中的最后一个节点对应的下标是len,只有当child=len时,“当前节点”才是只有左孩子节点),注意len的特点,len并不是数组长度,而是未排序数组中最后一个元素对应的下标。若此时”左孩子节点 < 右孩子节点“,则让clild++,此时child的位置对应的是当前节点的右孩子节点,即保证让child位置处的节点是“左、右孩子节点中较大的那一个”;
            child++;
        if (arr[child] > temp)  // 若上面的if判断成立,那么比较“此时child位置处对应的节点”和“存入temp中的当前节点”的大小;若上面的if判断不成立,说明当前节点只有左孩子,或者”左孩子节点 > 右孩子节点“,同样也是比较“此时child位置处对应的节点”和“存入temp中的当前节点”的大小;
            arr[pos] = arr[child]; // 若此时child位置处对应的节点大,那么将“此时child位置处对应的节点”放入原pos位置处,并将此时child的位置作为下次循环时的pos位置(即pos = child);
        else
            break; // 若上述2if判断都不成立,说明当前节点和其左右孩子构成的子树是一个堆,当前节点的位置本次不需要调整。
    }
    arr[pos] = temp; // 将存储在temp中的当前要调整的节点值,放在“跳出上面循环时的pos位置处(pos=child)”,此时的pos可能发生了变化,也可能没发生变化(比如上面的for循环中,直接执行else处的break语句时);
}


void MyMaxHeapSort(int arr[], int len)
{
    int i;
    for (i = len/2 - 1; i >= 0; i--) // 初始构建堆;将堆按层序从0开始编号,最后一个非叶节点的下标是(n/2 - 1)(向下取整)。
        AdjustMaxHeap(arr, i, len - 1); // 初始构建堆的过程中,假设数组元素一直都是无序的,所以此处使用 len - 1for (i = len - 1; i > 0; i--) // 初始构建堆后,len个数据,通过(len-1)次调整,可得到最终的排序结果;
    {
        swap(arr[0], arr[i]); // 交换堆顶元素和“未排序序列的最后一个元素”
        AdjustMaxHeap(arr, 0, i-1); // 交换后,对“未排序序列”重新调整,形成新的堆;
    }
}



int main(int argc, const char * argv[]) {

    int arr[] = {2,5,8,1,3,6,9,0,4,7};
    int len = sizeof(arr)/sizeof(int);

    MyMaxHeapSort(arr, len);

    for (int i = 0; i < len; i++)
        printf("%d ", arr[i]);

    printf("\n");

    return 0;
}

补充说明:
(1) if (child < len && arr[child] < arr[child + 1]) // 1、将第二个“<” 改为 “>”;
(2)if (arr[child] > temp) // 2、将 “>” 改为 “<”;
注意:将1、2 两处同时作出修改,得到的就是小顶堆排序;(降序)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值