二分查找描述
二分查找是一种在排序的线性表中常用的查找特定值的查找方式。它的常规过程是这样的:
- 对于一个长度为
n
的线性表nums
,我们在要寻找的目标值target
的可能所在的区域nums[left,right]
中对它进行寻找; - 最开始时,整个
nums
都是可能区域,因此初始化left=0,right=n-1
分别指向线性表nums
的最左端的下标和最右端的下标; - 根据
left
和right
计算得到mid=(left+right)/2
,即可能区域的中间位置的下标,若nums[mid]==target
就找到了目标值,否则将nums[mid]
和target
进行比较,如果nums[mid]<target
说明要寻找的目标值在mid
右侧,因此令left=mid+1
将可能区间缩小一半,如果nums[mid]>target
说明要寻找的目标值在mid
左侧,因此令right=mid-1
将可能区间缩小一半; - 在可能区间缩小、可能区间左右指针更新后继续重复上一步,直到找到
target
或left>right
,说明nums
中不存在target
,跳出循环
每一遍查找都会使可能区间缩小为当前可能区间的一半,因此称为“二分查找”。二分查找的时间复杂度为O(logn)
二分查找的灵活应用
上面是二分查找的一般使用场景和过程的描述,但是二分查找的原理和思想还可以用到其他的一些“非常规”场景下
这道题有几个限制条件:
- 不能更改原数组(假设数组是只读的)
- 只能使用额外的 O(1) 的空间
- 时间复杂度小于 O(n2)
- 数组中只有一个重复数字,但他可能不止出现一次
因为这几个条件限制,我们不能使用先排序后寻找重复数的方法,因为在原数组上排序会更改原数组,使用另一个数组存储排序结果会超过O(1)的空间限制;使用hash表存储每个数字出现次数同样会超过O(1)的时间限制;每次确定数组中的一个数字,再遍历整个数组查找是否有和它相等的数字,时间复杂度为O(n2),而题目要求时间复杂度必须小于O(2);而基于位运算寻找重复数字的方法必须确定重复数字的出现次数,同样不适合本题
我们可以灵活地使用二分查找,在满足限制条件的情况下寻找到数组的重复数:
- 对于一个长度为
n+1
的线性表nums
,它的数字都在1
到n
之间,我们在要寻找的重复数字target
的可能所在的区域[left,right]
中对它进行寻找; - 最开始时,整个
[1,n]
都是可能区域,因此初始化left=1
,right=n
分别指为线性表nums
的可能的最小值和最大值; - 根据
left
和right
计算得到mid=(left+right)/2
,然后遍历数组nums
并统计在[left,right]
范围中,比mid
小、与mid
相等和比mid
大的数,分别记为low
、equ
和hei
。若equ>1
,mid
就是重复数,程序结束;若low>mid-left
,说明在[left,right]
区间中比mid
小的的某一个数字不止出现了一次,重复数在[left,mid)
中,因此令right=mid-1
将可能区间缩小一半;否则一定有hei>right-mid
,说明在[left,right]
区间中比mid
大的某一个数字不止出现了一次,重复数字早(mid,right]
中,因此令left=mid+1
将可能区间缩小一半; - 在可能区间缩小、可能区间左右指针更新后继续重复上一步,直到找到重复数或
left==right
时,说明left
指向的就是重复数,跳出循环
示例一:
示例二:
class Solution {
public int findDuplicate(int[] nums) {
int l=1,r=nums.length-1;
while(l<r)
{
int mid=(l+r)/2;
int low=0,equ=0,hei=0; //记录nums中比mid小、相等和大的数字个数(只统计在[l,r]范围内的)
for(int i=0;i<nums.length;i++)
{
if(nums[i]==mid)
{
equ+=1;
}
else if(nums[i]>=l && nums[i]<mid)
{
low+=1;
}
else if(nums[i]<=r && nums[i]>mid)
{
hei+=1;
}
}
if(equ>1)
{
return mid;
}
if(low>mid-l) //重复数在[l,mid)中
{
r=mid-1;
}
else //重复数在(mid,r]中
{
l=mid+1;
}
}
return l;
}
}
时间复杂度:共需要进行O(logn)
趟二分查找,每趟查找需要O(n)
的时间统计大于、小于和等于mid
的数字个数,因此是O(nlogn)
空间复杂度:O(1)
我们使用这种二分查找的方法可以在满足题目中的几种限制条件的情况下成功解题。下面我们来比较一下在本题中使用的二分查找和常规二分查找的区别:
- 常规二分查找是寻找特定值的数字时候存在以及在数组中的索引位置。本题中的二分查找是寻找数组中存在的出现了不止一次的数字的值
- 常规二分查找需要提供排序的数组,也就是说随着数组索引的增加,索引对应的值也需要单调递增或单调递减。但本题中的二分查找不需要对数组排序
- 常规二分查找的
left
和right
指针指向的是数组的索引值,两个指针之间的索引对应的区域是目标值对应的索引可能存在的区间。本题中两指针直线的是目标数字(要寻找的重复数字)可能的值的值区间 - 常规二分查找根据索引区间算出一个索引
mid
,并根据mid
对应的值nums[mid]
和目标数的大小关系确定是否找到目标数或缩小可能区间。本题中的二分查找根据值区间计算出一个值mid
,并根据区间中mid
的个数、比mid
小的数字个数和比mid
大的数字出现个数来确定是否找到重复数或缩小可能区间
本题的基于非常规二分查找的解法可以在LeetCode官网中找到:LeetCode-378.有序矩阵中第K小的元素-基于二分查找的题解