【代码随想录算法训练营第1天】704. 二分查找、27. 移除元素

Day1-数组2023.5.20

2023.9.14二刷

Leetcode704 二分查找

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

初解

已经不记得二分查找了,遍历找O(n)其实也过了,只是借此复习一下二分,确实快很多O(logn)
二分的前提条件题目里也都明示了:无重复,(从小到大)排序。我没有用到这个条件,自然时间复杂度高于最优解。

9.14

绝绝子,二分的时候区间没有+1 -1卡死了,why??闭区间变成开区间了卧槽……

看完解答

我再看了一眼题目的标题,才知道是考BinarySearch嗯嗯啊啊,数算的一个小小算法已经忘记干净了。当然二分是非常实用的,解答里很贴心地喂了两种情况和模板,我默写一下:

闭区间[l,r]

1.while()里面是l<=r,因为取等是有意义的(最后剩一个)。
2.取中点之后,若sum[mid]<target,即目标区间缩小为右半边,就要改动左端点为mid+1,保证下一次循环还是闭区间。

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)/2;
            if(nums[middle]==target){
                return middle;
            }else if(nums[middle]<target){
                left=middle+1;
            }else{
                right=middle-1;
            }
        }
        return -1;
    }
};

开区间[l,r)

1.while()里面是l<r
2.取中点之后,若sum[mid]<target,即目标区间缩小为右半边,就要改动左端点为mid+1,保证下一次循环还是左闭右开;若sum[mid]>target,即目标区间缩小为左半边,就要改动右端点为mid,保证下一次循环还是左闭右开。

其实用表格就可以解决了(但是我不会快速建表插件之外的建表方法,,),精髓就是注意在循环中保持区间性质不变,还有循环出口。

小技巧:防止l+r溢出,写mid=l+(r-l)>>1;(这是遇到多少挠谭测试数据导致的。。)

Leetcode27 移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

初解

啊,空间复杂度O(1)啊,不会,作为复习看看题解也正常,正常学习就不慌,计概老师知道也无妨

还是写一下两种铁头娃做法。牺牲空间换时间:再开一个新的只记录下删除val之后的部分(本题不适用);牺牲时间换空间:两层for循环,第一层扫描原数组中的所有val,一旦发现一个就删一个,即第二层for循环把后面所有数组元素都左移一位。

看完解答

双指针法。这里也介绍两种,快慢指针和双向奔赴指针。

快慢指针法

我记忆里这个方法是用来解决判断链表是否有环的,据题解说在数组链表中这是个常用办法,好题目还在后面呢。
快慢指针法的基本原理就是使用一前一后两个指针,在本题中,快指针负责扫描不等于val的元素,慢指针负责维护数组。
具体说来,一开始两个指针都是++扫描,直到快指针发现等于val的元素,我们要删掉这个元素,也就是找一个后面的(最好是最近的,这样就不会改变元素的相对位置)元素覆盖到这里。慢指针在此停留等待新数据,快指针扫描到第一个不等于val的元素,然后慢指针再移动。这样是可行的,因为快指针到数组末尾后停止,在此期间快指针遍历了数组中每个元素,遇到等于val的就不赋给慢指针,遇到不等于的就赋给慢指针,一次各一步,所有不等于val的都没落下。数组末尾地址的内存可能会剩下一些val,但是题目说不用考虑。

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int len=nums.size();
        int slowp=0;
        for(int fastp = 0; fastp < len; ++fastp){
            if(nums[fastp]!=val){
                nums[slowp++]=nums[fastp];
            }
        }
        return slowp;
    }
};

相向双指针法

这个方法的想法是,扫描到左边有一个需要删除的地方,就从数组末尾拉一个不要删除的填上。

先上一个一错再错最后错了的实现:

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int lp=0;
        int rp=nums.size()-1;
        while(lp<=rp){
            while((nums[lp]!=val)&&(lp<=rp)) ++lp;
            while((nums[rp]==val)&&(lp<=rp)) --rp;
            nums[lp++]=nums[rp--];
        }
        return lp;
    }
};

再给标答:

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

到底差在哪里呢?通过平台给的阴间初始数据,我发现是数组越界问题。即lp可能走到len,rp也可能走到-1,然后数组遇到它们作为下标就会报错。这简单,两个while里面再加个边界……也不行。这样[3,2,2,3]中删除3时,经过第一轮,lp=rp=1,又经过一次nums[lp++]=nums[rp--];,lp多++一次。这是不对的。所以应该对这条语句,加上只在lp<rp时执行!

可惜这样也有问题。比如在数组[1]中删掉1,那么lp和rp都不会在while中移动,if里的语句也不执行,就陷入了死循环

这不是个死局吗?改变其他地方的取等条件,无异于改变我们一开始脑内的算法。比如改变大while的条件为lp<rp,在[3,2,2,3]中删除3时,就会因为第二轮lp=rp=1终止,少算第二位。

其实一开始我们就不用在while里面加入边界判断,因为数组越界只有lp和rp被当成数组下标才会发生。再仔细看代码,这发生在第一轮大while,rp进入循环,rp=-1后,第二轮小while,rp被当成了数组下标。这个问题非常好解决,如果你想到与运算符的短路性质。你把lp<=rp先判断,rp=-1时就直接滚蛋了,不会再当成数组下标去验证nums[rp]=?val了。
淦啊!

9.14 双向奔赴指针法

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int len=nums.size();
        int r=len-1;
        int l=0;
        while(r>=0&&nums[r]==val){
            r--;
        }
        if(r<0){
            return 0;
        }
        if(r==0){
            if(nums[0]==val){
                return 0;
            }else{
                return 1;
            }
        }
        for(l=0;l<=r;l++){
            if(nums[l]==val){
                while(r>=0&&nums[r]==val){
                    r--;
                }if(r<0){
                    return 0;
                }
                if(l<r){
                    nums[l]=nums[r];
                    r--;
                }
            }
        }
        return r+1;

    }
};

我不知道说啥了,我是傻逼。

今天就做了这两道题,收获真大(预习.jpg)

不知道能做几天,暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂暂

posted @   杰克40001  阅读(548)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示