代码训练营第一天:数组理论基础 二分查找:leetcode704,移除元素:leetcode27

理论文章:数组理论基础​​​​​​​

二分查找:二分查找  leetcode704:leetcode704

移除元素:移除元素 leetcode27: leetcode27

目录

二分查找:leetcode 704:

        1,这个我熟啊:

2,思考历程:

3,修改后通过

4,对卡哥文章的思考:

4.1,另一种写法:

4.2 两种写法的一致性:

4.3 细节问题:

5 最终代码

5.1 左闭右开

5.2 左闭右闭:

移除元素:leetcode 27

1,看上去不像暴力

2,似乎有更好的做法

3,双指针做法

4,再改进一下!

总结


二分查找:leetcode 704:

        1,这个我熟啊:

题目特征:单调数列,给定target找对应下标,标准的二分查找 时间复杂度O(logn),空间复杂度O(1)

class Solution {
public:
    int search(vector& nums, int target) {
        int left = 0;
        int right = nums.size();
        while(lefttarget){
                right = mid;
            }else{
                left = mid;
            }

        }
        return -1;
    }
};

第一次运行就出现了问题,运行不出结果:

代码训练营第一天:数组理论基础 二分查找:leetcode704,移除元素:leetcode27_第1张图片

出现该问题,大概率是跑到死循环里了:重新捋一下流程:

1,left = 0, right = length

2,mid = (left+right)/2,计算出中点

3,判断nums[mid]和target之间的关系:

        3.1 如果相等,找到了,返回mid即可

        3.2 如果nums[mid]更大,意味着target在mid左侧,让right左移到mid位置

        3.3 否则,就是target更大,target在mid右侧,让left右移到mid位置

粗略理下来,思路问题不大,那就是写的有问题

2,思考历程:

1,在我的代码中,首先,right = nums.size(); 我的思路是左闭右开的做法

2,在左闭右开的定义下,我的循环终止条件left

3,问题的核心在于二分分到mid还是mid+1

        3.1 首先,当nums[mid]已经大于target的情况下,mid已经不需要再考虑了,让right移动到mid的位置,根据左闭右开,是合理的,需要寻找的是[left,mid-1]的元素,即[left,mid)

        3.2 在nums[mid]小于target的情况下,mid同样不需要再考虑,到这一步已经找到了问题所在:

left = mid;

在左闭右开的区间上,原本不需要考虑的mid,查找区间应该是[mid+1,right),而第一次提交的代码中显然mid放入了第二次查找的区间,这就出现了错误。

3,修改后通过

修改后的代码是:

class Solution {
public:
    int search(vector& nums, int target) {
        int left = 0;
        int right = nums.size();
        while(lefttarget){
                right = mid;
            }else{
                left = mid+1;//不再考虑mid点
            }

        }
        return -1;
    }
};

顺利通过。

4,对卡哥文章的思考:

4.1,另一种写法:

        ​​​​​​​代码训练营第一天:数组理论基础 二分查找:leetcode704,移除元素:leetcode27_第2张图片

很明显,在二分法中,不仅可以考虑左闭右开,同样可以考虑左闭右闭区间:

在这种区间的划分上,有几处是需要与我的做法区分开的:

1,right初始必须是length-1,即有意义的最大值

2,由于right在闭区间内,是需要查找的点,因此终止条件是left<=right

3,在两端变化的过程中,由于mid已经不可能是所查找的点,那么查找区间应该是[left,mid-1]或者[mid+1,right],因此left=mid + 1, 或者right= mid - 1。

class Solution {
public:
    int search(vector& nums, int target) {
        int left = 0;
        int right = nums.size()-1;//1.right有意义
        while(left<=right){//2.终止条件
            int mid = (left + right)/2;
            if(nums[mid]==target){
                return mid;
            }
            else if(nums[mid]>target){
                right = mid-1;
            }else{
                left = mid+1;
            }//3. 找到正确区间

        }
        return -1;
    }
};

顺利通过。

4.2 两种写法的一致性:

实际上在以上梳理思路的过程中,有一点是不变的:

如果num[mid]不是target,那么mid是应该排除在下一次搜索区间范围外的

在这个基础上,确定的区间写法,就可以写出正确的二分过程。

4.3 细节问题:

在我的计算mid方法中,是

mid = (left+right)/2

而在卡哥的代码中,是

int middle = left + ((right - left) / 2);

区别在于,我的方法是对left和right做加法,显而易见的问题在于,如果数组最右侧的right在int范围内时,left+right有可能超出int范围,而right-left不会超出int范围,而

left + (right-left)/2 = right/2 + left/2

显然是一致的。

5 最终代码

5.1 左闭右开

class Solution {
public:
    int search(vector& nums, int target) {
        int left = 0;
        int right = nums.size();
        while(lefttarget){
                right = mid;
            }else{
                left = mid+1;
            }

        }
        return -1;
    }
};

5.2 左闭右闭:

class Solution {
public:
    int search(vector& nums, int target) {
        int left = 0;
        int right = nums.size()-1;
        while(left<=right){
            int mid = left + (right - left)/2;
            if(nums[mid]==target){
                return mid;
            }
            else if(nums[mid]>target){
                right = mid-1;
            }else{
                left = mid+1;
            }

        }
        return -1;
    }
};

移除元素:leetcode 27

1,看上去不像暴力

题目要求使用O(1)空间,很明显不能扫一遍复制这么做,最简单的就是找到一个值,然后一个一个往前挪:

class Solution {
public:
    int removeElement(vector& nums, int val) {
        int length = nums.size();
        for(int i = 0;i

通过,时间复杂度O(n^{2})

2,似乎有更好的做法

在现在的做法中,可以观察到一个现象,由于数组中可能不止有一个val,这意味着:

[0, 1, val, 2, 3, 4, val, 5, 6, 7]

在这样一个数组中,5,6,7三个数会被挪动两遍,但是理论上,每个数字只需要挪动一遍就可以得到最终结果。

3,双指针做法

代码训练营第一天:数组理论基础 二分查找:leetcode704,移除元素:leetcode27_第3张图片

在动画演示的效果可以看出来,双指针一前一后,遇到一个val后,将后面的一个一个往前挪。这就实现了时间复杂度O(n),

如果一前一后往前挪的时候遇到了第二个val咋办?

似乎可以让left不动,right再往后挪一个,此时left和right差了两个位置,而right仍然指向要挪动的元素,left指向了要挪动到的位置。

思路清晰了起来,双指针一个在前一个在后,遇到val就动快的,没遇到就一起挪。那在代码中可以采取一个简单的做法,就是我认为只要没有遇到val,就把right上的元素挪到left上,代码如下:

class Solution {
public:
    int removeElement(vector& nums, int val) {
        int left = 0;
        int right = left;
        for(;right

时间复杂度:O(n),空间复杂度O(1)

4,再改进一下!

一个很自然的想法是,上面这个双指针的代码的元素移动次数不是最少,在第一个val之前的移动都是不必要的,卡哥的第二段代码就提供了这个思路,我理解为:拆东墙补西墙

1,从左边找val,找到了用left标记,

2,从右边找一个不是val的元素,找到了直接将这个元素补到left位置上。

实际上,这道题特地提示了不需要保证元素顺序不变,因此拆东墙补西墙的做法确保只需要移动的次数与val出现的次数是相同的。第一次实现代码如下:

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

遇到报错:

代码训练营第一天:数组理论基础 二分查找:leetcode704,移除元素:leetcode27_第4张图片

left似乎是直线最后一个元素的下一个的,因此直接返回left才是长度,再在拆东墙补西墙那一步检查一下,left==right就别动了:

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

总结

1,这两道题都是数组题,整体思路不太复杂,但是对于代码准确性要求比较高,总要在是不是+1,-1里面思考一下

2,两种二分法核心是mid不包含在搜索区间内,再确定right和left变化就容易了

3,双指针确实减少了需要遍历的次数,在我的理解中,双指针实际上有点像分段函数,一段一段的处理数组序列,感觉上是通过两个指针提前试探一部分序列的分类来减少不必要的操作,感觉还是有点精妙,还得再理解理解。

你可能感兴趣的:(算法,leetcode,数据结构)