作者:几冬雪来
时间:2023年4月6日
内容:数据结构排序板块讲解
目录
前言:
在上一篇的博客中我们正式的进入了数据结构中的排序板块的学习,并且也认识了一种重要的排序方法——希尔排序。而今天我们将更加深入讲解数据结构排序部分的知识并且对希尔排序进行一个补充。
1.希尔排序的时间复杂度:
在上一篇博客中我们进行了对希尔排序的讲解,并且书写了有关希尔排序的代码。但是众所周知,要判断一个排序方法的快慢以及好坏,我们通常都是通过它的时间复杂度来进行断定。那么这里我们的希尔排序的时间复杂度是多少?
这里的希尔排序的时间复杂度并不是那么简单就可以判断出来的,这里我们通过画一个图来进行说明。
这里我们的第一层循环就不过多解释了,重要的是第二层循环在这里,当gap足够大,又或者是当排序过很多遍之后gap变为1的时候,在这种情况下我们的时间复杂度为O(N)。但是如图所示,这里的希尔排序的时间复杂度的量级可以算为O(N*logN),但是它实际上的时间复杂度并不是这个值。
这里通过一些网络上的资料可以得知:
我们也可以只记它的结论,将希尔排序的时间复杂度算作是n的1.3次方。
2.选择排序:
在讲解完了希尔排序之后,我们就要来学习新的排序方法了——选择排序。
在选择排序中我们还分直接选择排序和有技巧的选择排序。直接选择排序就类似我们生活中拍照时候的排升高,遍历一遍人群,然后把身高最矮的放在左边,把身高最高的放在右边。
这里我们来书写它的代码。
这就是我们选择排序的代码,首先在这里定义好我们的最左边的下标和最右边的下标。然后进入循环,这里再定义两个变量赋值为left(下标为0),接下来就是一个循环来进行判断,i指向下标为1的元素,如果i下标指向的元素小于mini下标指向的元素的时候,我们就将i赋值给mini,相反如果大于的话,我们就将i赋值给maxi。当我们的数组比较完了之后,这个时候的mini就是我们数组中最小的元素,maxi就是最大的元素,最后我们只需要对其与下标为left和下标为right与maxi和mini进行互换,最后left进行++操作,right进行--操作后进入下一次循环。
在这里我们给一组数组要求我们的选择排序将其排序好。但是这个时候我们看结果的话会发现,它并不是有序的,在6和7的中间莫名的多了一个3进去,那么这是怎么回事?
这里就是在交换的时候,mini的值和left的值进行互换,但是刚刚好那里也是下标为right的值的地址。后面再进行right和maxi的互换就是3和3之间进行互换,没有意义。
因此这里我们要对代码进行一个修正。
这里就可以解决我们的问题。而后面我们就可以不用进行修正,因为后面交换的话不会再影响到我们的数组排序。
3.选择排序的时间复杂度:
以上讲解了这么多关于选择排序的书写和讲解,但是很不幸的是,选择排序在排序这个大家族中可以说是最烂的一种排序方法,为什么?
可以看见,在选择排序中我们每一次循环就只是将两个数据交换到我们想要的位置而已,也就是我们经典的等差数列。那么选择排序最坏的情况为O(N^2),同时选择排序最好的情况时间复杂度也同样的是O(N^2)。 但是它的时间复杂度十分的稳定,不像希尔排序那样会发生时间复杂度的变化。
4.有技巧的选择排序:
通过上面分析直接选择排序的时间复杂度,我们可以断定它是一个很差的排序,但是选择排序中并不只有直接选择排序这个方法而已,这里我们通过选择排序来延伸出了另一种排序方法,这种排序方法也是我们的老朋友了——堆排序。
这里我们就不对其过多讲解了。
5.冒泡排序:
在讲解完了选择排序的两种排序方法之后,接下来就要到下一个排序的板块——交换排序。
而且在交换排序中首当其冲的就是我们的——冒泡排序。
这里就是我们冒泡排序的代码,就是让前一个数和后一个数进行比较,如果前一个数比后一个数大的话,我们就将其交换。这里我们的冒泡排序的时间复杂度也就是标准的O(N^2),但是我们可以对冒泡排序的代码进行一个优化,那从哪个方面开始优化它的代码呢?
这里如果在第一次冒泡排序的过程中,数组中没有数据进行交换,那也就是意味着我们的数组是有序的,这里跳出即可。
如果是这样的话,那么我们的冒泡排序的时间复杂度最好的就是O(N)。
6.直接插入排序与冒泡排序的对比:
这里冒泡排序和前面的直接插入排序的时间复杂度量级是一样的,那么如果在写排序的时候我们该选哪一个排序就成为了一个问题,这两个排序哪种更快呢?
这里首先要明白一件事情,两种排序的时间复杂度量级相同不能换算于它们的准确的时间复杂度就相同。
那么我们就来判断一下它什么时候时间复杂度相同,什么时候差的一点,又是什么时候差的最大。
如果是有序的话,这里都是O(N),但是当数组变得接近有序又或者是部分有序的话,这里直接插入就比冒泡排序要快。
7.快速排序(快排):
快速排序是我们交换排序中的一种排序方法。同样的也是我们排序学习中的一个重要的知识点,那么快速排序是怎么样运行的呢?
这里我们来讲一讲快速排序的方法——选出一个关键字/基准值命名为key,将其放到正确的位置上(最后排序要在的位置)。(注:key一般选数组最右边或者最左边的值)
这里我们就画一个图来表一下。
这里就是我们快速排序的运行原理,那么知道了它的运行原理之后,接下来就到我们去书写它的代码了。这里要记住,如果我们的key定义在左边,则我们需要右边先走,反之左边先走。
这里我们先运行一下代码来看看结果。
这里可以看见我们的代码并没有被排序好,这又是因为什么呢?如果遇到问题的话,第一时间就调试看看。
在这里可以看见,在这里我们的right找到了我们的小,但是这里的left的下标对应的是我们的6,而在循环中,它并不满足我们的条件。因此这里要对代码进行运行修改,当我们数组中出现和keyi下标对应地址的元素一样值的时候,我们还是要进行++或者--。如果在数组运行过程中不幸遇到了我们keyi下标对应地址相同元素的值的时候,这里我们的代码就会死循环。这里我们可以试验一下结果。
因为当数组中存在相同的值的时候,这里我们要找小和找大的代码就都不会进行,每次都会卡在这里不动,那要怎么解决?这里的解决方法就是——数组中存在相同的值,那么这个值我们可以不用去理会它。
因此我们只需要将其加上等于号就可以解决这个问题。
那么接下来我们就来讲解一下第二个要注意的点,这里我们画一个图来表示一下。
这里我们需要的是找小,但是因为在整个数组中我们并没有找到小,right就会一直--下去,最后导致越界访问,因此这里我们还要对其增加一个限制。
这里就是我们的限制。这里我们最前面的left++,也可能会导致我们的代码出现一些问题,那么是什么问题?
一样的我们还是画一个图出来表示一下。
如果我们的left++指向下标为1的时候,当我们right和left重叠之后,因为最下面的keyi和我们left(right)交换的代码,这里1和2之间还要进行一次调位。这里就过于多此一举了,解决这个问题的方法也是很简单,将其删除便可。当left和right都走到我们的下标为0的位置的时候,keyi也在下标为0的位置。这里就可以算是自己和自己进行交换。
在进行了诸多的修改之后,我们最后再来运行一次我们的代码。
从图中的结果来看,它也是十分符合我们预期的结果。
结尾:
到这里,这一期我们的博客就结束了,在这一期博客中我们的对排序的知识有了进一步的了解,并且也了解了博客中又一重要的排序方法——快速排序。在这后面我们还有一些其他的排序方法,并且针对我们的快速排序我们还有一些可以优化的点,这一些我们就留到下一节博客再来讲解。最后希望这篇博客可以给正在学习排序的同学一点帮助。