分治法将问题分割成更小的子问题,然后递归(迭代)地解决这些子问题,最后将它们的解合并起来得到原问题的解。因此,分治法常用于解决一些具有重复性质的问题,例如排序、查找、计算等问题。
分治法通常包括三个步骤:
1.分解(Divide):将原问题分解成若干个规模较小的子问题,这些子问题互相独立且与原问题形式相同。
2.解决(Conquer):递归地解决各个子问题。当子问题的规模足够小时,可以直接求解。
3.合并(Combine):将各个子问题的解合并成原问题的解。
分治法的优点是可以降低算法的时间复杂度,使得原本难以解决的问题可以得到有效的解决。
分治法的缺点是需要额外的空间来存储子问题的解,(递归的过程也会增加函数调用的开销)。
因此,在设计算法时需要权衡时间和空间的使用。
分治法应用:
(1)二分搜素
A.迭代实现
arr
表示有序数组,target
表示要查找的目标元素。函数binary_search
使用两个指针low
和high
来表示搜索范围。在每次循环中,计算中间位置mid
,如果arr[mid]
等于target
,则返回mid
。
如果arr[mid]
小于target
,则说明目标元素在mid
的右侧,将low
指针移到mid
的右侧,继续搜索。如果arr[mid]
大于target
,则说明目标元素在mid
的左侧,将high
指针移到mid
的左侧,继续搜索。如果搜索范围缩小到low > high
,则说明目标元素不存在于数组中,返回-1。
def binary_search(arr, target):
low, high = 0, len(arr) - 1
while low <= high:
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
low = mid + 1
else:
high = mid - 1
return -1
arr = [1, 2, 3, 4, 5, 6, 7, 8, 9]
target = 5
print(binary_search(arr, target)) # 输出:4
B.递归实现
def binary_search_recursive(arr, target, low, high):
if low > high:
return -1
mid = (low + high) // 2
if arr[mid] == target:
return mid
elif arr[mid] < target:
return binary_search_recursive(arr, target, mid + 1, high)
else:
return binary_search_recursive(arr, target, low, mid - 1)
(2)找最大最小元素(MINMAX)
arr
表示要查找的数组,low
和high
表示搜索范围的下标。在每次递归中,如果搜索范围缩小到只有一个元素,返回该元素作为最小值和最大值。如果搜索范围缩小到只有两个元素,比较这两个元素的大小,返回最小值和最大值。如果搜索范围缩小到超过两个元素,
将搜索范围分成两个部分,分别递归查找左半部分和右半部分的最小值和最大值,然后
比较左半部分的最小值和右半部分的最小值,返回较小值作为最小值,
比较左半部分的最大值和右半部分的最大值,返回较大值作为最大值。
def minmax(arr, low, high):
if low == high:
return arr[low], arr[low]
elif high == low + 1:
return (arr[low], arr[high]) if arr[low] < arr[high] else (arr[high], arr[low])
else:
mid = (low + high) // 2
left_min, left_max = minmax(arr, low, mid)
right_min, right_max = minmax(arr, mid + 1, high)
return (min(left_min, right_min), max(left_max, right_max))
arr = [3, 5, 2, 1, 7, 4, 9, 6, 8]
print(minmax(arr, 0, len(arr) - 1)) # 输出:(1, 9)
(3)寻找第k小元素
arr
表示要查找的数组,k
表示要查找的第k小元素。在每次递归中,随机选择一个元素作为基准点pivot
,将数组分成三个部分:小于pivot
的元素(lows)、等于pivot
的元素(pivots)和大于pivot
的元素(highs)。
如果lows的元素个数大于等于k,第k
小元素在lows中,对lows进行递归查找。
如果lows+pivots的元素个数大于等于k,第k
小元素等于pivot
。
否则,第k
小元素在highs中,对highs进行递归查找。
以上,其实也可以说是,进行一次快速排序,然后就可以确定这个第k小元素在基准点的哪一边了
import random
def select(arr, k):
if len(arr) == 1:
return arr[0]
pivot = random.choice(arr)
lows = [x for x in arr if x < pivot]
highs = [x for x in arr if x > pivot]
pivots = [x for x in arr if x == pivot]
if k < len(lows):
return select(lows, k)
elif k < len(lows) + len(pivots):
return pivots[0]
else:
return select(highs, k - len(lows) - len(pivots))
arr = [3, 5, 2, 1, 7, 4, 9, 6, 8]
print(select(arr, 6)) # 输出:6
当然,我们发现直接用排序然后来找也可以,最好的时间复杂度为O(logn)。而使用上述方法,时间复杂度就只有O(n)了。但是在n小于某个阀值时,直接用排序其实更快。(这个阀值可以经过推导的)。
‘’随机选取"也可以换成更加严谨的方式,比如把数组划分为多个元素为5的小数组,然后找他们的中位数,再找出这些中位数的中位数,来当这个基准点。这样可以避免基准点选成了最大最小值这样的影响效率的情况。
(4)大整数乘法
将两个大整数分别拆分成两个部分(一半高位和一半低位),然后通过递归计算四个部分的乘积,最后将四个部分的乘积组合起来得到最终结果。
如下,multiply
函数接收两个大整数x
和y
作为参数,并返回它们的乘积。
在函数内部,首先判断输入的大整数是否已经缩小到只有一位,如果是,则直接返回它们的乘积。否则,将两个大整数分别拆分成高位和低位部分,然后通过递归计算四个部分的乘积。最后,将四个部分的乘积组合起来得到最终结果。
def multiply(x, y):
if len(x) == 1 or len(y) == 1:
return int(x) * int(y)
n = max(len(x), len(y))
m = n // 2
a = x[:-m]
b = x[-m:]
c = y[:-m]
d = y[-m:]
ac = multiply(a, c)
bd = multiply(b, d)
ad_bc = multiply(str(int(a) + int(b)), str(int(c) + int(d))) - ac - bd
return 10**(2*m)*ac + 10**m*ad_bc + bd
x = "12345678901234567890"
y = "98765432109876543210"
result = multiply(x, y)
print(result)