第六章 排序算法
1.稳定性的内涵:如果一个排序算法是稳定的,当有两个相等键值的纪录R和S,且在原本的列表中R出现在S之前,在排序过的列表中R也将会是在S之前。假设(4, 1)、(3, 1)、(3, 7)、(5, 6)要以他们的第一个数字来排序。则可能产生两个结果:(3, 1) (3, 7) (4, 1) (5, 6) (维持次序),(3, 7) (3, 1) (4, 1) (5, 6) (次序被改变)根据前述定义而言,排序后维持原有次序的算法稳定,排序后原有次序被改变的算法不稳定(一定要注意排序是对于原对象而言,即排序完的列表即原列表)。
2.冒泡排序(排序算法一般是从内部控制交换的循环入手,根据一次交换循环后排出的数有几个,思考整个数列排序完的次数构造外层循环)的流程:(1)比较相邻的元素。如果第一个比第二个大(升序),就交换他们两个。(2)对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数。(3)针对所有的元素重复以上的步骤,除了最后一个。(4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
代码实现(先从一次完整的从头比较到尾的过程出发构建内循环,再分析进行遍历的次数进行外循环的构建):
# -*- coding:utf-8 -*-
def bubble_sort(a):
n=len(a)
for j in range(0,n-1):
for i in range(0,n-1-j):
count=0
if a[i]>a[i+1]:
a[i],a[i+1]=a[i+1],a[i]
count+=1
if count==0:
return
if __name__=='__main__':
list=[54,26,93,17,77,31,44,55,20]
bubble_sort(list)
print(list)
时间复杂度的分析:
最坏时间复杂度O(n^2):即内、外两层循环的次数的乘积。
最有时间复杂度O(n):即当进行一次从头到尾的比较遍历过程中,未发现有需要交换的元素时,则可以得知次序列为有序序列,则只遍历了一次内部循环,时间复杂度为O(n)。
稳定性:稳定。(由于相邻两数比较过程中并为对相等的相邻数字进行操作,所以两数相对位置与原始保持不变,所以稳定。)
3.选择排序(认为序列分为两部分,一部分有序,一部分无序,选择排序是从无序序列中选择最小的数放到最开始处):首先认为当前无序序列的第一个元素为最小值,并且在min_index记录下该最小值的下标,然后依次同无序序列中其他元素进行比较,若发现比当前记录的最小值还小,则将其下标更新到min_index中,当进行到结束时,此时无序序列中最小的数被放到数据的最左端。之后仍旧认为当前无序序列的第一个元素为最小值重复上述过程并将无序序列中次小的数放到有序数列的末尾。重复上述过程直至完成排序。
# -*- coding:utf-8 -*-
def select_sort(a):
n=len(a)
for j in range(0,n-1):
min_index=j
for i in range(j+1,n):
if a[j]>a[i]:
j=i
a[j],a[min_index]=a[min_index],a[j]
if __name__=='__main__':
list=[54,226,93,17,17,31,44,55,20]
select_sort(list)
print(list)
分析时间复杂度:
最优时间复杂度O(n^2):即使对于一组有序数列,这种选择排序每次内循环也要逐一比较后边的数a[i](i=j+1~n-1)才能确认当前的数a[j]是否最小,每次内循环只能选出一个当前序列中最小的数,整个列表中所有的数都要进行上述操作,所以最有时间复杂度也是O(n^2)。
最坏时间复杂度O(n^2):内外循环次数都是n,则乘积得到时间复杂度O(n^2)。
稳定性:例如对于[(26,1),(26,2),10,9]这个序列,第一次进行内循环时会选出最小的数的下标[3]与[0]进行交换,即list[0]=9,list[3]=(26,1),第二次进行内循环时会选出次小的数的下标[2]与[1]进行交换,即list[1]=10,list[2]=(26,2),这样我们发现序列拍好后变为[9,10,(26,2),(26,1)]则相等的两个元素的次序未维持原始的次序,因此选择算法不稳定。
4.插入排序:工作原理是通过构建有序序列,对于从未排序数列中每次选择首个数据,在已排序序列中从后向前扫描,找到相应位置并插入,重复这个过程直到所有的数排序完成。
#coding:utf-8
def insert_sort(a):
n=len(a)
for j in range(1,n):
#第一次比较时认为0号元素有序,
#所以从无序序列中取得第一个元素是1号
for i in range(j,0,-1):
#当前取得的无序数据要与有序数列进行相邻对比,
#若小则交换,此时位于插入数据后的后移元素,
# 仍旧是有序数据直至找到正确位置
if a[i]<a[i-1]:
a[i],a[i-1]=a[i-1],a[i]
else:
break #当已经找到正确位置后,不再与更小的数对比
if __name__=='__main__':
list = [54,26,93,17,77,31,44,55,20]
insert_sort(list)
print(list)
时间复杂度分析:
最优时间复杂度O(n) :当原序列有序时,取出的无序数据只许一次比较不需任何操作就可以找到正确位置,所以只剩外层循环的次数n,即时间复杂度为O(n)。
最坏时间复杂度O(n^2):当原序列无序时,外层循环的次数为n,内层循环的次数为n,即时间复杂度为O(n^2)。
稳定性:在插入排序算法中对于相等的数据并未进行任何操作,所以在往表前端的有序序列区存放时仍旧是原列表顺序,因此稳定。
5.希尔排序:希尔排序是插入排序的一种,插入排序是gap=1的希尔排序,或者说希尔排序最后会落到gap=1进行常规意义上的插入排序,其基本思想为:将数组列在一个表中并对列分别进行插入排序,重复这过程,不过每次用更长的列(步长更长了,列数更少了)来进行。最后整个表就只有一列了,进行插入排序。
# -*- coding:utf-8 -*-
def shell_sort(a):
n=len(a)
gap=n//2
while gap>0:
for i in range(gap,n):
j=i
while j>=gap:
if a[j]<a[j-gap]:
a[j],a[j-gap]=a[j-gap],a[j]
else:
break
j-=gap
gap//=2
if __name__=='__main__':
list=[54,26,93,17,77,31,44,55,20]
shell_sort(list)
print(list)
最优时间复杂度:根据步长序列的不同而不同。
最坏时间复杂度:O(n^2)(上来gap=1等同于插入排序)。
稳定想:不稳定(排序时交换非相邻元素的算法大体上都不稳定)。
6.快速排序:步骤为:从数列中挑出一个元素,称为"基准"(pivot),重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区操作。递归地把小于基准值元素的子数列和大于基准值元素的子数列排序。
def quick_sort(a,start,end):
mid_value=a[start]
low=start
high=end
while low<high:
while low<high and a[high]>=mid_value:
high-=1
a[low]=a[high]
while low<high and a[low]<mid_value:
low+=1
a[high]=a[low]
a[low]=mid_value
quick_sort(a,start,low-1)
quick_sort(a,low+1,end)
时间复杂度:最优情况为每次快排其基准值正好正确位置位于中间,此时将左右再分为两个部分进行快排,如果每个内部嵌套的快排也是如此,则外循环需要进行log2n次,但每次外循环下的内循环均会累加执行n次,则最有时间复杂度为O(nlog2n),最坏的情况是每次快排基准值正确位置刚好在一端,这时需要进行n次快排,所以为O(n^2)。
稳定性:不稳定(同样涉及到了low与high指针非相邻元素的交换。)
7.归并排序:归并排序的思想就是先递归分解数组,再合并数组。将数组分解最小之后,然后合并两个有序数组,基本思路是比较两个数组的最前面的数,谁小就先取谁,取了后相应的指针就往后移一位。然后再比较,直至一个数组为空,最后把另一个数组的剩余部分复制过来即可。
# -*- coding:utf-8 -*-
def merge_sort(a):
if len(a)<=1:
return a
n=len(a)
num=n//2
left=merge_sort(a[:num])
right=merge_sort(a[num:])
l,r=0,0
result=[]
while l<len(left) and r<len(right):
if left[l]<=right[r]:
result.append(left[l])
l+=1
else:
result.append(right[r])
r+=1
result+=left[l:]
result+=right[r:]
return result
if __name__=='__main__':
list= [54,26,93,17,77,31,44,44,55,20]
print(merge_sort(list))
时间复杂度:O(nlogn),由于上边的递归拆部分不涉及n的循环,是单纯的列表的切片操作,所以为数量级为1,而后边的合并排序涉及到同层合并遍历累计需要n次,且由众多长度为一的元素两两合并至一个整体需要logn次,所以总时间复杂度为O(nlogn)。
稳定性:稳定(在合并时比较操作中对于左右相同的元素采取让左边的先排进序列,所以维持了原有次序,稳定)。
8.排序的比较:
9.二分法查找的原理:二分查找又称折半查找,优点是比较次数少,查找速度快,平均性能好;其缺点是要求待查表为有序表,且插入删除困难。因此,折半查找方法适用于不经常变动而查找频繁的有序列表。首先,假设表中元素是按升序排列,将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;否则利用中间位置记录将表分成前、后两个子表,如果中间位置记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。重复以上过程,直到找到满足条件的记录,使查找成功,或直到子表不存在为止,此时查找不成功。(注意左右界限指到同一元素时也要进行查询)
时间复杂度:当中间值为查找值时时间复杂度最优O(1),最坏需要折半对分精确到每一个元素才可能找到查找值,此时复杂度为将n个元素折半到1,即为O(log2n)。
def binary_search(a,elem):
start=0
end=len(a)-1
while start<=end:
mid_index=(start+end)//2
if elem==a[mid_index]:
return True
elif elem<a[mid_index]:
end=mid_index-1
else:
start=mid_index+1
return False