IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 数据结构与算法 -> [Alg]排序算法之选择排序 -> 正文阅读

[数据结构与算法][Alg]排序算法之选择排序

[Alg]排序算法之选择排序

作者:屎壳郎 miaosg01@163.com

时间:Aug 2021

版次:初版


简介: 当数据量较大时,堆排序优于shell排序。但无论如何比不过快速排序。快排的效率只是平均意义上的(概率上说),最坏的情况会退化至 N 2 N^2 N2。堆排序的优势在于,在恶化的情况下也能保证时间复杂度在 O ( N lg ? N ) O(N\lg N) O(NlgN)。后续我们会讲到归并排序也有这个特性,任何情况下都能保证 N lg ? N N\lg N NlgN,但归并排序要求更多的空间。


1、直接选择排序

直接选择排序遍历所有记录N,确定最大值(或最小值),然后输出到最终位置。缩小范围至剩余N-1,重复这个步骤。这个过程和直接插入排序完全相反,直接插入排序是从1个记录开始,一个接一个的往里插入记录,并逐步扩大遍历范围。选择排序直接确定记录的最终位置,而插入排序直到插入最后一个记录后在能确定记录的最终位置。
直接选择排序很简单,直接列算法,一眼就看明白,不须解释。

算法S:(直接选择排序)

设记录 R 1 , R 2 , … , R N R_1,R_2,\ldots,R_N R1?,R2?,,RN?,其键值 K 1 , K 1 , … , K N K_1,K_1,\ldots,K_N K1?,K1?,,KN?

  • S1.[循环j] For j = N , N ? 1 , … , 2 j=N, N-1,\ldots,2 j=N,N?1,,2,执行S2和S3.
  • S2.[查找最大值 m a x ( K 1 , K 2 , … , K j ) max(K_1,K_2,\ldots,K_j) max(K1?,K2?,,Kj?)] 遍历 K j , K j ? 1 , … , K 1 K_j,K_{j-1},\ldots,K_1 Kj?,Kj?1?,,K1?,确定最大值 K i K_i Ki?.
  • S3.[交换] 交换 R i ? R j R_i\leftrightarrow R_j Ri??Rj?,此时 R j , … , R N R_j,\ldots,R_N Rj?,,RN?为最终位置。

要从N个记录中确定最大值,至少比较N-1次,所以直接选择排序的时间复杂度 O ( N 2 ) O(N^2) O(N2)。比较次数是没办法节省的,难道就没有改进的余地了吗?要改进算法的效率,无外乎两种途径,其一是对要待解决问题的本质有深入的洞察,并有深厚的数学功底来确立模型,建立高效算法。这点好像比较难。另一个相对简单的方法,不要每次都从零开始,要善于并充分利用前序的工作成果来减小后续的工作量或改善后续工作的复杂度。

对于第一种改进算法的方式,我们直接跳过就好了,毕竟那是计算机专家兼数学家或数学家兼计算机专家的饭碗,做人不能做绝了,给他们留口饭吃。我们重点来讲后一种方法。

我们在遍历 R j … R 1 R_j\ldots R_1 Rj?R1?过程中,如果没有第一次就选中最大值,遍历过程会发生替换,产生一个序列。例如(1 3 2 5 4)第一次选 m a x = 4 max=4 max=4,继续向左搜索,找到 5 > 4 5 > 4 5>4,替换 m a x = 5 max=5 max=5,确定5为最大值。直接选择排序没有充分利用这个序列,直接舍弃4(第二大值),下次又从头开始。虽然第一次遍历不能减少比较次数,但我们可以充分利用遍历过程产生的有价值信息。

直接选择排序改进1

对算法做如下改进:
我们需要一个辅助表AUX记录当前最大值信息。我们改为从左到右遍历,这样更方便一点,从max=1开始,遇到3令max=3并保存1的位置,继续向右搜索遇到5,令max=5,并保存3的位置信息2。当5和4交换后,不必从头开始查找,取出辅助表中的位置信息2,从此处开始,位置2代表的意义为,K[2]是从头到2位置的最大值,不必查找位置2以前的记录。这就充分利用了以前比较的成果,减少了不必要的比较。采取这项改进措施,能把平均比较次数减半。
在这里插入图片描述

2、方根选择

另一个利用前序比较结果的例子是分组,这种方法不需要额外的空间存储位置信息。下面介绍称之为方根选择的分组选择方法。设 N N N 项记录,分为 N \sqrt N N ? 组,每组 N \sqrt N N ? 项。
见图例
在这里插入图片描述

我们选出最大数908后,要确定第二大值,只需在(170,897,275)中确定最大值,然后与其它组的最大值比较(512,653,765)。我们从上图看到,不影响不含最大值的项。故方根选择要在选出最大值的组内进行 ( N ? 2 ) (\sqrt N -2) (N ??2)次比较,与其它组进行 ( N ? 1 ) (\sqrt N -1) (N ??1)次比较,其复杂度 O ( N N ) O(N\sqrt N) O(NN ?)

推广开来,我们也扩展至立方根,即 N N N N 3 \sqrt[3] N 3N ?组,每组包含 N 3 \sqrt[3] N 3N ?小组,每小组 N 3 \sqrt[3] N 3N ?个项,可降至 O ( N N 3 ) O(N\sqrt[3]N) O(N3N ?)

如果我们再推广,再分,再分,再分…最后每个组只含一个项的时候,是棵树。记得我们在《[Alg] 排序算法之插入排序》中提到两个算法改进,直接插入算法为减少搜索长度改为两路插入,再改进四路,再分,再分,再分…是棵树;链表插入中,为减小链表长度,再分,再分,再分…是棵树!!!我们好像发现了一个秘密!!!好多数据结进化的终点都是棵树!!!

3、选择树

我们先考虑一个典型的树选择的例子——乒乓球淘汰赛:
在这里插入图片描述
4 4 4个选手至少 4 ? 1 = 3 4-1=3 4?1=3场比赛才能决定冠军,但樊振东就是亚军吗?那可不一定,被马龙打败的奥恰洛夫也可能是亚军。所以要加赛,奥恰洛夫对樊振东才能确定亚军。但是,右侧樊振东对水谷隼不用比,我们利用了以前的结果。可以看出,选出冠军马龙后,只需要马龙这个分支的需要重新比较。我们再看一个更大一点的例子。

在这里插入图片描述
当取走最大值908后,要比较产生新的最大值只需比较原最大值908所在的分支即可,红色线标出部分,共要比较 lg ? N \lg N lgN次(蓝色线段标出)即可在原有基础上重新产生最大值。理解了上面的内容,是时候介绍今天的主角:堆排序。

4、堆排序

堆的定义:定义 K 1 , K 2 , … , K N K_1,K_2,\ldots,K_N K1?,K2?,,KN?满足下列条件称之为堆。

K ? j / 2 ? ≥ K j f o r ? 1 ≤ ? j / 2 ? < j ≤ N K_{\lfloor j/2\rfloor}\geq K_j \qquad for\, 1\leq\lfloor j/2\rfloor<j\leq N K?j/2??Kj?for1?j/2?<jN

根据堆的定义,知 K 1 ≥ K 2 K_1\geq K_2 K1?K2? K 1 ≥ K 3 K_1\geq K_3 K1?K3? K 2 ≥ K 4 K_2\geq K_4 K2?K4?
K 1 = m a x ( K 1 , K 2 , … , K N ) K_1= max(K_1,K_2,\ldots,K_N) K1?=max(K1?,K2?,,KN?)

根据我们优良的革命传统,举个例子说明:(5 9 1 8 2 6 4 7 3)。设LOC(k)代表位置k。
在这里插入图片描述

第一步: l = 4 l=4 l=4 LOC(4)8>LOC(8)7和LOC(9)3。满足堆条件。 l = l ? 1 l=l-1 l=l?1

第二步: l = 3 l=3 l=3 LOC(3)1<LOC(6)6和LOC(7)4,LOC(3)1与大者LOC(6)6交换。 l = l ? 1 l=l-1 l=l?1

第三步: l = 2 l=2 l=2 无需交换。

第四步: l = 1 l=1 l=1 R = 5 R=5 R=5,与子结点LOC(2)LOC(3)比较,LOC(2)9大,上浮;继续与LOC(2)的子结点LOC(4)LOC(5)比较,LOC(4)8大,上浮至LOC(2),继续与LOC(4)的子结点LOC(8)LOC(9)比较,LOC(8)大,上浮至LOC(4),无子结点 R = 5 R=5 R=5放入LOC(8)。第四步详细过程见图示:
在这里插入图片描述

见表(前四步为建堆阶段,后面为选择排序阶段)。

位置123456789lr
59182647359
第一步59182647349
第二步59682147339
第三步59682147329
第四步98672145319
87652143919
75632148918
65432178917
53412678916
43215678915
31245678814
21345678913
12345678912

算法H:(堆排序)

  • H1.[初始化] 置 l ← ? N / 2 ? + 1 l\gets\lfloor N/2\rfloor+1 l?N/2?+1 r ← N r\gets N rN
  • H2.[减小 l l l r r r] 如果 l > 1 l>1 l>1,置 l ← l ? 1 l\gets l-1 ll?1 R ← R l R\gets R_l RRl? K ← K l K\gets K_l KKl?。否则( l = 1 l=1 l=1),置 R ← R r R\gets R_r RRr? K ← K r K\gets K_r KKr? R r ← R l R_r\gets R_l Rr?Rl?,并置 r ← r ? 1 r\gets r-1 rr?1;如果 r = l r=l r=l,置 R l ← R R_l\gets R Rl?R并结束。
  • H3.[准备siftup] 置 j ← l j\gets l jl
  • H4.[下移] 置 i ← j i\gets j ij j ← 2 j j\gets 2j j2j。如果 j < r j<r j<r,运行下步H5;如果 j = r j=r j=r跳转至H6;如果 j > r j>r j>r跳转至H8.
  • H5.[比较子结点] 如果 K j < K j + 1 K_j<K_{j+1} Kj?<Kj+1?, 置 j ← j + 1 j\gets j+1 jj+1
  • H6.[与父节点比较] 如果 K ≥ K j K\geq K_j KKj?,转至H8。
  • H7.[上移] 置 R i ← R j R_i\gets R_j Ri?Rj?,并返回H2。
  • H8.[存入R] 置 R i ← R R_i\gets R Ri?R,并返回H2.

了解一个算法最有效的方法是拿出纸和笔实际跑一遍,掌握一个算法最有效的方法是编写程序运行一遍。别无它法!

从上面的表格看起来很难让人相信堆排序是一种高效的算法。当 N N N较小是确实如此,但随着 N N N增大,堆排序优于shell排序。但无论如何比不过快速排序。然而堆排序相对于快排有另一方面的优势,快排的效率只是平均意义上的(概率上说),最坏的情况会退化至 N 2 N^2 N2。堆排序的优势在于,在恶化的情况下也能保证时间复杂度在 O ( N lg ? N ) O(N\lg N) O(NlgN)。后续我们会讲到归并排序也有这个特性,任何情况下都能保证 N lg ? N N\lg N NlgN,但归并排序要求更多的存储空间。

堆排序边界条件问题

编程实践中一个很大的麻烦就是处理边界条件,很多错误往往也出现在边界条件上,我们看到堆排序算法H4中,有三个条件分支,分别为 j < r j<r j<r j = r j=r j=r j > r j>r j>r,都是为了防止插入操作越界。可以采用《[Alg]排序算法之插入排序》中提到的第一种应对措施,即在 N + 1 N+1 N+1处插入 K N + 1 = ? ∞ K_{N+1}=-\infty KN+1?=?确保 K ≥ K N + 1 K\geq K_{N+1} KKN+1?。为了在程序运行都满足这个条件,我们还要对其它方面进行改造,H2改为 R r + 1 ← R N + 1 R_{r+1}\gets R_{N+1} Rr+1?RN+1?,同样,在 r = 1 r=1 r=1之后置 R 2 ← R N + 1 R_2\gets R_{N+1} R2?RN+1?。这样的设置不同于在插入排序中作用,既不能使代码更简洁,也不能提高代码运行速度,只是减少了处理不当出错的机会。

堆排序的改进

如果你认认真真的拿着纸和笔推演了一遍堆排序算法,你会发现一个问题。第一阶段建堆完成,进入选择阶段,从头部取最大值放在末尾,末尾的值重新插入堆中,这个过程特别长。因为我们每次从堆末尾取值,值都很小,从头部最大值向左侧小值搜索位置且几乎总是 K < K i K<K_i K<Ki?,几乎总是在接近堆末端插入,这要进行大量的对比操作(H6步中)。如何规避这种情况?我们可以采取另一个策略,不进行对比,整体移动数据,直到堆的末尾,然后从尾端向前搜索然后插入。

具体实现如下:

  • 删除算法H中的H6,H8改为如下的:
  • H8’.[从尾端向前移] 置 j ← i j\gets i ji i ← ? j / 2 ? i\gets\lfloor j/2\rfloor i?j/2?
  • H9‘.[确定插入位置]如果 K ≤ K i K\leq K_i KKi? j = l j=l j=l,插入 R j ← R R_j\gets R Rj?R并返回H2。否则,置 R j ← R R_j\gets R Rj?R并返回H8‘。

堆插入操作

算法I:(堆插入)

  • I.[设置键值] 置 K K K为要插入的键值, j ← n + 1 j\gets n+1 jn+1
  • I.[初始化] 置 i ← ? j / 2 ? i\gets \lfloor j/2\rfloor i?j/2?
  • I.[寻找插入位置] 如果 i = 0 i=0 i=0 K i ≥ K K_i\geq K Ki?K,置 K j ← K K_j\gets K Kj?K,并结束程序。
  • I.[移动数据] 置 K j ← K i K_j\gets K_i Kj?Ki? j ← i j\gets i ji并返回I2。

删除堆内数据

算法D:(删除堆内数据)

给定位置 s s s,删除位置 s s s的数据。

  • D1.[初始化] K ← K N K\gets K_N KKN? l ← 1 l\gets 1 l1 r ← N ? 1 r\gets N-1 rN?1
  • D2.[上浮数据]置 j ← s j\gets s js。(后续与算法H相同)
  • D3.[下移] 置 i ← j i\gets j ij j ← 2 j j\gets 2j j2j。如果 j < r j<r j<r,运行下步D4;如果 j = r j=r j=r跳转至D5;如果 j > r j>r j>r跳转至D7.
  • D4.[比较子结点] 如果 K j < K j + 1 K_j<K_{j+1} Kj?<Kj+1?, 置 j ← j + 1 j\gets j+1 jj+1
  • D5.[与父节点比较] 如果 K ≥ K j K\geq K_j KKj?,转至D7。
  • D6.[上移] 置 R i ← R j R_i\gets R_j Ri?Rj?,并返回D3。
  • D7.[存入R] 置 R i ← R R_i\gets R Ri?R,并返回D3
  数据结构与算法 最新文章
【力扣106】 从中序与后续遍历序列构造二叉
leetcode 322 零钱兑换
哈希的应用:海量数据处理
动态规划|最短Hamilton路径
华为机试_HJ41 称砝码【中等】【menset】【
【C与数据结构】——寒假提高每日练习Day1
基础算法——堆排序
2023王道数据结构线性表--单链表课后习题部
LeetCode 之 反转链表的一部分
【题解】lintcode必刷50题<有效的括号序列
上一篇文章      下一篇文章      查看所有文章
加:2021-08-05 17:36:08  更:2021-08-05 17:39:26 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 18:37:41-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码