文章放置于:https://github.com/zgkaii/CS-Notes-Kz,欢迎批评指正!
分治
基本概念
分治算法(divide and conquer)和核心思想正如其字面含义,分而治之,就是把一个复杂问题分成两个或者更多的相同和相似的问题,直到最后问题可以简单的直接求解,原问题的解即是子问题解的合并。
这个定义看起来类似递归的定义,区别在于分治算法是一种处理问题的思想,而递归是一种编程技巧。实际上,分治算法一般比较适合用递归来实现,当然也可以用迭代来实现,这也是区别之一。递归算法从本质上来就是分治算法,无非就是有些问题递归需要将原问题分解成多个子问题,而有的只需要分解成一个子问题,前者为分治,后者即为递归。
分治算法能解决的问题一般满足下面几个条件:
- 分解(Divide):将原问题分解成若干个子问题。
- 解决(Conquer):分解的子问题足够小并可以独立求解的话,就直接求解。
- 合并(Merge):将子问题的解合并,形成原问题的解。(这个合并操作的复杂度不易过高)
分治算法一般体现在
归并排序
和快速排序
里面。
经典举例
以剑指 Offer 51. 数组中的逆序对为例,如果数组中的两个数字,前面一个数字大于后面的数字,则这两个数字组成一个逆序对。
例如数组[7,5,6,4],其逆序对有(7,5),(7,6),(7,4),(5,4),(6,4)
,逆序数为3+1+1=5
。正如这里罗列的一样,暴力解法即拿每个数字跟它后面的数字比较,统计比它小的数记着k
。n
个元素就有对应的n
个逆序数k
,然后求这n
个k
之和。这样操作的时间复杂度为O(n2)。有没有更加高效的处理方法呢?
这里,可以采用分治的思想来解决问题。先把数组分成前后两个部分leftPart
与rightPart
,分别计算leftPart
与rightPart
对应逆序对数k1
与k2
,然后再计算 leftPart
与 rightPart
之间的逆序对个数 K3
。那么总的逆序数为k1+k2+k3
了。这样,通过分治把问题分解成独立子问题直接求解,最后再合并子问题的解。
为保证合并的操作复杂度不高,可以采用归并排序算法
来解决。归并排序中有一个非常关键的操作,就是将两个有序的小数组,合并成一个有序的数组。如下图所示,为数组 [7, 3, 2, 6, 0, 1, 5, 4]的归并排序过程。
合并阶段本质上是合并两个排序数组的过程,而每当遇到 左子数组当前元素 > 右子数组当前元素 时,意味着 「左子数组当前元素 至 末尾元素」 与 「右子数组当前元素」 构成了若干 「逆序对」 。
public int reversePairs(int[] nums) {
if (nums.length < 2)
return 0;
int[] tmp = new int[nums.length];// 临时数组用于归并
return mergeSort(nums, tmp, 0, nums.length - 1);
}
public int mergeSort(int[] nums, int[] tmp, int left, int right) {
// 终止条件,子数组长度为1,停止划分
if (left >= right)
return 0;
// 递归划分左子数组与右子数组
int mid = left + right >>> 1;
int res = mergeSort(nums, tmp, left, mid) + mergeSort(nums, tmp, mid + 1, right);
// 合并阶段 idx为临时数组的移动指针
int i = left, j = mid + 1, idx = left, count = 0;
// 左右两数组都还剩有数字未排序时
while (i <= mid && j <= right) {
if (nums[i] > nums[j]) {
tmp[idx+&#