一般的排序方法的复杂度是O(n^2),这里介绍一下复杂度为O(nlogn)的更好的算法。这类算法的好处就是采用了分而治之的策略。也就是说,每一个算法都是找到了一种方法,将列表分解为更小的子列表。随后这些子列表再进行递归排序。理想的情况下,如果这些子列表的复杂度为log(n),而重新排列每一个子列表中的数据所需的工作量为n,那么这样的排序算法总的复杂度就是O(nlogn)。实践证明nlogn要比n^2好很多
快速排序的策略如下:
1、从列表的中点位置取一项,这一项叫做基准点(pivot,选择时同样有不同的方法)
2、将列表中的项进行分区,以便于小于基准点的所有项都移动到基准点的左边,而剩下的项都移动到基准点的右边,根据相关的实际项,基准点自身的最终位置也是随时变化的。
3、分而治之。对于在基准点分割列表而形成的子列表,递归的重复应用该过程。一个子列表包含了基准点左边的所有的项(较小的项),另一个子列表的包含了基准点右边的所有项(较大的项)
4、每次遇到少于2个项的一个子列表,就结束这个过程
以上的算法的最难的部分就是分割。流程如下:
1、将基准点和子列表的最后一下交换
2、在已知小于基准点的项和剩余的项之间简历一个边界,一开始这个边界就放在第一个项之前
3、从子列表的第一项开始,扫描整个列表。每次遇到小于基准点的项就将其与边界之后的第一项交换,并且边界向后移动
4、将基准点和边界之后的第一项交换,从而完成这个过程
快速排序的复杂度分析
在第一次的分割操作中,从列表中头部到尾部扫描了所有的项。这个操作过程的工作量和列表的长度成正比。在这次分割之后的工作量和左边的子列表的长度加上右边的子列表的长度(加在一起是n-1)成正比。当再次分割的时候,有了4个子列表,他们组合在一起的长度近似为n。随着将列表分割为更多的子列表的时候,总的工作量还是与n成正比
做一个乐观的假设,每一次分割的时候分割线总是在子列表的中央。通过二叉搜索的理论,当重复的居中分割一个列表的时候,大概会经过log2 n 步得到单个的元素,因此这个算法在最好的性能为O(log2 n)
对于最坏的情况下考虑一个列表已经排好序的情况。如果所选择的基准点元素是第一个元素。那么在第一分割的时候。基准点的右边就有n-1个元素,在第二次分割的时候。基准点的右边就是有n-2个元素,依次类推。
尽管没有元素进行直接的交换,但是总的分割的次数为n-1次,并且执行比较的总次数为是1/2n^2-1/2n,这和在选择排序和冒泡排序的次数是相同的。因此在最坏的情况下,快速排序算法的复杂度书O(n^2)
如果将快速排序实现为一个递归的算法,就必须考虑到调用栈所使用的内存。每一次递归调用都需要一个固定大小的内存用于栈,并且在每一次的分割之后有两次的递归调用。因此在最好的情况下,内存的使用是O(log2 n),在最坏的情况下内存的使用是O(n)
下列为快速排序的递归算法的函数:
顶层的函数:quicksortHelper函数,它隐藏了用于子列表终点的额外的参数
还有一个partition函数
def quickSort(lyst):
"""快速排序函数的主函数"""
quickSortHelper(lyst,0,len(lyst)-1)
pass
def quickSortHelper(lyst,left,right):
if left<right:
pivotLocation=partition(lyst,left,right)
quickSortHelper(lyst,left,pivotLocation-1)
quickSortHelper(lyst,pivotLocation+1,right)
def partition(lyst,left,right):
middle=(left+right)//2
pivot=lyst[middle]
lyst[middle]=lyst[right]
lyst[right]=pivot
boundary=left
for index in range(left,right):
if lyst[index]<pivot:
swap(lyst,index,boundary)
boundary+=1
swap(lyst,right,boundary)
return(boundary)
import random
def main(size=20,sort=quickSort):
lyst=[]
for count in range(size):
lyst.append(random.randint(1,size+1))
print(lyst)
sort(lyst)
print(lyst)
pass
合并排序的方法
概述如下:
1、计算一个列表的中间位置,并且递归的排序其左边和右边的子列表(分而治之)
2、将两个排好序的子序列重新合并为单个的排好序的列表
3、当列表不再能够划分的时候,停止这个过程
函数设计如下:
mergeSort()——用户调用的函数
mergeSortHelper()——一个辅助的函数,隐藏了递归所需要的额外参数
merge()——实现合并过程的一个函数
1、实现合并的过程
合并的过程使用了和列表相同大小的一个数组,这个数组的名为copyBuffer。为了避免每次调用merge的时候为copyBuffer分配和释放内存的开销,只在mergeSort()分配一次缓冲区,并且在后续将其作为一个参数传递给mergeSortHelper和merge。每次调用mergeSortHelper的时候都需要知道它所操作的子列表的边界,这些边界通过另外的两个参数low和high来提供其中mergeSort()的代码为:
def mergeSort(lyst):
"""合并排序法的用户调用函数"""
copyBuffer=Array(len(lyst))
mergeSortHelper(lyst,copyBuffer,0,len(lyst)-1)
在检查到至少有两项的子列表传递给它之后,mergeSortHelp()函数计算了这个子列表的中点。递归的对中点以上和中点以下 的部分进行排序。并且调用merge函数来合并结果,如下是mergeSortHelper的代码:
def mergeSortHelper(lyst,copyBuffer,low,high):
if low<high:
middle=(low+high)//2
mergeSortHelper(lyst,copyBuffer,low,middle)
mergeSortHelper(lyst,copyBuffer,middle+1,high)
merge(lyst,copyBuffer,lowmiddle,high)
pass
如下是一个merge函数的代码:
def merge(lyst,copyBuffer,low,middle,high):
i1=low
i2=middle+1
for i in range(low,high+1):
if i1>middle:
copyBuffer[i]=lyst[i2]
i2+=1
elif i2>high:
copyBuffer[i]=lyst[i2]
i1+=1
elif lyst[i1]<lyst[i2]:
copyBuffer[i]=lyst[i1]
i1+=1
else:
copyBuffer[i]=lyst[i2]
i2+=1
for i in range(low ,high+1):
lyst[i]=copyBuffer[i]
pass
merge函数是将两个排好序的子序列合并到一个大的排好序的子序列里面。第一个子列表在low和midlle之间,第二个子列表在midlle+1到high之间,这个过程包含了三个步骤:
1、将索引指针设计为每个子列表的第一项,这分别是low和middle+1的位置
2、从每一个子列表的第一项开始,重复的比较各项,将较小的项复制到复制缓存中,并且继续处理子列表的下一项。重复这个过程,直到两个子列表中的所有值都已经复制过了。如果先达到了其中一个子列表的末尾,通过从另外一个子列表复制剩余的项,从而结束这个步骤
3、将copyBuffer中间的low和hi gh部分,复制回lyst中对应的位置
合并排序的复杂度分析
合并排序的运行时间由两条for语句主导,其中每一条都是循环(high-low+1)次,结果,该循环的运行的时间是O(high-low),在一个层的所有的花费时间是O(n)。由于mergeSortHelper在每一个层都是尽可能平均的分割子列表,层级数是O(logn),并且在所有情况下,该函数的最大的运行时间是O(nlogn)。
根据列表的大小,合并排序有两个空间需求,首先支持递归调用的调用栈上,需要O(logn)的空间,其次,复制缓存需要使用O(n)的空间