结论直接看 题35的最后
二分查找
704.二分查找
需求
给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。
#示例 1:
输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4
#示例 2:
输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1
代码实现
class Solution {
public:
int search(vector<int>& nums, int target) {
int low = 0;
int high = nums.size()-1;
while(low <= high){
int mid = low+(high-low)/2;
int num = nums[mid];
if(target < num)
high = mid-1;
else if(target == num)
return mid;
else
{
low = mid+1;
}
}
return -1;
}
};
思路演示
图704.1 实现过程示意图
-
// 代码段 第6行,循环判断条件是 while (low <= high) 而不是while (low < high)。
-
// 新的语法:nums.size() | 数组.size() 可以返回数组的长度,如图1中的数组会返回6。
-
// int mid = (high-low)/2 的结果为向下取整,如下图所示3.5=3。
图704.2 除法规则示意图
278.第一个错误的版本
需求
你是产品经理,目前正在带领一个团队开发新的产品。不幸的是,你的产品的最新版本没有通过质量检测。由于每个版本都是基于之前的版本开发的,所以错误的版本之后的所有版本都是错的。
假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。
你可以通过调用 bool isBadVersion(version) 接口来判断版本号 version 是否在单元测试中出错。实现一个函数来查找第一个错误的版本。你应该尽量减少对调用 API 的次数(二分查找较快)。
#示例 1:
输入:n = 5, bad = 4
输出:4
解释:
调用 isBadVersion(3) -> false
调用 isBadVersion(5) -> true
调用 isBadVersion(4) -> true
所以,4 是第一个错误的版本。
#示例 2:
输入:n = 1, bad = 1
输出:1
代码实现
// The API isBadVersion is defined for you.
// bool isBadVersion(int version);
class Solution {
public:
int firstBadVersion(int n) {
int left = 1;
int right = n;
while(left < right){
int mid = left + (right-left)/2;//直接(left + right)/2会溢出
if(isBadVersion(mid))//如果是错误的版本,则在它之前找正确和错误版本的分界线,即把right左移,right= mid
{
right = mid;
}
else //如果是正确的版本,则在它之后找正确和错误版本的分界线,即把left右移,left= mid +1
{
left = mid +1;
}
}//left = right时,此版本即为第一个错误的版本
return left;
}
};
思路演示
如图所示,mid1=3,3是正确的版本。要找的版本是第一个错误的版本,向后找,故要找的区间缩减到【正确版本的后一版本4,当前的最右版本】即left等于mid1+1=4;
执行第二次循环,mid2 =4, 4是错误的版本,要找的版本是第一个错误的版本 ,向前找的同时此版本也可能是所要找的错误版本,故要找的区间缩减到【当前的最左版本4,错误的版本4】即right等于mid等于4。
此时right= left ,不满足循环的条件left< right,故退出循环,left= right= 第一个错误的版本。
#注意1:right = mid;而不是一般的二分查找right = mid-1;这是因为要找的是第一个错误的版本,从而每个错误的版本都可能是第一个错误的版本,right= mid-1可能会越过该版本直接到正确的版本。而当mid为正确的版本,第一个错误的版本一定在该版本之后,故left= mid + 1。
图278.1 代码实现过程
#注意2:关于c语言的除法规则,因为是向下取整,所以本题的代码可以实现,若是向上取整或四舍五入取整,本题的代码都需要改动,此结论是通过本题的图1所示例子测试得出的,具体测试结果不详述。若是向上取整或四舍五入取整,需要使用.ceil()函数强制向下取整。
#注意3:int mid = left + (right-left)/2;//直接(left + right)/2会溢出
图278.2 溢出错误如图所示
35.搜索插入位置
需求
给定一个排序(升序)数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。
请必须使用时间复杂度为 O(log n) 的算法(二分查找)。
#示例 1:
输入: nums = [1,3,5,6], target = 5
输出: 2
#示例2:
输入: nums = [1,3,5,6], target = 2
输出: 1
#示例3:
输入: nums = [1,3,5,6], target = 7
输出: 4
代码实现
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left = 0;
int right = nums.size()-1;
int mid = left + (right - left)/2;
while(left<right) {
mid = left + (right - left)/2;
if(nums[mid]==target)
{
return mid;
}
else if(nums[mid] < target)
{
left = mid+1;
}
else
{
right = mid;
}
}
if(right == nums.size()-1){//上列的循环最右的插入位置为right,不能解决target大于数组中的所有值,即需要在right+1的位置插入
if(nums[right] < target){
return right+1;
}
else{
return left;
}
}
else {
return left;
}//因为mid是在循环体里定义的,所以循环体外的返回值不能 用mid,退出循环的条件是left=right,返回left或right都可以
//1.target值与mid索引所在的值比较,相等则返回该索引mid
/*2.整个数组都没找到插入的位置,此时的mid位置就是要插入的位置,
执行插入操作:数组长度+1 -> 从原数组的最后一位开始遍历到当前的mid索引,每一位数向后移动一位
-> i从原数组最后一位开始,j从原数组的最后一位的后一位开始,执行nums[j] = nums[i]
*/
}
};
思路演示
本题十分重要,本模块将详细讲解二分查找的本质解法,避免在定区间,定左右的边界条件时混淆,即对二分查找问题的小总结
结论: 在大部分情况下,whlie()的循环条件定为while(left<right);left更改取值时取为
left = mid+1;right更改取值时取为right = mid都是可行的。
*** 此结论与c语言中整数除法的结果是向下取整的特性密切相关
因为向下取整的特性,在计算mid的值时,mid往往会指向right的左边(比如极端情况left= right-1时(left = right会直接跳出,不是极限条件),mid的取值会指向left而不是right),这就导致了left=mid +1的值最大也只能取到right而不是right+1,结合while()循环的执行条件是left<right,即left=right时会马上退出。即最后的中止条件一定是left=right,即精准的定位到某一位置确定的元素,此即二分查找最后找到的结果。此时返回left或right都是可以的。
但是此结论在遇到特定的问题要加以调整或添加循环外的条件,比如本题中的循环最右的插入位置为right时,不能解决target大于数组中的所有值的情况,此时需要在right+1的位置插入,在一开始查找的范围就是大于left到right的范围的。所做的调整是,在循环结束后加入一个判断条件,在检测到缩小范围后的right值仍然为一开始的right值即最右侧的范围没有缩小时(if(right == nums.size()-1)),可能会出现插入位置在检索范围的右侧的情况,此时需判断目标值target的值是否大于num[right],若大于,代表要插入的位置在检索范围的右侧,即right+1的位置;若不大于,仍然返回left或right即可。此部分的代码实现如下:
if(right == nums.size()-1){
if(nums[right] < target){
return right+1;
}
else{
return left;
}
}
else {
return left;
}
双指针
977.有序数组的平方
需求
给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。
#示例 1:
输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]
#示例 2:
输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]
代码实现
本题用枚举法较为简单,代码实现如下:
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
//定义一个新的数组ans存储平方后的数组
vector<int> ans;
//通过遍历将nums中的元素平方后加入到数组ans中
for(int num: nums) {
ans.push_back(num * num);
}
//利用c++自带的sort将ans排序
sort(ans.begin(), ans.end());
return ans;
//int a[],sort(a,a+n);或vector <int>a,sort(a.begin(),a.end())头文件为algorithm
//push_back()和pop_back()在尾部添加和删除元素
}
};
应注意的是本题中的前提给定的数组是非递减顺序 排序的利用本性质可以用双指针法,比枚举法可以提升很多效率,代码实现如下:
class Solution {
public:
vector<int> sortedSquares(vector<int>& nums) {
int len = nums.size();
int flag = -1;//flag要设置初值,负责数组中没有负数无法给flag赋值时会出错
for(int i = 0; i < len; i++) {
if(nums[i] < 0){
flag = i;//遍历过程中flag会持续被新的更接近非负数的负数覆盖,直到遇到第一个非负数,退出循环
}
else {
break;
}
}
vector<int> ans;//ans用于存放排序且平方后的数组
int left = flag, right = flag+1;
while(left >= 0 || right < len) {
if(left < 0) {
ans.push_back(nums[right] * nums[right]);
right = right +1;//判断:当left超过左边界, 则按从左到右顺序输出右边的剩余数的平方,全部输出完后,恰好满足了左边小于0,右边大于数组长度的退出循环要求
}
else if(right >= len) {
ans.push_back(nums[left] * nums[left]);
left = left-1;//判断:当right超过右边界, 则按从右到左顺序输出左边的剩余数的平方,全部输出完后,恰好满足了左边小于0,右边大于数组长度的退出循环要求
}
else if(abs(nums[left]) < abs(nums[right])) {
ans.push_back(nums[left] * nums[left]);
left = left-1;
}
else {
ans.push_back(nums[right] * nums[right]);
right = right+1;
}
}
return ans;
}
};
思路演示
着重介绍双指针法的思路,观察给定的示例,发现可以用两个指针分别指在仅靠负数和非负数分界线的两边的元素,比较两个指针所指位置的数字的绝对值的大小,将绝对值较小数字的平方储存在新数组ans中后,移动被存储数的指针,再进行比较,循环这一过程……
图977.1 双指针移动示意图
本题中退出循环的边界条件尤其值得注意。应按以下顺序分析边界条件:
当 左指针 超越 左边界,且 右指针 超越 右边界:说明数组内还有数字未被分析,在所有数字被分析前不应退出循环
当左指针 超越 左边界,但 右指针 没超越 右边界:
说明右边的元素未被完全压入新建数组ans,将right指向的数字压入ans,指针再向右移动,
再判断发现再次满足左指针 超越 左边界,但 右指针 没超越 右边界,直到满足了右指针超越
右边界,此时满足左右指针都超越边界,退出循环
图 977.2 边界分析1
当左指针 未超越 左边界,但 右指针 超越 右边界:
说明左边的元素未被完全压入新建数组ans,将left指向的数字压入ans,指针再向左移动,
再判断发现再次满足左指针 未超越 左边界,但 右指针 超越 右边界,直到满足了左指针超越
左边界,此时满足左右指针都超越边界,退出循环
图 977.3 边界分析2
不满足上述两个边界条件时,只需比较左右指针谁指向的数字的绝对值小,将其压入新数组,再移动相应指针即可
以上循环及判断过程的代码实现如下:
while(left >= 0 || right < len) {
if(left < 0) {
ans.push_back(nums[right] * nums[right]);
right = right +1;//判断:当left超过左边界, 则按从左到右顺序输出右边的剩余数的平方,全部输出完后,恰好满足了左边小于0,右边大于数组长度的退出循环要求
}
else if(right >= len) {
ans.push_back(nums[left] * nums[left]);
left = left-1;//判断:当right超过右边界, 则按从右到左顺序输出左边的剩余数的平方,全部输出完后,恰好满足了左边小于0,右边大于数组长度的退出循环要求
}
else if(abs(nums[left]) < abs(nums[right])) {
ans.push_back(nums[left] * nums[left]);
left = left-1;
}
else {
ans.push_back(nums[right] * nums[right]);
right = right+1;
}
}
189.轮转数组
需求
给定一个整数数组 nums,将数组中的元素向右轮转 k 个位置,其中 k 是非负数。
示例 1:
输入: nums = [1,2,3,4,5,6,7], k = 3
输出: [5,6,7,1,2,3,4]
代码实现
方法1:简单循环取数移数 (可以通过示例,但提交力扣会超时)
#include <stdio.h>
#include <stdlib.h>
#include <vector>
using namespace std;
//简单循环取数移数 (可以通过示例,但提交力扣会超时)
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int len = nums.size();
for(int i = 0; i < k; i++) {
int t = nums[len-1]; //取元素
for(int j = len-2; j >= 0; j--) { //移元素
nums[j+1] =nums[j];
}
nums[0] = t; //插元素
}
}
};
方法2:建立新数组存储移位后的数组,再用assign赋给原数组
#include <stdio.h>
#include <stdlib.h>
#include <vector>
using namespace std;
//
class Solution {
public:
void rotate(vector<int>& nums, int k) {
int n = nums.size();
vector<int> newArr(n);
for (int i = 0; i < n; i++) {
newArr[(i + k) % n] = nums[i];
}
nums.assign(newArr.begin(), newArr.end());
}
};
方法3:根据移位的特性设计数组翻转函数解决(双指针)
#include <stdio.h>
#include <stdlib.h>
#include <vector>
using namespace std;
class Solution {
public:
void reverse(vector<int>& nums,int begin,int end) {
while(begin < end) {
swap(nums[begin],nums[end]);
begin++;
end--;
}
}
void rotate(vector<int>& nums, int k) {
int len = nums.size();
k = k % len;
reverse(nums,0,len-1);
reverse(nums,0,k-1);
reverse(nums,k,len-1);
}
};
思路演示
方法一是简单循环取数移数 ,每次每个数移动一次,可以通过示例,但提交力扣会超时,本身也是效率最低的办法。为了提高效率,直接将最后的移位结果存入新数组显然更好。
方法二建立新数组后,还需将新数组复制到原数组。就要用到vector容器中的assign函数:
/*assign函数:
函数原型:
void assign(const_iterator first,const_iterator last);
void assign(size_type n,const T& x = T());
功能:
将区间[first,last)的元素赋值到当前的vector容器中,或者赋n个值为x的元素到vector容器中,这个容器会清除掉vector容器中以前的内容。
*/
方法三虽然简便,但要注意本题为什么可以这么用。
图189.1 方法三原理示意图1
如上图所示,以k=2即每个元素向后轮转2个位置为例。
可以看见最后两个元素移到了最前,靠前的三个元素则移到了最后,因此我们可以通过第一次整体翻转,将4,5两个数字整体移到前,1,2,3三个数字整体移到后。
图189.2 方法三原理示意图2
观察上图可以发现,虽然数字4,5和数字1,2,3各种作为一个整体移到了对应位置,但与目标数组的样子还是有所差别。
可以看出,两个整体4,5和1,2,3各自与目标数组是逆转的,而它们的两部分的交界线为第二个数组后也就是对应下标k-1和k之间,因此,再分别对[0,k-1]和[k,len-1]这两个部分进行翻转操作即可获得目标数组。
因为这个解法多次用到了翻转操作,因此定义一个翻转函数:
void reverse(vector<int>& nums,int begin,int end) 进行反复调用会更加方便。
283.移动零
需求
给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。
请注意 ,必须在不复制数组的情况下原地对数组进行操作。
示例 1:
输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]
示例 2:
输入: nums = [0]
输出: [0]
代码实现
1.简单交换移动
lass Solution {
public:
void moveZeroes(vector<int>& nums) {
int i,j,k;
int len = nums.size();
for(i=0; i<len; i++) {
//外层循环用于找到第一个零
if(nums[i]==0) {
//通过if语句找到了第一个零
//现在要通过内层循环把它送到最后没有0的地方,过程中要保证非0数字的相对位置,即每次都要做交换
k=i;//k为要送的0所在的位置的下标 ,它的初始值为当前i的位置,后续每遇到新的非0数,把这个0与那个数交换,并更新k的值为交换后0所在的下标
for(j=i+1; j<len; j++) {
if(nums[j]!=0) {
swap(nums[k], nums[j]);
k=j;
}
}
}
}
}
};