中希尔排序例题代码_这或许是分析10大排序算法最好的一篇文章(上)

000b2f02ec369c7cb5faa8fa6cc9a771.png

来源:公众号【五分钟学算法】

前言

本文全长 14237 字,配有 70 张图片和动画,和你一起一步步看懂排序算法的运行过程。

预计阅读时间 47 分钟,强烈建议先收藏然后通过电脑端进行阅读。

No.1 冒泡排序

冒泡排序无疑是最为出名的排序算法之一,从序列的一端开始往另一端冒泡(你可以从左往右冒泡,也可以从右往左冒泡,看心情),依次比较相邻的两个数的大小(到底是比大还是比小也看你心情)。

536c43870a69f2b3dced91b392164f9d.gif

冒泡排序动图演示

图解冒泡排序

以 [ 8,2,5,9,7 ] 这组数字来做示例,上图来战:

从左往右依次冒泡,将小的往右移动

810f90612a201fc32b508be7098eae73.png

冒泡排序1

首先比较第一个数和第二个数的大小,我们发现 2 比 8 要小,那么保持原位,不做改动。位置还是 8,2,5,9,7 。

指针往右移动一格,接着比较:

cf9cb0bdfd617fcca5c58a0e59818c8b.png

冒泡排序2

比较第二个数和第三个数的大小,发现 2 比 5 要小,所以位置交换,交换后数组更新为:[ 8,5,2,9,7 ]。

指针再往右移动一格,继续比较:

28e74a4968154b477f35a6db78e32945.png

冒泡排序3

比较第三个数和第四个数的大小,发现 2 比 9 要小,所以位置交换,交换后数组更新为:[ 8,5,9,2,7 ]

同样,指针再往右移动,继续比较:

523ed8f886629cd2d7b43f341aebcc9c.png

冒泡排序4

比较第 4 个数和第 5 个数的大小,发现 2 比 7 要小,所以位置交换,交换后数组更新为:[ 8,5,9,7,2 ]

下一步,指针再往右移动,发现已经到底了,则本轮冒泡结束,处于最右边的 2 就是已经排好序的数字。

通过这一轮不断的对比交换,数组中最小的数字移动到了最右边。

接下来继续第二轮冒泡:

4369c3999e4f083dc1f2149c46da40ad.png

冒泡排序5

919e0542a5b6b48502d05e4b3e400d1c.png

冒泡排序6

e811a126333d2723284b06715f7b96c7.png

冒泡排序7

由于右边的 2 已经是排好序的数字,就不再参与比较,所以本轮冒泡结束,本轮冒泡最终冒到顶部的数字 5 也归于有序序列中,现在数组已经变化成了[ 8,9,7,5,2 ]。

9847f04fd09cdc2d854058b42b7f648f.png

冒泡排序8

让我们开始第三轮冒泡吧!

b22d1c7e25aaf8d22004bb8fa1d174de.png

冒泡排序9

8159ef187d4dca6465159eb26630b7e1.png

冒泡排序10

由于 8 比 7 大,所以位置不变,此时第三轮冒泡也已经结束,第三轮冒泡的最后结果是[ 9,8,7,5,2 ]

紧接着第四轮冒泡:

d035ae7e12bbc5acc809a427dec6d9e4.png

冒泡排序11

9 和 8 比,位置不变,即确定了 8 进入有序序列,那么最后只剩下一个数字 9 ,放在末尾,自此排序结束。

代码实现
public static void sort(int arr[]){    for( int i = 0 ; i 

冒泡的代码还是相当简单的,两层循环,外层冒泡轮数,里层依次比较,江湖中人人尽皆知。

我们看到嵌套循环,应该立马就可以得出这个算法的时间复杂度为O(n2)。

冒泡优化

冒泡有一个最大的问题就是这种算法不管不管你有序还是没序,闭着眼睛把你循环比较了再说。

比如我举个数组例子:[ 9,8,7,6,5 ],一个有序的数组,根本不需要排序,它仍然是双层循环一个不少的把数据遍历干净,这其实就是做了没必要做的事情,属于浪费资源。

针对这个问题,我们可以设定一个临时遍历来标记该数组是否已经有序,如果有序了就不用遍历了。

public static void sort(int arr[]){    for( int i = 0;i 

No.2 选择排序

选择排序的思路是这样的:首先,找到数组中最小的元素,拎出来,将它和数组的第一个元素交换位置,第二步,在剩下的元素中继续寻找最小的元素,拎出来,和数组的第二个元素交换位置,如此循环,直到整个数组排序完成。

至于选大还是选小,这个都无所谓,你也可以每次选择最大的拎出来排,也可以每次选择最小的拎出来的排,只要你的排序的手段是这种方式,都叫选择排序。

853f3acb4a341861dbaa573ca23ff3cd.gif

选择排序动画演示

图解选择排序

我们还是以[ 8,2,5,9,7 ]这组数字做例子。

第一次选择,先找到数组中最小的数字 2 ,然后和第一个数字交换位置。(如果第一个数字就是最小值,那么自己和自己交换位置,也可以不做处理,就是一个 if 的事情)

3a7964bd9c9753e03fc265b9872948dd.png

选择排序1

第二次选择,由于数组第一个位置已经是有序的,所以只需要查找剩余位置,找到其中最小的数字5,然后和数组第二个位置的元素交换。

fd1a90a10033b2fc9d1db69744ff8b14.png

选择排序2

第三次选择,找到最小值 7 ,和第三个位置的元素交换位置。

340f27ad75bcfdf9c68c66104d344c0c.png

选择排序3

第四次选择,找到最小值8,和第四个位置的元素交换位置。

ec63ab62f7f2092ce112983b04a9e094.png

选择排序4

最后一个到达了数组末尾,没有可对比的元素,结束选择。

如此整个数组就排序完成了。

代码实现
public static void sort(int arr[]){    for( int i = 0;i 

双层循环,时间复杂度和冒泡一模一样,都是O(n2)。

No.3 插入排序

插入排序的思想和我们打扑克摸牌的时候一样,从牌堆里一张一张摸起来的牌都是乱序的,我们会把摸起来的牌插入到左手中合适的位置,让左手中的牌时刻保持一个有序的状态。

那如果我们不是从牌堆里摸牌,而是左手里面初始化就是一堆乱牌呢? 一样的道理,我们把牌往手的右边挪一挪,把手的左边空出一点位置来,然后在乱牌中抽一张出来,插入到左边,再抽一张出来,插入到左边,再抽一张,插入到左边,每次插入都插入到左边合适的位置,时刻保持左边的牌是有序的,直到右边的牌抽完,则排序完毕。

5a4050b8419b7de413894c41da89f4b5.gif

插入排序动画演示

图解插入排序

数组初始化:[ 8,2,5,9,7 ],我们把数组中的数据分成两个区域,已排序区域和未排序区域,初始化的时候所有的数据都处在未排序区域中,已排序区域是空。

05b1d42b3d210271fd614df78a28bee8.png

插入排序1

第一轮,从未排序区域中随机拿出一个数字,既然是随机,那么我们就获取第一个,然后插入到已排序区域中,已排序区域是空,那么就不做比较,默认自身已经是有序的了。(当然了,第一轮在代码中是可以省略的,从下标为1的元素开始即可)

b3b8da61e59e3fbe8c41a5267b0e0faa.png

插入排序2

第二轮,继续从未排序区域中拿出一个数,插入到已排序区域中,这个时候要遍历已排序区域中的数字挨个做比较,比大比小取决于你是想升序排还是想倒序排,这里排升序:

7d2b6011ae64c9298eb45aa60429fe3e.png

插入排序3

第三轮,排 5 :

2a7584c56b4bb301396048a25ee4c26c.png

插入排序4

第四轮,排 9 :

3d9d9ce7d87c96a57c7ea2ee3360fb89.png

插入排序5

第五轮,排 7

98db78e20979ffbfd8636cc48efa9d50.png

插入排序6

排序结束。

代码实现
public static void sort(int[] arr) {    int n = arr.length;    for (int i = 1; i = 0; j--) {            if (arr[j] > value) {                arr[j+1] = arr[j];//移动数据            } else {                break;            }        }        arr[j+1] = value; //插入数据    }}

从代码里我们可以看出,如果找到了合适的位置,就不会再进行比较了,就好比牌堆里抽出的一张牌本身就比我手里的牌都小,那么我只需要直接放在末尾就行了,不用一个一个去移动数据腾出位置插入到中间。

所以说,最好情况的时间复杂度是 O(n),最坏情况的时间复杂度是 O(n2),然而时间复杂度这个指标看的是最坏的情况,而不是最好的情况,所以插入排序的时间复杂度是 O(n2)。

No.4 希尔排序

希尔排序这个名字,来源于它的发明者希尔,也称作“缩小增量排序”,是插入排序的一种更高效的改进版本。

我们知道,插入排序对于大规模的乱序数组的时候效率是比较慢的,因为它每次只能将数据移动一位,希尔排序为了加快插入的速度,让数据移动的时候可以实现跳跃移动,节省了一部分的时间开支。

f97faf71ab96abf2ac0e8855138a53a6.gif

希尔排序动画演示

图解希尔排序

待排序数组 10 个数据:

68a5ac176340fae1f089b35dcb8ef76e.png

希尔排序1

假设计算出的排序区间为 4 ,那么我们第一次比较应该是用第 5 个数据与第 1 个数据相比较。

373137d948dee0bcdd444f43f3b7d08b.png

希尔排序2

调换后的数据为[ 7,2,5,9,8,10,1,15,12,3 ],然后指针右移,第 6 个数据与第 2 个数据相比较。

c6fe422b7d499376d1b8144c4908d733.png

希尔排序3

指针右移,继续比较。

9e022b7d6143adcf2f3d25f731d24f6c.png

希尔排序4

3bb3dfcea48d150f1213c014cfebb1f6.png

希尔排序5

如果交换数据后,发现减去区间得到的位置还存在数据,那么继续比较,比如下面这张图,12 和 8 相比较,原地不动后,指针从 12 跳到 8 身上,继续减去区间发现前面还有一个下标为 0 的数据 7 ,那么 8 和 7 相比较。

00d8fc901f8dcf59cb6a3802ff3000d4.png

希尔排序6

比较完之后的效果是 7,8,12 三个数为有序排列。

fa95263bf4436cf8be5ae77a3d3b6c48.png

希尔排序7

当最后一个元素比较完之后,我们会发现大部分值比较大的数据都似乎调整到数组的中后部分了。

假设整个数组比较长的话,比如有 100 个数据,那么我们的区间肯定是四五十,调整后区间再缩小成一二十还会重新调整一轮,直到最后区间缩小为 1,就是真正的排序来了。

3fb2778ff0d48fd574d944a3d076f27d.png

希尔排序8

指针右移,继续比较:

fd26efe76063c14509bc0307f18a6234.png

希尔排序9

重复步骤,即可完成排序,重复的图就不多画了。

我们可以发现,当区间为 1 的时候,它使用的排序方式就是插入排序。

代码实现
public static void sort(int[] arr) {    int length = arr.length;    //区间    int gap = 1;    while (gap  0) {        for (int i = gap; i = 0 && arr[j] > tmp) {                arr[j + gap] = arr[j];                j -= gap;            }            arr[j + gap] = tmp;        }        gap = gap / 3;    }}

可能你会问为什么区间要以 gap = gap*3 + 1 去计算,其实最优的区间计算方法是没有答案的,这是一个长期未解决的问题,不过差不多都会取在二分之一到三分之一附近。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值