一、归并排序
Merge Sort
❖
下面我们来看看
分治策略
在排序中的应用
❖
归并排序是递归算法,思路是将数据表持
续分裂为两半,对两半分别进行归并排序
递归的
基本结束条件
是:数据表仅有
1
个数据项
,自然是排好序的;
缩小规模
:将数据表分裂为相等的两半,规模减
为原来的二分之一;
调用自身
:将两半分别调用自身排序,然后将分
别排好序的两半进行归并,得到排好序的数据表
![](https://i-blog.csdnimg.cn/blog_migrate/a2aa124993cccfce94b4cda8a59f4b68.png)
源码:
def mergeSort(alist):
if len(alist) > 1:
mid = len(alist)//2
lefthalf = alist[:mid]
righthalf = alist[mid:]
mergeSort(lefthalf) #左排序
mergeSort(righthalf) #右排序
i=j=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 len(lefthalf) > i:
alist[k] = lefthalf[i]
i = i+1
k = k+1
#归并右半部分剩余项
while len(righthalf) > j:
alist[k] = righthalf[j]
j = j+1
k = k+1
return alist
testlist = [1,2,6,4,8,5,7,3]
print(mergeSort(testlist))
#归并排序优化
def merge_sort(alist):
if len(alist) > 1:
mid = len(alist)//2
left_half = alist[:mid]
right_half = alist[mid:]
left_half = merge_sort(left_half)
right_half = merge_sort(right_half)
merge_list = []
while left_half and right_half:
if left_half[0] <= right_half[0]:
merge_list.append(left_half.pop(0))
else:
merge_list.append(right_half.pop(0))
merge_list.extend(left_half if left_half else right_half)
return merge_list
else:
return alist
testlist = [1,2,6,4,8,5,7,3]
print(merge_sort(testlist))
归并排序:算法分析
❖
将归并排序分为两个过程来分析:
分裂
和
归并
❖
分裂的过程,借鉴二分查找中的分析结果
,是对数复杂度,时间复杂度为
O(log n)
❖
归并的过程,相对于分裂的每个部分,其
所有数据项都会被比较和放置一次,所以
是线性复杂度,其时间复杂度是
O(n)
综合考虑,每次分裂的部分都进行一次
O(n)
的数
据项归并,总的时间复杂度是O(nlog n)
❖
最后,我们还是注意到两个切片操作
为了时间复杂度分析精确起见,
可以通过
取消切片
操作,改为传递两个分裂部分
的起始点和终止点,也是没问题的,
只是算法可读性稍微牺牲一点点。
❖
我们注意到归并排序算法使用了
额外
1
倍
的存储空间用于归并
❖
这个特性在对特大数据集进行排序的时候
要考虑进去
二、快速排序算法及分析
❖
快速排序的思路是依据一个
“
中值
”
数据
项来把数据表分为
两半
:小于中值的一半
和大于中值的一半,然后每部分分别进行
快速排序(递归)
如果希望这两半拥有相等数量的数据项,则应该
找到数据表的“中位数”
但找中位数需要计算开销!要想没有开销,只能
随意找一个数来充当“中值”
比如,第
1
个数。
![](https://i-blog.csdnimg.cn/blog_migrate/53bed4b690022756730c8e2a166fa307.png)
❖
快速排序的递归算法
“
递归三要素
”
如下
❖
基本结束条件
:数据表仅有
1
个数据项,
自然是排好序的
❖
缩小规模
:根据
“
中值
”
,将数据表分为
两半,最好情况是相等规模的两半
❖
调用自身
:将两半分别调用自身进行排序
(排序基本操作在分裂过程中)
❖
分裂数据表的目标:找到
“
中值
”
的位置
❖
分裂数据表的手段
设置左右标(
left/rightmark
)
左标向右移动,右标向左移动
•
左标一直向右移动,碰到比中值大的就停止
•
右标一直向左移动,碰到比中值小的就停止
•
然后把左右标所指的数据项
交换
继续移动,直到左标移到右标的右侧,停止移动
这时右标所指位置就是“中值”应处的位置
将中值和这个位置交换
分裂完成,左半部比中值小,右半部比中值大
![](https://i-blog.csdnimg.cn/blog_migrate/40798e30e6779363495b6136bb266296.png)
源码为:
![](https://i-blog.csdnimg.cn/blog_migrate/2bdd7dcadd6d25a9d3c94c953d1c8487.png)
![](https://i-blog.csdnimg.cn/blog_migrate/848b6b20ada3c55a6accf6435ed97dd6.png)
快速排序:算法分析
❖
快速排序过程分为两部分:
分裂
和
移动
如果分裂
总能
把数据表分为
相等
的
两部分
,那么
就是
O(log n)
的复杂度;
而移动需要将每项都与中值进行比对,还是
O(n)
❖
综合起来就是
O(nlog n)
;
❖
而且,算法运行过程中不需要额外的存储
空间。
❖
但是,如果不那么幸运的话,中值所在的
分裂点过于偏离中部,造成左右两部分数
量不平衡
❖
极端情况,有一部分始终没有数据
,这样
时间复杂度就退化到
O(n
2
)
还要加上递归调用的开销(比冒泡排序还糟糕)
❖
可以适当改进下
中值
的选取方法,让中值
更具有代表性
比如“三点取样”,从数据表的头、尾、中间选
出中值
会产生额外计算开销,仍然不能排除极端情况