排序算法
希尔排序
希尔排序(Shell Sort)是插入排序的一种。也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本。希尔排序是非稳定排序算法。希尔排序是把记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,魅族包含的关键词越来越多,当增量减至1时,整个文件恰好被分成一组,算法便终止。
希尔排序过程
希尔排序的基本思想是 :将数组列在一个表中并对列分别进行插入排序,重复这个过程,不过每次用更长的列(步长更长了,列书更少了)来进行。最后整个表就只有一列。通过这样的步骤就可以得到一个有序的序列。
首先,假设我们有一组含有9个元素的序列为[54,26,93,17,77,31,44,55,20],想要对其进行希尔排序,第一步时选择合适的步长,这里我们通过9/2取整,设置步长gap=4
对每一个分组分别进行插入排序。得到第一次排序后的结果:
接着,再将gap调小为2(4/2),再对上面得出的序列进行分组插入排序:
接着继续调小gap=1(2/2),进行分插入排序:
最终就构成了完整的希尔排序过程。
确定了gap的大小后,就需要改善时间复杂度,达到算法优化的目的。
通过插入排序可以知道,插入算法的核心是:
i=1
if alist[i]<alist[i-1]:
alist[i],alist[i-1]=alist[i-1],alist[i]
else:
break
那么我们可以考虑,何如才能在代码现实时,尽量优化算法?之前学习插入排序时,我们知道,插入排序操作的时序列有序的一端:不断从无序序列中拿出元素插入到有序序列中,那么在希尔排序中,我们也可以将序列按照gap位置来分为两部分,对需要进行交换的元素进行表示。
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200727182530998.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5
希尔排序代码实现:
def shell_sort(alist):
"""希尔排序"""
n=len(alist)
gap=n//2#整除
#gap变化到0之前,插入算法执行的次数
while gap>0:#外层循环不会碰到gap之前的元素,因为通过gap和i就可以表示,因此外层循环j直接从gap开始表示
#插入算法,与普通插入算法的区别就是gap步长
for j in range(gap,n):
i=j
while i>0:
if alist[i]<alist[i-gap]:
alist[i],alist[i-gap]=alist[i-gap],alist[i]
i-=gap
else:
break
#缩短步长
gap//=2
if __name__=="__main__":
li=[54,26,93,17,77,31,44,55,20]
print(li)
shell_sort(li)#调用选择算法
print(li)
运行结果:
希尔排序的内层循环控制的是子序列中执行特定插入算法的比较和交换(确定单个元素到底放在哪一个位置),中间层循环控制的是遍历所有子序列中的所有元素。控制gap的长度又需要再加一层最外层循环,每次执行一次循环,gap就需要缩短一次。当gap取到1时,相当于一个插入算法。
动画演示:
时间复杂度
- 最优时间复杂度:根据步长序列的不同而不同
- 最坏时间复杂度:O(n2)
去特殊情况,gap=1时,就是一般插入排序算法,执行的时间复杂度就是O(n2) - 稳定性:不稳定
因为希尔排序存在元素分组交换的情况,当两个相等的元素被gap分成不同组进行排序是,就会有出现相等元素前后次序交换的情况,因此说希尔排序是不稳定的。
快速排序
快速排序(英语:Quicksort),又称划分交换排序(partition-exchange sort),通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
步骤为:
1.快速排序,选中第一个元素,作为参照元素,再设置两个指针,一个low只能指向比参照元素小的元素,一个high指针只能指向比参照元素大的元素。
2.当low指向的元素比参照元素大时,交换low和high指针指向的元素。
交换low和high指向的元素数据
3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。直到low和high指针指向位置重合
递归的最底部情形,是数列的大小是零或一,也就是永远都已经被排序好了。虽然一直递归下去,但是这个算法总会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。
快速排序特点:一次排序后,所有参照元素左边的元素都要比参照元素小,右边的都要比参照元素大,每一轮排序后都有一个元素存在在它最终的位置。
快速排序过程
这里取第一个元素作为参照元素mid_value=54.当high所指小于mid_value,就交换low和high所指元素。
if alist[high]<mid_value
alist[low]=alist[high]
因为第一次移动让high指向比mid_value小的元素到low所指位置,因此low一定比mid_value小,直接判断low的下一位。
if alist[high]<mid_value:
alist[low]=alist[high]
low+=1
if alist[low]<mid_value:
low+=1
elif alist[low]>mid_value:
alist[high]=alist[low]
high拿到元素后,需要直接向前移动一位,因为不需要判断high所指元素大小(一定比mid_value更大),当high比mid元素大时,high指针要不断向前移动位置。
if alist[high]<mid_value:
alist[low]=alist[high]
low+=1
elif alist[high]>mid_value:
high-=1
if alist[low]<mid_value:
low+=1
elif alist[low]>mid_value:
alist[high]=alist[low]
high-=1
理清楚指针移动和元素交换的步骤后,我们直到指针终止移动的条件是low和high指针重合的时候,因此需要有一个判断条件来控制指针移动的次数:while low<high and alist[high]>mid_value:
退出这个循环只有两种情况:一是high比low所指地址更小, 一个是low和high重合的时候。
根据上面的步骤分析,我们直到,low指针和high指针的循环操作是交替执行的,因此我们需要在其外层加一个循环来控制循环的执行(只要low和high没有相遇,就会一直交替执行)
但是也会有特殊情况出现:
1.当指针只想的元素和参照元素相等时,有一个原则是让相等的元素都在参照元素的一边(可以在low指针一边,也可以在high指针一边)但上述的代码显然无法判断如何处理。因此,需要再加一个判断元素值相等的条件。
2.在指针移动到high和low指针重合时。一次一次的循环发现,当high已经和low相等时,不在进入内层循环,但是还会执行交换指向位置元素的操作,出现low指针指向位置大于high指针的情况。因此,需要在low和high指针相遇的时候增加退出循环的操作。
def quick_sort(alist):
"""快速排序"""
n=len(alist)
mid_value=alist[0]
low=0
high=n-1
while low<high:
#high左移
while low<high and alist[high]>=mid_value:
#low<high保证low指针始终指向比high指针指向位置小的位置
#后面条件判断high指针所指元素和参照元素的大小
high-=1
alist[low]=alist[high]
while low<high and alist[low]<mid_value:
low+=1
alist[high]=alist[low]
#从循环退出时,low==high
alist[low]=mid_value
解决了快速排序的元素交换的问题,接下来就要考虑如何实现递归。
将序列不断分割为两部分,知道最后处理的元素只有一个的时候,函数调用就结束了。想要递归调用函数,需要对函数进行一定的修改: 每次递归都是对列表的某一个部分进行操作,而不是新建的一个列表,因此函数调用传参的时候,需要增加一个起始位置和一个终止位置。
def quick_sort(alist,first,last):
"""快速排序"""
if first>=last:
return
mid_value=alist[first]
low=first
high=last
while low<high:
#high左移
while low<high and alist[high]>=mid_value:
#low<high保证low指针始终指向比high指针指向位置小的位置
#后面条件判断high指针所指元素和参照元素的大小
high-=1
alist[low]=alist[high]
while low<high and alist[low]<mid_value:
low+=1
alist[high]=alist[low]
#从循环退出时,low==high
alist[low]=mid_value
#对low左边的列表执行快速排序
quick_sort(alist,first,low-1)
#对low右边的列表排序
quick_sort(alist,low+1,last)
if __name__=="__main__":
li=[54,26,93,17,77,31,44,55,20]
print(li)
quick_sort(li,0,(len(li)-1))#调用快速排序算法
print(li)
时间复杂度
- 最优时间复杂度:O(nlogn)
在最好的情况,每次我们运行一次分区,我们会把一个数列分为两个几近相等的片段。这个意思就是每次递归调用处理一半大小的数列。因此,在到达大小为一的数列前,我们只要作log n次嵌套的调用。这个意思就是调用树的深度是O(log n)。但是在同一层次结构的两个程序调用中,不会处理到原来数列的相同部分;因此,程序调用的每一层次结构总共全部仅需要O(n)的时间(每个调用有某些共同的额外耗费,但是因为在每一层次结构仅仅只有O(n)个调用,这些被归纳在O(n)系数中)。结果是这个算法仅需使用O(n log n)时间。 - 最坏时间复杂度:O(n2)
初始基本有序或逆序的序列进行快速排序的时间复杂度是最坏时间复杂度 - 稳定性:不稳定
涉及分组排序,因此会不稳定。(同希尔排序一样的原理)
动画演示: