搜索篇:排序数组的搜索优先考虑二分查找
有重复元素如何借助二分优化时间
35. 搜索插入位置
考察点:
搜索算法,时间复杂度,边界
题目理解:
要插入位置的选择一共有四种:
- 目标值在数组所有元素之前
- 目标值等于数组中某一个元素
- 目标值插入数组中的位置
- 目标值在数组所有元素之后
暴力解法:
public int searchInsert(int[] nums, int target) {
for (int i = 0; i < nums.length; i++) {
if (nums[i]>=target){
return i;
}
}
return nums.length;
}
但是暴力解法的时间复杂度是O(n),题目要求的是O(logN),题目还给定了无重复元素的排序数组
只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法。但是如果有重复元素,二分查找可能需要判断边界,下文会给出对应有重复元素的二分查找的解答
二分查找:
左闭右闭的做法:
模拟四种情况的情况,之后会发现规律
-
找到的target
找到就直接return mid;
-
没找到
没找到就会跳出循环,此时会发现规律就是right < left
-
在数组所有元素之前:right =-1; left =0;
-
在数组所有元素之后:right = nums.length - 1 ; left = nums.length;
-
在数组所有元素之间:right = x ;left = x+1;
发现要插入的索引其实就是此时对应的left的值。
public static int searchInsert(int[] nums, int target) {
int left = 0;
int right = nums.length-1;
while (left<=right){
//防溢出
int mid = left + (right-left)/2;
if (nums[mid] ==target){
return mid;
}else if (nums[mid]>target){
right = mid-1;
}else {
left = mid+1;
}
}
return left;
}
704. 二分查找
接上个题目,这里对二分查找进行一些扩展实现,比如对有重复值的有序数组进行查询。
考察点
区间的判断:
在while寻找中每一次边界的处理都要坚持根据区间的定义来操作
我个人比较喜欢的一种写法:
左闭右闭:
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length-1;
while (left <= right){
int mid = left + (right-left)/2;
if (nums[mid] > target){
right = mid-1;
}else if (nums[mid] < target){
left = mid+1;
}else {
return mid;
}
}
//如果找不到,返回-1
return -1;
}
重点:
- 循环判断的条件 left <=right, 因为left == right是有意义的,意义就是 num[left] = num[right] 和target还需要比较一下,不然就少比较了一个元素
- if (nums[middle] > target) right 要赋值为 middle - 1,**因为当前这个nums[middle]一定不是target,如果是的话会直接return mid ,**所以接下来要查找的左区间结束下标位置就是 middle - 1
还有一种左闭右开,我不做讲解,给出代码:
class Solution {
public int search(int[] nums, int target) {
int left = 0, right = nums.length;
while (left < right) {
int mid = left + ((right - left) >> 1);
if (nums[mid] == target)
return mid;
else if (nums[mid] < target)
left = mid + 1;
else if (nums[mid] > target)
right = mid;
}
return -1;
}
}
剑指 Offer 53 - I. 在排序数组中查找数字 I
**注意:**本题与主站 34 题相同(仅返回值不同):https://leetcode-cn.com/problems/find-first-and-last-position-of-element-in-sorted-array/
考察点:
搜索算法
题目理解:
5,7,7,8,8,8,8,9,10
如上,排序数组,但是是重复的,是否仍然可以使用二分查找呢?
答案:可以 。
解法一:
但是需要再 nums[mid] == target 时做一些统计处理,但是这样的时间复杂度极端情况下会退化成O(n).不推荐
//在这里统计
ans++;
for (int i = mid-1; i >=0 ; i--) {
if (nums[i]==target){
ans++;
}
}
for (int i = mid+1;i<nums.length;i++){
if (nums[i]==target){
ans++;
}
}
题解如下:
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length-1;
int ans = 0;
while (left <= right){
int mid = left + (right-left)/2;
if (nums[mid] > target){
right = mid-1;
}else if (nums[mid] < target){
left = mid+1;
}else {
//在这里统计
ans++;
for (int i = mid-1; i >=0 ; i--) {
if (nums[i]==target){
ans++;
}
}
for (int i = mid+1;i<nums.length;i++){
if (nums[i]==target){
ans++;
}
}
break;
}
}
return ans;
}
解法二:
思路:
使用两次二分查找,找到左右边界,相减即可;
难点:
如何确定边界
如何处理结果
确定左边界为例:只需要在target==nums[mid] 的时候做如下处理。
right = mid-1 是重点,一般的二分查找,找到就停止了,但是让它继续向左寻找,通过 right = mid -1 实现。这样就可以找到左边的第一个target。
leftIndex = mid;
right = mid-1; // 重点
确定右边界同理
代码如下:
public int search(int[] nums, int target) {
int left = 0;
int right = nums.length-1;
int rightIndex = -1;
int leftIndex = -1;
//左边界
while (left <= right){
int mid = left + (right-left)/2;
if (nums[mid] > target){
right = mid-1;
}else if (nums[mid] < target){
left = mid+1;
}else {
leftIndex = mid;
right = mid-1;
}
}
//右边界
left = 0;
right = nums.length-1;
while (left <= right){
int mid = left + (right-left)/2;
if (nums[mid] > target){
right = mid-1;
}else if (nums[mid] < target){
left = mid+1;
}else {
rightIndex = mid;
left = mid+1;
}
}
//如果有边界是-1,那就说明没有这个target
if(rightIndex == -1){
return 0;
}
// 不然就返回下标 的差+1
return rightIndex-leftIndex +1;
}
34. 在排序数组中查找元素的第一个和最后一个位置
题目理解:
同上解法二,两次二分,获得下标即可;
代码:
public int[] searchRange(int[] nums, int target) {
int left = 0;
int right = nums.length-1;
int rightIndex = -1;
int leftIndex = -1;
//左边届
while (left <= right){
int mid = left + (right-left)/2;
if (nums[mid] > target){
right = mid-1;
}else if (nums[mid] < target){
left = mid+1;
}else {
leftIndex = mid;
right = mid-1;
}
}
left = 0;
right = nums.length-1;
while (left <= right){
int mid = left + (right-left)/2;
if (nums[mid] > target){
right = mid-1;
}else if (nums[mid] < target){
left = mid+1;
}else {
rightIndex = mid;
left = mid+1;
}
}
return new int[]{leftIndex,rightIndex};
}