- 1713得到子序列的最少操作次数:
- 162寻找峰值(二分法爬坡,细节满满)
- 4.寻找两个正序数组的中位数(这题不是用二分写的,但是思想非常重要)
- 53-1.在排序数组中查找数字(查找左右边界)
- 53-2 0~n-1中缺失的数字(注意特殊条件)
- 69. Sqrt(x)(巧妙)">69. Sqrt(x)(巧妙)
- 34. 在排序数组中查找…..(标准二分逼近写法)">34. 在排序数组中查找…..(标准二分逼近写法)
- 81. 搜索旋转排序数组 II(二分变种)">81. 搜索旋转排序数组 II(二分变种)
- 154. 寻找旋转排序数组中的最小值 II(旋转数组变种)">154. 寻找旋转排序数组中的最小值 II(旋转数组变种)
- 540. 有序数组中的单一元素">540. 有序数组中的单一元素
- 744. 寻找比目标字母大的最小字母">744. 寻找比目标字母大的最小字母
- 668. 乘法表中第k小的数(hard题目)">668. 乘法表中第k小的数(hard题目)
- 719. 找出第 K 小的数对距离(巧妙)">719. 找出第 K 小的数对距离(巧妙)
1713得到子序列的最少操作次数:
贪心算法的思想,想要使得子序列最长结尾的元素序号应该尽可能的小,使用二分法来查找第一个大于或者等于目标元素的数据被目标元素替换,来达到序号最小的目的。
162寻找峰值(二分法爬坡,细节满满)
这道题寻找峰值,用爬坡的思想,因为默认num[n] = 负无穷,往递增的地方走,必有峰值。然而若要复杂度为logn,单纯的递归行不通,使用二分法来进行搜索。这题还有一个非常重要的细节,labda表达式的返回值为pair类型的,这是因为下标-1和0默认负无穷,为了使他们能表现出这种特征,他们的first设置为0,正常的pair的first值设置为1。
是因为pair之间的比较都是先比较first然后再比较second。这样就能实现-1 和 n 永远都是最小的情况。
说实话这个pair在这里的设计没什么用,直接二分也能解决,但是提供一种很好的思路。
class Solution {
public:
int findPeakElement(vector<int>& nums) {
int n = nums.size();
int l = 0, r = n;
auto get = [&](int i)->pair<int,int>{
if(i == -1 || i == n){
return {0, 0};
}
return {1, nums[i]};
};
int mid = l + (r - l)/2;
while(!(get(mid-1) < get(mid)&&get(mid) > get(mid+1))){
if(get(mid-1) > get(mid)){
r = mid;
mid = l + (r - l)/2;
} else if(get(mid) < get(mid+1)){
l = mid + 1;
mid = l + (r - l)/2;
}
}
return mid;
}
};
4.寻找两个正序数组的中位数(这题不是用二分写的,但是思想非常重要)
- 这一题的主要结构是用一个k来表示取合并之后数组的第k个最小的值,通过维护这个合并之后的数组以及更新k的值,当k=1时目标值就是两个分数组中的一个了。
- 这里的k的更新使用k-= newindex - index+1这里的加一最好的判别方式就是直接找特殊情况,当newindex等于index的时候,应该删除一个值,k应该减一
- 需要分开判断是奇数和偶数的情况
- 应当判断index为边界的情况,此时直接从另一边的第k个数就可以了
k是目标数字所在的位置,index1和index2分别是两个数组的目标位置。
class Solution {
public:
int getKthElement(const vector<int>& nums1, const vector<int>& nums2, int k) {
/* 主要思路:要找到第 k (k>1) 小的元素,那么就取 pivot1 = nums1[k/2-1] 和 pivot2 = nums2[k/2-1] 进行比较
* 这里的 "/" 表示整除
* nums1 中小于等于 pivot1 的元素有 nums1[0 .. k/2-2] 共计 k/2-1 个
* nums2 中小于等于 pivot2 的元素有 nums2[0 .. k/2-2] 共计 k/2-1 个
* 取 pivot = min(pivot1, pivot2),两个数组中小于等于 pivot 的元素共计不会超过 (k/2-1) + (k/2-1) <= k-2 个
* 这样 pivot 本身最大也只能是第 k-1 小的元素
* 如果 pivot = pivot1,那么 nums1[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums1 数组
* 如果 pivot = pivot2,那么 nums2[0 .. k/2-1] 都不可能是第 k 小的元素。把这些元素全部 "删除",剩下的作为新的 nums2 数组
* 由于我们 "删除" 了一些元素(这些元素都比第 k 小的元素要小),因此需要修改 k 的值,减去删除的数的个数
*/
int m = nums1.size();
int n = nums2.size();
int index1 = 0, index2 = 0;
while (true) {
// 边界情况
if (index1 == m) {
return nums2[index2 + k - 1];
}
if (index2 == n) {
return nums1[index1 + k - 1];
}
if (k == 1) {
return min(nums1[index1], nums2[index2]);
}
// 正常情况
int newIndex1 = min(index1 + k / 2 - 1, m - 1);
int newIndex2 = min(index2 + k / 2 - 1, n - 1);
int pivot1 = nums1[newIndex1];
int pivot2 = nums2[newIndex2];
if (pivot1 <= pivot2) {
k -= newIndex1 - index1 + 1;//相当于(k = k - (newindex - index1 + 1)),剪掉小于的部分。
index1 = newIndex1 + 1;
}
else {
k -= newIndex2 - index2 + 1;//至于这里为什么要+1,建议从边界来看,当newindex与index相等时,k相当于往前动
//一位,因此多一个加1
index2 = newIndex2 + 1;
}
}
}
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
int totalLength = nums1.size() + nums2.size();
if (totalLength % 2 == 1) {
return getKthElement(nums1, nums2, (totalLength + 1) / 2);
}
else {
return (getKthElement(nums1, nums2, totalLength / 2) + getKthElement(nums1, nums2, totalLength / 2 + 1)) / 2.0;
}
}
};
53-1.在排序数组中查找数字(查找左右边界)
这题用到二分法,先找右边界,在找左边界。边界确定就看特殊情况。注意,无论是左闭右开还是左闭右闭都是看l来确定边界
class Solution {
public:
int search(vector<int>& nums, int target) {
int n = nums.size();
int left = 0, right = n;
while(left < right){
int mid = left + (right - left)/2;
if(nums[mid] >= target){
right = mid;
} else {
left = mid + 1;
}
}
int first = left;
left = 0, right = n;
while(left < right){
int mid = left + (right - left)/2;
if(nums[mid] > target){
right = mid;
} else {
left = mid + 1;
}
}
return left -first;
}
};
53-2 0~n-1中缺失的数字(注意特殊条件)
二分法,判断的依据是,nums[i]==i。这里没想出来太可惜了。返回值需要思考一下,我刚开始的时候返回的是nums[l]-1,没有考虑到数组越界的情况。因为求出来的l就是缺数的位置,直接返回l即可。
class Solution {
public:
int missingNumber(vector<int>& nums) {
int l = 0, r = nums.size(), mid;
while(l < r){
mid = l + (r- l)/2;
if(nums[mid] == mid){
l = mid + 1;
} else {
r = mid;
}
}
return l;
}
};
69. Sqrt(x)(巧妙)
暴力
注意会出现越界,for循环里面必须是long类型,因为c++在计算时如果两个都是int类型会先将结果转存到int类型的空间中。
例如:
int i;
long sum = i + i;(这里可能越界,因为会先将结果转存到int中,然后在转换为long类型)
正确:
long i;
long sum = i + i;class Solution {
public:
int mySqrt(int x) {
for(long i = 0; i <= x; i++){//这里要注意
long num1 = i*i;
long num2 = (i+1)*(i+1);
if(num1==x||(num1<x&&num2>x)){
return i;
}
}
return 0;
}
};
二分法(巧妙)
我估计会经常使用这个技巧。将一道题目看成是求一个等式的解,然后通过二分法来解决。
此题就可以看成是 求 f(x) = x**2 - a = 0的解在[0,a]这个区间里面应该有f(x)等于0的解。class Solution {
public:
int mySqrt(int x) {
if(x == 0) return x;
int l = 1, r = x, mid, sqrt;
while(l <= r){
mid = l + (r-l)/2;
sqrt = x/mid;
if(sqrt == mid){
return mid;
} else if(sqrt < mid){
r = mid - 1;
} else {
l = mid + 1;
}
}
return r;
}
};
34. 在排序数组中查找…..(标准二分逼近写法)
标准写法
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
if(nums.empty()) return {-1, -1};
int lower = lower_bound(nums, target);
int upper = upper_bound(nums, target);
if(lower == nums.size()||nums[lower]!= target){
return {-1, -1};
}
return {lower, upper};
}
//左侧逼近
int lower_bound(vector<int>&nums, int target){
int l = 0, r = nums.size(), mid;
while(l < r){
mid = l + (r-l)/2;
if(nums[mid] >= target){
r = mid;
} else {
l = mid + 1;
}
}
return l;
}
//右侧逼近
int upper_bound(vector<int>&nums, int target){
int l = 0, r = nums.size(), mid;
while(l < r){
mid = l + (r-l)/2;
if(nums[mid] > target){
r = mid;
} else {
l = mid + 1;
}
}
return l - 1;
}
};
81. 搜索旋转排序数组 II(二分变种)
考验二分的特殊情况。不难把情况分门别类好就行了。
class Solution {
public:
bool search(vector<int>& nums, int target) {
int l = 0, r = nums.size()-1;
while( l <= r){
int mid = l + (r - l)/2;
if(nums[mid] == target){
return true;
}
if(nums[l] == nums[mid]){
l++;
} else if(nums[mid] <= nums[r]){
if(target>nums[mid]&&target <= nums[r]){
l = mid + 1;
} else {
r = mid;
}
} else {
if(target >= nums[l]&&target < nums[mid]){
r = mid;
} else {
l = mid + 1;
}
}
}
return false;
}
};
154. 寻找旋转排序数组中的最小值 II(旋转数组变种)
这里注意最好和右边比不要和左边界比较。因为数组是从小到大的规则,和左边界比较的时候1234这种递增的情况会判断错误。
注意边界的问题
class Solution {
public:
int findMin(vector<int>& nums) {
int l = 0, r = nums.size() - 1;
while(l < r){
int mid = l + (r - l)/2;
if(nums[mid] == nums[r]){
r--;
} else if(nums[mid] > nums[r]){
l = mid+1;
} else {
r = mid;
}
}
return nums[l];
}
540. 有序数组中的单一元素
这题出现大问题!!!!!!!!!!!!!!!!!!!!!!!!!
0
class Solution {
public:
int singleNonDuplicate(vector<int>& nums) {
int l = 0, r = nums.size(), n = nums.size();
if(n==1){
return nums[0];
}
while(l < r){
int mid = l + (r-l)/2;
if((0<mid&&mid<n-1&&nums[mid]!=nums[mid+1]&&nums[mid]!=nums[mid-1])||(mid==0&&nums[mid]!=nums[mid+1])||(mid==n-1&&nums[mid]!=nums[mid-1])){
return nums[mid];
} else if(mid>0&&nums[mid]==nums[mid-1]&&mid%2==1||mid<n-1&&nums[mid]==nums[mid+1]&&mid%2==0){
l = mid+1;
} else {
r = mid;
}
}
return nums[l];
}
};
744. 寻找比目标字母大的最小字母
- 有点类似前面的一题,注意出去的时候的问题
注意越界,避免陷入无线循环
class Solution {
public:
char nextGreatestLetter(vector<char>& letters, char target) {
int n = letters.size();
if(target >= letters[n-1]) return letters[0];
int l = 0, r = n-1;
while(l <= r){
int mid = l + (r-l)/2;
if(letters[mid] > target){
r = mid-1;
} else {
l = mid+1;
}
}
return letters[l];
}
};
668. 乘法表中第k小的数(hard题目)
这里面的数学推导很重要
- 这里用的是左闭右开的写法
公式在这里:
class Solution {
public:
int findKthNumber(int m, int n, int k) {
int left = 1, right = m * n;
while (left <= right) {
int x = left + (right - left) / 2;
int count = x / n * n;//这里的先除n是向下取整。
for (int i = x / n + 1; i <= m; ++i) {//根据推导的公式而来
count += x / i;
}
if (count >= k) {//这里的count表示不大于x的数个数总和,
//因为当count==k时x是有可能为第count+1大的数。
//因此缩小范围。找到第一个count为k的数。
right = x-1;
} else if(count < k){
left = x + 1;
}
}
return left;
}
};
719. 找出第 K 小的数对距离(巧妙)
做二分法的题目,一定要想到怎么和二分法挂钩,有时候不是那么简单能够想出来。但是需要找到二分法的切入点,才方便写。
有个疑问,方法一的最后返回的left,如何保证这个left是数组中存在的差值呢?
- 这就是lower_bound的美妙之处,和668乘法表那个题目类似。如果实际的为[1,100,1000],那么在这个算法中及时某次中间mid为300,二分法会不断迫使寻找100。这里可以看下面第14行,right = mid - 1。会不断迫近已存在的!!!
class Solution {
public:
int smallestDistancePair(vector<int>& nums, int k) {
sort(nums.begin(), nums.end());
int n = nums.size(), left = 0, right = nums.back() - nums.front();
while (left <= right) {
int mid = (left + right) / 2;
int cnt = 0;
for (int j = 0; j < n; j++) {
int i = lower_bound(nums.begin(), nums.begin() + j, nums[j] - mid) - nums.begin();//枚举所有数对的右端点j,二分查找大于等于nums[j]-mid的最小值下标i。那么右端点为j且距离小于等于mid的数对数目为j-i,计算这些数目之和。
//mid为距离,推断 nums[j] - nums[i] <= mid, nums[j] - mid <= nums[i],所以要找到第一个大于等于nums[j]-mid的位置i,满足上述公式。
cnt += j - i;//这里的cnt计算的应该是小于绝对值差小于等于mid的数量
}
if (cnt >= k) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return left;
}
};
- 这就是lower_bound的美妙之处,和668乘法表那个题目类似。如果实际的为[1,100,1000],那么在这个算法中及时某次中间mid为300,二分法会不断迫使寻找100。这里可以看下面第14行,right = mid - 1。会不断迫近已存在的!!!