LC上二分查找那一章有这么些题:
二分查找的题经常用于考,因为它虽然看似简单,但其实要完全正确却不容易,很容易写出死循环的程序。一个二分查找的程序可以很容易判断出一个人功底扎不扎实。
457. Classical Binary Search
这是一道非常经典的二分查找题,给出一个有序数组以及一个目标值target,要求返回target在数组中的位置,若数组里不存在target,则返回-1。套用经典的二分查找模板即可:
public int findPosition(int[] A, int target) {
if (A == null || A.length == 0) {
return -1;
}
int start = 0, end = A.length - 1;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (A[mid] == target) {
return mid;
} else if (A[mid] < target) {
start = mid;
} else {
end = mid;
}
}
if (A[start] == target) {
return start;
}
if (A[end] == target) {
return end;
}
return -1;
}
给定一个有序数组和一个目标值,要求在O(logn)的时间内找到那个目标值第一个出现的位置(数组中可能存在多个相同的目标值)
public int binarySearch(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return -1;
}
int start = 0, end = nums.length - 1;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (nums[mid] >= target) {
end = mid;
} else {
start = mid;
}
}
if (nums[start] == target) {
return start;
}
if (nums[end] == target) {
return end;
}
return -1;
}
需要注意的是,当找到一个数和target相等时,要把end指针,即右指针指向target,并继续循环搜索,直到整个区间的长度<=2,这个时候就跳出循环,看看这最后两个数哪个是第一个就返回它。
给定一个有序数组和一个目标值,要求在O(logn)的时间内找到那个目标值最后1个出现的位置(数组中可能存在多个相同的目标值)。跟上道题类似,需要注意的是,当找到一个数和target相等时,要把start指针,即左指针指向target,并继续循环搜索,直到整个区间的长度<=2,这个时候就跳出循环,看看这最后两个数哪个是最后一个就返回它。
public int lastPosition(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return -1;
}
int start = 0, end = nums.length - 1;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (nums[mid] <= target) {
start = mid;
} else {
end = mid;
}
}
if (nums[end] == target) {
return end;
}
if (nums[start] == target) {
return start;
}
return -1;
}
459. Closest Number in Sorted Array
在一个有序数组中,给定一个target,要求在数组中找到离target最近的数。
Given [1, 4, 6]
and target = 3
, return 1
.
Given [1, 4, 6]
and target = 5
, return 1
or 2
.
Given [1, 3, 3, 4]
and target = 2
, return 0
or 1
or 2
.
public int closestNumber(int[] A, int target) {
if (A == null || A.length == 0) {
return -1;
}
int start = 0, end = A.length - 1;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (A[mid] == target) {
return mid;
} else if (A[mid] < target) {
start = mid;
} else {
end = mid;
}
}
int left = Math.abs(A[start] - target);
int right = Math.abs(A[end] - target);
return left < right ? start : end;
}
要在一个有序数组中求某个target出现了多少次,这道题可以在上上一道题的基础上来做,找到数组中第一个出现target的位置,然后再数它出现了几次就行。
public int totalOccurrence(int[] A, int target) {
if (A == null || A.length == 0) {
return 0;
}
int start = 0, end = A.length - 1;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (A[mid] < target) {
start = mid;
} else {
end = mid;
}
}
int count;
if (A[start] == target) {
count = 0;
for (int i = start; i < A.length; i++) {
if (A[i] == target) {
count++;
}
}
return count;
}
if (A[end] == target) {
count = 0;
for (int i = end; i < A.length; i++) {
if (A[i] == target) {
count++;
}
}
return count;
}
return 0;
}
给定一个排序数组和一个目标值,如果在数组中找到目标值则返回索引。如果没有,返回到它将会被按顺序插入的位置。你可以假设在数组中无重复元素。
[1,3,5,6],5 → 2
[1,3,5,6],2 → 1
[1,3,5,6],7 → 4
[1,3,5,6],0 → 0
public int searchInsert(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return 0;
}
int start = 0, end = nums.length - 1;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] < target) {
start = mid;
} else {
end = mid;
}
}
if (target <= nums[start]) {
return start;
}
if (target <= nums[end]) {
return end;
}
return end + 1;
}
28. Search a 2D Matrix
要求在一个二维数组中找到某个数target。这个矩阵具有以下特性:每行中的整数从左到右是排序的。每行的第一个数大于上一行的最后一个整数。
由于这个数组是从左到右有序、从上到下有序的。所以可以把它重排成一维的有序数组。利用除法和取模运算,把它映射到一位数组上,然后再用一位数组的二分搜索来做。
public boolean searchMatrix(int[][] matrix, int target) {
// write your code here
if (matrix == null || matrix.length == 0) {
return false;
}
int m = matrix.length;
int n = matrix[0].length;
if (n == 0) {
return false;
}
int left = 0, right = m * n - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (matrix[mid/n][mid%n] == target) {
return true;
} else if (matrix[mid/n][mid%n] < target) {
left = mid + 1;
} else if (matrix[mid/n][mid%n] > target) {
right = mid - 1;
}
}
return false;
}
547. Intersection of Two Arrays
求两个数组的交集,最后的交集必须是唯一的,不得出现重复元素。有多种方法可以求解。我先只列出HashSet的解法:
public int[] intersection(int[] nums1, int[] nums2) {
if (nums1 == null || nums2 == null) {
return null;
}
HashSet set1 = new HashSet();
for (int i = 0; i < nums1.length; i++) {
set1.add(nums1[i]);
}
HashSet res = new HashSet();
for (int i = 0; i < nums2.length; i++) {
if (set1.contains(nums2[i]) && !res.contains(nums2[i])) {
res.add(nums2[i]);
}
}
int[] result = new int[res.size()];
int index = 0;
for (Integer tmp : res) {
result[index] = tmp;
index++;
}
return result;
}
解法一:用HashSet,扫描第一个数组,加进HashSet1中,得到的HashSet1是唯一的。然后扫描第二个数组,如果第二个数组的元素在HashSet1中存在,则加进HashSet2中。最后得到的HashSet2就是答案了。
解法二:先排序,然后扫描第二个数组,在扫描第二个数组的过程中使用binarySearch,binarySearch即在第一个数组中找第二个数组的某个元素,如果找到了则加入HashSet,这样能保证答案是唯一的。
548. Intersection of Two Arrays II
跟上道题类似,但是不同的是最后的答案是可以出现重复元素的,即在原数组中出现了几次,最后就得出现几次。这道题得用一个HashMap,把第一个数组的元素以及出现次数存进HashMap中,然后再对第二个数组进行扫描。扫描的过程中,如果发现有匹配的,就加进res里,并且把HashMap中对应的出现次数减1。
public int[] intersection(int[] nums1, int[] nums2) {
if (nums1 == null || nums2 == null) {
return null;
}
HashMap map = new HashMap();
for (int i = 0; i < nums1.length; i++) {
if (map.containsKey(nums1[i])) {
map.put(nums1[i], 1 + map.get(nums1[i]));
} else {
map.put(nums1[i], 1);
}
}
ArrayList res = new ArrayList();
for (int i = 0; i < nums2.length; i++) {
if (map.containsKey(nums2[i]) && map.get(nums2[i]) > 0) {
res.add(nums2[i]);
map.put(nums2[i], map.get(nums2[i]) - 1);
}
}
int[] result = new int[res.size()];
for (int i = 0; i < result.length; i++) {
result[i] = res.get(i);
}
return result;
}
给一个整数,要求它的平方数,当然不能直接调用库函数,这道题得用二分来模拟平方的操作。记得要用long来处理,因为可能会出现溢出。
public int sqrt(int x) {
long start = 0, end = x;
while (start + 1 < end) {
long mid = start + (end - start) / 2;
if (mid * mid > x) {
end = mid;
} else if (mid * mid == x) {
return (int)mid;
} else {
start = mid;
}
}
if (start * start <= x && end * end > x) {
return (int)start;
}
return (int)end;
}
586. Sqrt(x) II
要求出一个数的开根号,只不过从整数换成了小数。当然小数是没办法精确地计算根号值的,只能近似的去模拟,比如误差不大于多少1e-10。
有2种方法可以解决这个问题,第一种是二分法。因为根号值的数肯定是大于0的,如果那个数大于1,那么它的根号值就肯定也大于1,如果那个数小于1,那么他的根号值肯也小于1.这样我们就有了查询的初始区间,也就是[0, n]。然后我们去用二分法逼近,直到区间的长度缩小到了一定范围,这样我们就能得到近似值了。
第二种方法是牛顿法逼近,这篇博客讲的很详细:点击打开链接 可以在纸上画图体验一下牛顿逼近法的美妙之处。
public class Solution {
/**
* @param x a double
* @return the square root of x
*/
public double sqrt(double x) {
return binary_search_sqrt(x);
}
public double newton_sqrt(double n) {
double result = 1.0;
double eps = 1e-12;
while (Math.abs(result * result - n) > eps) {
result = (result + n / result) / 2;
}
return result;
}
public double binary_search_sqrt(double n) {
double start = 0, end = n > 1 ? n : 1.0;
while (end - start > 1e-12) {
double mid = (start + end) / 2;
if (mid * mid > n) {
end = mid;
} else {
start = mid;
}
}
return start;
}
}
428. Pow(x, n)
要自己实现一个求幂函数。
最直观容易想到的方法就是用递归方法求n个x的乘积,注意考虑n的正负号,时间复杂度为O(n)
public double myPow(double x, int n) {
if (n == 0) {
return 1.0;
}
if (n < 0) {
return 1.0/myPow(x, -n);
}
return x * myPow(x, n - 1);
}
public double myPow(double x, int n) {
if (n == 0) {
return 1.0;
}
if (n < 0) {
return 1.0/myPow(x, -n);
}
double half = myPow(x, n / 2);
if (n % 2 == 0) {
return half * half;
}
return x * half * half;
}
二分法变种题,类似于first postion of target那道题
public int findFirstBadVersion(int n) {
int start = 1, end = n;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (!SVNRepo.isBadVersion(mid)) {
start = mid;
} else {
end = mid;
}
}
if (SVNRepo.isBadVersion(start)) {
return start;
} else {
return end;
}
}
找一个数组的峰值,峰值也就是他这个点比左右两边的元素都要大。只要找到一个峰值就行。所以根据定义,数组的第一个元素和最后一个元素不可能是峰值。
二分的时候会有四种情况:
1)mid落在了上升区间,那这个时候的峰值肯定在mid右边的区间
2)mid落在了下降区间,那这个时候的峰值肯定在mid左边的区间
3)mid就在峰值,那就直接return
4)mid在极小值,那就随便往右边或者往左边走都行
public int findPeak(int[] A) {
int start = 0, end = A.length - 1;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (A[mid - 1] <= A[mid] && A[mid] <= A[mid + 1]) {
start = mid;
continue;
}
if (A[mid - 1] >= A[mid] && A[mid] >= A[mid + 1]) {
end = mid;
continue;
}
if (A[mid - 1] <= A[mid] && A[mid] >= A[mid + 1]) {
return mid;
}
end = mid;
}
if (A[start] < A[end]) {
return end;
}
return start;
}
在一个有序数组里,找到一个数出现的范围。跟那道total occurrence of target是一样的思想。就是求一个数出现了几次,只不过这道题是要返回第一个出现的位置以及最后一个出现的位置:
public int[] searchRange(int[] A, int target) {
int[] res = new int[2];
if (A == null || A.length == 0) {
return new int[]{-1, -1};
}
int start = 0, end = A.length - 1;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (A[mid] < target) {
start = mid;
} else {
end = mid;
}
}
if (A[start] == target) {
res[0] = start;
for (int i = start; i < A.length; i++) {
if (A[i] == target) {
res[1] = i;
}
}
return res;
}
if (A[end] == target) {
res[0] = end;
for (int i = end; i < A.length; i++) {
if (A[i] == target) {
res[1] = i;
}
}
return res;
}
return new int[]{-1, -1};
}
447. Search in a Big Sorted Array
在一个很大的全都是正数的有序数组流里面找target,你只能通过API来获取特定位置的元素。由于二分法是需要边界才能进行的,而这个数组流来说,我们需要找到它的右边界,即第一个 ≥ target的元素的位置。这里要用到倍增法。只要当前get到的元素比target小,我就把区间乘以2,一直到get到一个比target大的元素为止。这时候我们就在Ologn的时间内把一个无限的数组流转换成了一个有限有序数组。然后再用有序数组里二分就好了。
不过有一个地方需要注意的是,这个不是真正无限的数组,他有一定边界,但是他没有告诉你边界,但是如果你通过API get到的那个值为-1的时候,就说明越界了,所以在倍增的同时需要保持不越界。
public int searchBigSortedArray(ArrayReader reader, int target) {
int index = 1;
while (reader.get(index - 1) < target && reader.get(index - 1) != -1) {
index *= 2;
}
int start = 0, end = index - 1;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (reader.get(mid) == -1) {
end = mid;
continue;
}
if (reader.get(mid) < target) {
start = mid;
} else {
end = mid;
}
}
if (reader.get(start) == target) {
return start;
}
if (reader.get(end) == target) {
return end;
}
return -1;
}
在一个旋转有序数组里找到最小值。旋转有序的数组的意思是:(i.e., 0 1 2 4 5 6 7 might become 4 5 6 7 0 1 2)。数组里的元素都是唯一的,没有重复。
总之这个数组一定是有序的,要么是整段有序,要么就是由2段有序数组组成。并且没有重复元素的存在。
假如是整段有序,那就直接返回第一个元素就是最小值了。假如是后面那种分2段有序的情况,那就用二分查找找那个0的位置。比前一个元素小,比后一个元素要小的就是最小值了。我们需要指定一个target,找到第一个小于等于target的数,在这里我们需要指定数组的最后一个元素为target。
public int findMin(int[] num) {
if (num == null || num.length == 0) {
return -1;
}
int start = 0, end = num.length - 1;
if (num[end] > num[start]) {
return num[start];
}
int target = num[end];
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (num[mid] <= target) {
end = mid;
} else {
start = mid;
}
}
if (num[start] <= target) {
return num[start];
} else {
return num[end];
}
}
160. Find Minimum in Rotated Sorted Array II
跟上道题类似,但是数组可能会出现重复元素。比如{1,1,1,1,1,1}。这个时候,就没法用二分来做了,所以只有从头到尾遍历所有元素才可以。
62. Search in Rotated Sorted Array
在一个旋转有序数组里找到某个target。旋转有序的数组的意思是:(i.e., 0 1 2 4 5 6 7 might become 4 5 6 7 0 1 2)。并且里面没有重复元素。
总之这个数组一定是有序的,要么是整段有序,要么就是由2段有序数组组成。如果是整段有序,那用二分查找很好做。但是关键是处理后面那种分段有序的情况稍微棘手一点。
如果最后一个元素比第一个元素大,那就说明这个是单调递增的有序数组,可以直接用二分查找模板来做。假如最后一个元素比第一个元素小的话,那么则首先看它是在哪段区间里,如果start对应的元素比mid对应的元素小,则是左边那段上升区间。如果start对应的元素比mid对应的元素大,则是右边那段上升区间。
public int search(int[] A, int target) {
if (A == null || A.length == 0) {
return -1;
}
int start = 0, end = A.length - 1;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (A[mid] == target) {
return mid;
}
// 左边上升区间
if (A[start] < A[mid]) {
if (A[start] <= target && target <= A[mid]) {
end = mid;
} else {
start = mid;
}
} else { // 右边上升区间
if (A[mid] <= target && target <= A[end]) {
start = mid;
} else {
end = mid;
}
}
}
if (A[start] == target) {
return start;
}
if (A[end] == target) {
return end;
}
return -1;
}
63. Search in Rotated Sorted Array II
跟上道题类似,但是数组可能会出现重复元素。比如{1,1,1,1,1,1}。这个时候,就没法用二分来做了,所以只有从头到尾遍历所有元素才可以。
38. Search a 2D Matrix II
在一个二维数组里找给定值出现了几次,二维数组的每一行都是有序的,每一列都是有序的。每一行、每一列中都没有重复元素。
[
[1, 3, 5, 7],
[2, 4, 7, 8],
[3, 5, 9, 10]
]
比如这个数组里找3,则返回3出现的次数:2次。
有个巧妙的方法,就是从左下角开始搜索,如果左下角元素比target小,则往右移动;若比target大,则往上移动;若相等,则count++,并同时x--,y++
public int searchMatrix(int[][] matrix, int target) {
if (matrix == null || matrix.length == 0) {
return 0;
}
int m = matrix.length;
int n = matrix[0].length;
if (n == 0) {
return 0;
}
int x = m - 1;
int y = 0;
int count = 0;
while (x >= 0 && y < n) {
if (matrix[x][y] < target) {
y++;
} else if (matrix[x][y] > target) {
x--;
} else {
count++;
x--;
y++;
}
}
return count;
}
460. K Closest Numbers In Sorted Array
要求在一个数组中找到K个离target最近的数。我们可以先找到第一个大于等于target的数在数组中的位置,然后再从这个位置往两边用双指针同时扫描,扫描到K个数的时候,就停止扫描,这样就得到了K个最近的数了。
// find the first index of element >= target
private int firstIndex(int[] A, int target) {
int start = 0, end = A.length - 1;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (A[mid] >= target) {
end = mid;
} else {
start = mid;
}
}
if (A[start] >= target) {
return start;
}
if (A[end] >= target) {
return end;
}
return A.length;
}
public int[] kClosestNumbers(int[] A, int target, int k) {
if (A == null || A.length == 0) {
return A;
}
if (k > A.length) {
return A;
}
int index = firstIndex(A, target);
int start = index - 1, end = index;
int[] res = new int[k];
for (int i = 0; i < k; i++) {
if (start < 0) {
res[i] = A[end++];
} else if (end >= A.length) {
res[i] = A[start--];
} else {
if (target - A[start] <= A[end] - target) {
res[i] = A[start--];
} else {
res[i] = A[end++];
}
}
}
return res;
}
183. Wood Cut
切木头问题,给定几块木头,和一个需要的木头块数目,你要如何砍这些木头,使得在达到需要的木头块数目的同时又保证每块木头的长度一样并且最大。
For L=[232, 124, 456], k=7, return 114. 比如给了你三块长度分别为232、124、456的木头,你需要把他们均分成7块等长的木头,并且保证木头长度最大。
在这里的话其实是一个二分问题,114就是我们要找的值。而区间则是[0, 最长的木头的长度]。然后我在这个区间内进行二分,113短了,115大了,只有114才是满足条件的。
private int count(int[] L, int n) {
int res = 0;
for (int i = 0; i < L.length; i++) {
res += L[i] / n;
}
return res;
}
public int woodCut(int[] L, int target) {
int max = 0;
for (int i = 0; i < L.length; i++) {
max = Math.max(max, L[i]);
}
int start = 1, end = max;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (count(L, mid) < target) {
end = mid;
} else {
start = mid;
}
}
if (count(L, end) >= target) {
return end;
}
if (count(L, start) >= target) {
return start;
}
return 0;
}
248. Count of Smaller Number
这道题最好用线段树来做,不过暂时还没接触线段树,就可以先用排序+二分查找的方法来做。先对数组排序,然后对于每个查询,用二分查找在数组中进行查询,找到原数组中第一个比查询数大的数,然后再从后往前统计。
private int count(int[] num, int target) {
if (num == null || num.length == 0) {
return 0;
}
int start = 0, end = num.length - 1;
while (start + 1 < end) {
int mid = start + (end - start) / 2;
if (target <= num[mid]) {
end = mid;
} else {
start = mid;
}
}
if (num[start] >= target) {
return start;
}
if (num[end] >= target) {
return end;
}
return 0;
}
public ArrayList countOfSmallerNumber(int[] A, int[] queries) {
Arrays.sort(A);
ArrayList res = new ArrayList();
for (int i = 0; i < queries.length; i++) {
int n = count(A, queries[i]);
res.add(n);
}
return res;
}
633. Find the Duplicate Number
解法:时间复杂度O(n * log n) 二分查找(Binary Search)+ 鸽笼原理(Pigeonhole Principle) 参考维基百科关于鸽笼原理的词条链接:https://en.wikipedia.org/wiki/Pigeonhole_principle “不允许修改数组” 与 “常数空间复杂度”这两个限制条件意味着:禁止排序,并且不能使用Map等数据结构 小于O(n2)的运行时间复杂度可以联想到使用二分将其中的一个n化简为log n 参考LeetCode Discuss:https://leetcode.com/discuss/60830/python-solution-explanation-without-changing-input-array 二分枚举答案范围,使用鸽笼原理进行检验 根据鸽笼原理,给定n + 1个范围[1, n]的整数,其中一定存在数字出现至少两次。 假设枚举的数字为 n / 2: 遍历数组,若数组中不大于n / 2的数字个数超过n / 2,则可以确定[1, n /2]范围内一定有解, 否则可以确定解落在(n / 2, n]范围内。
可以考虑构造出边界条件数组:[5,6,6,6] 来帮助确定二分查找的边界条件
public class Solution {
/**
* @param nums an array containing n + 1 integers which is between 1 and n
* @return the duplicate one
*/
public int findDuplicate(int[] nums) {
int start = 1, end = nums.length - 1;
while (start + 1 < end) {
int mid = (start + end) / 2;
if (checkSmaller(nums, mid) <= mid) {
start = mid;
} else if (checkSmaller(nums, mid) > mid) {
end = mid;
}
}
if (checkSmaller(nums, start) > start) {
return start;
}
return end;
}
public int checkSmaller(int[] nums, int target) {
int count = 0;
for (int i = 0; i < nums.length; i++) {
if (nums[i] <= target) {
count++;
}
}
return count;
}
}
一个数组既有正数也有负数,要求出长度大于等于K的最大平均子数组的平均长度是多少。也就是说需要找出一个长度大于等于K的子数组,这个子数组的平均数是最大的。只需要返回那个平均数即可,不需要求出整个子数组是什么。比如说一个数组是[1, 12, -5, -6, 50, 3], k = 3。那么就返回15.667。因为 (-6 + 50 + 3) / 3 = 15.667
如果是要把整个子数组打印输出,那可能这道题就没啥可以优化的地方了,直接就循环比较,找出平均数最大的子数组输出。但是问题是这道题并不要求把整个数组打印出来,而是需要你找到一个最大的平均数。要找一个最大的数,那其实可以想到或许可以用二分搜索去搜索这个数。
而平均数是一个小数,我们只要满足精度的要求就好了,那么二分搜索的循环中止条件就可以是左右区间的精度小于某个可以接受的范围。
假设数组是A,最后需要返回的平均数是avg。假如子数组{A[1], A[2], A[3]}是我们想要的结果的话,那sum[3] = A[1] + A[2] + A[3] - avg - avg - avg应该是大于等于0的。
假设数组的最大值和最小值分别是max和min,那么avg肯定是介于max和min之间的。所以二分搜索初始化的时候可以吧left和right赋值为min和max。搜索的时候,假如遍历数组发现有sum值是大于0的,那么就可以肯定目标avg值是会比当前的mid要大的。反之则比mid小。
checksum的时候只需要检查长度大于等于K的sum和就好,如果发现前面有badAss拖后腿,则直接删掉badAss。然后badAss是要随着遍历数组的进行而不断更新的。
public boolean checkRest(int[] nums, int k, double mid) {
double[] sum = new double[nums.length + 1];
sum[0] = 0;
double badAss = 0.0;
for (int i = 1; i <= nums.length; i++) {
sum[i] = sum[i - 1] + nums[i - 1] - mid;
if (i >= k) {
if (sum[i] - badAss >= 0) {
return true;
}
badAss = Math.min(badAss, sum[i - k + 1]);
}
}
return false;
}
public double maxAverage(int[] nums, int k) {
double min = Integer.MAX_VALUE, max = Integer.MIN_VALUE;
for (int i = 0; i < nums.length; i++) {
if (nums[i] >= max) {
max = nums[i];
}
if (nums[i] <= min) {
min = nums[i];
}
}
while (max - min > 1e-6) {
double mid = (min + max) / 2.0;
if (checkRest(nums, k, mid)) {
min = mid;
} else {
max = mid;
}
}
return min;
}
600. Smallest Rectangle Enclosing Black Pixels
这道题给我们一个二维矩阵,表示一个图片的数据,其中1代表黑像素,0代表白像素,现在让我们找出一个最小的矩阵可以包括所有的黑像素,还给了我们一个黑像素的坐标。这里需要注意的一点是,所有黑色像素都是连通的。
那我们需要找的就是最小矩阵的上边界、下边界、左边界、右边界。
再来通过如下的图来理解一下,假设有如下的矩阵:
"000000111000000"
"000000101000000"
"000000101100000"
"000001100100000"
那其实投影后就变成了如下的样子:
"000001111100000"
由于二维矩阵里黑像素是连通的,所以投影到一维的时候肯定是连续的。
row search为例, 所有含有black pixel的column,映射到row x上时,必定是连续的。这样我们可以使用binary search,在0到y里面搜索最左边含有black pixel的一列。接下来可以继续搜索上下和右边界。
public class Solution {
/**
* @param image a binary matrix with '0' and '1'
* @param x, y the location of one of the black pixels
* @return an integer
*/
public int minArea(char[][] image, int x, int y) {
if (image == null || image.length == 0) {
return 0;
}
int left = binarySearchLeft(image, 0, y);
int right = binarySearchRight(image, y, image[0].length - 1);
int high = binarySearchHigh(image, x, image.length - 1);
int low = binarySearchLow(image, 0, x);
return (right - left + 1) * (high - low + 1);
}
public int binarySearchLeft(char[][] image, int left, int right) {
while (left + 1 < right) {
int mid = (left + right) / 2;
boolean black_flag = false;
for (int i = 0; i < image.length; i++) {
if (image[i][mid] == '1') {
black_flag = true;
break;
}
}
if (black_flag) {
right = mid;
} else {
left = mid;
}
}
for (int i = 0; i < image.length; i++) {
if (image[i][left] == '1') {
return left;
}
}
return right;
}
public int binarySearchRight(char[][] image, int left, int right) {
while (left + 1 < right) {
int mid = (left + right) / 2;
boolean black_flag = false;
for (int i = 0; i < image.length; i++) {
if (image[i][mid] == '1') {
black_flag = true;
break;
}
}
if (black_flag) {
left = mid;
} else {
right = mid;
}
}
for (int i = 0; i < image.length; i++) {
if (image[i][right] == '1') {
return right;
}
}
return left;
}
public int binarySearchHigh(char[][] image, int left, int right) {
while (left + 1 < right) {
int mid = (left + right) / 2;
boolean black_flag = false;
for (int i = 0; i < image[0].length; i++) {
if (image[mid][i] == '1') {
black_flag = true;
break;
}
}
if (black_flag) {
left = mid;
} else {
right = mid;
}
}
for (int i = 0; i < image[0].length; i++) {
if (image[right][i] == '1') {
return right;
}
}
return left;
}
public int binarySearchLow(char[][] image, int left, int right) {
while (left + 1 < right) {
int mid = (left + right) / 2;
boolean black_flag = false;
for (int i = 0; i < image[0].length; i++) {
if (image[mid][i] == '1') {
black_flag = true;
break;
}
}
if (black_flag) {
right = mid;
} else {
left = mid;
}
}
for (int i = 0; i < image[0].length; i++) {
if (image[left][i] == '1') {
return left;
}
}
return right;
}
}
在二维数组里面找peak,peak的定义是它比上下左右四个方向的数字都要小。而且只要找到一个peak就可以返回了。首先对每行进行二分,找到一行,然后用二分法找到这行里面最大的元素,如果这个元素比上面的小,那就继续往上进行二分。如果这个元素比下面的小,那就往下面进行二分。如果这个元素比上下都大,那就可以返回了。由于根据题意,每行每列都是满足先变大后变小的,所以方法是奏效的。
public List findPeakII(int[][] A) {
List res = new ArrayList();
int low = 1, high = A.length - 2;
while (low <= high) {
int mid = (low + high) / 2;
int col = findPeak(A, mid);
if (A[mid][col] < A[mid - 1][col]) {
high = mid - 1;
} else if (A[mid][col] < A[mid + 1][col]) {
low = mid + 1;
} else {
res.add(mid);
res.add(col);
return res;
}
}
return res;
}
public int findPeak(int[][] A, int row) {
int col = 0;
for (int i = 0; i < A[row].length; i++) {
if (A[row][i] > A[row][col]) {
col = i;
}
}
return col;
}
假设输入是n*n的矩阵,findPeak函数的时间复杂度是n,而findPeak被执行了logn次,所以最坏的时间复杂度是logn * n, 最好的情况是logn + n(横竖各扫一遍就找到peak了)