代码随想录第一天 | 数组理论基础:二分法(leetcode 704, 35, 34, 69);双指针(leetcode 27, 844, 977)

1、数组

随机存取,连续,相同数据类型
代码随想录数组理论基础

2、二分法

2.1 leetcode 704:二分法寻找非重复元素

第一遍代码,递归

class Solution {
public:
    int start = 0;
    int search(vector<int>& nums, int target) {
        int ii = nums.size()/2;
        if(nums.size() == 0) {
            return -1;
        }
        while(nums[ii] > target) {//大于还是小于想清楚
            vector<int> a;
            for(int j = 0; j < ii; j++) {
                a.push_back(nums[j]);
            }
            return search(a, target);
        }
        while(nums[ii] < target) {
            start += ii + 1;
            vector<int> a;
            for(int j = ii+1; j < nums.size(); j++) {
                a.push_back(nums[j]);//a中没有值不能直接a[ii-j-1]
            }
            return search(a, target);
        }
        if(nums[ii] == target) {
            return start + ii;
        }
        return -1;
    }
};

通过下标对数组操作即可不需要反复递归

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size();
        int middle = (left+right)/2;//使用下标避免递归传入新数组,通过下标对数组操作;如果left和right数值都很大的话,left+right相加会溢出,建议使用left + ((right - left) / 2
        while(left<right)
        {
            middle = (left+right)/2;
            if(nums[middle] == target)
            {
                return middle;
            }
            else if(nums[middle]>target)
            {
                right = middle;
            }
            else
            {
                left = middle + 1; // 注意这里的if条件 与 left / right变动情况,别写反了
            }
        }
        return -1;
    }
};

2.2 使用二分法特征/前提

1、强调数组中无重复元素重复也可考虑,转化为求第一个/最后一个的边界,因为除了刚开始初始值之外,往后在left左边的一定小于left,在right右边的一定大于right;例如34),因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的
2、另一个前提是数组是有序数组,这也是使用二分查找的基础条件
这些都是使用二分法的前提条件,当看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了

2.3 使用二分法注意点

while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则,区间的定义一般为两种,左闭右闭即[left, right](就是下面代码),或者左闭右开即[left, right)(就是上面代码),复杂度为 O(log n)

// 左闭右闭
class Solution {
public:
    int search(vector<int>& nums, int target) {
        int minNum = 0;
        int maxNum = nums.size() - 1;
        while (minNum <= maxNum) {
            int middle = (minNum + maxNum) / 2;
            if (nums[middle] == target) {
                return middle;
            }
            else if (nums[middle] < target) {
                minNum = middle + 1;
            }
            else {
                maxNum = middle - 1;
            }
        }
        return -1;
    }
};

2.4 leetcode 35:二分法寻找插入位置

二分法:
分别处理如下四种情况
(注意)目标值在数组所有元素之前 [0, -1] return right + 1 / left
目标值等于数组中某一个元素 return middle;
目标值插入数组中的位置 [left, right],return right + 1 / left
目标值在数组所有元素之后的情况 [left, right], 因为是右闭区间,所以 return right + 1 / left

class Solution {
public:
    int searchInsert(vector<int>& nums, int target) {
        int n = nums.size();
        int left = 0;
        int right = n - 1; // 定义target在左闭右闭的区间里,[left, right]
        while (left <= right) { // 当left==right,区间[left, right]依然有效
            int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2
            if (nums[middle] > target) {
                right = middle - 1; // target 在左区间,所以[left, middle - 1]
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,所以[middle + 1, right]
            } else { // nums[middle] == target
                return middle;
            }
        }
        return left; // 应插入位置就是left的位置
    }
};

也可以使用暴力解决
暴力解题 不一定时间消耗就非常高,关键看实现的方式,就像是二分查找时间消耗不一定就很低,是一样的。

for (int i = 0; i < nums.size(); i++) {
        // 分别处理如下三种情况
        // 目标值在数组所有元素之前
        // 目标值等于数组中某一个元素
        // 目标值插入数组中的位置
        if (nums[i] >= target) { // 一旦发现大于或者等于target的num[i],那么i就是我们要的结果
	        return i;
        }        
}

2.5 leetcode 34:二分法寻找重复元素的边界

第一次代码,多种提交出现情况后多次加补丁,没有以正确方式有条理地考虑问题

class Solution {
public:
    vector<int> searchRange(vector<int>& nums, int target) {
        vector<int> result;
        int start = 0;
        int end = nums.size() - 1;//前闭后闭
        int middle = start + (end - start)/2;
        while(start <= end) {
            middle = start + (end - start)/2;
            if(nums[middle] > target) {
                end = middle - 1;
            }
            else if(nums[middle] < target) {
                start = middle + 1;
            }
            else {
                int i;
                int judge = 0;//如果有三个连续的如[3,3,3],target=3只记录一头一尾
                for(i = start; i <= end; i++) {
                    if(nums[i] == target) {
                        if(judge == 0) {
                            result.push_back(i);
                            judge = 1;
                        }
                    }
                    else if(result.size() != 0){//判断nums[i] != target时要已经之前有过等的才跳出
                        break;//如果不等了要及时跳出,因为下面判断一个时要用到i
                    }
                }
                if(result.size() == 1) {
                    result.push_back(i-1);//补充特殊情况只有一个时,如nums=[1],target=1; nums=[1,3], target=1,注意i加了1了,要减掉
                }
                return result;
            }
        }
        result.push_back(-1);
        result.push_back(-1);
        return result;
    }

正确的思路:
二分法 重复也可考虑,转化为求第一个/最后一个的边界,因为除了刚开始初始值之外(情况一要单独看),往后在left左边的一定小于left,在right右边的一定大于right
以左右边界为考虑对象,需要将情况考虑仔细了:
把所有情况都讨论一下。

寻找target在数组里的左右边界,有如下三种情况:

情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1}

情况二:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}

情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
二分查找,寻找target的右边界(不包括target)

接下来,在去寻找左边界,和右边界了。

采用二分法来去分别寻找左右边界,关键是当nums[middle] == target的时候 的处理,见注释

代码随想录实现方式:
寻找右边界:

// 如果rightBorder为没有被赋值(即target在数组范围的左边,例如数组[3,3],target为2),为了处理情况一
int getRightBorder(vector<int>& nums, int target) {
    int left = 0;
    int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right]
    int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况
    while (left <= right) { // 当left==right,区间[left, right]依然有效
        int middle = left + ((right - left) / 2);
        if (nums[middle] > target) {
            right = middle - 1; // target 在左区间,所以[left, middle - 1]
        } else { // 当nums[middle] == target的时候,更新left,这样才能得到target的右边界(如果有target的话最后一定过了)
            left = middle + 1;//只要小于等于target就往右移,直到left超过right,这样一定在右端,二分法只有不断对 等于的 往右推才能覆盖有重复的情况
            rightBorder = left;
        }
    }
	return rightBorder;
}

左边界同理:

int getLeftBorder(vector<int>& nums, int target) {
        int left = 0;
        int right = nums.size() - 1;
        int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况
        while (left <= right) {
            int middle = left + ((right - left) / 2);
            if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right
                right = middle - 1;
                leftBorder = right;
            } else {
                left = middle + 1;
            }
        }
        return leftBorder;
    }

对三种情况的处理:

int leftBorder = getLeftBorder(nums, target);
int rightBorder = getRightBorder(nums, target);
// 情况一
if (leftBorder == -2 || rightBorder == -2) return {-1, -1};
// 情况三
if (rightBorder - leftBorder > 1) return {leftBorder + 1, rightBorder - 1};
// 情况二
return {-1, -1};

自己实现方式:

class Solution {
public:
    int getRightboard(vector<int>& nums, int target) { // 寻找右边界,直接返回的就是右边界的值
        int left = 0;
        int right = nums.size() - 1;
        while (left <= right) {
            int middle = left + (right - left) / 2;
            if (nums[middle] > target) {
                right = middle - 1;
            }
            else if (nums[middle] < target) {
                left = middle + 1;
            }
            else {
                left = middle + 1; // 与普通二分的区别,相等的时候left也要往右推,等right减过left,left再推过right 或 left直接推过right
            }
        }
        return right;// 情况一不需要特殊处理,因为right自然会不停减1,直到-1
    }

    int getLeftboard(vector<int>& nums, int target) { // 寻找左边界,直接返回的就是左边界的值
        int left = 0;
        int right = nums.size() - 1;
        if (nums.size() > 0 && nums[0] > target) return -1; // 情况一需要特殊处理,因为left最小就是0
        while (left <= right) {
            int middle = left + (right - left) / 2;
            if (nums[middle] > target) {
                right = middle - 1;
            }
            else if (nums[middle] < target) {
                left = middle + 1;
            }
            else {
                right = middle - 1;
            }
        }
        return left;
    }

    vector<int> searchRange(vector<int>& nums, int target) {
        vector<int> res;
        int right = getRightboard(nums, target);
        int left = getLeftboard(nums, target);
        if(left > right) { // 情况二,整体代码退化到35
            res.push_back(-1);
            res.push_back(-1);
        }
        else { // 情况一,三
            res.push_back(left);
            res.push_back(right);
        }
        return res;
    }
};

2.6 leetcode 69:二分法寻找平方根

二分法寻找的部分中:
        int start = 0;
        int end = x;
        int middle = start + (end - start)/2;
        while(start < end) {
            middle = start + (end - start)/2;
            if((long long)middle * middle > x) {//if的前后与结果是偏大偏小无关,middle的取值才有关,middle位置的值决定了start/end谁动
                end = middle - 1;
            }
            else if((long)middle * middle < x) {
                start = start + 1;
            }
            else {
                return middle;
            }
        }
        return end;
    }
};

出现Memory Limit Exceeded情况,可以 直接用值 不需要把值放在数组里

没必要做成一个数组,直接用middle平方比,注意middle平方可以用long long数据类型防止越界。
注意不可以写成(long long)(middle*middle),因为这样会先算middle*middle,int显然可能超范围

对于无法控制二分法查找偏大偏小问题,只记录left侧的答案即可,无论小于等于都动left,因为left对应记录一次答案所以等于条件也要放入其中

class Solution {
public:
    int mySqrt(int x) {
        int start = 0;
        int end = x;
        int middle;
        int ans;//记录结果
        while(start <= end) {//一定加等号因为可能end等于x
            middle = start + (end - start)/2;
            if((long long)middle*middle <= x) {//注意不可以写成(long long)(middle*middle),因为这样会先算middle*middle,int显然可能超范围
                ans = middle;
                start = middle + 1;
            }
            else {
                end = middle - 1;
            }
        }
        return ans;
    }
};

不能用上一题的思路,因为 上一题 就算找到了目标 左边界还是往前退一格的
也不能 写成右边界的情况,因为目标是小一(8右边界的结果是3)

middle = start + (end - start)/2;
if((long long)middle*middle < x) {
	start = middle + 1;
}
else {
	ans = middle;
	end = middle - 1;
}

3、双指针

3.1 leetcode 27:双指针的两种思路

写法一:使用同向一快一慢两个指针:

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int j = 0;
        for(int i = 0; i < nums.size(); i++) {//双指针
            if(nums[i] != val) {
                nums[j++] = nums[i];
            }
        }
        nums.resize(j);
        return j;
    }
};

写法二:左右相向而行的两个指针,不容易想到,元素顺序改变了

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int leftIndex = 0;
        int rightIndex = nums.size() - 1;
        while (leftIndex <= rightIndex) {
            // 找左边 等于 val的元素,一定要确保leftindex<=rightindex
            while (leftIndex <= rightIndex && nums[leftIndex] != val){
                ++leftIndex;
            }
            // 找右边 不 等于val的元素
            while (leftIndex <= rightIndex && nums[rightIndex] == val) {
                -- rightIndex;
            }
            // 将右边 不等于val的元素覆盖左边 等于val的元素
            if (leftIndex < rightIndex) {
                nums[leftIndex++] = nums[rightIndex--];
            }
        }
        return leftIndex;   // leftIndex一定指向了最终数组末尾的下一个元素
    }
};

自己实现:
left < right的话最后要返回left+1,因为left直接停在最后一个元素的下标处,但由此带来一个问题,对于输入为空的时候他也+1了,为了统一逻辑,需要left <= right,返回left
这边要判断left是否小于right,因为等于的那个其实不需要记录,记录了left就大1了且多赋了一次值,考虑[3,2,2,3],输出[2,2,2],预期结果[2,2](如果输入不为空的话,其实应该left < right)

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int left = 0;
        int right = nums.size() - 1;
        while (left <= right) { // left < right的话最后要返回left+1,因为left直接停在最后一个元素的下标处,但由此带来一个问题,对于输入为空的时候他也+1了,为了统一逻辑,需要left <= right,返回left
            while (left <= right && nums[left] != val) {
                left++;
            }
            while (left <= right && nums[right] == val) {
                right--;
            }
            if (left < right)
                nums[left++] = nums[right--]; // 这边要判断left是否小于right,因为等于的那个其实不需要记录,记录了left就大1了且多赋了一次值,考虑[3,2,2,3],输出[2,2,2],预期结果[2,2]
        }
        return left;
    }
};

3.2 leetcode 844:两个字符串分别使用同向快慢指针

两个字符串分别使用同向快慢指针,其中慢指针还会回退
第一遍代码:

class Solution {
public:
    bool backspaceCompare(string s, string t) {
        int j1 = 0;
        for(int i1 = 0; i1 < s.size(); i1++) {//一定要从0开始,从1开始的话c#d#会有问题
            if(s[i1] == '#' && j1 == 0) {//对特殊情况进行处理,对空#仍为空
                continue;
            }
            if(s[i1] == '#') {
                j1--;
            }
            else {
                s[j1++] = s[i1];
            }
        }
        s.resize(j1);
        int j2 = 0;
        for(int i2 = 0; i2 < t.size(); i2++) {
            if(t[i2] == '#' && j2 == 0) {
                continue;
            }
            if(t[i2] == '#') {
                j2--;//目标位置进行回退(最后resize一下相当于删除了)
            }
            else {
                t[j2++] = t[i2];
            }
        }
        t.resize(j2);
        return s==t;//compare:若参与比较的两个串值相同,则函数返回 0;若字符串 S 按字典顺序要先于 S2,则返回负值;反之,则返回正值。下面举例说明如何使用 string 类的 compare() 函数。
    }
};

为什么不能用compare(compare用法):

若参与比较的两个串值相同,则函数返回 0;若字符串 S 按字典顺序要先于 S2,则返回负值;反之,则返回正值。下面举例说明如何使用 string 类的 compare() 函数。

第二次错误的思路:

class Solution {
public:
    bool backspaceCompare(string s, string t) {
        int s1 = s.size() - 1; // 不为空才可以这么写
        int t1 = t.size() - 1; // 从后往前考虑有个问题,如果出现"ab##"就会失效,所以只能老老实实遇到一个退格删一个,对单一字符串分别使用同向双指针
        while (s1 >= 0 && t1 >= 0) {
            if (s[s1] == '#')
                s1 -= 2;
            if (t[t1] == '#')
                t1 -= 2;
            if(s1 >= 0 && t1 >= 0 && s[s1] != t[t1])
                return false;
            s1--;
            t1--;
        }
        
        if (s1 == t1) // 考虑第一个字母都是退格
            return true;
        return false;
    }
};

第二次错误的实现:

class Solution {
public:
    bool backspaceCompare(string s, string t) {
        int low = 0;
        for (int fast = 0; fast < s.size(); fast++) {
            if(low == 0 && s[fast] == '#') // 注意low = 0,不是fast = 0,s[fast] == '#',不是s[0] == '#' 考虑输入"a##c"理解
                continue;
            if(s[fast] != '#') {
                s[low++] = s[fast];
            }
            else {
                low--;
            }
        }
        s.resize(low);

        low = 0;
        for (int fast = 0; fast < t.size(); fast++) {
            if(low == 0 && t[fast] == '#')
                continue;
            if(t[fast] != '#') {
                t[low++] = t[fast];
            }
            else {
                low--;
            }
        }
        t.resize(low);
        return s==t;
    }
};

3.3 leetcode 977:相向而行的双指针

采用相向而行的双指针(因为在待放入结果的数列里面只有左右两边的数可能是最大的,所以从大到小排序)

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        vector<int> result(nums.size());
        int z = nums.size() - 1;
        int j;
        for(int i = 0, j = nums.size() - 1; i <= j; ){
            if(nums[i] * nums[i] >= nums[j] * nums[j]){
                result[z--] = nums[i] * nums[i];
                i++;
            }
            else{
                result[z--] = nums[j] * nums[j];
                j--;
            }
        }
        return result;
    }
};
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值