| |
|
开发:
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 日期:July 2021 版次:初版 简介: 交换排序是排序算法中的一大类,通过构成反序数据对的两两交换来消除反序,最终得到有秩序的排列。那么排列交换的本质是什么呢?说来话长,让我们回到远古时代,重温数据结构,从环形链表开始说起。 0、 交换排序的本质环形链表的合并: 假设现在我们有两个指针分别指向两个环形链表,要想把两个环形链表合并成一个的操作为:
L
I
N
K
(
p
t
r
1
)
?
L
I
N
K
(
p
t
r
2
)
LINK(ptr1)\leftrightarrow LINK(ptr2)
LINK(ptr1)?LINK(ptr2),即交换这两个指针的链接地址。见下图: 环形链表的拆分: 那么反过来,我们想把一个环形链表拆分成两个环形链表,如何操作呢?同样的操作,只是两个指针要指向同一个环形链表内:
L
I
N
D
(
p
t
r
1
)
?
L
I
N
K
(
p
t
r
2
)
LIND(ptr1)\leftrightarrow LINK(ptr2)
LIND(ptr1)?LINK(ptr2),即交换这两个指针的链接地址。区别就在于合并两个链表时,两个指针指向不同的环形链表;而拆分环形链表时,两个指针指向同一个链表。 见下图: 讲了这么多,环形链表的合并拆分跟交换排序又有什么关系?我要说数组就是一个或多个环形链表组成,你信吗?我们举个例子:
(
3
?
7
?
6
?
9
?
8
?
1
?
4
?
5
?
2
)
(3\,7\,6\,9\,8\,1\,4\,5\,2)
(376981452),
R
1
R_1
R1?的值为3,表示
R
1
R_1
R1?存储的指针指向
R
3
R_3
R3?,即
R
1
→
R
3
R_1\rightarrow R_3
R1?→R3?,依此推算
R
1
→
R
3
→
R
6
→
R
1
R_1\rightarrow R_3\rightarrow R_6\rightarrow R_1
R1?→R3?→R6?→R1?构成了一个环形链表(红色表示);同理
(
R
5
,
R
8
)
(R_5,R_8)
(R5?,R8?)(蓝色表示)和
(
R
2
,
R
7
,
R
4
,
R
9
)
(R_2,R_7,R_4,R_9)
(R2?,R7?,R4?,R9?)(黑色表示)分别构成了两个环形链表。所以这个排列可表示为:
(
(
3
?
6
?
1
)
(
2
?
7
?
4
?
9
)
(
5
?
8
)
)
((3\,6\,1)(2\,7\,4\,9)(5\,8))
((361)(2749)(58))共三个环形链表构成的数组排列(在《排列与反序》中,应用这个思想解决了由反序表生成对应的排列问题,同时提供了数组转链表、链表转数组的算法)。见下图: 我们在不同的环中交换一次数据,环数减少1;在同环中交换一次数据环数增加1。我们交换排序的目标是把它排列为 ( 1 ? 2 ? 3 ? 4 ? 5 ? 6 ? 7 ? 8 ? 9 ) (1\,2\,3\,4\,5\,6\,7\,8\,9) (123456789),只包含一个元素、指向自身的环。还是以上面的例子为例,有3个环,我们的排序目标是9个元素要形成9个环,那么最少的交换次序 9 ? 3 = 6 9-3=6 9?3=6,即最少要交换(同环交换)6次才能完成排序。理解了排列是由不同的环构成的,也就明白为什么第一类斯特林数满足 ∑ k [ n k ] = n ! \sum_k{n\brack k}=n! ∑k?[kn?]=n!性质—— n n n个元素形成不同环的数量总和等于 n n n的排列数,其有一一的对应关系。(参考《排列与反序》,里面有详细的数组转链表、链表转数组的讲解,可加深对这方面知识的理解。) 1、 最短的排序程序,没有之一最基本的算法就是与相邻的数据两两比较、交换,这是冒泡排序的基本思路。与直接插入排序类似,两两的比较交换数据是非常低效的,因其数据需要的平均净移动量为 1 3 N {1\over3}N 31?N,见《[Alg]排序算法之插入排序》。当然,万事万物都有其两面性,牺牲了性能,得到的好处是最简洁的代码,下面是最短的排序程序描述:
这个算法原理非常之简单,从头开始遍历数据记录并与其相邻的记录比较,发现一个反序后即对进行数据交换,然后从头开始重新遍历,直到消除所有的反序,至此if分支不成立,不执行条件语句中的goto跳转,然后从循环退出。这是一个比冒泡排序还苯的算法,大约平均执行 1 4 N 2 {1\over4}N^2 41?N2(见拆分数,反序的均值)次循环(一个反序对应一次循环)时间复杂度 O ( N 3 ) O(N^3) O(N3)。这个算法除了能增加一点我们的见识,貌似没有其它作用了(在快排中,选头、中、尾三个数据时用到了,这个排序很简洁,比写一大堆的if语句要好)。 回过头来接着聊冒泡排序,冒泡排序相邻的数据两两比较,成反序则交换。一趟遍历后,大数据上浮;我们可以在下一趟遍历时,让小数据下沉。这点改进稍稍能提高点性能,但与直接插入比起来,虽然都很垃圾,但冒泡排序更垃圾,其代码量相当于直接插入排序的两倍。好像除了冒泡排序妇孺皆知、广为传诵的名字,也没有其它让人欣赏的东西了。 2、Batcher’s 并行排序算法我们从《[Alg]排序算法之插入排序》的分析中得知,要提高算法的效率,就要增大比较和交换的跨度。下面提到的算法和shell排序算法类似,增加比较交换跨度。虽然其原理相似,但其具体的实现手段却相去甚远。这种增大跨度的方式很新奇,亦非常的错综复杂,不容易理解。我们先列出算法的实现,再配以例子逐条解释。可能也只有这个方法能把算法过程说的明白一点,我实在想不出其它更好的方式了。 算法M:(Batcher’s 并行算法)设记录 R 0 , R 1 , … , R N ? 1 R_0,R_1,\ldots, R_{N-1} R0?,R1?,…,RN?1?,其关键词为: K 0 , K 1 , … , K N ? 1 K_0,K_1,\ldots,K_{N-1} K0?,K1?,…,KN?1?,假设 N ? 1 > 2 N-1>2 N?1>2。
先设置一个例子,设记录 R R R其值为 ( R 0 , R 1 , R 2 , R 3 , R 4 , R 5 , R 6 , R 7 ) (R_0,R_1,R_2,R_3,R_4,R_5,R_6,R_7) (R0?,R1?,R2?,R3?,R4?,R5?,R6?,R7?)。
第一趟: p = 4 p=4 p=4
M3中条件
0
≤
i
<
4
0\leq i<4
0≤i<4选出大范围保证不越界,然后条件进一步精确到数据段
i
∧
p
=
0
i\wedge p=0
i∧p=0,
p
=
4
(
0100
)
2
p=4(0100)_2
p=4(0100)2?选取的为 即为选中 ( R 0 , R 1 , R 2 , R 3 ) (R_0,R_1,R_2,R_3) (R0?,R1?,R2?,R3?)(在第一步中两条件重合,后续会看的更清楚)在跨度 d = 4 d=4 d=4的情况下比较交换,即: ( R 0 : R 4 ) , ( R 1 : R 5 ) , ( R 2 : R 6 ) , ( R 3 : R 7 ) (R_0:R_4),(R_1:R_5),(R_2:R_6),(R_3:R_7) (R0?:R4?),(R1?:R5?),(R2?:R6?),(R3?:R7?)。 p = 2 p=2 p=2时:
首先选出大范围 0 ≤ i < 6 ( N ? d ) 0\leq i<6(N-d) 0≤i<6(N?d)以双竖线标出,再在这个大范围内选出长度2的数据段 ( R 0 , R 1  ̄ , R 2 , R 3 , R 4 , R 5  ̄ ∣ ∣ R 6 , R 7 ) (\overline{R_0,R_1},R_2,R_3,\overline{R_4,R_5}||R_6,R_7) (R0?,R1??,R2?,R3?,R4?,R5??∣∣R6?,R7?),上划线的项与随后的不带上划线的项比较交换。 在M5处,改变跨度 d ← q ? p d\gets q-p d←q?p, q ← q / 2 q\gets q/2 q←q/2;反转数据段选择 r ← p r\gets p r←p,即改为上次没选中的 ( R 0 , R 1 , R 2 , R 3  ̄ , R 4 , R 5  ̄ ∣ ∣ R 6 , R 7 ) (R_0,R_1,\overline{R_2,R_3},\underline{R_4,R_5}||R_6,R_7) (R0?,R1?,R2?,R3??,R4?,R5??∣∣R6?,R7?)以跨度 d = 2 d=2 d=2与后面的项比较交换,即上划线为选中数据与下划线部分比较交换。 p = 1 p=1 p=1时
从上面的详解中可以看出,这个算法是一个非常机械的过程,只要确定了 N N N,后续所有的比较、交换、跨度等等要素全部确定,它不依赖于任何前序的执行结果,这点与后面要介绍的快速排序完全不同,快排依赖于上次的划分。万事万物都有其两面性,这个特点使得该算法笨重,但是它这个特征使得它也特别适合于并行计算,在M3步骤中,满足条件多个执行过程数据的对比交换过程没有交叉重叠,完全可同步执行。整个执行过程可在 O ( 1 2 ? lg ? N ? ) ( ? lg ? N ? + 1 ) O({1\over2}{\lceil\lg N\rceil})(\lceil\lg N\rceil+1) O(21??lgN?)(?lgN?+1)完成,这是串行算法所不能达到的。 3、快速排序快排应该是应用范围最广、广为人知的一种算法,它是典型的分治思想的应用。其基本原理是选出一项,如 R 1 R_1 R1?为基准,与其它项逐个比较,比它大的项放置在右侧,比它小的项放置左侧,这样一个大问题就划分成了两个同样的子问题。依此递归下去,便得到结果。 算法Q:(快速排序)设记录 ( R 1 , R 2 , … , R N ) (R_1,R_2,\ldots, R_N) (R1?,R2?,…,RN?),其对应键值 ( K 1 , K 2 , … , K N ) (K_1,K_2,\ldots,K_N) (K1?,K2?,…,KN?)。 同时需要辅助stack,容量为 ? lg ? N ? \lfloor\lg N\rfloor ?lgN?。我们对算法实现在了如下处理:
快排退化问题不同于其它排序算法,快排喜欢乱序的输入,对接近排好序(秩序越好)是非常不友好的,最坏的情况出现在完全排好序的排列,快排只能每次分离出一个数据,且时间复杂度退化至 O ( N 2 ) O(N^2) O(N2)。为避免这种窘境,可以采取以以下措施来改善。
我们现在考虑一种更极端的情况,假设所有的排序关键词 K K K都相等,会出现什么情况? i i i从头到尾与 j j j逐个交换,在中点结束,形成完美的二等分;假设我们把 Q 3 Q3 Q3、 Q 4 Q4 Q4的 < < <改为 ≤ \leq ≤呢? i i i j j j不交换任何值,只是在 Q 5 Q5 Q5步交换 R l ? R j R_l\leftrightarrow R_j Rl??Rj?,即 R 0 R_0 R0?与 R 1 R_1 R1?,随后 R 0 R_0 R0?与 R 2 R_2 R2?交换,依次的 R 0 R_0 R0?与 R 3 , R 4 , … , R N R_3,R_4,\ldots,R_N R3?,R4?,…,RN?交换,哇——进入灾难模式。 消除边界条件检查在算法前置条件中提到,在头尾分别插入 K 0 = ? ∞ K_0=-\infty K0?=?∞和 K N + 1 = ∞ K_{N+1}=\infty KN+1?=∞,它们的作用分别是 K 0 K_0 K0?消除最后直接插入排序算法的边界条件检查,后面的 K N + 1 K_{N+1} KN+1?的作用是当选择 K l K_l Kl?为最大值时,防止寻找 K i > K l K_i>K_l Ki?>Kl?值时发生越界。 我们在讨论插入排序时,已经采用了第二种消除边界检查的方法,故可取消 K 0 = ? ∞ K_0=-\infty K0?=?∞;对于 K N + 1 = ∞ K_{N+1}=\infty KN+1?=∞,我们采用如下方法,针对上面提到的快排退化的改进措施,我们稍加改造,可达到一箭双雕的目的,即可预防快排退化,又可消除右侧边界条件检查,具体如下:
中位数问题快速排序算法的这种大事化小、分而治之(divide and conquer)的思想有着广泛的应用。下面这个问题的解决方案就是一个典型的应用范例:给出一个乱序的排列,寻找排序后位于中间位置的值。通常第一个想到的就是排序,然后就容易确定中间值,为了确定一个值而排序是很浪费的,尤其是数据很多的时候。如果我们不采用排序的手段,借鉴快排的处理思路,选取第一个 K l K_l Kl?为基准,进行交换,运行后的结果是,小于 K l K_l Kl?的项位于左侧,大于 K l K_l Kl?的位于右侧,设 K l K_l Kl?的位置为 P O S ( K l ) POS(K_l) POS(Kl?)。如果 P O S ( K l ) > m i d POS(K_l)>mid POS(Kl?)>mid(设 m i d mid mid为中间位置),表明中位数位于左侧数据部分,则继续重复该步骤在左侧寻找;如果 P O S ( K l ) < m i d POS(K_l)<mid POS(Kl?)<mid,则在右侧数据中寻找;直到找到 P O S ( K l ) = m i d POS(K_l)=mid POS(Kl?)=mid的值为止。也就是运行快排前几步确定中点的值,因舍弃了不需要的另一部分,故不需要stack存储。 我们以此为例说明了寻找中位数的方法,当然,这个方法可以寻找任意一个位置的数,应该称为K位数问题。 荷兰国旗问题红色、白色、蓝色的标识随机的排成一列,如何通过两两交换使得红色表示位于顶部,蓝色的位于底部(见荷兰国旗图)。我们要把快排的两分改进为三分天下的问题(是不是叫三国问题或隆中对问题更符合国情)。 算法D:(三分法快速排序——Tripartitioning Quicksort)
上面的算法D2、D3设置了条件检测 b ≤ c b\leq c b≤c。如果进入D2之前确保 a < b a<b a<b和 c < d c<d c<d,上述条件检测完全可以取消。 当键值大量重复或在接近排序尾声时键值接近,这时要花大量精力的检查键值以确定后续操作,这严重降低了算法的效率。可以转为三分快排,因为 = K =K =K的部分为最终确定的位置,后续排序不须再移动,这极大的加快了排序速度。 4、基数交换排序基数交换排序与快速排序没有什么本质不同,只是把比较 K K K改为了按位比较0和1。下面列出算法: 算法R:(基数排序)设关键词
K
K
K的最高有效位为
m
m
m位,则需要容量
m
?
1
m-1
m?1的辅助stack。
特别注意b处理,b应该取最大值的最高有效位,保证键值中最少有一个b位为1。如果取值比所有键值的最高有效位还高,那第一趟遍历,所有都为0, i i i从左向右搜索时遇不到1,这会导致越界;程序会报段错误。只要第一趟发生一次交换,就能保证边界。后续没有必要边界检查。 基数交换排序也有上面提到的键值接近时的退化问题,例如好多键值 k k k位以前都相等,必须检查 k + 1 k+1 k+1甚至 k + 2 k+2 k+2, k + 3 k+3 k+3甚至更多位才能确定后续执行的指令。我们也可以采用三分快排的思想来解决这个问题,就是把记录三分为 ( l , i ? 1 , k ) (l,i-1,k) (l,i?1,k), ( i , j , k + 1 ) (i,j,k+1) (i,j,k+1), ( j + 1 , r , k ) (j+1,r,k) (j+1,r,k)三个部分,采用三分快排的思路来处理。 好漫长的一章,我觉得我把交换排序的方方面面都介绍清除了,但是要完全消化吸收,里面还是有好多东西要好好咀嚼的。 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
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:35:20- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |