LeetCode链接:https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/
题目:
给定一个按照升序排列的整数数组nums,和一个目标值target,找出给定目标值在数组中的开始位置和结束位置,当数组中不存在目标值时返回[-1, -1];
要求:
算法的时间复杂度必须是O(log n)级别
我有两种解决该问题的方案:
方案一:先通过二分查找,找到目标值target在数组中的位置mid,然后以mid为中心,向两边依次查找是否有与target相同的数组元素
1 public class Solution { 2 public int[] searchRange(int[] nums, int target) { 3 if(nums == null || nums.length == 0) return new int[] {-1, -1}; 4 if(nums.length == 1) return nums[0] == target ? new int[] {0, 0} : new int[] {-1, -1}; 5 int left = 0, right = nums.length - 1, mid = 0; 6 while(left <= right) { // 二分查找target在数组中的位置,如果数组中有多个与target相等的元素,则返回其中的某一个元素的下标 7 mid = (left + right) / 2; 8 if(nums[mid] == target) { 9 break; 10 } 11 else if(nums[mid] < target) { 12 left = mid + 1; 13 } 14 else { 15 right = mid - 1; 16 } 17 } 18 if(left > right) return new int[] {-1, -1}; // 如果数组中不存在该目标值,则返回[-1, -1] 19 left = mid - 1; 20 right = mid + 1; 21 while(left > -1 && nums[left] == nums[mid]) { // 以mid为中心,向左边查找与target值相同的数组元素 22 left--; 23 } 24 while(right < nums.length && nums[right] == nums[mid]) { // 以mid为中心,向右边查找与target相同的元素 25 right++; 26 } 27 return new int[] {left+1, right-1}; // 返回target元素在数组中的开始位置和结束位置 28 } 29 }
时间复杂度:二分查找mid的过程时间复杂度为O(log n),以mid为中心向两边查找的过程,每一次查找的时间复杂度为O(1),但在最坏情况下, 即数组的每个元素的值都为target的情况下(如nums=[8, 8, 8, 8, 8, 8], target=8),整个查找过程的时间复杂度为O(n),所以在最坏情况下不满足题目要求的时间复杂度为O(log n)级别。
方案一中,二分查找mid的过程时间复杂度为O(log n),只是查找左右边界的过程时间复杂度达到O(n),所以关键在于找到mid过后,应该如何查找左右边界的问题。
方案二:二分查找mid的过程不变,但在查找左右边界的过程中将不再采用逐个比较查找,而是二分查找算法,这样就可以在O(log n)的时间内找到左右边界
1 public class Solution { 2 public int[] searchRange(int[] nums, int target) { 3 if(nums == null || nums.length == 0) return new int[] {-1, -1}; 4 if(nums.length == 1) return nums[0] == target ? new int[] {0, 0} : new int[] {-1, -1}; 5 int pos = findTargetIndex(nums, 0, nums.length-1, target); 6 int left = findLeftBoundaryIndex(nums, 0, pos - 1, target); 7 int right = findRightBoundaryIndex(nums, pos + 1, nums.length-1, target); 8 left = left == -1 ? pos : left; 9 right = right == -1 ? pos : right; 10 return new int[] {left, right}; 11 12 } 13 14 public int findLeftBoundaryIndex(int[] nums, int left, int right, int target) { // 二分法查找左边界 15 int mid = 0; 16 int boundary = -1; 17 while(left <= right) { 18 mid = (left + right) / 2; 19 if(nums[mid] != target) { 20 left = mid + 1; 21 } 22 else { 23 right= mid - 1; 24 boundary = mid; 25 } 26 } 27 return boundary; 28 } 29 30 public int findRightBoundaryIndex(int[] nums, int left, int right, int target) { // 二分法查找右边界 31 int mid = 0; 32 int boundary = -1; 33 while(left <= right) { 34 mid = (left + right) / 2; 35 if(nums[mid] != target) { 36 right = mid - 1; 37 } 38 else { 39 left= mid + 1; 40 boundary = mid; 41 } 42 } 43 return boundary; 44 } 45 46 public int findTargetIndex(int[] nums, int left, int right, int target) { 47 int mid = 0; 48 while(left <= right) { 49 mid = (left + right) / 2; 50 if(nums[mid] == target) { 51 return mid; 52 } 53 else if(nums[mid] < target) { 54 left = mid + 1; 55 } 56 else { 57 right = mid - 1; 58 } 59 } 60 return -1; 61 } 62 }
这个方案虽然满足时间复杂度为O(log n)级别,但是在算法中仍然有几处冗余,第一个是在二分查找pos的过程和二分查找左右边界的过程,这两大过程中有些地方做了重复判断的;第二个是二分查找左右边界这两个过程的代码,存在大部分冗余,只有几行关键代码是不同的,有没有什么方法可以让这两个函数合并成一个查找左右边界的接口呢?
看了别的题解,可以解决我上面的两个问题,看这里
1 class Solution { 2 public int[] searchRange(int[] nums, int target) { 3 int[] range = new int[] {-1, -1}; 4 5 if(nums == null || nums.length == 0) return range; 6 if(nums.length == 1) return nums[0] == target ? new int[] {0, 0} : range; 7 8 int leftIndex = searchRange(nums, target, true); 9 if(leftIndex == nums.length || nums[leftIndex] != target) { // 如果数组中不存在target 10 return range; 11 } 12 range[0] = leftIndex; 13 range[1] = searchRange(nums, target, false) - 1; // 寻找右边界时之所以不用判断,是因为前面在找左边界的过程中已经确保了能走到这一步说明数组中必然存在target 14 return range; 15 } 16 17 public int searchRange(int[] nums, int target, boolean flag) { 18 int left = 0; 19 int right = nums.length; 20 int mid = 0; 21 while(left < right) { 22 mid = (left + right) / 2; 23 if(nums[mid] > target || (flag && nums[mid] == target)) { 24 right = mid; 25 } 26 else { 27 left = mid + 1; 28 } 29 } 30 return left; 31 } 32 }
还有一种线性扫描思路,从左右两边往中间寻找的方法,时间复杂度为O(n),看这里
1 class Solution { 2 public int[] searchRange(int[] nums, int target) { 3 int[] range = new int[] {-1, -1}; 4 if(nums == null || nums.length == 0) return range; 5 if(nums.length == 1) return nums[0] == target ? new int[] {0, 0} : range; 6 7 int left = 0; 8 int right = nums.length - 1; 9 while(left < nums.length && nums[left] != target) { // 顺序遍历查找左边界 10 left++; 11 } 12 if(left == nums.length) { // 如果数组中不存在target 13 return range; 14 } 15 range[0] = left; 16 while(nums[right] != target) { // 顺序遍历查找右边界 17 right--; 18 } 19 range[1] = right; 20 return range; 21 } 22 }