二分查找的原理
二分查找也称折半查找(Binary Search),它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。也就是说如果我们使用二分查找就要求数组中元素是有序的, 每次通过找到中间值并和目标值进行比对来缩减区间范围,进而查找目标值。为什么说二分查找的效率更高呢,是因为如果我们使用遍历的方法来查找目标值的话时间复杂度为O(N), 但是如果我们使用二分查找的话时间复杂度为O(log(N)),可能如果直观来看的可能并看不出差异,但是如果N的数值变得非常大的时候,这种差异将是非常明显的。下面我们来看看二分查找的具体代码和几种变种形式!
二分查找模板
下面我们通过具体的一道题来看下二分查找是怎么写的。
题目链接:704.二分查找
左闭右闭的二分查找
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size()-1;
//这里我们使用的是左闭右闭的二分查找
while(left <= right)
{
//这里使用这种写法的目的是为了防止数值过大发生整型溢出
int mid = left + ((right - left) >> 1);
if(nums[mid] > target) left = mid - 1;
else if(nums[mid] < target) right = mid + 1;
else return mid;
}
return -1;
}
};
左闭右开的二分查找
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size();
while(left < right)
{
int mid = left + ((right - left) >> 1);
if(nums[mid] < target) left = mid + 1;
else if(nums[mid] > target) right = mid;
else return mid;
}
return -1;
}
};
二分查找的变种形式
在平时得二分查找中我们往往是要求找到target的位置,但是如果数组中出现重复元素时我们找到的元素可能并不是我们预期想找到元素的位置,那么我们就需要利用二分查找的变种形式来查找二分查找最左边的target和最右边的target,具体的代码如下。
二分查找向左取版本
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while(left < right)
{
int mid = left + ((right - left) >> 1);
if(nums[mid] < target) left = mid + 1;
else right = mid;
}
if(nums[left] == target) return left;
else return -1;
}
};
二分查找向右取版本
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0, right = nums.size() - 1;
while(left < right)
{
int mid = left + ((right - left + 1) >> 1);
if(nums[mid] > target) right = mid - 1;
else left = mid;
}
if(nums[left] == target) return left;
else return -1;
}
};
下面我们通过一道题来进一步熟练运用二分查找的两种变种形式
题目链接:34.再排序数组中查找元素的第一个位置和最后一个位置
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int left = 0, right = nums.size()-1;
vector<int> ret(2,-1); //将结果数组初始化为-1,-1
if(nums.size() == 0) return ret;
//先找第一个出现的target,使用向左取的二分查找
while(left < right)
{
int mid = left + ((right - left) >> 1);
if(nums[mid] < target) left = mid + 1;
else right = mid;
}
if(nums[right] != target) return ret;
ret[0] = right;
//再找最后一个target使用向右取的二分查找
left = 0, right = nums.size()-1;
while(left < right)
{
int mid = left + ((right - left + 1) >> 1);
if(nums[mid] > target) right = mid - 1;
else left = mid;
}
ret[1] = right;
return ret;
}
};
暴力解法 vs 双指针解法
暴力解法顾名思义就是不讲方法,不考虑效率的算法,通常时间复杂度会很高,而双指针则是通过两个指针指向不同的位置通过移动两个指针减少重复的移动来减小时间复杂度,下面我们通过一道题的两种解法来对比一下暴力解法和双指针解法。
题目链接:27.移除元素
暴力解法
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int size = nums.size();
for(int i = 0; i < size; i++)
{
if(nums[i] == val)
{
//j的值取i后面的值
for(int j = i + 1; j < size; j++)
{
//每次都要移动数组用后面的值将前面的值覆盖
nums[j - 1] = nums[j];
i--;
size--;
}
}
}
return size;
};
双指针解法
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int prev = 0, cur = 0;
int size = nums.size();
while(cur < size)
{
if(nums[cur] == val) cur++;
else{
std::swap(nums[cur], nums[prev]);
cur++;
prev++;
}
}
return prev;
};
通过对比可以发现暴力解法的时间复杂度为O(N^2),双指针的时间复杂度为O(N),因为两个指针同时移动减少了遍历的次数,双指针主要用于需要遍历的额场合,这样通过两个指针可以减少遍历时间。