微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

简单选择排序、堆排序详解及C++代码详细实现

选择排序

选择排序的基本思想是:每一趟(如第 i i i 趟)在后面 n − i + 1 ( i = 1 , 2 , … , n − 1 ) n-i+1 \quad (i=1,2,\dots,n-1) ni+1(i=1,2,,n1) 个待排序元素中选取关键字最小的元素,作为有序子序列的第 i i i 个元素,直到第 n − 1 n-1 n1 趟做完,待排序元素只剩下1个,就不用再选了。

简单选择排序

根据上面选择排序的思想,可以很直观地得岀简单选择排序算法的思想:假设排序表为L[1..n],第 i i i 趟排序即从L[i..n]中选择关键字最小的元素与L(i)交换,每一趟排序可以确定一个元素的最终位置,这样经过 n − 1 n-1 n1 趟排序就可使得整个排序表有序。

简单选择排序算法的代码如下:

void SelectSort(int *A, int n) {
    for (int i = 0; i < n - 1; i++) {   //一共进行n-1趟
        int min = i;    //记录最小元素位置
        for (int k = i + 1; k < n; k++)   //在A[i...n-1]中选择最小的元素
            if (A[k] < A[min])
                min = k;    //更新最小元素位置

        if (min != i) {     //交换
            int temp = A[min];
            A[min] = A[i];
            A[i] = temp;
        }
    }
}

简单选择排序算法的性能分析如下:

空间效率:仅使用常数个辅助单元,故空间效率为 O ( 1 ) O(1) O(1)

时间效率:从上述伪码中不难看出,在简单选择排序过程中,元素移动的操作次数很少,不会超过 3 ( n − 1 ) 3(n-1) 3(n1) 次,最好的情况是移动0次,此时对应的表已经有序;但元素间比较的次数与序列的初始状态无关,始终是 n ( n − 1 ) / 2 n(n-1)/2 n(n1)/2 次,因此时间复杂度始终是 O ( n 2 ) O(n^2) O(n2)

稳定性:在第 i i i 趟找到最小元素后,和第 i i i 个元素交换,可能会导致第 i i i 个元素与其含有相同关键字元素的相对位置发生改变。例如,表L={2, 2, 1},经过一趟排序后L={1, 2, 2},最终排序序列也是L={1, 2, 2},显然,2与2的相对次序已发生变化。因此,简单选择排序是一种不稳定的排序方法

堆排序

堆的定义如下, n n n 个关键字序列L[1...n]称为堆,当且仅当该序列满足:
1)L(i)>=L(2i)L(i)>=L(2i+1)
2)L(i)<=L(2i)L(i)<=L(2i+1) ( 1 ≤ i ≤ ⌊ n / 2 ⌋ ) (1 \le i \le \left \lfloor n/2 \right \rfloor ) (1in/2)

可以将该一维数组视为一棵完全二叉树,满足条件1的堆称为大根堆(大顶堆),大根堆的最大元素存放在根结点,且其任一非根结点的值小于等于其双亲结点值。满足条件2的堆称为小根堆(小顶堆),小根堆的定义刚好相反,根结点是最小元素。下图所示为一个大根堆。

image-20220815184327726

一个大根堆示意图

堆排序的思路很简单:首先将存放在L[1...n]中的 n n n 个元素建成初始堆,由于堆本身的特点(以大顶堆为例),堆顶元素就是最大值。输出堆顶元素后,通常将堆底元素送入堆顶,此时根结点已不满足大顶堆的性质,堆被破坏,将堆顶元素向下调整使其继续保持大顶堆的性质,再输出堆顶元素。如此重复,直到堆中仅剩一个元素为止。

可见堆排序需要解决两个问题:
1)如何将无序序列构造成初始堆
2)输出堆顶元素后,如何将剩余元素调整成新的堆?

堆排序的关键是构造初始堆。 n n n 个结点的完全二叉树,最后一个结点是第 ⌊ n / 2 ⌋ \left \lfloor n/2 \right \rfloor n/2 个结点的孩子。对第 ⌊ n / 2 ⌋ \left \lfloor n/2 \right \rfloor n/2 个结点为根的子树筛选(对于大根堆,若根结点的关键字小于左右孩子中关键字较大者,则交换),使该子树成为堆。之后向前依次对各结点( ⌊ n / 2 ⌋ − 1 ∼ 1 \left \lfloor n/2 \right \rfloor -1 \sim 1 n/211 )为根的子树进行筛选,看该结点值是否大于其左右子结点的值,若不大于,则将左右子结点中的较大值与之交换,交换后可能会破坏下一级的堆,于是继续采用上述方法构造下一级的堆,直到以该结点为根的子树构成堆为止。反复利用上述调整堆的方法建堆,直到根结点。

如下图所示:
1)初始时调整L(4)子树,09<32,交换,交换后满足堆的定义;
2)向前继续调整L(3)子树,78 <左右孩子的较大者87,交换,交换后满足堆的定义;
3)向前调整L(2)子树,17 <左右孩子的较大者45,交换后满足堆的定义;
4)向前调整至根结点L(1) ,53 <左右孩子的较大者87,交换,交换后破坏了L(3)子树的堆,采用上述方法L(3)进行调整,53<左右孩子的较大者78,交换,至此该完全二叉树满足堆的定义。

image-20220815190946937

自下往上逐步调整为大根堆

输出堆顶元素后,将堆的最后一个元素与堆顶元素交换,此时堆的性质被破坏,需要向下进行筛选。将09和左右孩子的较大者78交换,交换后破坏了L(3)子树的堆,继续对L(3)子树向下筛选,将09和左右孩子的较大者65交换,交换后得到了新堆,调整过程如下图所示。

image-20220816110351324

输出堆顶元素后再将剩余元素调整成新堆

下面是建立大根堆的算法:

void HeadAdjust(int *A, int k, int len) {
    //函数HeadAdjust将元素k为根的子树进行调整
    A[0] = A[k];    //A[0]暂存子树的根结点
    for (int i = 2 * k; i < len; i *= 2) {  //沿key较大的子结点向下筛选
        if (i < len && A[i] < A[i + 1])
            i++;    //取key较大的子结点的下标

        if (A[0] > A[i])
            break;  //筛选结束
        else {
            A[k] = A[i];  //将A[i]调整到双亲结点上
            k = i;        //修改k值,以便继续向下筛选
        }
    }
    A[k] = A[0];    //被筛选结点的值放入最终位置
}

void BuildMaxHeap(int *A, int len) {
    for (int i = len / 2; i > 0; i--)
        HeadAdjust(A, i, len);  //从i=[n/2]〜1,反复调整堆
}

调整的时间与树高有关,为 O ( h ) O(h) O(h) 。在建含 n n n 个元素的堆时,关键字的比较总次数不超过 4 n 4n 4n,时间复杂度为 O ( n ) O(n) O(n),这说明可以在线性时间内将一个无序数组建成一个堆。

下面是堆排序算法:

void HeapSort(int *A, int len) {
    BuildMaxHeap(A, len);   //初始建堆
    for (int i = len; i > 1; i--) {     //n-1趟的交换和建堆过程
        cout << A[1] << "\t";   //输出堆顶元素

        int temp = A[1];    //堆顶堆底交换
        A[1] = A[i];
        A[i] = temp;

        HeadAdjust(A, 1, i - 1);  //调整,把剩余的i-1个元素整理成堆
    }
    cout << A[1] << "\t";   //输出最后一个堆顶元素
}

同时,堆也支持插入操作。对堆进行插入操作时,先将新结点放在堆的末端,再对这个新结点向上执行调整操作。大根堆的插入操作示例如下图所示。

image-20220816190527273

大根堆的插入操作示例

堆的插入操作代码如下:

void HeapInsert(int *A, int len, int element) {
    A[len + 1] = element;   //将新结点放在堆的末端

    //从这个新结点开始向上调整堆
    int i = len + 1;
    while (i > 1) {
        if (A[i] > A[i / 2]) {  //和双亲结点交换
            int temp = A[i];
            A[i] = A[i / 2];
            A[i / 2] = temp;
        } else
            break;   //筛选结束
        i /= 2;
    }
}

堆排序适合关键字较多的情况。例如,在1亿个数中选出前100个最大值?首先使用一个大小为100的数组,读入前100个数,建立小顶堆,而后依次读入余下的数,若小于堆顶则舍弃, 否则用该数取代堆顶并重新调整堆,待数据读取完毕,堆中100个数即为所求。

堆排序算法的性能分析如下:

空间效率:仅使用了常数个辅助单元,所以空间复杂度为 O ( 1 ) O(1) O(1)

时间效率:建堆时间为 O ( n ) O(n) O(n) ,之后有 n − 1 n-1 n1 次向下调整操作,每次调整的时间复杂度为 O ( h ) O(h) O(h) ,故在最好、最坏和平均情况下,堆排序的时间复杂度为 O ( n log ⁡ 2 n ) O(n\log_{2}{n}) O(nlog2n)

稳定性:进行筛选时,有可能把后面相同关键字的元素调整到前面,所以堆排序算法是一种不稳定的排序方法。例如,表L={1, 2, 2},构造初始堆时可能将2交换到堆顶,此时L={2, 1, 2},最终排序序列为L={1, 2, 2},显然,2与2的相对次序已发生变化。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐