排序与查找-归并与快排 [TOC]
归并排序Merge Sort 分治策略在排序中的应用,归并排序是递归算法, 思路是将数据表持续分裂为两半, 对两半分别进行归并排序
递归的基本结束条件是:数据表仅有1个数据项,自然是排好序的;
缩小规模:将数据表分裂为相等的两半,规模减为原来的二分之一;
调用自身:将两半分别调用自身排序,然后将分别排好序的两半进行归并,得到排好序的数据表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 def mergeSort (alist ): print("Splitting " ,alist) if len(alist)>1 : mid = len(alist)//2 lefthalf = alist[:mid] righthalf = alist[mid:] mergeSort(lefthalf) mergeSort(righthalf) i=0 j=0 k=0 while i<len(lefthalf) and j<len(righthalf): if lefthalf[i]<righthalf[j]: alist[k]=lefthalf[i] i=i+1 else : alist[k]=righthalf[j] j=j+1 k=k+1 while i<len(lefthalf): alist[k]=lefthalf[i] i=i+1 k=k+1 while j<len(righthalf): alist[k]=righthalf[j] j=j+1 k=k+1 print("Merging " ,alist) alist = [54 ,26 ,93 ,17 ,77 ,31 ,44 ,55 ,20 ] mergeSort(alist) print(alist)
另一个归并排序代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 def mergeSort (lst ): print("Splitting " ,lst) if len(lst)>1 : return lst mid = len(lst)//2 lefthalf = lst[:mid] righthalf = lst[mid:] merged = [] while left and right: if left[0 ] <= right[0 ]: merged.append(left.pop(0 )) else : merged.append(right.pop(0 )) merged.extend(right if right else left) return merged
归并排序:算法分析
将归并排序分为两个过程来分析: 分裂和归并
分裂的过程, 借鉴二分查找中的分析结果, 是对数复杂度, 时间复杂度为O(log n)
归并的过程, 相对于分裂的每个部分, 其所有数据项都会被比较和放置一次, 所以是线性复杂度, 其时间复杂度是O(n)综合考虑,每次分裂的部分都进行一次O(n)的数据项归并,总的时间复杂度是O(nlog n)
最后, 我们还是注意到两个切片操作为了时间复杂度分析精确起见,可以通过取消切片操作,改为传递两个分裂部分的起始点和终止点,也是没问题的,只是算法可读性稍微牺牲一点点。
我们注意到归并排序算法使用了额外1倍的存储空间用于归并
这个特性在对特大数据集进行排序的时候要考虑进去
快速排序Quick Sort 快速排序的思路是依据一个“中值”数据项来把数据表分为两半:小于中值的一半和大于中值的一半, 然后每部分分别进行快速排序(递归)如果希望这两半拥有相等数量的数据项,则应该找到数据表的“中位数”,但找中位数需要计算开销!要想没有开销,只能随意找一个数来充当“中值”,比如,第1个数
快速排序的递归算法“递归三要素”如下
基本结束条件:数据表仅有1个数据项,自然是排好序的
缩小规模:根据“中值”, 将数据表分为两半, 最好情况是相等规模的两半
调用自身:将两半分别调用自身进行排序(排序基本操作在分裂过程中)
分裂数据表的目标:找到“中值”的位置,分裂数据表的手段
设置左右标(left/rightmark)
左标向右移动,右标向左移动
左标一直向右移动,碰到比中值大的就停止
右标一直向左移动,碰到比中值小的就停止
然后把左右标所指的数据项交换
继续移动,直到左标移到右标的右侧,停止移动,这时右标所指位置就是“中值”应处的位置,将中值和这个位置交换,分裂完成,左半部比中值小,右半部比中值大
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 def quickSort (alist ): quickSortHelper(alist,0 ,len(alist)-1 ) def quickSortHelper (alist,first,last ): if first<last: splitpoint = partition(alist,first,last) quickSortHelper(alist,first,splitpoint-1 ) quickSortHelper(alist,splitpoint+1 ,last) def partition (alist,first,last ): pivotvalue = alist[first] leftmark = first+1 rightmark = last done = False while not done: while leftmark <= rightmark and \ alist[leftmark] <= pivotvalue: leftmark = leftmark + 1 while alist[rightmark] >= pivotvalue and \ rightmark >= leftmark: rightmark = rightmark -1 if rightmark < leftmark: done = True else : temp = alist[leftmark] alist[leftmark] = alist[rightmark] alist[rightmark] = temp temp = alist[first] alist[first] = alist[rightmark] alist[rightmark] = temp return rightmark alist = [54 ,26 ,93 ,17 ,17 ,77 ,31 ,44 ,55 ,20 ] quickSort(alist) print(alist)
快速排序:算法分析
快速排序过程分为两部分: 分裂和移动如果分裂总能把数据表分为相等的两部分,那么就是O(log n)的复杂度;而移动需要将每项都与中值进行比对,还是O(n), 综合起来就是O(nlog n);而且, 算法运行过程中不需要额外的存储空间。
但是, 如果不那么幸运的话, 中值所在的分裂点过于偏离中部, 造成左右两部分数量不平衡
极端情况, 有一部分始终没有数据, 这样时间复杂度就退化到O(n2)还要加上递归调用的开销(比冒泡排序还糟糕)
参考: https://www.bilibili.com/video/BV1VC4y1x7uv?p=59