基于python的七种经典排序算法.docx
《基于python的七种经典排序算法.docx》由会员分享,可在线阅读,更多相关《基于python的七种经典排序算法.docx(19页珍藏版)》请在冰豆网上搜索。
基于python的七种经典排序算法
基于python的七种经典排序算法
一、排序的基本概念和分类
所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
排序算法,就是如何使得记录按照要求排列的方法。
排序的稳定性:
经过某种排序后,如果两个记录序号同等,且两者在原无序记录中的先后秩序依然保持不变,则称所使用的排序方法是稳定的,反之是不稳定的。
内排序和外排序
内排序:
排序过程中,待排序的所有记录全部放在内存中
外排序:
排序过程中,使用到了外部存储。
通常讨论的都是内排序。
影响内排序算法性能的三个因素:
∙时间复杂度:
即时间性能,高效率的排序算法应该是具有尽可能少的关键字比较次数和记录的移动次数
∙空间复杂度:
主要是执行算法所需要的辅助空间,越少越好。
∙算法复杂性。
主要是指代码的复杂性。
根据排序过程中借助的主要操作,可把内排序分为:
∙插入排序
∙交换排序
∙选择排序
∙归并排序
按照算法复杂度可分为两类:
∙简单算法:
包括冒泡排序、简单选择排序和直接插入排序
∙改进算法:
包括希尔排序、堆排序、归并排序和快速排序
以下的七种排序算法只是所有排序算法中最经典的几种,不代表全部。
二、冒泡排序
冒泡排序(Bubblesort):
时间复杂度O(n^2)
交换排序的一种。
其核心思想是:
两两比较相邻记录的关键字,如果反序则交换,直到没有反序记录为止。
其实现细节可以不同,比如下面3种:
1.最简单排序实现:
bubble_sort_simple
2.冒泡排序:
bubble_sort
3.改进的冒泡排序:
bubble_sort_advance
#!
/usr/bin/envpython#-*-coding:
utf-8-*-#Author:
LiuJiang#Python3.5#冒泡排序算法
classSQList:
def__init__(self,lis=None):
self.r=lis
defswap(self,i,j):
"""定义一个交换元素的方法,方便后面调用。
"""
temp=self.r[i]
self.r[i]=self.r[j]
self.r[j]=temp
defbubble_sort_simple(self):
"""最简单的交换排序,时间复杂度O(n^2)"""
lis=self.r
length=len(self.r)
foriinrange(length):
forjinrange(i+1,length):
iflis[i]>lis[j]:
self.swap(i,j)
defbubble_sort(self):
"""冒泡排序,时间复杂度O(n^2)"""
lis=self.r
length=len(self.r)
foriinrange(length):
j=length-2
whilej>=i:
iflis[j]>lis[j+1]:
self.swap(j,j+1)
j-=1
defbubble_sort_advance(self):
"""冒泡排序改进算法,时间复杂度O(n^2)设置flag,当一轮比较中未发生交换动作,则说明后面的元素其实已经有序排列了。
对于比较规整的元素集合,可提高一定的排序效率。
"""
lis=self.r
length=len(self.r)
flag=True
i=0
whileiflag=False
j=length-2
whilej>=i:
iflis[j]>lis[j+1]:
self.swap(j,j+1)
flag=True
j-=1
i+=1
def__str__(self):
ret=""
foriinself.r:
ret+="%s"%i
returnret
if__name__=='__main__':
sqlist=SQList([4,1,7,3,8,5,9,2,6])
#sqlist.bubble_sort_simple()
#sqlist.bubble_sort()
sqlist.bubble_sort_advance()
print(sqlist)
三、简单选择排序
简单选择排序(simpleselectionsort):
时间复杂度O(n^2)
通过n-i次关键字之间的比较,从n-i+1个记录中选出关键字最小的记录,并和第i(1<=i<=n)个记录进行交换。
通俗的说就是,对尚未完成排序的所有元素,从头到尾比一遍,记录下最小的那个元素的下标,也就是该元素的位置。
再把该元素交换到当前遍历的最前面。
其效率之处在于,每一轮中比较了很多次,但只交换一次。
因此虽然它的时间复杂度也是O(n^2),但比冒泡算法还是要好一点。
#!
/usr/bin/envpython#-*-coding:
utf-8-*-#Author:
LiuJiang#Python3.5#简单选择排序
classSQList:
def__init__(self,lis=None):
self.r=lis
defswap(self,i,j):
"""定义一个交换元素的方法,方便后面调用。
"""
temp=self.r[i]
self.r[i]=self.r[j]
self.r[j]=temp
defselect_sort(self):
"""简单选择排序,时间复杂度O(n^2)"""
lis=self.r
length=len(self.r)
foriinrange(length):
minimum=i
forjinrange(i+1,length):
iflis[minimum]>lis[j]:
minimum=j
ifi!
=minimum:
self.swap(i,minimum)
def__str__(self):
ret=""
foriinself.r:
ret+="%s"%i
returnret
if__name__=='__main__':
sqlist=SQList([4,1,7,3,8,5,9,2,6,0])
sqlist.select_sort()
print(sqlist)
四、直接插入排序
直接插入排序(StraightInsertionSort):
时间复杂度O(n^2)
基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增1的有序表。
#!
/usr/bin/envpython#-*-coding:
utf-8-*-#Author:
LiuJiang#Python3.5#直接插入排序
classSQList:
def__init__(self,lis=None):
self.r=lis
definsert_sort(self):
lis=self.r
length=len(self.r)
#下标从1开始
foriinrange(1,length):
iflis[i]temp=lis[i]
j=i-1
whilelis[j]>tempandj>=0:
lis[j+1]=lis[j]
j-=1
lis[j+1]=temp
def__str__(self):
ret=""
foriinself.r:
ret+="%s"%i
returnret
if__name__=='__main__':
sqlist=SQList([4,1,7,3,8,5,9,2,6,0])
sqlist.insert_sort()
print(sqlist)
该算法需要一个记录的辅助空间。
最好情况下,当原始数据就是有序的时候,只需要一轮对比,不需要移动记录,此时时间复杂度为O(n)。
然而,这基本是幻想。
五、希尔排序
希尔排序(ShellSort)是插入排序的改进版本,其核心思想是将原数据集合分割成若干个子序列,然后再对子序列分别进行直接插入排序,使子序列基本有序,最后再对全体记录进行一次直接插入排序。
这里最关键的是跳跃和分割的策略,也就是我们要怎么分割数据,间隔多大的问题。
通常将相距某个“增量”的记录组成一个子序列,这样才能保证在子序列内分别进行直接插入排序后得到的结果是基本有序而不是局部有序。
下面的例子中通过:
increment=int(increment/3)+1来确定“增量”的值。
希尔排序的时间复杂度为:
O(n^(3/2))
#!
/usr/bin/envpython#-*-coding:
utf-8-*-#Author:
LiuJiang#Python3.5#希尔排序
classSQList:
def__init__(self,lis=None):
self.r=lis
defshell_sort(self):
"""希尔排序"""
lis=self.r
length=len(lis)
increment=len(lis)
whileincrement>1:
increment=int(increment/3)+1
foriinrange(increment+1,length):
iflis[i]temp=lis[i]
j=i-increment
whilej>=0andtemplis[j+increment]=lis[j]
j-=increment
lis[j+increment]=temp
def__str__(self):
ret=""
foriinself.r:
ret+="%s"%i
returnret
if__name__=='__main__':
sqlist=SQList([4,1,7,3,8,5,9,2,6,0,123,22])
sqlist.shell_sort()
print(sqlist)
六、堆排序
堆是具有下列性质的完全二叉树:
每个分支节点的值都大于或等于其左右孩子的值,称为大顶堆;
每个分支节点的值都小于或等于其做右孩子的值,称为小顶堆;
因此,其根节点一定是所有节点中最大(最小)的值。
如果按照层序遍历的方式(广度优先)给节点从1开始编号,则节点之间满足如下关系:
堆排序(HeapSort)就是利用大顶堆或小顶堆的性质进行排序的方法。
堆排序的总体时间复杂度为O(nlogn)。
(下面采用大顶堆的方式)
其核心思想是:
将待排序的序列构造成一个大顶堆。
此时,整个序列的最大值就是堆的根节点。
将它与堆数组的末尾元素交换,然后将剩余的n-1个序列重新构造成一个大顶堆。
反复执行前面的操作,最后获得一个有序序列。
#!
/usr/bin/envpython#-*-coding:
utf-8-*-#Author:
LiuJiang#Python3.5#堆排序
classSQList:
def__init__(self,lis=None):
self.r=lis
defswap(self,i,j):
"""定义一个交换元素的方法,方便后面调用。
"""
temp=self.r[i]
self.r[i]=self.r[j]
self.r[j]=temp
defheap_sort(self):
length=len(self.r)
i=int(length/2)
#将原始序列构造成一个大顶堆
#遍历从中间开始,到0结束,其实这些是堆的分支节点。
whilei>=0:
self.heap_adjust(i,length-1)
i-=1
#逆序遍历整个序列,不断取出根节点的值,完成实际的排序。
j=length-1
whilej>0:
#将当前根节点,也就是列表最开头,下标为0的值,交换到最后面j处
self.swap(0,j)
#将发生变化的序列重新构造成大顶堆
self.heap_adjust(0,j-1)
j-=1
defheap_adjust(self,s,m):
"""核心的大顶堆构造方法,维持序列的堆结构。
"""
lis=self.r
temp=lis[s]
i=2*s
whilei<=m:
ifii+=1
iftemp>=lis[i]:
break
lis[s]=lis[i]
s=i
i*=2
lis[s]=temp
def__str__(self):
ret=""
foriinself.r:
ret+="%s"%i
returnret
if__name__=='__main__':
sqlist=SQList([4,1,7,3,8,5,9,2,6,0,123,22])
sqlist.heap_sort()
print(sqlist)
堆排序的运行时间主要消耗在初始构建堆和重建堆的反复筛选上。
其初始构建堆时间复杂度为O(n)。
正式排序时,重建堆的时间复杂度为O(nlogn)。
所以堆排序的总体时间复杂度为O(nlogn)。
堆排序对原始记录的排序状态不敏感,因此它无论最好、最坏和平均时间复杂度都是O(nlogn)。
在性能上要好于冒泡、简单选择和直接插入算法。
空间复杂度上,只需要一个用于交换的暂存单元。
但是由于记录的比较和交换是跳跃式的,因此,堆排序也是一种不稳定的排序方法。
此外,由于初始构建堆的比较次数较多,堆排序不适合序列个数较少的排序工作。
七、归并排序
归并排序(MergingSort):
建立在归并操作上的一种有效的排序算法,该算法是采用分治法(DivideandConquer)的一个非常典型的应用。
将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
若将两个有序表合并成一个有序表,称为二路归并。
#!
/usr/bin/envpython#-*-coding:
utf-8-*-#Author:
LiuJiang#Python3.5#归并排序
classSQList:
def__init__(self,lis=None):
self.r=lis
defswap(self,i,j):
"""定义一个交换元素的方法,方便后面调用。
"""
temp=self.r[i]
self.r[i]=self.r[j]
self.r[j]=temp
defmerge_sort(self):
self.msort(self.r,self.r,0,len(self.r)-1)
defmsort(self,list_sr,list_tr,s,t):
temp=[Noneforiinrange(0,len(list_sr))]
ifs==t:
list_tr[s]=list_sr[s]
else:
m=int((s+t)/2)
self.msort(list_sr,temp,s,m)
self.msort(list_sr,temp,m+1,t)
self.merge(temp,list_tr,s,m,t)
defmerge(self,list_sr,list_tr,i,m,n):
j=m+1
k=i
whilei<=mandj<=n:
iflist_sr[i]list_tr[k]=list_sr[i]
i+=1
else:
list_tr[k]=list_sr[j]
j+=1
k+=1
ifi<=m:
forlinrange(0,m-i+1):
list_tr[k+l]=list_sr[i+l]
ifj<=n:
forlinrange(0,n-j+1):
list_tr[k+l]=list_sr[j+l]
def__str__(self):
ret=""
foriinself.r:
ret+="%s"%i
returnret
if__name__=='__main__':
sqlist=SQList([4,1,7,3,8,5,9,2,6,0,12,77,34,23])
sqlist.merge_sort()
print(sqlist)
∙归并排序对原始序列元素分布情况不敏感,其时间复杂度为O(nlogn)。
∙
归并排序在计算过程中需要使用一定的辅助空间,用于递归和存放结果,因此其空间复杂度为O(n+logn)。
∙
∙
归并排序中不存在跳跃,只有两两比较,因此是一种稳定排序。
∙
总之,归并排序是一种比较占用内存,但效率高,并且稳定的算法。
八、快速排序
快速排序(QuickSort)由图灵奖获得者TonyHoare发明,被列为20世纪十大算法之一。
冒泡排序的升级版,交换排序的一种。
快速排序的时间复杂度为O(nlog(n))。
快速排序算法的核心思想:
通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,然后分别对这两部分继续进行排序,以达到整个记录集合的排序目的。
#!
/usr/bin/envpython#-*-:
utf-8-*-#Author:
LiuJiang#Python3.5#快速排序
classSQList:
def__init__(self,lis=None):
self.r=lis
defswap(self,i,j):
"""定义一个交换元素的方法,方便后面调用。
"""
temp=self.r[i]
self.r[i]=self.r[j]
self.r[j]=temp
defquick_sort(self):
"""调用入口"""
self.qsort(0,len(self.r)-1)
defqsort(self,low,high):
"""递归调用"""
iflowpivot=self.partition(low,high)
self.qsort(low,pivot-1)
self.qsort(pivot+1,high)
defpartition(self,low,high):
"""快速排序的核心代码。
其实就是将选取的pivot_key不断交换,将比它小的换到左边,将比它大的换到右边。
它自己也在交换中不断变换自己的位置,直到完成所有的交换为止。
但在函数调用的过程中,pivot_key的值始终不变。
:
paramlow:
左边界下标:
paramhigh:
右边界下标:
return:
分完左右区后pivot_key所在位置的下标"""
lis=self.r
pivot_key=lis[low]
whilelowwhilelow=pivot_key:
high-=1
self.swap(low,high)
whilelowlow+=1
self.swap(low,high)
returnlow
def__str__(self):
ret=""
foriinself.r:
ret+="%s"%i
returnret
if__name__=='__main__':
sqlist=SQList([4,1,7,3,8,5,9,2,6,0,123,22])
sqlist.quick_sort()
print(sqlist)
∙快速排序的时间性能取决于递归的深度。
∙当pivot_key恰好处于记录关键码的中间值时,大小两区的划分比较均衡,接近一个平衡二叉树,此时的时间复杂度为O(nlog(n))。
∙当原记录集合是一个正序或逆序的情况下,分区的结果就是一棵斜树,其深度为n-1,每一次执行大小分区,都要使用n-i次比较,其最终时间复杂度为O(n^2)。
∙在一般情况下,通过数学归纳法可证明,快速排序的时间复杂度为O(nlog(n))。
∙但是由于关键字的比较和交换是跳跃式的,因此,快速排序是一种不稳定排序。
∙同时由于采用的递归技术,该算法需要一定的辅助空间,其空间复杂度为O(logn)。
基本的快速排序还有可以优化的地方:
1.优化选取的pivot_key
前面我们每次选取pivot_key的都是子序列的第一个元素,也就是lis[low],这就比较看运气。
运气好时,该值处于整个序列的靠近中间值,则构造的树比较平衡,运气比较差,处于最大或最小位置附近则构造的树接近斜树。
为了保证pivot_key选取的尽可能适中,采取选取序列左中右三个特殊位置的值中,处于中间值的那个数为pivot_key,通常会比直接用lis[low]要好一点。
在代码中,在原来的pivot_key=lis[low]这一行前面增加下面的代码:
m=low+int((high-low)/2)iflis[low]>lis[high]:
self.swap(low,high)iflis[m]>lis[high]:
(high,m)iflis[m]>lis[low]:
self.swap(m,low)
如果觉得这样还不够好,还可以将整个序列先划分为3部分,每一部分求出个pivot_key,再对3个pivot_key再做一次上面的比较得出最终的pivot_key。
这时的pivot_key应该很大概率是一