细解二分+滑动窗口
前言
二分查找是一个细节较多的查找方法,但归类其细节,就会清晰明了。滑动窗口思想针对连续子数组较有用。
一、和大于等于target的最短子数组
二、题解
主要为滑动窗口,每次求和大于等于target时,就缩小窗口来寻找最短连续子数组。
package com.xhu.offer.offerII;
//和大于等于target的最短子数组
public class MinSubArrayLen {
public int minSubArrayLen(int target, int[] nums) {
//暴力法:设置1到n的窗口来判断是否有和大于target
for (int i = 0; i < nums.length; i++) {
int begin = 0, end = i;
int sum = 0;
for (int j = begin; j <= end; j++) sum += nums[j];
while (end < nums.length) {
if (sum >= target) return i + 1;
sum -= nums[begin++];
if (end + 1 == nums.length) break;
sum += nums[++end];
}
}
return 0;
}
//进阶:o(NlogN),可否利用连续?
public int minSubArrayLen2(int target, int[] nums) {
int i = 1;
for (; i <= nums.length; i <<= 1) {
int begin = 0, end = i;
int sum = 0;
for (int j = begin; j < end; j++) sum += nums[j];
boolean flag = false;
if (sum >= target) flag = true;
while (end < nums.length) {
sum -= nums[begin++];
sum += nums[end++];
if (sum >= target) {
flag = true;
break;
}
}
if (flag) break;
}
if (i == 1) return 1;
for (int j = (i >> 1) + 1; j <= i; j++) {
int sum = 0, begin = 0, end = j;
for (int n = begin; n <= end; n++) sum += nums[n];
while (end < nums.length) {
if (sum >= target) return j + 1;
sum -= nums[begin++];
if (end + 1 < nums.length) sum += nums[++end];
}
}
return 0;
}
//进阶O(n)
public int minSubArrayLen3(int target, int[] nums) {
//滑动窗口,利用已知条件全为正整数。
int minLen = nums.length + 1;
int sum = 0, begin = 0, end = 0;
while (end < nums.length) {
sum += nums[begin];
if (sum >= target) {
minLen = minLen < end - begin + 1 ? minLen : end - begin + 1;
while (begin <= end) {
sum -= nums[begin++];
if (sum >= target) minLen = minLen < end - begin + 1 ? minLen : end - begin + 1;
else break;
}
}
end++;
}
return minLen < nums.length + 1 ? minLen : 0;
}
//参考官方的其它解法:前缀和+二分法查找前缀和之差最接近target,更新最小长度。
public int minSubArrayLen4(int target, int[] nums) {
//滑动窗口,利用已知条件全为正整数。,比较全是正整数,那么前缀和一定是递增的。
int len = nums.length, minLen = len + 1;
int[] prefix = new int[len + 1];
prefix[0] = -1;
int sum = 0;
for (int i = 0; i < len; i++) prefix[i + 1] = (sum += nums[i]);
//遍历前缀和,然后每次在左边的前缀和找key = prefix[cur] - target的前缀和.
//因为有序,可二分O(logN)寻找key.
for (int i = 1; i <= len; i++) {
int key = prefix[i] - target;
int idx = binarySearch(prefix, 0, i, key);
if (prefix[i] >= target) {
int gap = prefix[i] - prefix[idx] >= target ? i - idx : i - idx + 1;
minLen = minLen < gap ? minLen : gap;
}
}
return minLen == len + 1 ? 0 : minLen;
}
//二分,取下整 + high = mid,low < high用来避免high = mid带来的死循环问题。
private int binarySearch(int[] nums, int fromIndex, int toIndex,
int target) {
int low = fromIndex;
int high = toIndex - 1;
while (low < high) {
int mid = low + high >>> 1;
int midVal = nums[mid];
if (midVal < target) low = mid + 1;
else high = mid;
}
return high;
}
public static void main(String[] args) {
System.out.println(-2 << 1);
}
}
总结
1、对于二分,一种是找值,一种是插入位置。
1)对于找单个值,
[high取length - 1] + [mid向下取整] + [low<=high] + [找到即返回。]
2)对于取左右或找到升序插入位置,
1-取左或找到升序插入位置:
[high 取length而不是length - 1] + [mid向下取整] + [low<high] + [high=mid] + [return high];
2-取右或找到升序插入位置:
[high 取length而不是length - 1] + [mid向上取整] + [low<high] + [low=mid] + [return low];
注:mid向上下取整是为了配合high=mid、low=mid,从而避免死循环。
2、对于滑动窗口,只需要双指针遇到相应的情况进行相应的前后指针滑动。
参考文献
[1] LeetCode 原题
[2] LeetCode 官方题解