分治介绍
主要思想
分治算法的主要思想是将原问题递归地分成若干个子问题,直到子问题满足边界条件,停止递归。将子问题逐个击破(一般是同种方法),将已经解决的子问题合并,最后,算法会层层合并得到原问题的答案。
分治算法的步骤
分:递归地将问题分解为各个的子问题(性质相同的、相互独立的子问题);
治:将这些规模更小的子问题逐个击破;
合:将已解决的子问题逐层合并,最终得出原问题的解;
分治法适用的情况
- 原问题的计算复杂度随着问题的规模的增加而增加。
- 原问题能够被分解成更小的子问题。
- 子问题的结构和性质与原问题一样,并且相互独立,子问题之间不包含公共的子子问题。
- 原问题分解出的子问题的解可以合并为该问题的解。
相关概念:
有序度:表示一组数据的有序程度
逆序度:表示一组数据的无序程度
分治法的设计思想是:将一个难以直接解决的大问题,分割成一些规模较小的相同问题,以便各个击破,分而治之。
分治策略是:对于一个规模为n的问题,若该问题可以容易地解决(比如说规模n较小)则直接解决,否则将其分解为k个规模较小的子问题,这些子问题互相独立且与原问题形式相同,递归地解这些子问题,然后将各子问题的解合并得到原问题的解。这种算法设计策略叫做分治法。
分治法的复杂性分析
一个分治法将规模为n的问题分成k个规模为n/m的子问题去解。设分解阀值n0=1,且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。用T(n)表示该分治法解规模为|P|=n的问题所需的计算时间,则有:
T(n)= k T(n/m)+f(n)
通过迭代法求得方程的解:
递归方程及其解只给出n等于m的方幂时T(n)的值,但是如果认为T(n)足够平滑,那么由n等于m的方幂时T(n)的值可以估计T(n)的增长速度。通常假定T(n)是单调上升的,从而当 mi≤n<mi+1时,T(mi)≤T(n)<T(mi+1)。
可使用分治法求解的一些经典问题
(1)二分搜索
(2)大整数乘法
(3)Strassen矩阵乘法
(4)棋盘覆盖
(5)合并排序
(6)快速排序
(7)线性时间选择
(8)最接近点对问题
(9)循环赛日程表
(10)汉诺塔
运用举例
二分查找
分治算法的一个经典应用是二分查找。所谓二分查找,就是在一个排序好的数组中找到目标值,并且输出目标值的坐标。比如在 [0,1,2,3,4,5,6,9,10,12,14,15,16] 中查找 3 返回 3,查找 9 返回 7,查找 8 返回 False。
最简单的查找办法就是按顺序排查,从 0 开始,到 1,到 2,直到找到目标值为止。这样的时间复杂度是 O(n)。在最坏的情况下,我们会将所有的数字检查一遍。
用分治算法我们可以把时间复杂度降到 O(log n)。也就是说在最坏的情况下,我们只需要检查 log2(n) 个数字。通过下面的例子我们看分治算法是怎样提高效率的。我们设目标值为 3。
- 在分治算法的第一步中,我们取数组的中间值 6。因为 6 比 3 大,所以我们可以放心地直接排除掉数组的后半部分,并肯定目标值只能在数组的前半部分。
- 第二步,遵循同样的步骤,对比目标值和中间值,因为 2 比 3 小,所以排除子数组的前半部分。
- 第三步,对比 4 和 3,排除子数组的后半部分,最终剩余的部分是 [3]。
- 第四步,因为数组[3] 的长度为 1,所以停止递归。检查数组中的数是否为目标值,如果得到的答案是肯定的,所以直接输出坐标,如果是否定的,就返回 False。因为 3 等于目标值,所以返回 3 的坐标:3。
在以上过程中,每一步我们都将数组一分为二,将原数组变成两个相互独立的子数组,并肯定目标值在其中的一个数组里。通过对比中间值,我们直接排除一个子数组,由此节省时间。
def binarySearch(nums,target): #传入数组,目标值
def helper(i,j): #递归方法,i,j为当前数组的起点,终点坐标
if i==j and nums[i]==target:
return i
if i>=j:
return False
mid = (j-i)//2 + i #中间值
if nums[mid] == target: #如果中间值等于目标值
return mid
elif nums[mid]<target: #如果中间值小于目标值
return helper(mid+1,j) #递归,目标值在右侧
else:
return helper(i,mid-1) #目标值在左侧
return helper(0,len(nums)-1)
代码实践
1.Pow(x, n)(50)
实现 pow(x, n) ,即计算 x 的 n 次幂函数。
示例 1:
输入: 2.00000, 10
输出: 1024.00000
示例 2:
输入: 2.10000, 3
输出: 9.26100
说明:
-100.0 < x < 100.0
n 是 32 位有符号整数,其数值范围是 [−231, 231 − 1] 。
通过次数117,838提交次数323,801
解法(来自LeetCode):
class Solution(object):
def myPow(self, x, n):
"""
:type x: float
:type n: int
:rtype: float
"""
if n==0:
return 1
if n<0:
return 1/self.myPow(x,-n)
if n%2:
return x*self.myPow(x,n-1)
return self.myPow(x*x,n/2)
2.多数元素
给定一个大小为 n 的数组,找到其中的众数。众数是指在数组中出现次数大于 [n/2] 的元素。
你可以假设数组是非空的,并且给定的数组总是存在众数。
class Solution(object):
def majorityElement2(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
# 【不断切分的终止条件】
if not nums:
return None
if len(nums) == 1:
return nums[0]
# 【准备数据,并将大问题拆分为小问题】
left = self.majorityElement(nums[:len(nums)//2])
right = self.majorityElement(nums[len(nums)//2:])
# 【处理子问题,得到子结果】
# 【对子结果进行合并 得到最终结果】
if left == right:
return left
if nums.count(left) > nums.count(right):
return left
else:
return right
3.最大子序和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大为6。
解题思路:
class Solution(object):
def maxSubArray(self, nums):
"""
:type nums: List[int]
:rtype: int
"""
# 【确定不断切分的终止条件】
n = len(nums)
if n == 1:
return nums[0]
# 【准备数据,并将大问题拆分为小的问题】
left = self.maxSubArray(nums[:len(nums)//2])
right = self.maxSubArray(nums[len(nums)//2:])
# 【处理小问题,得到子结果】
# 从右到左计算左边的最大子序和
max_l = nums[len(nums)//2 -1] # max_l为该数组的最右边的元素
tmp = 0 # tmp用来记录连续子数组的和
for i in range( len(nums)//2-1 , -1 , -1 ):# 从右到左遍历数组的元素
tmp += nums[i]
max_l = max(tmp ,max_l)
# 从左到右计算右边的最大子序和
max_r = nums[len(nums)//2]
tmp = 0
for i in range(len(nums)//2,len(nums)):
tmp += nums[i]
max_r = max(tmp,max_r)
# 【对子结果进行合并 得到最终结果】
# 返回三个中的最大值
return max(left,right,max_l+ max_r)