数据结构(9)排序

一、常见排序算法

排序在生活中无处不在,上学这么多年班级排名啥的总有吧,不可能一次都没见过;打游戏有的排行榜不也是有排序的思想在里面,排序倒不是什么特殊的数据结构,但是是非常重要的算法思想,所以在初阶数据结构的最后,学习一下常见的几种排序算法。

当然,有些算法已经实现过了,比如冒泡排序和堆排序,到时候可以重写复习一下,但是不再展开具体细节了。

二、插入排序

1.直接插入排序

直接插入排序的思想最经典的对比就是打斗地主,抓牌以后习惯性的就是相同的牌放在一起,一般玩的多的老手也会顺手按照升序或者降序排列(当然,按照牌规大小放不一定跟数值升降序一样)。这就是最常见的插入排序。

当然,实际到对数组的排序肯定是有细节上的差异的,之前我们也大大小小写过排序的代码,排序函数的参数也就只数组arr和数组内元素个数,肯定不可能真的存在一个一个插的情况的,但是我们可以自己想想出来一个一个插。

怎么想象出来呢,比如给一个数组:

想想数组为空,现有数组的数据都是要一个一个插入进去的,所以刚开始是:

当然,只看第一个肯定看不出来什么门道,接下来继续往里插入:

有数据的话再插入肯定就得比较了,将{}类比成手,那里面就是牌,牌肯定按大小排列好看一点。

继续:

一比较,直接放

比较后前移:

前面几个例子只能体会到一个一个往后遍历的循环,但是想象中插入比较交换的循环却没能体会到,这个例子就得好好看了。

可见插入(想象出来的)比较,交换实际上也是一个循环。

所以代码思路基本就有了,外层循环依次遍历数组中的元素,内层循环使这次遍历到的元素与前面的元素进行比较,如果不符合想要的顺序,那就产生交换,而且比较和交换不止一次,有两种情况会停止比较和交换,一个是已经符合顺序了(比如上面的9,6),另一个是把已经排序(想象中一个一个插入)的数据比较完了,那就该停手将元素放到数组首元素的位置了。

思路一有具体细节代码见:

外层循环遍历数组都写臭了,倒是没什么好说的。

但是内层循环每次都得有已经排序过的数据的下标,初始值倒是好算,比如这个例子:

最开始插入2,2是待排的(或者说待插的)数据,下标就是i,那i的前一个元素下标也好算,直接i-1即可,但是这个例子很明显不是比较了一次,而是比较了多次,那么每次下标都得计算:

这样的话就不得不创建一个变量专门存储这次比较的两个数的下标了,这个就有点像冒泡排序内层循环了:

不妨以end为已排序数组的最后一个元素,可知每次进入循环end = i - 1。

那么这次比较的就是end和end + 1(为什么不用i,进循环走几次就理解了)。

如果想要实现交换肯定得先临时存下来2,也就是进入循环不仅要计算下标end,也需要记录下这次插入进的元素。

不过2也别着急放,9一比较往后放是就算了,2可不一定比较一次就放了,很明显:

一比较2又该前移,或者说6该后移:

循环比较,直到:

比的不能再比了,再比较越界了,那就直接跳出循环,所以进入循环的条件是end>=0。

当然,最后别忘了跳出循环时候把存起来的值放到end+1的位置。

直接写代码:

内层循环就是以end作为循环的条件,如果需要换那就把end位置的后移,如果不需要换就结束循环,最后在arr[end + 1]处放置这次插入的数据,思路里面说的非常清楚了。

测试代码:

为了方便查看就写了个Print函数,不多说。

经测试直接插入排序无误。

时间复杂度

如果按照最坏的肯定是如果想排升序,给的降序数组,这样的话如果数组大小为n,那么前移的次数就为1 + 2 + ...+ n - 1所以最坏情况下直接插入排序的时间复杂度就是O(n^2)(但是注意,这种情况实在是太少太少,所以一般直接插入排序倒不至于是O(n^2),一般比这个小)。

2.希尔排序

直接插入排序如果遇到与目标顺序完全相反的数组序列的话,或者说假如你要排升序序列,结果给的数据是这样的:

大的在前小的在后,那么这样也基本n^2的时间复杂度。

为了解决这样的问题,人们在直接插入排序的基础上就创造出了希尔排序。

希尔排序的思想如下:

选定一个整数gap(这个gap是步距的意思,具体到例子里就明白了),依据gap将数组分成gap组,对每个gap组进行直接插入排序,所有gap组直接插入排序完以后进行某种运算(一般为gap = gap / 3 + 1,初始为gap = n / 3 + 1,只是为了缩小步距),当gap缩小为1是则为直接插入排序。

可见希尔排序在直接插入排序的思想上提前进行了多次预排序,避免出现与待排序顺序完全相反的序列。

注意,这里的gap变化不是依据gap = gap / 3,只是画图辅助理解gap的含义。

gap是5,所以就往后数5个数,第gap个正好就是同一组的。

gap理解了以后就得一点一点想代码怎么写了:

以这个为例,先考虑一次排序怎么做。

错误想法

既然每次排序的每组都是以直接插入排序实现的,直接考虑考虑怎么套直接插入排序看看:

刚上去i = 0肯定也不用移,但是可以知道,如果要进行下一次直接插入排序,不能再i++,如果直接i++就成另外的组了,所以循环变量的变化应为i += gap,同样由此end每次不能再--而是end -= gap,比较的内层循环也免不了改end初始化为i - gap,后移代码改为arr[end + gap] = arr[end]。

两层循环的条件再检查检查:

for循环只要不超出数组范围即可:

while循环

这么一看还是不越界就行。

但是这才是一组的排序:

如果好几组的话还得再嵌套循环,容易发现gap是几有几组,所以用for循环更好:

gap也得变啊,还得套循环计算gap去:

咱就不说时间复杂度到底多少了,就这么多层循环,也少不到哪去,肯定比直接插入排序大,而且说实话,两层循环的时候调试或者改代码都难读的很,这都4层了,那还不得炸了。

正确做法

所以上面的思路肯定行不通,毕竟希尔排序可是直接插入排序的优化,索性这么干:

不套直接插入排序的代码,依次遍历实现直接插入排序,什么意思呢?

外层for循环还是遍历整个数组,内层循环实现组内比较和互换即可,画图理解:

这样分组i = 0和i = 1没啥好看的都是组里第一个元素,也不比也不移,直接从i = 2开始。

明显end初始化就为i - gap,需要存的temp还是i所指元素,进入while循环显然的比较对象应该是end和end - gap,且end不能越界,end如果是下标为0,再让arr[end]和temp比较,故循环条件为end >= 0,内层比较条件和循环变量注意即可。

最外层gap注意循环条件即可,最内层弄清楚下标。

时间复杂度

由于希尔排序时间复杂度实在太复杂(可不要以为三层循环就是n^3),可以说到了算无可算的地步了,只需要知道肯定比直接插入排序的n^2低即可。

三、选择排序

1.直接选择排序

直接选择排序就是一次次的遍历数组,把这次遍历到的数组最大和数组最小放在数组的头和尾,直至遍历完整个数组。

还是以这个数组为例:

遍历数组肯定免不了用下标,而下标起名也好起:

从两边往中间遍历,这样的话,最后找到min和max就能直接给begin和end位置的数据赋值。

细节直接看代码:

一次选择排序选最大选最小,比较需要注意的是for循环的起始条件和循环的限制。

因为min和max初始为begin所以遍历的时候不需要自己跟自己比较了,并且越界条件是超出未排序元素的下标(已排序的已经保证比现在待排的最大或者最小了)。

只不过在遍历的时候出了一点意外:

如果按照我们写出来的代码来看,arr[begin]和arr[min]互换一次,arr[max]和arr[end]互换一次,这样不就相当于没有换嘛,所以这种begin是max,end是min的前情况得想办法克服。

要不然如果碰见就交换一次,但是有点小麻烦,干脆如果max刚好是begin,min刚好是end,就还是先交换,但是max换为了得让max跟上:

最后外层while循环条件基本上就得往begin和end的关系靠一靠了:

可以很明显看出来,相等就不用再排了,同样的,如果begin比end还大,那也不用排了,所以循环条件是:

最后代码:

测试代码:

时间复杂度

基本上看来是这样的,因为我们这个也算是改进过的直接选择排序(一般直接选择排序每次找最小的放到前面就行了),进入循环的次数是n - 1 + n - 3 + n - 5 +......+0(当然,最后不一定是0),再估算基本就是O(n ^ 2)。

2.堆排序

堆排序代码直接手打了:

测试代码:

时间复杂度

没啥好说的nlogn。

四、交换排序

1.冒泡排序

接触过的第一个排序算法,那时候还在学C,现在数据结构初阶都快干完了。

不多bb:

测试代码:

时间复杂度

n^2,经典的循环套循环。

2.快速排序

快速排序人如其名,可以说没有什么突出的特点,就是一个快字,借助的思想就是二叉树的思想,可不是跟堆排序一样,时时刻刻都在保证数组就是个堆,只不过如果画图的话,看起来像一棵二叉树。

核心思路就是选取一个基准值,依照基准值的大小,将数组序列分成左右两个子序列,左子序列是比基准值小的值(不一定有序),右子序列是比基准值大的值(不一定有序)。重复此过程,最终,所有元素都会排列到相应位置。

基准值倒是可以随便选,但是基准值可不是生来就是左序列比它小,右序列比它大,我们得想办法实现出来。

当然,这个操作的实现可不止一种,所以接下来慢慢讲:

①hoare版本

比如这样一个序列:

咱也不乱弄,就让数组第一个元素当基准值,接下来就借用两个指针和一个哨兵:

key站在这次的基准值

left指针从左往右遍历,找比基准值要大的数

right指针从右往左遍历,找比基准值要小的数

为什么left和right要这么做呢?

其实是如果在key左边有比key大的数,肯定得放到key后面;如果在key右边有比key小的数,肯定要放在key前面。但是在确定key位置前,很明显的我们不知道该把left往哪放才算往后放,right往哪放才算往前放,所以这时候就要swap:left和right。

我们可以遍历试试:

每次判断left和right的指向值,不符合我们找的值就left++right--。

交换过以后也要++--。

相等也要继续遍历:

但是这么一找,明显越界了,所以一旦越界了,就该插入key了,因为我们已经整理好左右子序列了,这就俩指针,一个right,一个left,我也不知道为啥,直接跟right交换就行。

这样一次找key就结束了,结果是这样的:

key指向的元素归位了,接下来就是循环这两个子序列了,基本操作是一样的,key就是子序列最开头的元素:

确定好区间:


刚才光顾着讨论跳出循环以及什么时候互换,忘了说一个玩意,我们left和right指针遍历的应该是除了key以外的元素,毕竟key所指元素一直要被比,所以left初始弄个key + 1或者说key就是此次数组的第一个元素,left再++。

循环截止的条件也能看出来点门道,如果只有一个元素,或者说没有元素的话,直接就可以停止循环了,当然,带上下标更清楚一点:

如果再分,就是[4,4]和[6,5]区间,很明显,一个是左等右,一个是区间不存在。

最左侧的还得再分,但是最后肯定可以得到:

成功实现了排序,从这里也可以看出来类似于二叉树的左右子树一样,所以干脆我们还用递归写代码:

快速排序的参数讲究的有点多,因为我们快速排序的时候要的是区间,有的人一看就说,你传n以后自己计算区间从哪到哪不完了,但是递归呢,递归以后还要自己计算吗,这个left和right很好的代表了区间的左右,而不仅仅是为排序数组的首元素下标和尾元素下标。

放key的实现讲究太多,单独封装成函数来看:

right和left的移动可不止一次,所以直接给一个while循环,while循环去找right和left目标值是没错,但是在这之前还得保证不能越界:

这里就是很好的例子,一越界就停手吧,虽然成功找到了比key小的数,这样再交换岂不成无底洞了,早晚right得到key的位置,这其实是我们不想看到的。

另外,碰见相等的也不能等,有这样的例子:

如果写了等号,那就会疯狂的向左遍历right指针,一越界就停止,这样会导致递归的树稳定为n层。

即:

不写等号可能对半分分递归就结束了,递归的次数基本上就是能分出来的二叉树的层数,但是一旦写上等号,不说right稳定得遍历到数组最左端,光说产生的递归的树都得n

即logn变n。

②lomuto版本

lomuto版本的主体思路大概如下:

用两个指针,一个是pcur,一个是prev,prev指针初始指向pcur指针前一个。pcur的作用是用来遍历整个数组,找比key要小的值。

具体细节如下:

pcur指针往后遍历,寻找比key指向的值要小的值。

1)如果找到了,prev++,并将此时prev和pcur指向的元素互换,互换完以后prev继续站岗,pcur++

2)如果找不到,pcur++

比如这样的例子:
符合交换条件,prev++,再swap掉prev和pcur

但是遇到prev == pcur就没有必要再互换,直接pcur++就行。

循环遍历:

又是相等不用变,pcur++

这次遍历到的很明显就比key要大了,那就不管它继续往后遍历,直到碰见比key小的。但是在此之前,观察到prev++后指向的要比key要大,所以这种遍历prev后面的要不然就直接是pcur指向的,不用互换,要不然就是比key要大的,一跟pcur互换不就是实现了小在key前,大在key后之类的操作吗。

展示其中一次不相等的互换:

找到了就prev++

swap

swap结束pcur++

也不废话了,直接展示遍历完以后:

这时如果swap key和prev所指向:

刚好实现基准值前比它小,基准值后比它大。

代码实现:

当然,内层if嵌套可以合并一下:

递归的代码不变:

测试代码:

递推版本快速排序的时间复杂度和空间复杂度的分析

时间复杂度基本上得类比我们做二叉树oj题的思路,因为代码产生了递归,可知,时间复杂度 = 递归次数*每次递归的函数的循环次数。

但是对于我们这段代码时间复杂度却不能死板的套公式去算:

如果按照公式顺着走下去的话,倒是可以硬解,假设结点刚好能够凑够满二叉树,每层还都能对半分序列,这样的话有:

每层都是结点个数 * 每个子序列时间复杂度(因为相当于遍历整个数组,所以基本等于结点个数)

1 * n + 2 * n / 2 + 4 * n / 4 + ......+ * 1

* 1基本也是O(n)

有多少层呢logn层

所以时间复杂度为O(nlogn)。

当然,可以用分割的思想,如果一层一层来看的话,其实每层子序列合起来刚好就是原数组的长度,这样想的话很容易想到每层时间复杂度都是O(n),并且有logn层,很容易得到时间复杂度就是O(nlogn)。

空间复杂度一般是不必多说的,但是对于这种类似于在栈区构建出来函数二叉树的代码还是来见见世面。这种递归代码时间复杂度的产生就是因为函数栈帧的创建:

如果没有回归的话,就会从一个函数栈帧跳跃到另一个函数栈帧里去,这样的话很容易想到,总有一条路是最长的,比如其它最多分两三层,它能分5层,这样得创建5次函数栈帧,其实这个最大值应该就是树的高度,我们估算的话就是logn层嘛,所以空间复杂度就是O(logn)。

③非递归版本------借助数据结构栈

栈里面存的是本次需要找基准值的区间,通过入栈出栈实现对不同区间的基准值的查找以及递归区间的分割。

找基准值我们实现了两种方法,所以找基准值到时候直接写就行,问题是我们如果通过栈实现类似于函数递归的效果呢?

比如现在第一次找到基准值以后,就应该分开左右子序列,按照上面所说,我们分别需要在栈里存0,4和6,9并分别在子序列中寻找新的基准值,后面的不再口头描述,直接看到最后:

带上栈:

左边界大于右边界的就写了一个,只要key在边缘就会产生,可知,如果子区间左值等于右值,或者子区间左值大于右值,都是无需再排列的子序列,不再入栈。

代码表达:

测试代码:

五、归并排序

1.归并排序基本思路

归并排序基本思路就是把不断的把得到的数组二分拆开,得到有序的子序列后再不断合并得到最终的有序序列。

比如上图,最开始输入的序列基本都是无序的吧,不断的二分,使得数组最终被分割为一个又一个的元素,如果只看每个元素的话,每个元素自己都是有序的,接下来合并,思路就是合并两个有序数组,这个思路在OJ题里见识过,大致思路还是一样的。最终得到的数组就是有序的。

而分割数组类似于快排每次找完基准值后的分割,所以可以尝试用递归实现,即大体步骤为:

递归分割和合并两个有序数组

2.代码实现

递归分割

很明显如果想要分割,看成区间的话,区间左端和右端都得十分清楚,才能算出来区间中点,所以递归的函数我们就传左端点和右端点的值。

看着图其实很容易写出来代码,实际上这个分割类似于快排,不是真的分割,只不过给的区间导致能访问到的元素类似于分割的效果。

递归有递推肯定还得有回归,回归条件很明显,如果这次区间左等右,也就是只有一个元素的时候,就该回归了。

拆分完全以后就可以两两进行合并了:

就看这两个序列,合并两个有序数组的代码肯定免不了循环,因为需要往目标数组里存放的值肯定不止一个,而且如果一个数组遍历完以后,就应该停下来,防止越界比较,这样肯定会存在一个数组放完了,另一个数组还没有访问,所以得三个循环才能保证最终两个数组的元素都被有序的放到了目标数组中。

有思路了,写代码前我想应该画一下图,看看执行思路大概是什么:

我们从单个的元素的序列合并成两个元素的序列我们采取原地修改会将原数组顺序改成:

现在有左序列区间[left,mid]右序列区间[mid + 1,right],这时候就发现了一个问题:

不管是找两个序列的最大的放到序列末尾,还是找到最小的放到序列开头,都可能会导致未排序的数据被覆盖,如果这样肯定排序就会失败。

我们每次放之前存一下的话等于还得用一个变量index去记录这次该放的位置的下标,以及temp保存下标对应的元素,这样的话每次取出来的可就不只是有序数组里的两个元素了,也就是每次放都得考虑两个有序数组里取出来的和temp里存放的。

这么写又出现问题了,假如现在插入小的:

你temp存的就应该是6对吧,存6以后遍历左边子序列的指针肯定不能再指向6了,得指向temp,但是如果指向temp的话你该怎样遍历原子序列6后的10呢,难不成还得再存一下吗?

所以原地插入实在是太麻烦了,我们OJ题里可以实现原地插入纯纯是因为当时的题给的有空位:

所以才往空的地方先插大的。

原地实现归并排序太麻烦为何不尝试一下创建一个等大的临时数组呢?

递归函数分割的时候确实不需要temp这个临时数组,但是一旦开始合并就需要了,所以索性递归函数加上temp这个参数,而排序结束及时归还操作系统我们申请的空间,所以归并排序主函数逻辑就是如此了,那么细节就是看递归函数了:

分割其实还是不用变,但是一旦分割到头,如:

这里就是左右子树都分割好了,就得合并了,直接就着现有指针实在是太抽象了,所以我们不妨创建几个变量:

而且注意,我们的思路是拿着有序序列,检测着先后顺序往temp里放,一旦组合完成就返回给arr,所以我们循环内部判断大小引用的是arr里的值(如果刚分割完还没放,temp为空也没办法访问,都是随机值)

但是写着写着发现又有问题了,如果以遍历子序列的指针遍历temp数组的话,就会很明显的发现,你begin1小那放begin1,遍历过了就++,但是如果begin2大的话,begin1现在指向的元素实际上是没有插入temp里,但是你为了遍历temp的指针放到正确的位置,还是++了,所以现在就应该单独给遍历temp数组创建一个变量:

这样就合理多了。

我们说过了,这个循环结束一定还有元素未放:

最后把temp里的元素给arr复制过去:

选择用memcpy,但是不知道得传多大空间,所以画图看一下:

很明显(right - left + 1) * sz(int)个字节。

但是还有细节,如果遍历到这里:

arr是数组首元素的地址,这两个数组合并的话应该是从temp + left开始逐个从arr + left开始复制,不然只会去修改从首元素开始的right - left + 1个数据。

所以最终代码:

测试代码:

时间复杂度和空间复杂度分析

时间复杂度还是和快速排序类似,毕竟都是分树,树每层总得时间复杂度就是O(n),容易得到树的层数就是logn,所以时间复杂度就是O(nlogn)。

空间复杂度直接看总的代码:

一个是temp,一个是递归函数的函数栈帧的创建,很明显O(n + logn),那么空间复杂度就是O(n)。

六、测试所有比较排序的性能

有这样一段代码:

创造十万个数据来查看具体时间的快慢,与时间复杂度对照:

直接插入排序最坏是O(n^2),但是最坏情况是小的数据都在后面,大的数据都在前面,实际情况下其实不一定达到,所以直接插入排序时间复杂度实际达不到的多。

希尔排序很少讨论它的时间复杂度,因为希尔排序时间复杂度不好计算,一般认为O(nlogn~n^2)

直接选择排序我们实现的时候是用两个指针,一个找最小,一个找最大,其实每次都遍历完了、最后不管数据什么情况都是O(n^2),但是由于数据量太小,所以显得我们直接插入排序时间复杂度其实小于冒泡排序,但是多次试验以后基本都是冒泡的一半。

堆排序不必多说,O(nlogn)深入人心。

快排归并都刚写,O(nlogn)

冒泡排序典型循环套循环,O(n^2)毋庸置疑。

七、计数排序

1.原理即实现

实际上我们常见的排序是八大排序,但是我们见得比较排序只有其中啊,插入排序的直接插入排序和希尔排序,选择排序的直接选择排序和堆排序,比较排序的冒泡排序和快速排序,以及归并排序。

既然计数排序不属于比较排序,难道属于不比较排序,吗,但是这其实有点匪夷所思,不比较大小我怎么能知道哪个大哪个小,不妨来看一下计数排序:

计数排序的原理是运用哈希直接定址法对相同的数据进行计数,并最终将计数结果还原到原数组里。

具体操作如下:

从小到大计数:

1:2

2:2

4:3

6:1

9:1

创建一个数组,专门用来存储计数的结果,其中下标为原数据,数组元素为计数数据:

当然,空白的地方不是没有数据嘛,就存0,所以到时候要不然calloc,要不然malloc以后memset,反正动态开辟的数组默认值为0。

遍历我们的计数数组,写一个循环,外层循环的次数是数组元素大小,内层不断往原数组里塞下标,直至遍历完整个计数数组。

即:

老说什么哈希映射,哈希不哈希不管,确实是体会到了映射。

但是写代码之前再整一个例子,因为开辟的数组空间大小还有个讲究:

如果仍旧按照最大元素的值创建数组,按照下标照应原数值的原理,我们realloc的大小实际上是max + 1,因为数组下标是从0开始的嘛,这样就会造成其实基本100个空间的浪费,因为0~99根本不会存放元素,当然,下标100往后也不是每个都存,但是至少可以接受那么一点点浪费。

所以一般会做此处理:

写一个循环遍历整个数组,找出最大值最小值,根据最大值和最小值的差确定数组大小,比如这里的max = 109,min = 100,假设这中间每个元素都存在的话就是max - min + 1个元素,所以到时候数组的大小就得搞个max - min + 1个元素。

这时候往里存肯定就得先-min,不然下标不好照样,到时候还原的时候+min就行,比如这个例子

处理后应为:{0,1,9,5,1,5}

如果存起来的话应该是:

0:1

1:2

5:2

9:1

到时候还原的时候下标+min就行。

思路清楚代码表达:

说啥做啥,所以代码不再解释。

可以看到其实还是比较了,至少遍历整个数组给出了最大值和最小值,只不过正儿八经改变数组的时候确实不需要比较。

测试代码:

2.限制

分析计数排序的时间复杂度可以得到:

O(n + range)

复杂度其实不老好分析,毕竟range取决于所给序列的最大值和最小值的差值。

所以为使时间复杂度趋近于O(n),一般只适用于数据大小比较密集的序列。

八、所有排序算法时间复杂度及稳定性分析

|--------|---------------------------------------------------|------------------------------------------|----------------------------------------|---------|-----|
| 排序⽅法 | 平均情况 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
| 直接插入排序 | O() | O(n) | O() | O(1) | 稳定 |
| 希尔排序 | O(nlogn) ~O() | O() | O() | O(1) | 不稳定 |
| 直接选择排序 | O() | O() | O() | O(1) | 不稳定 |
| 堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
| 冒泡排序 | O() | O(n) | O() | O(1) | 稳定 |
| 快速排序 | O(nlogn) | O(nlogn) | O() | O(logn) | 不稳定 |
| 归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |

其中,关于稳定性的定义:

若待排序序列中存在多个​ 值相同的元素(如:r[i] = r[j]),且排序前r[i]位于r[]j之前,而排序后r[i]仍然位于r[j]之前,则称该排序算法是​ 稳定的​​;反之,若排序后相同元素的相对次序可能改变,则算法是​不稳定的​。

目前见的排序算法的场景太少,其实还理解不了稳定不稳定到底有什么用。

除此之外,给出例子证明排序的算法不稳定:

直接选择排序:5 8 5 2 9

希尔排序:5 8 2 5 9

堆排序:2 2 2 2

快速排序:5 3 3 4 3 8 9 1 0 1 1

数据结构初阶完,后续还会对数据结构初阶文章进行补充修改。

相关推荐
黑听人28 分钟前
【力扣 中等 C++】90. 子集 II
开发语言·数据结构·c++·算法·leetcode
黑听人1 小时前
【力扣 简单 C】21. 合并两个有序链表
c语言·开发语言·数据结构·算法·leetcode
ling__wx1 小时前
go部分语法记录
数据结构
黑听人1 小时前
【力扣 简单 C】83. 删除排序链表中的重复元素
c语言·开发语言·数据结构·算法·leetcode
怀旧,4 小时前
【数据结构】7. 栈和队列
数据结构
W说编程6 小时前
算法导论第一章:算法基础与排序艺术
c语言·数据结构·算法
titan TV man6 小时前
上海市计算机学会竞赛平台2022年5月月赛丙组最远城市距离
数据结构·算法
慢半拍iii14 小时前
数据结构——D/串
c语言·开发语言·数据结构·c++
怀旧,14 小时前
【数据结构】5. 双向链表
数据结构·windows·链表
会不再投降21914 小时前
《算法复杂度:数据结构世界里的“速度与激情”》
数据结构·算法