!文章内容如有错误或排版问题,请提交反馈,非常感谢!
分治法概念
分治法(divide-and-conquer)字面上的解释是“分而治之”,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。分治有两个特点:
- 子问题相互独立且与原问题形式相同
- 反复分治,使划分得到的子问题小到不能再分时,直接对其求解
分治法所能解决的问题一般具有以下特征:
- 该问题的规模缩小到一定程度就可以容易的解决。
- 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构的性质。
- 利用该问题分解出的子问题的解可以合并为该问题的解(关键性质)
- 该问题所分解出的各个子问题最好是相互独立的,即子问题之间不包含公共的子问题。
其中:
- 第一条特征是绝大多数问题都能满足的,因为计算复杂性一般随着规模的减小而降低。
- 第二条特征是应用分治法的前提,它也是大多数问题可以满足的,此特征反映了递归思想的应用。
- 第三条特征是关键,能否利用分治法完全取决于问题是否具有第三条特征。如果具备了第一条特征和第二条特征,而子问题的解不可以合并,则可以考虑贪心算法或动态规划。
- 第四条特征涉及分治法的效率,如果各子问题不是独立的,则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
分治模式在每一层递归上都有三个步骤:
- 分解(Divide):将原问题分解为若干个规模较小、相互独立、与原问题形式相同的子问题。
- 解决(Conquer):若子问题规模较小而容易求解则直接求解,否则递归的解决各个子问题。
- 合并(Combine):将各个子问题的解合并为原问题的解。
分治法的一般算法框架如下:
divide_and_conquer(problem) { if(|problem|<n) adhoc(problem); /*解决小规模的问题*/ divide Problem into smaller subinstances P1,P2,....Pk; /*分解问题*/ for(i=1;i<=k(子问题个数);++i){ result[i]=divide_and_conquer(P[i]); /*递归的解各个子问题*/ } return merge(result[1]...result[k]); /*将各个子问题的解合并为原问题的解*/ }
分治法求解的一些经典问题:
- 二分搜索
- 大整数乘法
- Strassen矩阵乘法
- 棋盘覆盖
- 合并排序
- 快速排序
- 线性时间选择
- 最接近点对问题
- 循环赛日程表
- 汉诺塔
分治法的典型实例
二分搜索
问题描述:给定已按照升序排列好的n个元素a[0:n-1],现在要在这n个元素中找出一特定元素。
问题分析:采用分治法的思想,充分利用元素之间已经存在的次序关系,将序列划分为两个个数相同的左右两个部分,取最中间的元素和x进行比较,如果等于中间的元素就找到,如果不等于,就递归的在左边(x<中间值)或者右边(x>中间值)二分查找。
注意事项:对于二分查找,一般需要建立两个不变性:
- 当前待查找序列,肯定包含目标元素
- 每次待查找序列的规模都会变小。
Python示例:
# -*- coding: utf-8 -*- def binary_search(array, t): low = 0 height = len(array)-1 while low < height: mid = low + (height-low)//2 # 用mid=(begin+end)//2是有溢出危险的,对于一个规模足够大的数组(极端假设为2147483647个元素),那么在对后半部(2/n~n)取mid时,使用mid=(end+start)/2溢出(结果会得到一个负数) if array[mid] < t: low = mid + 1 elif array[mid] > t: height = mid - 1 else: return array[mid] return -1 if __name__ == "__main__": print(binary_search([1,2,3,34,56,57,78,87],57))
快速排序
快速排序(Quicksort),是一种排序算法,最坏情况复杂度:Ο(n2),最优时间复杂度:Ο(nlogn),平均时间复杂度:Ο(nlogn)。快速排序的基本思想也是用了分治法的思想:找出一个元素X,在一趟排序中,使X左边的数都比X小,X右边的数都比X要大。然后再分别对X左边的数组和X右边的数组进行排序,直到数组不能分割为止。
快速排序的基本思想:设当前待排序的无序区为R[low..high],利用分治法可将快速排序的基本思想描述为:
- 分解:在R[low..high]中任选一个记录作为基准(Pivot),以此基准将当前无序区划分为左、右两个较小的子区间R[low..pivotpos-1)和R[pivotpos+1..high],并使左边子区间中所有记录的关键字均小于等于基准记录(不妨记为pivot)的关键字key,右边的子区间中所有记录的关键字均大于等于pivot.key,而基准记录pivot则位于正确的位置(pivotpos)上,它无须参加后续的排序。(注意:划分的关键是要求出基准记录所在的位置pivotpos。划分的结果可以简单地表示为(注意pivot=R[pivotpos]):R[low..pivotpos-1].keys≤R[pivotpos].key≤R[pivotpos+1..high].keys,其中low≤pivotpos≤high。)
- 求解:通过递归调用快速排序对左、右子区间R[low..pivotpos-1]和R[pivotpos+1..high]快速排序。
- 组合:因为当”求解”步骤中的两个递归调用结束时,其左、右两个子区间已有序。对快速排序而言,”组合”步骤无须做什么,可看作是空操作。
Python实现:
#-*- coding: utf-8 -*- # 方法1 def quickSort(arr): less = [] pivotList = [] more = [] if len(arr) <= 1: return arr else: pivot = arr[0] # 将第一个值做为基准 for i in arr: if i< pivot: less.append(i) elif i > pivot: more.append(i) else: pivotList.append(i) less = quickSort(less) # 得到第一轮分组之后,继续将分组进行下去。 more = quickSort(more) return less + pivotList + more # 方法2 # 分为<, >, = 三种情况,如果分为两种情况的话函数调用次数会增加许多,以后几个好像都有相似的问题 # 如果测试1000个100以内的整数,如果分为<, >= 两种情况共调用函数1801次,分为<, >, = 三种情况,共调用函数201次 def qsort(L): return ( qsort([y for y in L[1:] if y< L[0]]) + L[:1] + [y for y in L[1:] if y == L[0]] + qsort([y for y in L[1:] if y > L[0]]) ) if len(L) > 1 else L # 方法3 # 基本思想同上,只是写法上又有所变化 # def qsort(list): # if not list: # return [] # else: # pivot = list[0] # less = [x for x in list if x< pivot] # more = [x for x in list[1:] if x >= pivot] # return qsort(less) + [pivot] + qsort(more) # 方法4 from random import * def qSort(a): if len(a) <= 1: return a else: q = choice(a) # 基准的选择不同于前,是从数组中任意选择一个值做为基准 return qSort([elem for elem in a if elem< q]) + [q] * a.count(q) + qSort([elem for elem in a if elem > q]) # 方法5 # 这个最狠了,一句话搞定快速排序,瞠目结舌吧。 qs = lambda xs: \ ((len(xs) <= 1 and [xs]) or [ qs([x for x in xs[1:] if x< xs[0]]) + [xs[0]] + qs([x for x in xs[1:] if x >= xs[0]])])[ 0] if __name__ == "__main__": a = [4, 65, 2, -31, 0, 99, 83, 782, 1] print(quickSort(a)) print(qsort(a)) print(qSort(a)) print(qs(a))