https://leetcode.com/problems/search-for-a-range/
Given a sorted array of integers, find the starting and ending position of a given target value.
Your algorithm's runtime complexity must be in the order of O(log n).
If the target is not found in the array, return [-1, -1]
.
For example,
Given [5, 7, 7, 8, 8, 10]
and target value 8,
return [3, 4]
.
解题思路:
看到O(logn),就知道这题仍然是比较典型的二分查找。二分查找想要写正确的关键就在于把握分类的原则,就是方法内部所有分支都没有交集,但所有分支加起来又正好是全集。这个原则非常关键,有助于写出清晰的程序。前面讲过,如果前者违背了,可能导致mid被多次更新,有些区间就搜不到了;后者被违背可能搜到无法更新start或end的区间,引起死循环。我们来看看这道题目。
A[mid]大于或者小于taget的情况下,去搜另一边,这个和普通的二分查找没什么区别。问题是,如果A[mid]==target,就能return吗?这里是不行的,必须分别往两边搜,找出target可能的最大区域。但是,这时已经可以更新区域的边界值了,这里用一个int[] result来记录。分别存储mid的最大和最小值即可。但是注意第一次更新最小值的时候不能直接Math.min(result[0], mid),因为-1永远最小,必须强行更新。
递归终止的条件是start > end。
有了上面的思路,这段代码可以非常清晰的写出,一次AC,鼓舞人心啊。刷题到现在可以感觉自己的coding能力在提高。
public class Solution { public int[] searchRange(int[] A, int target) { int[] result = new int[]{-1, -1}; searchRangeHelper(A, 0, A.length - 1, target, result); return result; } public void searchRangeHelper(int[] A, int start, int end, int target, int[] result){ int mid = start + (end - start) / 2; if(start > end){ return; } if(A[mid] < target){ searchRangeHelper(A, mid + 1, end, target, result); } if(A[mid] > target){ searchRangeHelper(A, start, mid - 1, target, result); } if(A[mid] == target){ if(result[0] == -1){ result[0] = mid; }else{ result[0] = Math.min(result[0], mid); } result[1] = Math.max(result[1], mid); searchRangeHelper(A, start, mid - 1, target, result); searchRangeHelper(A, mid + 1, end, target, result); } } }
下面是网上有人给出的算法,方法和我的一样,为什么贴出来呢?是因为它用了递归,但其实思路是比较混乱的,因为递归内部还用了循环。循环一般是迭代时候采用的退出条件,而递归一般在进入方法的时候判断,如果l<=r的时候就return了。所以他这不是一个好方法,贴出来纯粹为了举反例,告警自己。
public class Solution { public int[] searchRange(int[] A, int target) { int[] ans = new int[]{-1,-1}; searchRange(A, target, 0, A.length-1, ans); return ans; } private void searchRange(int[] A, int target, int start, int end, int[] ans){ if(end<start) return; int l =start, r = end, mid; while(l<=r){ mid = l+(r-l)/2; if(A[mid]==target){ ans[0] = (ans[0]>=0)? Math.min(ans[0],mid):mid; ans[1] = Math.max(ans[1],mid); searchRange(A,target,l,mid-1,ans); searchRange(A,target,mid+1,r,ans); return; } else if(A[mid]>target) r = mid-1; else l = mid+1; } return; } }
回头说这道题的解法,二分查找可以用递归可以用迭代,一般简单的我们都用迭代,可是为什么这道题用递归?因为A[mid] == target的时候,需要分别去两边搜索,这时递归就能写出比较清晰的思路。那么用迭代如何去实现呢?两次二分查找。第一次找range的最左边界,第二次找最右边界。
public class Solution { public int[] searchRange(int[] A, int target) { int[] result = new int[]{-1, -1}; int start = 0; int end = A.length - 1; while(start <= end){ int mid = start + (end -start) / 2; if(A[mid] < target){ start = mid + 1; }else if(A[mid] > target){ end = mid - 1; }else{ if(result[0] == -1){ result[0] = mid; }else{ result[0] = Math.min(result[0], mid); } result[1] = Math.max(result[1], mid); //找最左边界 end = mid - 1; } } start = 0; end = A.length - 1; while(start <= end){ int mid = start + (end -start) / 2; if(A[mid] < target){ start = mid + 1; }else if(A[mid] > target){ end = mid - 1; }else{ if(result[0] == -1){ result[0] = mid; }else{ result[0] = Math.min(result[0], mid); } result[1] = Math.max(result[1], mid); //找最右边界 start = mid + 1; } } return result; } }
继续优化,第一次更新最左边界,那么只要更新result[0]就可以了。第二次更新最右边界,再去更新result[1]。而且第二次只需要在result[0]到A.length - 1的区间内去搜索右边界即可。但是要注意,如果result[0]==-1,则要从0开始。
public class Solution { public int[] searchRange(int[] A, int target) { int[] result = new int[]{-1, -1}; int start = 0; int end = A.length - 1; while(start <= end){ int mid = start + (end -start) / 2; if(A[mid] < target){ start = mid + 1; }else if(A[mid] > target){ end = mid - 1; }else{ if(result[0] == -1){ result[0] = mid; }else{ result[0] = Math.min(result[0], mid); } //找最左边界 end = mid - 1; } } start = result[0] == -1 ? 0 : result[0]; end = A.length - 1; while(start <= end){ int mid = start + (end -start) / 2; if(A[mid] < target){ start = mid + 1; }else if(A[mid] > target){ end = mid - 1; }else{ result[1] = Math.max(result[1], mid); //找最右边界 start = mid + 1; } } return result; } }