分治算法(Divide and Conquer)
算法
分治,“分而治之”,即把一个复杂的问题分成两个或者更多的相同的或者相似的子问题,再把子问题分成更小的子问题,直到最后的子问题可以简单的求解,原问题的解就是子问题解的合并
基本思想
分治算法在每一层的递归上都有三个步骤:
分解: 将原问题分解为规模更小、相互独立且与原问题形式相同的子问题
求解: 如果子问题的规模足够小,可以很容易解决则直接解决,否则,进入下一层递归
合并: 将求得的子问题的解合并为上一个原问题的解,直到合并为最初原问题的解
适用问题
- 该问题的规模缩小的一定程度可以比较简单地解决
- 该问题可以分解为若干规模较小的相同问题
- 利用该问题分解出的子问题的解可以合并为该问题的解
- 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子问题
时间复杂度
分治算法将规模为 n n n 的问题分为 k k k 个规模为 n / k n/k n/k 的子问题解决。
设分解阈值为 n 0 = 1 n_0 = 1 n0=1;且求解规模为 1 1 1 的问题需耗费 1 1 1 个单位时间;
再设将原问题分解为 k k k 个子问题,以及合并 k k k 个子问题的解合并为原问题的解需要用 f ( n ) f(n) f(n) 个单位时间;
用
T
(
n
)
T(n)
T(n) 表示使用该分治算法求解规模为
n
n
n 的问题所需的时间,则有:
T
(
n
)
=
k
∗
T
(
n
/
k
)
+
f
(
n
)
T(n) = k*T(n/k) + f(n)
T(n)=k∗T(n/k)+f(n)
案例之二分查找
二分查找是典型的分治算法的应用,将原有的 有序 数列划分为左右两个子序列,然后再对两个子序列中的其中一个进行划分,直到查找成功
算法流程
二分查找的前提:必须是有序数列
算法流程:(查找一个数)
- 选择一个标志
i
将数列分为左右两个数列 - 判断
L[i]
是否与要查找的数ans
相等,相等则返回i
- 否则,判断
L[i]
与ans
的大小 - 基于第三步的结果,决定之后是向左寻找还是向右寻找
- 递归,直到找到数
ans
,或者确定数组L
中找不到
代码示例
Python
'''
最基础的二分查找算法
在一个有序的数列中,寻找特定元素的位置
如果找到搜索的数,则返回其索引,否则返回 None
'''
def basic_BinarySearch(search_list, item):
low = 0
high = len(search_list)-1
while low <= high:
# 防止 mid 计算溢出
mid = low + (high-low)//2
guess = search_list[mid]
if guess == item:
return mid
elif guess > item:
high = mid - 1
else:
low = mid + 1
return None
'''
寻找左边界的二分查找算法
'''
def left_BinarySearch(search_list, item):
if not search_list:
return None
low = 0
high = len(search_list) # 与 basic 不同
while low < high: # 相比 basic ,没有等号
mid = low + (high-low)//2
if search_list[mid] >= item:
high = mid
else:
low = mid + 1
return low
'''
寻找右边界的二分查找算法
'''
def right_BinarySearch(search_list, item):
if not search_list:
return None
low = 0
high = len(search_list)
while low < high:
mid = low + (high-low)/2
if search_list[mid] <= item:
low = mid + 1
else:
high = mid
return low-1
Java
/**
* 通过二分查找法,在有序数组 nums 中查找 item 是否存在
* 如果存在则返回索引,否则返回 -1
* */
public static int basicBinarySearch(int[] nums, int item) {
int low = 0; // 数组的第一个索引
int high = nums.length - 1; // 数组的最后一个索引
while (low <= high) {
int mid = low + (high - low) / 2;
int temp = nums[mid];
if (temp == item) {
return mid;
} else if (temp < item) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return -1;
}
/**
* 寻找左边界的二分查找算法
* 在有序数组中查找 nums 中元素是否存在大于等于 item
* 如果存在,返回第一个大于等于 item 的元素的索引,否则返回 -1
* */
public static int leftBinarySearch(int[] nums, int item) {
if (nums[0] >= item) {
return 0;
}
if (nums[nums.length - 1] < item) {
return -1;
}
int low = 0;
int high = nums.length - 1;
while (low < high) {
int mid = low + (high - low) / 2;
if (nums[mid] >= item) {
high = mid;
} else {
low = mid + 1;
}
}
return low;
}
/**
* 寻找右边界的二分查找算法
* 在有序数组中查找 nums 中元素是否存在小于等于 item
* 如果存在,返回最后一个小于等于 item 的元素的索引,否则返回 -1;
* */
public static int rightBinarySearch(int[] nums, int item) {
if (nums[0] > item) {
return -1;
}
if (nums[nums.length - 1] <= item) {
return nums.length - 1;
}
int low = 0;
int high = nums.length - 1;
while (low < high) {
int mid = low + (high - low) / 2;
if (nums[mid] <= item) {
low = mid + 1;
} else {
high = mid;
}
}
return low - 1;
}
案例之全排列问题
LeetCode 题目 46
给定一个 没有重复 数字的序列,返回其所有可能的全排列
示例:
输入:
[1,2,3]
输出:
[
[1,2,3],
[1,3,2],
[2,1,3],
[2,3,1],
[3,1,2],
[3,2,1]
]
问题分析
采用分治思想,首先要把大问题分解为很多子问题,大问题是所有的排列方法;
分解得到的子问题就是:以 1 开头的排列、以 2 开头的排列、以 3 开头的排列;
现有的子问题仍可继续分解,例如,以 1 开头的子问题,确定了 1 的位置,但是没有确定 2、3 的位置,可以继续分解,直到分解成的子问题只有一个数字,不再分解
代码示例
Python
def permute(self, nums: List[int]) -> List[List[int]]:
def fullSort(sort_nums: List[int], start: int, end: int):
if start == end:
res.append(nums[:])
else:
for i in range(start, end):
temp = sort_nums[start]
sort_nums[start] = sort_nums[i]
sort_nums[i] = temp
fullSort(sort_nums, start+1, end)
sort_nums[i] = sort_nums[start]
sort_nums[start] = temp
end = len(nums)
res = []
fullSort(nums, 0, end)
return res
Java
/**
* 给定一个没有重复数字的序列,返回其所有可能的全排列
* */
public static List<List<Integer>> permute(int[] nums) {
int end = nums.length;
List<List<Integer>> result = new ArrayList<>();
List<Integer> answer = new ArrayList<>();
for (int num : nums) {
answer.add(num);
}
permuteAdd(result, answer,0, end);
return result;
}
public static void permuteAdd(List<List<Integer>> result, List<Integer> answer, int start, int end) {
if (start == end) {
result.add(new ArrayList<>(answer));
}
for (int i = start; i < end; i++) {
Collections.swap(answer, start, i);
permuteAdd(result, answer,start + 1, end);
Collections.swap(answer, start, i);
}
}
案例之归并排序
归并排序
将两个或两个以上的 有序 数组合并成一个新的 有序 数组,
即,将待排序序列分成若干个子序列,每个子序列是有序的,然后再把有序子序列合并为整体有序序列
实现方法——迭代法:
- 申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列
- 设定两个指针,最初位置分别为两个已经排序序列的起始位置
- 比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置
- 重复步骤 3 ,直到某一个指针到达序列尾
- 将另一序列剩下的所有元素直接复制到合并序列尾
实现方法——递归法:
- 将序列中每相邻两个数字进行归并操作,形成 [ n / 2 ] [n/2] [n/2] 个序列,排序后每个序列都包含两个元素
- 将上述序列再次合并,形成 [ n / 4 ] [n/4] [n/4] 个序列,每个序列中的元素数倍增
- 重复步骤 2 ,直到所有元素排序
代码示例
Python
def merge(s1, s2, s):
'''
将两个列表 s1 、s2 按顺序融合为一个列表 s
:param s1:
:param s2:
:param s:
:return:
'''
i = j = 0 # 分别指向 s1 、s2
while i+j < len(s):
if j == len(s2) or (i<len(s1) and s1[i]<s2[j]):
s[i+j] = s1[i]
i += 1
else:
s[i+j] = s2[j]
j += 1
def merge_sort(s):
'''归并排序'''
n = len(s)
if n < 2:
return
mid = n // 2
s1 = s[0:mid]
s2 = s[mid:n]
merge_sort(s1)
merge_sort(s2)
merge(s1, s2, s)
Java
public class MergeSortDemo {
public static void main(String[] args) {
int[] arr = {8, 4, 5, 7, 1, 3, 6, 2, 9, 0, 29};
int[] temp = new int[arr.length];
mergerSort(arr, 0, arr.length - 1, temp);
System.out.println(Arrays.toString(arr));
}
// 分解过程
public static void mergerSort(int[] arr, int left, int right, int[] temp) {
if (left < right) {
int mid = (left + right) / 2;
mergerSort(arr, left, mid, temp);
mergerSort(arr, mid + 1, right, temp);
merge(arr, left, mid, right, temp);
}
}
// 合并过程
public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; // 表示左边有序序列的初始索引
int j = mid + 1; // 表示右边有序序列的初始索引
int t = 0; // 指向 temp 的当前索引
// 先把左右两边有序的数据按照规则填充到 temp 中
// 直到左右两边的有序序列,有一边处理完毕
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[t] = arr[i];
t += 1;
i += 1;
} else {
temp[t] = arr[j];
t += 1;
j += 1;
}
}
// 把剩余的一个序列,全部填充到 temp
while (i <= mid) {
temp[t] = arr[i];
t += 1;
i += 1;
}
while (j <= right) {
temp[t] = arr[j];
t += 1;
j += 1;
}
// 把 temp 复制到 arr
// 并不是每次都拷贝所有的数据
t = 0;
int tempLeft = left;
while (tempLeft <= right) {
arr[tempLeft] = temp[t];
t += 1;
tempLeft += 1;
}
}
}
案例之快速排序
快速排序
快速排序使用分治算法来把一个序列分为较小和较大的两个序列,然后递归地排序两个序列
实现步骤:
- 挑选基准值:从数列中挑出一个值,作为“基准”(pivot)
- 分割:重新排序数列,所有比基准值小的元素摆放在基准的前面,所有比基准大的元素摆放在基准的后面(与基准相等的元素可以放在任何一边);分割完成后,对基准值的排序也就完成了
- 递归排序子序列:递归地将小于基准值元素的子序列和大于基准值元素的子序列排序
递归到最底部的判断条件:
数列的大小为 0 或者 1,此时数列已为有序数列
选取基准的方法对排序的时间性能有决定性影响
代码示例
实现方式1:
Python
def partition(arr, low, high):
i = low-1 # index of minimum-element
pivot = arr[high]
for j in range(low, high):
# if current-element <= pivot
if arr[j] <= pivot:
i = i+1
arr[i], arr[j] = arr[j], arr[i]
arr[i+1], arr[high] = arr[high], arr[i+1]
return (i+1)
def quickSort(arr, low, high):
if low < high:
pi = partition(arr, low, high)
print('results of each step sorting')
print('arra:',arr)
quickSort(arr, low, pi-1)
quickSort(arr, pi+1, high)
arr = [10, 7, 8, 9, 1, 5]
n = len(arr)
quickSort(arr, 0, n-1)
print('arr: ', arr)
实现方式2:
# 额外开辟空间
def quick_sort(s):
if len(s)<2:
return
pivot = s[0]
lp = []
ep = []
hp = []
while len(s)>0:
if s[-1] < pivot:
lp.append(s.pop())
elif s[-1] == pivot:
ep.append(s.pop())
else:
hp.append(s.pop())
quick_sort(lp)
quick_sort(hp)
s.extend(lp)
s.extend(ep)
s.extend(hp)
实现方法3:
# 不需要额外开辟空间
def inplace_quick_sort(s, a, b):
if a >= b:
return
pivot = s[b]
left = a
right = b-1
while left <= right:
while left <= right and s[left] < pivot:
left += 1
while left <= right and pivot < s[right]:
right -= 1
if left <= right:
s[left], s[right] = s[right], s[left]
left, right = left+1, right-1
s[left], s[b] = s[b], s[left]
inplace_quick_sort(s, a, left-1)
inplace_quick_sort(s, left+1, b)
Java
public class QuickSortDemo {
public static void main(String[] args) {
int[] arr = {-9, 78, 0, 23, -5, 70, 900, -560, 893};
System.out.println(Arrays.toString(arr));
quickSort(arr, 0, arr.length - 1);
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int[] arr, int left, int right) {
int l = left; // 左下标
int r = right; // 右下标
int pivot = arr[(left + right) / 2];
int temp;
// 通过 while 循环,将比 pivot 值小的放到左边
// 将比 pivot 值大的放到右边
while (l < r) {
// 找到比 pivot 值小于等于的值
while (arr[l] < pivot) {
l += 1;
}
// 找到比 pivot 值大于等于的值
while (arr[r] > pivot) {
r -= 1;
}
if (l >= r) { // 说明 pivot 左右两边的值满足
break; // 左边小于 pivot;右边大于 pivot
} else { // l < r
temp = arr[l];
arr[l] = arr[r];
arr[r] = temp;
// 如果交换完后, arr[l] == pivot,r--,前移
if (arr[l] == pivot) {
r -= 1;
}
// 如果交换完后,arr[r] == pivot,l++,后移
if (arr[r] == pivot) {
l += 1;
}
}
}
// 一定要做如下判断,否则出现栈溢出
// 因为是满足上述条件的,无法从循环中退出
if (l == r) {
l += 1;
r -= 1;
}
if (left < r) {
quickSort(arr, left, r);
}
if (right > l) {
quickSort(arr, l, right);
}
}
}
案例之汉诺塔(Hanoi Tower)
汉诺塔问题
一个梵塔,塔内有三个盘座A、B、C,A上有64个盘子,盘子大小不等,大在下,小在上。若要把盘子从A移到B,每次只能移动一个,且移动过程中,3个座上始终保持:大在上,小在下
问题分析
- 如果只有一个盘子,则不需要借助C,直接从A移动到B
- 如果只有两个盘子,则可以先把A的最上面一个盘子2移动到C;将盘子1移动到B;再讲盘子2移动到B。;这说明了借助C可以将2个盘子从A移动到B;同样,也说明借助B可以将2个盘子从A移动到C
- 如果有3个盘子,那么根据2个盘子的结论,可以借助B将盘子1上的两个盘子(盘子2和盘子3)从A移动到C;将盘子1从A移动到B,A则变为空座;借助A座,将C上的两个盘子移动到B
- 以此类推,上述思路可以扩展到 n 个盘子的情况,将较小的 n-1 个盘子看做一个整体,也就是我们要求的子问题,初始A上有 n 个盘子,B、C上为空;可以借助B,将A上面的 n-1 个盘子从A移动到C;将A上最大的盘子1移动到B,A为空座;;可以借助B,将C上的 n-2 个盘子从C移动到A;将C上最大的盘子2(从整体看,为第二大)移动到B,C为空座;;…
代码示例
def move(n, source, target):
print('The', n, 'th', 'plate move: ', source, '------>', target)
def hanoi(n, source, temp, target):
if n == 1:
# only one plate, move it directly from source to target
move(n, source, target)
else:
# move the top n-1 plates from source to temp through target
hanoi(n-1, source, target, temp)
# move the n plate(the largest and lowest plate) from source to target
move(n, source, target)
# move the top n-1 plates from temp to target through source
hanoi(n-1, temp, source, target)
hanoi(3, 'A', 'C', 'B')