1.数组理论基础
文章链接:代码随想录
note:数组的下标是从0开始的,而且内存中连续存储。
二维数组在不同的语言中的存储方式是不同的。在C++中二维数组的地址分布也是连续的,但是在Java里不是。
2.二分查找
代码:(左闭右闭区间)
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
// 左闭右闭
while(left <= right){
int middle = left + (right - left) / 2;
if(target > nums[middle]){
left = middle + 1;
}else if(target < nums[middle]){
right = middle - 1;
}else{
return middle;
}
}
return -1;
}
};
代码:(左闭右开)
class Solution {
public:
int search(vector<int>& nums, int target) {
int left = 0;
int right = nums.size();
// 左闭右开
while(left < right){
int middle = left + (right - left) / 2;
if(target > nums[middle]){
left = middle + 1;
}else if(target < nums[middle]){
right = middle;
}else{
return middle;
}
}
return -1;
}
};
note:
二分查找的前提是数组已经排过序了!!!!!!
之前在一刷的时候,一直在这个判断条件后,应该更新哪边的边界值进行犹豫。这次二刷的时候,想清楚了:只要关注target值的位置在哪里就好了,我们的目标一直是缩短target所在的区间大小。因此,如果target>nums[middle],即target值在区间的右半部分,我们就更新left值;反之就去更新right值。
关于这个循环不变量(其实就是区间的开闭要时刻保持一致),我们主要是有两种情况——左闭右闭和左闭右开。它们的区别主要是在更新边界以及循环判断条件不同。
这里主要抓住什么时候区间是个有效区间就好了。[3,3]是一个有效区间,但是[3,3)不是一个有效区间。
以及,在缩小区间范围的时候,要把之前判断不是target的范围全部剔除,来保证我们的效率。因此,如果是左闭右闭的话,如果还需要缩小区间,那就把边界值也剔除掉。如果是左闭右开的话,我们的有边界其实没有取到有效值,所以下一次的区间,右边界可以直接被赋值为middle。因为它取不到这个以及被我们应该剔除的数字。
相关题目练习
代码:(左闭右闭)
class Solution {
public:
int searchInsert(vector<int>& nums, int target) {
int left = 0;
int right = nums.size() - 1;
// 左闭右闭
while(left <= right){
int middle = left + (right - left) / 2;
if(target > nums[middle]){
left = middle + 1;
}else if (target < nums[middle]){
right = middle - 1;
}else{
return middle;
}
}
return left;
}
};
note:
这里如果查找不到返回升序应该插入的位置,其实就是我们遍历完数组的left值。
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
int leftBorder = getLeftBorder(nums,target);
int rightBorder = getRightBorder(nums,target);
// 情况1 目标值不在数组范围内,左右边界没有在循环中被重新赋值
if(leftBorder == -2 || rightBorder == -2){
return{-1,-1};
// 情况2 目标值在数组范围内,且在数组中,因为求出的边界值是不包含目标值的,需要稍加调整
}else if(rightBorder - leftBorder > 1) return {leftBorder + 1,rightBorder - 1};
// 情况3 目标值在数组范围内,但不在数组中,这样的左右边界大小会颠倒
return {-1,-1};
}
private:
int getRightBorder(vector<int>& nums,int target){
int left = 0;
int right = nums.size() - 1;
int rightBorder = -2;
while(left <= right){
int middle = left + (right - left) / 2;
if(target < nums[middle]){
right = middle - 1;
}else{
left = middle + 1;
rightBorder = left;
}
}
return rightBorder;
}
int getLeftBorder (vector<int>& nums,int target){
int left = 0;
int right = nums.size() - 1;
int leftBorder = -2;
while(left <= right){
int middle = left + (right - left) / 2;
if(target <= nums[middle]){
right = middle - 1;
leftBorder = right;
}else{
left = middle + 1;
}
}
return leftBorder;
}
};
note:
左右边界的求法:求右边界,那就把更新left和找到目标值的条件判断合并在一起即(target >= nums[middle]),在更新left的时候,把left赋值给rightBorder。为什么是赋值left呢?因为我们在用二分法的时候,有可能我们找到了目标值,但是定位的是目标值范围的中间数值,把left赋值给rightBorder可以利用上我们的循环的判断条件,left会一直更新,直到它和right相等,这样就可以从左逐渐逼近rightBorder。求左边界同理。
还有难点就是,要分析好目标值与所给数组的三种关系情况:
1.目标值不在数组大小范围内,这样我们的左右边界在整个过程中都不会被重新赋值,保持着初始值。要返回{-1,-1}
2.目标值在数组大小范围内,同时也在数组元素中。我们在这之后,还需要对左右边界进行调整,因为当我们不满足循环条件时,才会跳出循环,那么这就导致右边界其实是真实边界的有边界向右偏移了1位。左边界同理,向左偏移了1位。
3.目标值在数组范围内,但不在数组元素中,那这样就会导致左右边界的大小值不合法,也就是rightBorder-leftBorder <= 1。这样也是要返回{-1,-1}
class Solution {
public:
int mySqrt(int x) {
int left = 0;
int right = x;
int ans = -1;
while(left <= right){
int mid = left + (right - left) / 2;
if(x >= (long long)mid*mid){
ans = mid;
left = mid + 1;
}else{
right = mid - 1;
}
}
return ans;
}
};
note:
这道题主要是难以分析和下手,其实这道题的目标值是x,我们的目标是让我们的mid*mid逐渐逼近x,left是0,right是x,求的是整个过程不断更新的left的最终值。
class Solution {
public:
bool isPerfectSquare(int num) {
int left = 0;
int right = num;
while(left <= right){
int mid = left + (right - left) / 2;
long square = (long) mid * mid;
if(num < square){
right = mid - 1;
}else if(num > square){
left = mid + 1;
}else{
return true;
}
}
return false;
}
};
note:
这道题的目标值是num,left初始值是0,right初始值是num,让mid*mid逐渐逼近目标值。这里我们不是求左右边界,而是去判断有没有找到x。
3.移除元素
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
int left = 0;
for(int right = 0; right < nums.size(); right++){
if(nums[right] != val){
nums[left] = nums[right];
left++;
}
}
return left;
}
};
note:
这道题是要移除所有数值等于val的元素。我们采用双指针的主要思路其实是,让left作为新的数组的索引号,而让right去判断哪些元素可以被我们加到新的数组里。
新数组最后的大小其实就是left的数值大小。
相关题目练习
class Solution {
public:
int removeDuplicates(vector<int>& nums) {
int left = 1;
for(int right = 1; right < nums.size();right++){
if(nums[right] != nums[right - 1]){
nums[left] = nums[right];
left++;
}
}
return left;
}
};
note:当数组里的元素和前一个不相等时,更新新数组的值。
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int left = 0;
for(int right = 0; right < nums.size(); right++){
if(nums[right] != 0){
nums[left] = nums[right];
left++;
}
}
for(int i = left; i < nums.size(); i++){
nums[i] = 0;
}
}
};
note:第一个循环,将所有不等于0的元素加入到新数组中;第二个循环,将新数组的剩余部分都变为0.
class Solution {
public:
bool backspaceCompare(string s, string t) {
int i = s.length() - 1;
int j = t.length() - 1;
int skipS = 0; // 记录#的个数
int skipT = 0;
while(i >= 0 || j >= 0){
// 处理退格问题
while(i >= 0){
if(s[i] == '#'){
skipS++;
i--;
}else if(skipS > 0){
skipS--;
i--;
}else{
break;
}
}
while(j >= 0){
if(t[j] == '#'){
skipT++;
j--;
}else if(skipT > 0){
skipT--;
j--;
}else{
break;
}
}
// 比较
if(i >= 0 && j >= 0){
if(s[i] != t[j]){
return false;
}
}else{
if(i >= 0 || j >= 0){
return false;
}
}
i--;
j--;
}
return true;
}
};
note:这里用了skipS和skipT来记录'#'的数量,我们在遍历数组进行比较前,首先会跳过'#'字符,同时会根据此时的skip的大小来决定是否跳过其它的字符。
在排除这些干扰后,就把所有不等的情况找出来——两个字符串都没有遍历完,出现了字符不等的情况;在同步遍历比较的时候,有一个字符串提前遍历完了,说明两个字符串连长度都不想等。
这里的易错点是:为了把长度不等的情况也找出来,最外层的循环的条件判断用的是或运算。