下面是一个标准的二分算法及其细节解读:
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left=0,right=nums.size()-1;
int mid=0;
while(left<=right){
mid=((right-left)>>1)+left;
//中间点不用(ight+left)/2防止溢出;
//位运算速度比除法快,可以节省时间
if(nums[mid]==target)
return mid;
else if(nums[mid]>target)
//大小号可以重载为某种排序规则,比如是否是偶数、元素和下标的值是否一致,或者其他实际要求
right=mid-1;
else left=mid+1;
//左右指针不断将不符合target的元素挤出去,同时定位插入位置
//为什么要取mid-1?实际上是为了让两侧指针每次都运动起来
//如果不取mid-1而是mid,随着算法进行,必然只剩left=n和right=n+1指向的两个元素,则left元素也就是mid元素
//如果此时mid<target,那么执行left=mid,两个指针毫无变化,下一轮还是mid=left,将进入死循环
}
return left;//左指针及其左边的值<=target,是为“下界”,因此一定是最终答案,如果必定能查找成功,那么不需要这个返回值
}
};
(点击下方标题即可进入leetcode对应题目页面,全部题解语言为C++,难度为简单)
class Solution {
public:
int missingNumber(vector<int>& nums) {
//记没有出现的数为missnum,则missnum左边的下标和元素值一致,右边的不一致
//寻找缺失的数实际上就是寻找第一个对不上下标的元素
//类比二分查找比大小,实际上这里相当于重载大小符号运算为是否对齐下标
sort(nums.begin(),nums.end());//先排序
int left=0,right=nums.size()-1;
while(left<=right){
int mid=((right-left)>>1)+left;
if(nums[mid]!=mid)
right=mid-1;
else left=mid+1;
}
return left;//由于这里不存在“=”,只有“>”和“<”,因此左指针指向的必定是第一个对不上下标的位置
}
};
解析:二分查找也不一定是线性二分,只要能将元素分为两个可以用映射表示的部分就可以,例如元素的一个单调递增或递减函数。本题的target实际上相当于mid^2函数。
class Solution {
public:
int mySqrt(int x) {
int left=0,right=x;
while(left<=right){
long mid=((right-left)>>1)+left;//mid*mid可能会溢出,因此建议使用长整形
long sq=mid*mid;
if(sq==x)
return mid;
else if (sq>x)
right=mid-1;
else left=mid+1;
}
return right;//要求值的平方不超过target,选择右指针
}
};
441.排列硬币
解析:又是一个很好的例子,这里的大小符号重载为n和k(k+1)/2即1+2+3+。。。+k的比较。小于k级的阶梯都是可以完整排出来的,大于k的阶梯则不能完整甚至不能构成
public:
int arrangeCoins(int n) {
int left=1;
int right=n;
while(left<=right){
long mid=((right-left)>>1)+left;
long stairs=(mid*(mid+1))>>1;
if(stairs==n)
return mid;
else if (stairs>n)//比较大小重载为和阶梯数比较大小
right=mid-1;
else left=mid+1;
}
return left-1;
}
};
744. 寻找比目标字母大的最小字母
解析:目前我们遇到了二分查找的两个经典情境,包括存在target的情况下进行简单查找和不存在target的情况下确定上下界。本题则是存在target但要求查找上界,因此将查找成功的情形归入下界即可。
class Solution {
public:
char nextGreatestLetter(vector<char>& letters, char target) {
int left=0;
int right=letters.size()-1;
while(left<=right){
int mid=((right-left)>>1)+left;
if (letters[mid]<=target)
left=mid+1;//即使查找到了target也没有结束,实际上相当于查找到了一个小于真正目标的数字
else right=mid-1;
cout<<right;
}
return letters[left%letters.size()];
}
};
解析:最简单的方法是遍历一个数组,对其中每一个元素都查重一次,复杂度为O(mn); 更好的方法是采用哈希表存储,这样查重的复杂度下降到常数级,复杂度为O(m+n),即两个数组容量之和。
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
unordered_set<int> set1, set2;
for (auto& num : nums1) {//auto自动使用num来遍历nums1的元素
set1.insert(num);//利用哈希表去除重复元素
}
for (auto& num : nums2) {
set2.insert(num);
}
return getIntersection(set1, set2);
}
vector<int> getIntersection(unordered_set<int>& set1, unordered_set<int>& set2) {
if (set1.size() > set2.size()) {
return getIntersection(set2, set1);//用短表检验长表节省时间,尤其是当两表长度相差很大时
}
vector<int> intersection;
for (auto& num : set1) {
if (set2.count(num)) {
intersection.push_back(num);
}
}
return intersection;
}
};
另有一种方法是采用双指针,实质上类似于归并排序:
class Solution {
public:
vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {
sort(nums1.begin(), nums1.end());
sort(nums2.begin(), nums2.end());
int length1 = nums1.size(), length2 = nums2.size();
int index1 = 0, index2 = 0;
vector<int> intersection;
while (index1 < length1 && index2 < length2) {
int num1 = nums1[index1], num2 = nums2[index2];
if (num1 == num2) {
// 保证加入元素的唯一性,如果要求返回哈希表就无所谓了
if (!intersection.size() || num1 != intersection.back()) {
intersection.push_back(num1);
}
index1++;
index2++;
} else if (num1 < num2) {
index1++;//如果num1较小,那么它就是当前两个有序数组中唯一的最小,必定不是重合元素
} else {
index2++;//同样原因
}
}
return intersection;
}
};
350.两个数组的交集II
解析:本题要求的并不是确切的交集,而是展示交集数字的频率(取较小值)。可以使用哈希表统计一个数组的全部数字和频率,然后遍历另一个数组后更新交集数字的频率即可。
class Solution {
public:
vector<int> intersect(vector<int>& nums1, vector<int>& nums2) {
if (nums1.size() > nums2.size()) {
return intersect(nums2, nums1);
}
unordered_map <int, int> m;
for (int num : nums1) {
++m[num];//由于两个数组都很小,所以可以直接使用下标来统计
}
vector<int> intersection;
for (int num : nums2) {
if (m.count(num)) {
intersection.push_back(num);//加入交集统计。
--m[num];
if (m[num] == 0) {
m.erase(num);//如果元素在nums2中的数量较少,那么也是取最小值
}
}
}
return intersection;
}
};
240.搜索二维矩阵II
解析:
我们从初始矩阵开始,每次都检索其右上角元素UR=matrix[x,y],显然一开始的时候x=0,y=n
如果UR==target,则结束搜索
如果UR>target,那么显然这一列都不符合要求,将矩阵减去这一列(y- -)
如果UR<target,那么显然这一行也不符合要求,将矩阵减去这一行(x++)
最后必定收敛到一个元素,如果还不符合,则查找失败
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size(), n = matrix[0].size();
int x = 0, y = n - 1;
while (x < m && y >= 0) {
if (matrix[x][y] == target) {
return true;
}
if (matrix[x][y] > target) {
--y;
}
else {
++x;
}
}
return false;
}
};
378.有序矩阵中第K小的元素
解析:
直接对矩阵降维排序就可以找到答案。
也可以考虑做插入排序然后再查找,但归并也是O(n^2)数量级。合并链表的方法有顺序合并、分治合并(采用双指针即可)、优先队列合并(类似于多路归并)。
二分查找的target不一定是一个元素,也可能是元素的一个单调函数,这样同样可以二分查找,如本题的target可以看做是比某元素更大、小的元素数量。
二分查找是本题的最优解,复杂度为线性。
class Solution {
public:
bool check(vector<vector<int>>& matrix, int mid, int k, int n) {
int i = n - 1;
int j = 0;
int num = 0;
while (i >= 0 && j < n) {
if (matrix[i][j] <= mid) {
num += i + 1;
j++;
} else {
i--;
}
}
return num >= k;
}
int kthSmallest(vector<vector<int>>& matrix, int k) {
int n = matrix.size();
int left = matrix[0][0];
int right = matrix[n - 1][n - 1];
while (left < right) {
int mid = left + ((right - left) >> 1);
if (check(matrix, mid, k, n)) {
right = mid;
} else {
left = mid + 1;
}
}
return left;
}
};