[算法系列] 递归应用: 快速排序+归并排序算法, 核心思想与拓展 … 附赠 堆排序算法
写完发现本文过于杂乱, 列个纲要叭:
- 本文是递归系列的第二篇, 在上篇文章 搞懂递归, 看这篇就够了 !! 递归设计思路 + 经典例题层层递进 介绍递归后, 本文将举熟悉的快速排序和归并排序算法小小地介绍分治的思想.
- 将对快排的划分partition算法和归排的合并merge算法进行介绍并适当扩展
- 将介绍递归和树的关系, 顺带介绍堆排序算法
分治: 将原问题划分成若干个规模较小而原问题一致的子问题; 递归地解决这些子问题, 然后再合并其结果, 就得到原问题的解
分治模式在每一层递归上都有三个步骤:
- 分解devide : 将原问题分解(划分)成一系列的子问题
- 解决conquer: 递归地解决各子问题, 若子问题足够小, 则直接有解
- 合并combine:将子问题合并成原问题的解
分治的关键点
- 原问题可以一直分解为形式相同的子问题, 当子问题规模较小时,就可以自然求解, 比如一个元素自然有序
- 子问题可以通过合并得到原问题的解
- 子问题的分解以及解的合并一定是比较简单的, 额外开销不可能太多, 否则分解和合并的时间可能超过暴力解法, 得不偿失
回顾+引子
我们先回顾上篇文章搞懂递归, 看这篇就够了 !! 递归设计思路 + 经典例题层层递进中的把插入排序修改为递归形式的例子:
# 插入排序的递归形式
def insert_sort(arr , k):
if k == 0 :
return ;
#对前n -1 个元素排序
insert_sort(arr, k -1)
# 把位置k的元素插入到前面的部分
x = arr[k]
index = k -1
while index > -1 and x <arr[index] :
arr[index + 1] = arr[index]
index -= 1
arr[index+1] = x
我们将索引k作为变化参数进行递归, 相当于我们每次分解为 数组长度减1 的子问题进行求解, 所以k 初始为最后一个元素的下标, 然后逐次减少 , 直到k = 0. 再看上述代码, 实际的插入过程是在递归调用后进行的, 由上篇文章中提到的在归回来时产生的副作用, 这就相当于一来 k就一直减减,减到0, 然后层层归来, 逐层返回, 再进行下面插入操作. 因此实际的执行顺序和循环版插排是一样的, 这也体现了递归和循环的统一.
结合上面的图, 我们可以看出, 每一层递归中只有一次划分, 每次划分只划了一个元素(k到k -1) 得到子问题. 在不断归回来的时, 保证前k-1是排好序的, 第k个按照同样方法插到某一位置使得前k个有序, 这就是我们的合并 .
哦… 原来插排一次只划了最后一个走, 那我们为什么不从中间划, 一次划个一半呢?
我们来考虑它的子问题, k-1个有序是k个有序的子问题, 那么从中间划分是不是可以把左右分别有序作为整体有序的子问题呢? (太聪明了, 这就是归并算法), 那我们再想想, 左右两边有序使得整体有序…这个过程稍显繁琐(合并费劲)…
那能不能我们从中间某个位置划一刀, 使得比这个位置上大的元素全放在右边, 小的全放在左边呢? 当然可以, 而且这就是没有合并的事了(划分费劲). 当然, 此划出来的位置上的元素, 一定也在他最终的位置了
快速排序算法
- 分解: 数组arr[p…r] 被划分成两个子数组arr[p…q - 1] 和 arr[p+1 … r], 使得arr[q]为大小居中的数, 左侧arr[p…q - 1]每个元素都小于它, 右侧 arr[p…q - 1] 每个元素都大于等于它. 其中计算下标q也是划分的一部分.
- 解决: 通过递归调用快速排序对两个子数组arr[p…q - 1] 和 arr[p+1 … r] 分别进行排序
- 合并: 因为子数组都是原址排序的,所以每一轮的arr[q]总是有序的, 不需要合并.
伪代码如下:
QuickSort(arr,p,r)
if p < r
q = partition(arr, p, r)
QuickSort(arr,p,q-1)
QuickSort(arr,q+1,r)
问题的关键就在于得到这个划分位置的parition函数. 但是以哪一个元素来划分呢? 我们规定以每次数组的第一个元素(主元pivot)来进行划分.
pariition函数思路
双向扫描法, 头尾指针往中间扫, 从左侧找到大于主元的元素, 从右侧找到小于等于主元的元素二者交换, 继续扫描, 直到左侧无大元素, 右侧无小元素.
def quick_sort(arr,p,r):
if p < r:
q = partition(arr, p, r)
quick_sort(arr,p, q-1)
quick_sort(arr,q+1 ,r)
def partition(arr, p, r):
pivot = arr[p]
left = p + 1
right = r
while left <= right:
# left 不断往右走, 直到遇到大于主元的元素
while left <= right and arr[left] <= pivot:
left += 1 # 循环退出时, left一定是指向第一个大于主元的位置
while left <= right and arr[right] > pivot:
right -= 1 # 循环退出时, right 一定是指向第一个小于等于主元的位置
if left < right:
swap(arr, left, right)
swap(arr, p, right)
# while 退出时,两者交错,right指向的是最后一个小于等于主元的位置,也就是主元应该待的位置
swap(arr, p ,right)
return right
def swap(arr , i ,<