当前位置: 首页 > news >正文

数据结构第八章(三)-选择排序

数据结构第八章(三)

  • 选择排序
  • 一、简单选择排序
    • 1.算法过程
    • 2.性能
  • 二、堆排序
    • 1.堆(Heap)
    • 2.堆的建立
    • 3.堆排序
    • 4.性能
  • 三、堆的插入和删除
    • 1.插入
    • 2.删除
  • 总结


选择排序


选择排序,人如其名,就是选择一个然后放前面排序。

再简单来说,就是在最开始画一条线,每次都在线后面的元素中找到最小或者最大的放前面,放一个后挪一格,那么线前面的就都是有序的。这个线一个一个往后挪,直到挪到最后,线后面没东西了,我们的排序也完成了。

选择排序:每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列。

这就是选择排序。

一、简单选择排序

1.算法过程

用一个栗子来说简单选择排序,我们的待排序元素如下:

49(1)38659776132749(2)
01234567

我们每一趟在待排序元素中选取关键字最小的元素加入有序子序列

我们肯定还是需要用到辅助指针,一个指针 i,一个指针 j。指针 i 从位置 1 开始,指针 j 遍历指针 i 后面的元素,找到最小的之后和指针 i 所在位置的元素进行交换。

第1趟

指针 i = 0 指向元素”49“,指针 j 遍历指针 i 后面的元素,从 位置 1 开始 往后遍历,用一个临时变量min记录最小位置的下标,遇到比此时的 j 指向元素更小的就更新 min

遍历完之后我们发现 min 是 5,指向的元素为 “13”,我们再看它和指针 i
指向的元素相不相同,不相同就换,相同就不换。

发现不同,交换指针 i 和 指针 min 指向的元素:

133865977649(1)2749(2)
01234567

第2趟

我们指针 i 往后挪现在为 1 ,指针 i 之前的元素都是找到其最终位置的(排好序的),所以不用再管了。 现在的待排序元素是“1~7”,指针 i 及 i 之后的部分

指针 i = 1 指向元素”38“,指针 j 遍历指针 i 后面的元素,从 位置 2 开始 往后遍历,用一个临时变量min记录最小位置的下标,遇到比此时的 j 指向元素更小的就更新 min

遍历完之后我们发现 min 是 6,指向的元素为 “27”,我们再看它和指针 i
指向的元素相不相同,不相同就换,相同就不换。

发现不同,交换指针 i 和 指针 min 指向的元素:

1327 65977649(1)3849(2)
01234567

第3趟

我们指针 i 往后挪现在为 2 ,指针 i 之前的元素都是找到其最终位置的(排好序的),所以不用再管了。 现在的待排序元素是“2~7”,指针 i 及 i 之后的部分

指针 i = 2 指向元素”65“,指针 j 遍历指针 i 后面的元素,从 位置 3 开始 往后遍历,用一个临时变量min记录最小位置的下标,遇到比此时的 j 指向元素更小的就更新 min

遍历完之后我们发现 min 是 6,指向的元素为 “38”,我们再看它和指针 i
指向的元素相不相同,不相同就换,相同就不换。

发现不同,交换指针 i 和 指针 min 指向的元素:

1327 38 977649(1)6549(2)
01234567

第4趟

我们指针 i 往后挪现在为 3 ,指针 i 之前的元素都是找到其最终位置的(排好序的),所以不用再管了。 现在的待排序元素是“3~7”,指针 i 及 i 之后的部分

指针 i = 3 指向元素”97“,指针 j 遍历指针 i 后面的元素,从 位置 4 开始 往后遍历,用一个临时变量min记录最小位置的下标,遇到比此时的 j 指向元素更小的就更新 min

遍历完之后我们发现 min 是 5,指向的元素为 “49”,我们再看它和指针 i
指向的元素相不相同,不相同就换,相同就不换。

发现不同,交换指针 i 和 指针 min 指向的元素:

13273849(1) 76 976549(2)
01234567

第5趟

我们指针 i 往后挪现在为 4 ,指针 i 之前的元素都是找到其最终位置的(排好序的),所以不用再管了。 现在的待排序元素是“4~7”,指针 i 及 i 之后的部分

指针 i = 4 指向元素”76“,指针 j 遍历指针 i 后面的元素,从 位置 5 开始 往后遍历,用一个临时变量min记录最小位置的下标,遇到比此时的 j 指向元素更小的就更新 min

遍历完之后我们发现 min 是 7,指向的元素为 “49”,我们再看它和指针 i
指向的元素相不相同,不相同就换,相同就不换。

发现不同,交换指针 i 和 指针 min 指向的元素:

13273849(1)49(2) 9765 76
01234567

第6趟

我们指针 i 往后挪现在为 5 ,指针 i 之前的元素都是找到其最终位置的(排好序的),所以不用再管了。 现在的待排序元素是“5~7”,指针 i 及 i 之后的部分

指针 i = 5 指向元素”97“,指针 j 遍历指针 i 后面的元素,从 位置 6 开始 往后遍历,用一个临时变量min记录最小位置的下标,遇到比此时的 j 指向元素更小的就更新 min

遍历完之后我们发现 min 是 6,指向的元素为 “65”,我们再看它和指针 i
指向的元素相不相同,不相同就换,相同就不换。

发现不同,交换指针 i 和 指针 min 指向的元素:

13273849(1)49(2)65 9776
01234567

第7趟

我们指针 i 往后挪现在为 6 ,指针 i 之前的元素都是找到其最终位置的(排好序的),所以不用再管了。 现在的待排序元素是“6~7”,指针 i 及 i 之后的部分

指针 i = 6 指向元素”97“,指针 j 遍历指针 i 后面的元素,从 位置 7 开始 往后遍历,用一个临时变量min记录最小位置的下标,遇到比此时的 j 指向元素更小的就更新 min

遍历完之后我们发现 min 是 7,指向的元素为 “76”,我们再看它和指针 i
指向的元素相不相同,不相同就换,相同就不换。

发现不同,交换指针 i 和 指针 min 指向的元素:

13273849(1)49(2)6576 97
01234567

现在待排序元素只剩最后一个了,不用再处理了

所以我们发现,一共8个元素,第7趟排序就已经有序排完了

so n 个元素的简单选择排序需要 n-1 趟处理

刚刚我说的就是按照算法实现来说的,所以上代码就会发现异常清晰:

//交换
void swap(int &a, int &b){int temp = a;a = b;b = temp;
}//简单选择排序
void selectSort(int A[],int n){for(int i = 0; i < n-1; i++){       //一共进行 n-1 趟int min = 1;                    //记录最小元素位置for(int j = i+1; j<n; j++){     //在A[i……n-1]中选择最小的元素if(A[j] < A[min]){          //更新元素最小位置min = j;}}if(min != i){swap(A[i], A[min]);         //交换,封装的swap共移动元素3次}}
}

是吧。

2.性能

空间复杂度不用说了,显然是O(1)

那我们的时间复杂度该怎么说?

首先这个简单选择排序,

不论是有序、逆序还是乱序,都一定需要 n-1 趟处理

每一趟可能交换也可能不交换,所以元素总交换次数 ≤ n-1

总共需要对比关键字 (n-1) + (n-2)+ …… + 1 = n(n-1)/2 次

so 时间复杂度 = O(n2)

那它的稳定性如何?

比如我们的待排序序列为:2(1) 2(2) 1

那第1趟排序结束 肯定把最小的“1”换到最前面,所以就变成了 1 2(2) 2(1)

再进行第2趟排序,不是比指针 i 指向小的就不交换
所以最终排序结果为 1 2(2) 2(1)

简单选择排序是不稳定的

简单选择排序既可以适用于顺序表,也可以适用于链表。

二、堆排序

还记得我们选择排序是什么吗?

选择排序:每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列。

我们堆排序也是一种选择排序。

1.堆(Heap)

首先我们要堆排序,就得直到什么是“堆”。

若 n 个关键字序列L[1……n] 满足下面某一条性质,则成为堆(Heap)

  • 若满足: L(i) ≥ L(2i) 且 L(i) ≥ L(2i+1) ( 1 ≤ i ≤ n/2)—— 大根堆(大顶堆)
  • 若满足: L(i) ≤ L(2i) 且 L(i) ≤ L(2i+1) ( 1 ≤ i ≤ n/2)—— 小根堆(小顶堆)

//图1

那么这个显然比较抽象,这是存储视角。我们怎么理解呢?其实理解成一棵树就好了,叶就是二叉树顺序存储。

大根堆就是所有双亲结点都比左、右孩子大的树,小根堆就是所有双亲结点都比左、右孩子小的树

逻辑视角如下:

//图4

//图5

还记得我们的二叉树顺序存储吗?回顾一下

基本操作:

  • i 的左孩子下标:2i
  • i 的右孩子下标:2i+1
  • i 的父结点下标:⌊i/2⌋
  • i 所在的层次:⌈log2(n+1)⌉ 或 ⌊log2n⌋+1

若完全二叉树中共有n个结点,则

  • 判断 i 是否有左孩子:看 2i ≤ n成不成立
  • 判断 i 是否有右孩子:看 2i+1 ≤ n成不成立
  • 判断 i 是否是叶子/分支结点:看 i > ⌊n/2⌋ 成不成立

比较直观的就是打比方这个完全二叉树:

//图2

在顺序表中就是这样:

//图3

其中红色是根结点,绿色部分是叶子结点,红色和蓝色部分是非叶子结点。

我们要想找到非叶子结点,只需要找下标 i ≤ ⌊n/2⌋

回到正题。什么是堆?简单来说,就是

大根堆:完全二叉树中,根≥左、右;
小根堆:完全二叉树中,根≤左、右

多说一嘴,我们二叉排序树BST是左<根<右,堆和这个还是很不一样的。

那么我们知道什么是“堆”了,如果给我们一个“堆”,我们怎么进行选择排序?

选择排序不是每一趟都在待排序元素中选取关键字最小(或最大)的元素加入有序子序列吗?

那么我们有了“堆”之后,由于“堆”本身的性质,所以堆顶的元素一定是关键字最大(最小)的,所以待排序元素如果是一个“堆”,直接取堆顶元素就可以了,这样就非常地方便又快捷。

问题不会消失,只会转移,所以我们的问题现在就变成了怎么把待排序元素变成一个“堆”,也就是堆的建立。

2.堆的建立

我们还用一个栗子来讲,描述建立“大根堆”(根 ≥ 左、右)的过程。

首先给出我们的初始序列,及按照初始序列作为二叉树结点的顺序存放给出的二叉树如下:

//图6

这显然不是一个大根堆。

所以我们就得 自底向上 把所有非终端结点都检查一遍,看是否满足大根堆的要求,如果不满足,则进行调整

怎么调整?

我们知道“大根堆”肯定是满足 根≥左、右 的,不满足那就是根没有左右孩子大,所以我们就直接找到左、右孩子里面最大的那个,然后和根换就完了呗。

就是这样。

so我们上面也说了,要想找到非叶子结点,只需要找下标 i ≤ ⌊n/2⌋ 就可以了,所以这个栗子就是下标从 1~⌊8/2⌋=4 的结点

就这四个结点。

那么我们开始吧!

自底向上调整,先看下标为“4”的结点:

//图7

检查当前结点是否满足 根≥左、右,若不满足,将当前结点与更大的一个孩子互换。

显然下标为“4”的结点“9”不满足我们的要求,所以看它左孩子(2i,下标为“8”)右孩子(2i+1,下标为“9”,超出顺序表长度,所以 i 为“4”时没有右孩子)哪个更大,发现它只有左孩子,所以就和左孩子换:

//图8

再看下标为“3”的结点:

//图9

检查当前结点是否满足 根≥左、右,若不满足,将当前结点与更大的一个孩子互换。

显然下标为“3”的结点“78”不满足我们的要求,所以看它左孩子(2i,下标为“6”,指向元素为“65”)右孩子(2i+1,下标为“7”,指向元素为“87”)哪个更大,发现右孩子更大,所以就和右孩子换:

//图10

接着看下标为“2”的结点:

//图11

检查当前结点是否满足 根≥左、右,若不满足,将当前结点与更大的一个孩子互换。

显然下标为“2”的结点“17”不满足我们的要求,所以看它左孩子(2i,下标为“4”,指向元素为“32”)右孩子(2i+1,下标为“5”,指向元素为“45”)哪个更大,发现右孩子更大,所以就和右孩子换:

//图12

最后看下标为“1”的结点:

//图13

检查当前结点是否满足 根≥左、右,若不满足,将当前结点与更大的一个孩子互换。

显然下标为“1”的结点“53”不满足我们的要求,所以看它左孩子(2i,下标为“2”,指向元素为“45”)右孩子(2i+1,下标为“3”,指向元素为“87”)哪个更大,发现右孩子更大,所以就和右孩子换:

//图14

但是!!!!你有没有发现,现在下标为“3”的结点,由于下标为“1”的结点的调整,不满足“大根堆”的要求了!!!

所以我们应该继续调整

也就是说,如果元素互换破坏了下一级的堆,则采用相同的方法继续往下调整(小元素不断“下坠”)

什么意思呢?我们下标为“1”的结点的右孩子比较大,不是和右孩子换吗

那么换到右孩子这个地方(也就是下标为“3”的地方)之后,还要再以下标为“3”为顶点,继续看它的左右孩子是不是满足大根堆的要求

不满足则继续换,换完之后还要再看看,要么满足大根堆要求了停止,要么没有左右孩子了停止。

so我们还有一波:

由于破坏了下一级的“堆”,所以看下标为“3”的结点:

在这里插入图片描述

检查当前结点是否满足 根≥左、右,若不满足,将当前结点与更大的一个孩子互换。

显然下标为“3”的结点“53”不满足我们的要求,所以看它左孩子(2i,下标为“6”,指向元素为“65”)右孩子(2i+1,下标为“7”,指向元素为“78”)哪个更大,发现右孩子更大,所以就和右孩子换:

//图14

满足大根堆的要求,小元素(53)无法继续“下坠”,调整完成

ok,我们上代码:

//将以 k 为根的子树调整为大根堆
void HeadAdjust(int A[], int k, int len){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){//ATTENTION!注意我们这里是从最底层的分支结点开始调整!!!for(int i = len/2; i>0; i--){   //从后往前调整所有非终端结点HeadAdjust(A, i, len);}
}

还是那么清晰。

我们刚刚肉眼看的流程是一眼能看到满不满足大根堆,不满足就调整,但是我们代码一眼看不出来,所以算法是先找出当前结点左右孩子最大的那个,再和根节点比,要是根节点比较大那没事,要是根节点比较小就换。

还有就是上面一个函数是调整一个结点,我们建立“大根堆”,对所有的非叶子结点从后往前都进行一次这样操作即可。

还有我们 A[0] 刚好是空的,所以我们在对一个非叶子结点进行调整的时候,直接让 A[0] 当临时变量即可。

3.堆排序

我们已经会将一个序列调整成一个大根堆了,那么现在就可以根据大根堆进行排序了。

选择排序:每一趟在待排序元素中选取关键字最小(或最大)的元素加入有序子序列。

所以我们堆排序就是:每一趟将堆顶元素加入有序子序列(与待排序序列中的最后一个元素交换),并将待排序元素序列再次调整为大根堆(小元素不断“下坠”)

什么意思呢?就是两步,①堆排序加到有序序列中②调整。

我们还是用之前那个栗子来描述:

这是刚刚那个调整完成的大根堆:

//图16

第1趟堆排序

堆排序:①将堆顶元素加入有序子序列(与待排序元素序列中的最后一个元素交换),②将待排序元素序列再次调整为大根堆(小元素不断“下坠”)

①堆顶元素是“87”,将它与待排序元素序列中的最后一个元素(“9”)交换

//图17

虚线表示已经加入有序序列,我们就不用管它了。

②现在待排序元素是下标 1~7,我们将它调整为大根堆

还是按照上面的方式,从 下标 1~7 中的自底向上的第一个叶子结点(n为7,第一个非叶子结点下标为 ⌊7/2⌋ = 3)开始调整,发现满足大根堆的要求;再看下标为“2”的结点,也满足大根堆的要求;于是来到了结点“1”,发现不满足:

//图18

我们开始调整,检查当前结点是否满足 根≥左、右,若不满足,将当前结点与更大的一个孩子互换。

显然下标为“1”的结点“9”不满足我们的要求,所以看它左孩子(2i,下标为“2”,指向元素为“45”)右孩子(2i+1,下标为“3”,指向元素为“78”)哪个更大,发现右孩子更大,所以就和右孩子换:

//图19

我们再往下看发现它破坏了下一级的堆,所以还要继续向下调整

换到了下标为“3”这里,那么就开始针对这个下标的结点进行调整:

检查当前结点是否满足 根≥左、右,若不满足,将当前结点与更大的一个孩子互换。

显然下标为“3”的结点“9”不满足我们的要求,所以看它左孩子(2i,下标为“6”,指向元素为“65”)右孩子(2i+1,下标为“7”,指向元素为“53”)哪个更大,发现左孩子更大,所以就和左孩子换:

//图20

换完发现没有孩子了,小元素无法下坠,所以调整完成

那么我们第1趟也完成了,经过调整后带排序序列再次构成一个“大根堆”,就可以进行第2趟堆排序了:

第2趟堆排序

堆排序:①将堆顶元素加入有序子序列(与待排序元素序列中的最后一个元素交换),②将待排序元素序列再次调整为大根堆(小元素不断“下坠”)

①堆顶元素是“78”,将它与待排序元素序列中的最后一个元素(“53”)交换

//图21

虚线表示已经加入有序序列,我们就不用管它了。

②现在待排序元素是下标 1~6,我们将它调整为大根堆

还是按照上面的方式,从 下标 1~6 中的自底向上的第一个叶子结点(n为6,第一个非叶子结点下标为 ⌊6/2⌋ = 3)开始调整,发现满足大根堆的要求;再看下标为“2”的结点,也满足大根堆的要求;于是来到了结点“1”,发现不满足:

//图22

我们开始调整,检查当前结点是否满足 根≥左、右,若不满足,将当前结点与更大的一个孩子互换。

显然下标为“1”的结点“53”不满足我们的要求,所以看它左孩子(2i,下标为“2”,指向元素为“45”)右孩子(2i+1,下标为“3”,指向元素为“65”)哪个更大,发现右孩子更大,所以就和右孩子换:

//图23

换完发现左孩子比它小,小元素无法下坠,所以调整完成

那么我们第2趟也完成了,经过调整后带排序序列再次构成一个“大根堆”,就可以进行第3趟堆排序了:

第3趟堆排序

堆排序:①将堆顶元素加入有序子序列(与待排序元素序列中的最后一个元素交换),②将待排序元素序列再次调整为大根堆(小元素不断“下坠”)

①堆顶元素是“65”,将它与待排序元素序列中的最后一个元素(“9”)交换:

//图24

虚线表示已经加入有序序列,我们就不用管它了。

②现在待排序元素是下标 1~5,我们将它调整为大根堆

还是按照上面的方式,从 下标 1~5 中的自底向上的第一个叶子结点(n为5,第一个非叶子结点下标为 ⌊5/2⌋ = 2)开始调整,发现满足大根堆的要求;于是来到了结点“1”,发现不满足:

在这里插入图片描述

我们开始调整,检查当前结点是否满足 根≥左、右,若不满足,将当前结点与更大的一个孩子互换。

显然下标为“1”的结点“9”不满足我们的要求,所以看它左孩子(2i,下标为“2”,指向元素为“45”)右孩子(2i+1,下标为“3”,指向元素为“53”)哪个更大,发现右孩子更大,所以就和右孩子换:

在这里插入图片描述

换完发现左孩子比它小,小元素无法下坠,所以调整完成。(虚线是已排好序的不用管,别弄混了)

那么我们第3趟也完成了,经过调整后带排序序列再次构成一个“大根堆”,就可以进行第4趟堆排序了:

第4趟堆排序

堆排序:①将堆顶元素加入有序子序列(与待排序元素序列中的最后一个元素交换),②将待排序元素序列再次调整为大根堆(小元素不断“下坠”)

①堆顶元素是“53”,将它与待排序元素序列中的最后一个元素(“17”)交换:

在这里插入图片描述

虚线表示已经加入有序序列,我们就不用管它了。

②现在待排序元素是下标 1~4,我们将它调整为大根堆

还是按照上面的方式,从 下标 1~4 中的自底向上的第一个叶子结点(n为4,第一个非叶子结点下标为 ⌊4/2⌋ = 2)开始调整,发现满足大根堆的要求;于是来到了结点“1”,发现不满足:

在这里插入图片描述

我们开始调整,检查当前结点是否满足 根≥左、右,若不满足,将当前结点与更大的一个孩子互换。

显然下标为“1”的结点“17”不满足我们的要求,所以看它左孩子(2i,下标为“2”,指向元素为“45”)右孩子(2i+1,下标为“3”,指向元素为“9”)哪个更大,发现左孩子更大,所以就和左孩子换:

在这里插入图片描述

我们再往下看发现它破坏了下一级的堆,所以还要继续向下调整

换到了下标为“2”这里,那么就开始针对这个下标的结点进行调整:

检查当前结点是否满足 根≥左、右,若不满足,将当前结点与更大的一个孩子互换。

显然下标为“2”的结点“17”不满足我们的要求,所以看它左孩子(2i,下标为“4”,指向元素为“32”)右孩子(2i+1,下标为“5”,超出待排序顺序表长度,所以 i 为“4”时待排序元素中没有右孩子)哪个更大,发现左孩子更大,所以就和左孩子换:

在这里插入图片描述

换完发现没有孩子了,小元素无法下坠,所以调整完成

那么我们第4趟也完成了,经过调整后带排序序列再次构成一个“大根堆”,就可以进行第5趟堆排序了:

第5趟堆排序

堆排序:①将堆顶元素加入有序子序列(与待排序元素序列中的最后一个元素交换),②将待排序元素序列再次调整为大根堆(小元素不断“下坠”)

①堆顶元素是“45”,将它与待排序元素序列中的最后一个元素(“17”)交换:

在这里插入图片描述

虚线表示已经加入有序序列,我们就不用管它了。

②现在待排序元素是下标 1~3,我们将它调整为大根堆

还是按照上面的方式,从 下标 1~3 中的自底向上的第一个叶子结点(n为3,第一个非叶子结点下标为 ⌊3/2⌋ = 1)开始调整,发现不满足大根堆的要求:

在这里插入图片描述

我们开始调整,检查当前结点是否满足 根≥左、右,若不满足,将当前结点与更大的一个孩子互换。

显然下标为“1”的结点“17”不满足我们的要求,所以看它左孩子(2i,下标为“2”,指向元素为“32”)右孩子(2i+1,下标为“3”,指向元素为“9”)哪个更大,发现左孩子更大,所以就和左孩子换:

在这里插入图片描述

换完发现没有孩子了,小元素无法下坠,所以调整完成

那么我们第5趟也完成了,经过调整后带排序序列再次构成一个“大根堆”,就可以进行第6趟堆排序了:

第6趟堆排序

堆排序:①将堆顶元素加入有序子序列(与待排序元素序列中的最后一个元素交换),②将待排序元素序列再次调整为大根堆(小元素不断“下坠”)

①堆顶元素是“32”,将它与待排序元素序列中的最后一个元素(“9”)交换:

在这里插入图片描述

虚线表示已经加入有序序列,我们就不用管它了。

②现在待排序元素是下标 1~2,我们将它调整为大根堆

还是按照上面的方式,从 下标 1~2 中的自底向上的第一个叶子结点(n为2,第一个非叶子结点下标为 ⌊2/2⌋ = 1)开始调整,发现不满足大根堆的要求:

在这里插入图片描述

我们开始调整,检查当前结点是否满足 根≥左、右,若不满足,将当前结点与更大的一个孩子互换。

显然下标为“1”的结点“9”不满足我们的要求,所以看它左孩子(2i,下标为“2”,指向元素为“17”)右孩子(2i+1,下标为“3”,超出待排序顺序表长度,所以 i 为“3”时待排序元素中没有右孩子)哪个更大,发现左孩子更大,所以就和左孩子换:

在这里插入图片描述

换完发现没有孩子了,小元素无法下坠,所以调整完成

那么我们第6趟也完成了,经过调整后带排序序列再次构成一个“大根堆”,就可以进行第7趟堆排序了:

第7趟堆排序

我们现在待排序序列只剩下一个元素了,只剩一个元素就不用再调整了

在这里插入图片描述

所以我们一共有 8 个元素,经过 n-1 (7)趟排序后,得到了一个有序的序列:

在这里插入图片描述

注意:基于“大根堆”的堆排序得到“递增序列”

操作流程讲完了,我们话不多说,上代码:

//堆排序的完整逻辑
void HeapSort(int A[],int len){BuildMaxHeap(A, len);       //初始建堆for(int i = len; i>1; i--){ // n-1 趟的交换和建堆过程swap(A[i], A[1]);       //堆顶元素和堆底元素交换HeadAdjust(A, 1, i-1);  //把剩余的待排序元素整理成堆}
}//将以 k 为根的子树调整为大根堆
void HeadAdjust(int A[], int k, int len){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 指向的是待排序序列中的最后一个(堆底元素),每进行一趟待排序序列就会少一个。

4.性能

空间复杂度不用说了,显然是O(1)

废话多预警!!!只要知道结论即可,我下面说的有点细了可不看。

由上面的代码可以看到,我们堆排序主要调用了两个函数,一个是建立大根堆(建堆也是一个接一个调整成堆),一个是遍历待排序元素每一个遍历把all待排序序列调整成堆,这没毛病吧。

而我们被调用的“将以 k 为根的子树调整为大根堆”的函数中,其实针对每一个被调整的结点“k”,都要看它的孩子是不是满足“大根堆”的要求。

怎么看,就是先把它的左右孩子作对比,看哪个大,再把该结点和左右孩子最大的那个作对比,看哪个大,该结点大则不用下坠,该结点小则下坠一层。

so 显然,我们一个结点,每“下坠”一层,最多只需要对比关键字2次,这也没毛病吧。

所以,如果树高为 h ,某结点在第 i 层,那么将这个结点向下调整最多只需要“下坠” h-i 层,关键字对比次数不超过 2(h-i) 次

因为最多就下降到底嘛,再多也没了是吧。

我们又知道 n 个结点的完全二叉树树高 h = ⌊log2n⌋ + 1 ,所以 n 个结点的树,某结点在第 i 层,那么将这个结点向下调整最多只需要“下坠” h-i 即 ⌊log2n⌋ + 1 - i 层,关键字对比次数不超过 2(h-i) 即 2(⌊log2n⌋ + 1 - i) 次

而第 i 层最多有 2i-1 个结点,这个我们是知道的吧。那最后一层的结点肯定是无法下坠的,因为已经在最底层了,所以只有第 1~(h-1) 层的结点才有可能需要“下坠”调整

so 将整棵树调整为大根堆,关键字对比次数不超过 1 * 2(h-1)+ 21 * 2(h-2) + …… + 2h-2 * 2 次

啥意思呢,就是刚刚说的列了个式子,第 1 层 1 个结点,每个结点最多下坠 h-1 层,每次下坠最多对比关键字2次,所以第 1 层最多对比 1 * 2(h-1) 次;第 2 层 2 个结点,每个结点最多下坠 h-2 层,每次下坠最多对比关键字2次,所以第 2 层最多对比 21 * 2(h-2) 次……以此类推,第 h-1 层 有 2h-2 个结点,每个结点最多下坠 1 层,每次下坠最多对比关键字2次,所以第 h-1 层最多对比 2h-2 * 2 次

所以将整棵树调整为大根堆,关键字对比次数不超过 1 * 2(h-1)+ 21 * 2(h-2) + …… + 2h-2 * 2 次

然后把 h = ⌊log2n⌋ + 1 代入进去,再经过一些数学运算,什么基本不等式之类的,再来一点差比数列求和,再来个错位相减法啥的……最终可以得到这个加和 ≤ 4n(感兴趣的可以自己查一下哈,计算机的尽头是数学……)

所以!!!我们建堆的过程,关键字对比次数不超过 4n,建堆时间复杂度 = O(n)

建堆时间复杂度其实就是将整棵树调整为堆的时间复杂度,没差。

而我们堆排序算法中,首先调用建堆是吧,但建堆之后还有呢,建堆之后需要进行 n-1 趟,每一趟交换后都需要将根节点进行“下坠”调整。(弱弱说一句,注意这里不是将整棵树调整成堆了,是只将根结点调整成堆,只调整根结点一个结点)

根结点最多下坠 h-1 层,每次下坠最多对比关键字 2 次,所以每一趟排序时间复杂度不超过 O(h)=O(log2n)

刚刚又说了建堆之后需要进行 n-1 趟,总的时间复杂度就是O(nlog2n)

所以我们完整堆排序的时间复杂度就是 O(n) + O(nlog2n) = O(nlog2n)

那么稳定性如何?

假设我们有一个初始序列,那要进行堆排序,我们首先要做的就是把它初始化为大根堆:

在这里插入图片描述

由我们的 “将以 k 为根的子树调整为大根堆” 代码可以看到,若左右孩子一样大,则我们的规则是优先和左孩子进行交换的。

so 调整后就变成这样:

在这里插入图片描述

我们的大根堆就建立完成了。

大根堆建立完成后我们就可以进行堆排序了。先把堆顶元素加入到有序子序列:

在这里插入图片描述

发现待排序序列还是满足大根堆。再把堆顶元素加入有序子序列:

在这里插入图片描述

现在待排序序列就剩一个元素了,排序完成了:

在这里插入图片描述

我们发现一样的元素调换位置了,所以堆排序是不稳定的

当然,我们上面说的堆排序都是大根堆,得到的是递增序列,那么基于小根堆的堆排序得到的就是递减序列了,其实过程都是一样的,不再赘述。

三、堆的插入和删除

1.插入

先提前说一下,我们插入一个元素都是放在表尾。

对于小根堆新元素放到表尾,与新元素的双亲结点对比,若新元素比双亲结点更小,则将二者互换。新元素就这样一路“上升”,直到无法继续上升为止。

显然堆的插入其实主要操作还是调整堆。

我们来看,这是一个平平无奇水灵灵的小根堆:

在这里插入图片描述

现在我们要往这个堆里面插入元素“13”

新元素放到表尾,所以放到末尾:

在这里插入图片描述

此时我们应该调整“堆”,把新元素与它的的双亲结点对比,若新元素比双亲结点更小,则将二者互换。新元素就这样一路“上升”,直到无法继续上升为止。

13<32,所以我们调换:

在这里插入图片描述

13<17,所以我们再调换:

在这里插入图片描述

此时 13>9,无法再“上升”了,这就是它的最终位置。

我们对比关键字的次数是 3 次。

现在我们要往这个堆里面插入元素“46”

新元素放到表尾,所以放到末尾:

在这里插入图片描述

此时我们应该调整“堆”,把新元素与它的的双亲结点对比,若新元素比双亲结点更小,则将二者互换。新元素就这样一路“上升”,直到无法继续上升为止。

46>45,无法再“上升”,这就是它的最终位置,我们不用换:

在这里插入图片描述

对比关键字的次数只有 1 次。

好了,堆的插入就说完了,现在我们来说堆的删除:

2.删除

我们在堆里面删除元素,被删除的元素用堆底元素替代,然后让该元素不断“下坠”,直到无法下坠为止。

这是我们刚刚插入新元素调整后的小根堆:

在这里插入图片描述

现在我们要在这个堆中删除元素“13”

在这里插入图片描述

被删除的元素用堆底元素替代,然后让该元素不断“下坠”,直到无法下坠为止。

所以我们用“46”来代替被删元素:

在这里插入图片描述

45>17,46>17,所以我们调换“46”和“17”:

在这里插入图片描述
53>32,46>32,所以我们再调换:

在这里插入图片描述

此时元素“46”无法再“下坠”了,这就是它的最终位置:

在这里插入图片描述

我们对比关键字的次数是 4 次(下坠的时候,左右孩子也要对比,和小的那个换)

现在我们要在这个堆中删除元素“65”:

在这里插入图片描述

被删除的元素用堆底元素替代,然后让该元素不断“下坠”,直到无法下坠为止。

所以我们用“46”来代替被删元素:

在这里插入图片描述

87>78,46<78,无法再“下坠”,这就是它的最终位置,我们不用换。

我们对比关键字的次数是 2 次


总结

这篇选择排序中简单选择排序和堆排序都是不稳定的,第一篇插入排序中我们学的插入排序是稳定的,希尔排序是不稳定的,第二篇交换排序中我们学的冒泡排序是稳定的,快速排序是不稳定的。

简单来说,简单选择排序就是在最开始画一条线,每次都在线后面的元素中找到最小或者最大的放前面,放一个后挪一格,那么线前面的就都是有序的。这个线一个一个往后挪,直到挪到最后,线后面没东西了,我们的排序也完成了。

堆排序就是先把一个序列调整变成堆,比如大根堆,也就是找到第一个非叶子结点(从底向上找),以这个结点打头,一个一个从后往前开始调整,每次先看这个结点左右孩子谁大,谁大就和这个结点换,如果破坏了下一层的堆就继续调整,调整完就换结点,直到从第一个非叶子结点开始调整完所有非叶子结点为止。

变成堆之后,再每次把堆顶元素加入有序子序列中(逻辑视角就是和树的最后一个子结点进行交换),完了再针对交换过去的根节点进行下坠调整,调整完则一趟堆排序就完成了,到待排序序列只剩一个元素的时候整个堆排序过程就完成了。

简单选择排序时间复杂度是O(n2),堆排序时间复杂度是O(nlog2n)。

我们在堆里面插入元素的时候,新元素放到表尾,与新元素的双亲结点对比,若新元素比双亲结点更小,则将二者互换。新元素就这样一路“上升”,直到无法继续上升为止;

在堆里面删除元素的时候,被删除的元素用堆底元素替代,然后让该元素不断“下坠”,直到无法下坠为止。

堆的删除需要“下坠”,和我们调整堆一样的,都是先要比左右孩子,再拿双亲结点和左右孩子中最大(小)的那个比;但是我们“上升”就不一样了,上升只需要该元素和双亲结点比就可以了,这个要注意一下。

相关文章:

  • 如何用div手写一个富文本编辑器(contenteditable=“true“)
  • AT_abc410_f [ABC410F] Balanced Rectangles 题解
  • 远程桌面连接 - 允许电脑从网络外部访问计算机
  • 视频设备:直联正常,通过卫星无画面,因为延迟太大
  • Flutter动画全解析:从AnimatedContainer到AnimationController的完整指南
  • 从源码出发:全面理解 Kafka Connect Jdbc与Kafka Connect 机制
  • 基于RISC-V架构的服务器OS构建DevOps体系的全方位方案
  • 神经网络课设
  • 关于 常见 JavaScript 混淆类型
  • 八股---9.消息中间件
  • Redis中的分布式锁之SETNX底层实现
  • 资深Java工程师的面试题目(一)并发编程
  • Agent开发相关工具
  • 迭代器模式:集合遍历的统一之道
  • 【web应用】在 Vue 3 中实现饼图:使用 Chart.js实现饼图显示数据分析结果
  • wpf 队列(Queue)在视觉树迭代查找中的作用分析
  • 行列式展开定理(第三种定义) 线性代数
  • 系统思考:渐糟之前先变好
  • 笑傲江湖版大模型:武侠智能体的构建与江湖法则
  • Java日志使用
  • vs2012解决方案做网站/关键词推广优化app
  • 微信分销网站建设电话/百度seo排名软件
  • 老铁推荐个2021网站好吗/兰州seo快速优化报价
  • 无锡便宜做网站/最新新闻热点话题
  • 吉林做网站找谁/百度关键词怎么优化
  • 搜索引擎优化与推广的产生及发展/汕头seo优化