方法一:二分查找到指定元素后找到最左边
核心思想:
首先使用二分查找,寻找到一个与目标值相等的数,之后往左侧线性查找到最左边或到不等于目标值的元素
核心代码:
class Solution { public: int search(vector<int>& nums, int target) { int n = nums.size(); if(n < 1 || nums[0] > target || nums[n - 1] < target) { return -1; //数组为空,数组中所有元素都大于/小于目标值时,直接返回-1 } int i = 0, j = n - 1, mid = 0; while(i <= j) { mid = (i + j) / 2; if(nums[mid] < target) { i = mid + 1; } else if(nums[mid] > target) { j = mid - 1; } else { //找到目标值,往左侧线性查找 while(mid > 0 && nums[mid - 1] == target) { --mid; } return mid; } } return -1;//找不到目标值 } };
复杂度分析:
时间复杂度:$O(n)$,最坏情况下,整个数组左侧都等于目标值,则相对于对数组进行线性遍历,需要O(n)时间
空间复杂度:$O(1)$,过程中只使用了常数个变量
方法二:优化的二分查找
核心思想:
方法一中,寻找到一个目标值后的方案是对左侧进行线性查找,这在最坏情况下可能导致$O(n)$的时间复杂度,可以对二分查找进行优化,保证往左侧逼近目标值。
具体解释:
当$nums[mid] < targe$t时,说明目标值绝对在mid右侧,此时可以令$i=mid+1$
当$nums[mid] = target$时,说明目标值可能为mid,也可能在mid左侧,此时可以令$j = mid$,保证不会丢失答案
当$nums[mid] > target$时,说明目标值绝对在mid左侧,此时可以令$j = mid - 1$
终止条件,因为当$i=j=target$时,$j$并不会步进,所以终止条件必须为$i<j$,在循环结束后再判断$nums[i]$是否等$target$决定答案值。
图示:
核心代码:
class Solution { public: int search(vector<int>& nums, int target) { int n = nums.size(); if(n < 1 || nums[0] > target || nums[n - 1] < target) { return -1; //数组为空,数组中所有元素都大于/小于目标值时,直接返回-1 } int i = 0, j = n - 1, mid = 0; while(i < j) { mid = (i + j) / 2; if(nums[mid] < target) { i = mid + 1; } else if(nums[mid] > target) { j = mid - 1; } else { j = mid;//为避免丢失答案,不能为mid-1 } } return nums[i] == target ? i : -1; //最后进行一次判断 } };
复杂度分析:
时间复杂度:$O(log(n)),$即为二分查找的复杂度,不会出现方法一中的最坏情况
空间复杂度:$O(1)$,过程中只使用了常数个变量