分治算法的基本思想是将一个规模为N的问题分解为K个规模较小的子问题,这些子问题相互独立且与原问题性质相同。求出子问题的解,就可得到原问题的解。即一种分目标完成程序算法,简单问题可用二分法完成。
- 分治法基本思想是将一个规模为n的问题分解为k个规模较小的子问题,这些子问题相互独立且与原问题相同
- 递归的解这些子问题,然后将各子问题的解合并得到原问题的解
题号 | 题目 | 备注 |
4 | Median of Two Sorted Arrays两个有序数组的中位数 | |
50 | Pow(x, n) 求x的n次方 | |
53 | Maximum Subarray 最大连续子数组和 | 分治法 / 动态规划 |
69 | Sqrt(x) 求平方根 | |
215 | 分治法 / 堆排序 | |
4. [LeetCode] Median of Two Sorted Arrays
两个有序数组的中位数
给定两个已经升序排序过的数组,求这两个数组的中位数;中位数的定义为把两个数组合并过后进行升序排序后,处于数组中间的那个数,此时如果合并后的数组元素个数为偶数,则为中间两个数的平均值。
最简单的就是采用归并排序的思想把两个数组进行合并,然后取中间的数就可以了。但问题在于,这个题目限定了时间复杂度为O(log(m+n)),而合并算法的时间复杂度为O(nlogn),另外一个方法是设置一个双指针,一开始都指向两个数组的开头,不停地比较两个指针指向的元素的大小,指向小元素的指针的往前移一个元素去追指向大元素的指针,一直移动(len1+len2)/2次后就能得到中位数,但是这个算法的时间复杂度仍然不符合题意,为O(n)。
在有序又要求log级的时间复杂度,可以考虑分治策略,采用二分法
需要找的就是第k小的元素问题
对于一个长度为n的已排序数列a,若n为奇数,中位数为a[n / 2 + 1] ,
若n为偶数,则中位数(a[n / 2] + a[n / 2 + 1]) / 2
如果我们可以在两个数列中求出第K小的元素,便可以解决该问题
不妨设数列A元素个数为n,数列B元素个数为m,各自升序排序,求第k小元素
取A[k / 2] B[k / 2] 比较,
如果 A[k / 2] > B[k / 2] 那么,所求的元素必然不在B的前k / 2个元素中(证明反证法)
反之,必然不在A的前k / 2个元素中,于是我们可以将A或B数列的前k / 2元素删去,求剩下两个数列的
k - k / 2小元素,于是得到了数据规模变小的同类问题,递归解决
如果 k / 2 大于某数列个数,所求元素必然不在另一数列的前k / 2个元素中,同上操作就好。
func findMedianSortedArrays(nums1 []int, nums2 []int) float64 {
lenNums1 := len(nums1)
lenNums2 := len(nums2)
return float64(findKth(nums1, 0, nums2, 0, (lenNums1+lenNums2+1)/2)+findKth(nums1, 0, nums2, 0, (lenNums1+lenNums2+2)/2)) / 2
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func findKth(nums1 []int, s1 int, nums2 []int, s2 int, k int) int {
if s1 >= len(nums1) {
return nums2[s2+k-1]
}
if s2 >= len(nums2) {
return nums1[s1+k-1]
}
if k == 1 {
return min(nums1[s1], nums2[s2])
}
if s1+k/2-1 >= len(nums1) {
return findKth(nums1, s1, nums2, s2+k/2, k-k/2)
} else if s2+k/2-1 >= len(nums2) {
return findKth(nums1, s1+k/2, nums2, s2, k-k/2)
}
if nums1[s1+k/2-1] > nums2[s2+k/2-1] {
return findKth(nums1, s1, nums2, s2+k/2, k-k/2)
} else {
return findKth(nums1, s1+k/2, nums2, s2, k-k/2)
}
}
参考资料:
50. [LeetCode] Pow(x, n) 求x的n次方
Implement pow(x, n), which calculates x raised to the power n(xn).
Example 1:
Input: 2.00000, 10
Output: 1024.00000
Example 2:
Input: 2.10000, 3
Output: 9.26100
Example 3:
Input: 2.00000, -2
Output: 0.25000
Explanation: 2-2 = 1/22 = 1/4 = 0.25
Note:
-100.0 < x < 100.0
n is a 32-bit signed integer, within the range [−231, 231 − 1]
递归来折半计算,每次把n缩小一半,这样n最终会缩小到0,任何数的0次方都为1,这时候我们再往回乘,如果此时n是偶数,直接把上次递归得到的值算个平方返回即可,如果是奇数,则还需要乘上个x的值。还有一点需要引起我们的注意的是n有可能为负数,对于n是负数的情况,我们可以先用其绝对值计算出一个结果再取其倒数即可
class Solution {
public:
double myPow(double x, int n) {
if (n < 0) return 1 / power(x, -n);
return power(x, n);
}
double power(double x, int n) {
if (n == 0) return 1;
double half = power(x, n / 2);
if (n % 2 == 0) return half * half;
return x * half * half;
}
};
53. Maximum Subarray 最大连续子数组和
Given an integer array
nums
, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.Example:
Input: [-2,1,-3,4,-1,2,1,-5,4], Output: 6 Explanation: [4,-1,2,1] has the largest sum = 6.
Follow up:
If you have figured out the O(n) solution, try coding another solution using the divide and conquer approach, which is more subtle.
分治法解决:
数组分为左半部分下标为(l,mid-1)和右半部分下标为(mid+1,r)以及中间元素下标为mid,接下来递归求出左半部分的最大子序和:left=helper(nums,l,mid-1); 右半部分最大子序和right=helper(nums,mid+1,r)
将数组均分为两个部分,那么最大子数组会存在于:
- 左侧数组的最大子数组
- 右侧数组的最大子数组
- 左侧数组的以右侧边界为边界的最大子数组+右侧数组的以左侧边界为边界的最大子数组
func maxSubArray(nums []int) int {
return divide(nums, 0, len(nums)-1)
}
func max(x, y int) int {
if x < y {
return y
}
return x
}
func divide(nums []int, left, right int) int {
if left >= right {
return nums[left]
}
var mid = (left + right) / 2
var leftSum = divide(nums, left, mid-1)
var rightSum = divide(nums, mid+1, right)
var midSum = nums[mid]
var curSum = nums[mid]
for i := mid - 1; i >= left; i-- {
curSum += nums[i]
if midSum < curSum {
midSum = curSum
}
}
curSum = midSum
for i := mid + 1; i <= right; i++ {
curSum += nums[i]
if midSum < curSum {
midSum = curSum
}
}
return max(leftSum, max(rightSum, midSum))
}
69. [LeetCode] Sqrt(x) 求平方根
Implement
int sqrt(int x)
.Compute and return the square root of x.
二分搜索法来找平方根
class Solution {
public:
int mySqrt(int x) {
if (x <= 1) return x;
int left = 0, right = x;
while (left < right) {
int mid = left + (right - left) / 2;
if (x / mid >= mid) left = mid + 1;
else right = mid;
}
return right - 1;
}
};
215. [LeetCode] Kth Largest Element in an Array
堆时间复杂度nlogk,快排时间最好n,最差情况n方 。用到了快速排序Quick Sort的思想
快速排序中,每一次迭代,我们需要选取一个关键元素pivot,然后将数组分割成三个部分:
- 小于关键元素pivot的元素
- 等于关键元素pivot的元素
- 大于关键元素pivot的元素
func findKthLargest(nums []int, k int) int {
var start, end = 0, len(nums) - 1
for {
var pos = partition(nums, start, end)
if pos == k-1 {
return nums[pos]
} else if pos < k-1 {
start = pos + 1
} else {
end = pos - 1
}
}
}
func partition(nums []int, start, end int) int {
var pivot, l, r = nums[start], start, end
for l < r {
for nums[r] <= pivot && l < r {
r--
}
if l < r {
nums[l] = nums[r]
l++
}
for nums[l] >= pivot && l < r {
l++
}
if l < r {
nums[r] = nums[l]
r--
}
}
nums[l] = pivot
return l
}
一. 分治算法的控制抽象
Type DAndC(P)
{
if Small (P) return S(P);
else {
divide P into smaller instances P1, P2, ..., Pk, K >= 1;
Apply DAndC to each of these subproblems;
return Combin (DAndC(p1) , ... , DAndC(Pk));
}
}
1. 查找最大与最小值
分治算法 P= {n, a[i], ....... , a[j]} 。 如果n = 1, 最大最小为a[i], 如果n = 2, 可以通过一次比较解决问题。
void MaxMin(int i, int j, Type &max, Type &min)
{
if (i == j)
max = min = a[i];
else if (i == j - 1)
{
if (a[i] < a[j]) {
max = a[j]; min = a[i];
}
else {
max = a[i]; min = a[j];
}
}
else {
int mid = (i + j) / 2;
MaxMin(i, mid, max, min);
MaxMin(mid+1, j, max1, min2);
if (max < max1) max = max1;
if (min > min1) min = min1;
}
}
2. 归并排序算法
是分治法(Divide-and-Conquer)的典型应用。该算法的时间复杂度为O(nlogn), 其操作的步骤如下:
- Divide:把n个元素的序列分为两个元素个数为n/2的子序列。
- Conquer:递归的调用归并排序算法对两个子序列进行排序Combine:对排好序的子序列进行合并,得到最后排序的结果
归并算法用示意图表示如下:
void MergeSort (int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
MergeSort (low, mid);
MergeSort (mid + 1, high);
Merge (low, mid , high);
}
}
void Merge (int low, int mid, int high) {
int i = low, m = mid, j = mid + 1, n = high, k = 0;
while ((low <= m) && (j <= n)) {
if (a[i] < a[j])
temp[k++] = a[i++];
else
temp[k++] = a[j++]
}
while (i <= m)
temp[k++] = a[i++];
while (j <= n)
temp[k++] = a[j++];
}
3. 快速排序
该方法的基本思想是:
- 先从数列中取出一个数作为基准数。
- 分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。
- 再对左右区间重复第二步,直到各区间只有一个数。
对挖坑填数进行总结
- i =L; j = R; 将基准数挖出形成第一个坑a[i]。
- j--由后向前找比它小的数,找到后挖出此数填前一个坑a[i]中。
- i++由前向后找比它大的数,找到后也挖出此数填到前一个坑a[j]中。
- 再重复执行2,3二步,直到i==j,将基准数填入a[i]中。
void QuickSort (int a[], int l, int r) { int i = l, j = r, temp = a[l]; while (i < j) { while ((i < j) && temp <= a[j]) j--; if (i < j) a[i++] = a[j]; while ((i < j) && temp > a[i]) i++; if (i < j) a[j--] = a[i]; } a[i] = temp; QuickSort(a, l, i-1); QuickSort(a, i+1, r); }