副露のMagic的弱智算法学习 day1

今日主要内容:数组基础(数组理论基础,704. 二分查找,27. 移除元素)

前置准备

1:不知道二分法是什么了,康复训练

2:回顾数组的基本知识(array和vector,虽然才看不久,但感觉掌握的非常有限)

based on C++ 学习依据代码随想录:)

内容学习

1:二分法

        看了两个简单的博客,回想起印象中的二分法:大致是首先给一组数按照大小进行排序,然后每次都通过跟区间的最中间的元素进行对比,将需要查找的区间缩小为之前的一半。看上去还挺容易的,不知道实际做起来如何。

2:回顾数组基础

        简单复习了下最基础的数组内容,一些语法语句基本上。

题目1

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


示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

愚蠢的尝试(该部分都是杂乱想法,不一定正确)

        总体思路是这样的,首先获得输入数组的长度n,使用array.size()函数。然后设定一个left(l);right(r)和middle(mid)分别代表左右和中间位置。起始将中间元素设置为l和r的平均数,l设置为0,r设置为n-1(数组长度-1)。

        其后使用while,条件是r大于l(?),如果目标target大于mid下标的数值,就将l修改为mid+1,再重新计算mid的数值;如果目标target小于mid下标的数值,就将r修改为mid-1;如果二者相等,也就说明找到了该数值,即返回当前mid的值。如果一直无法找到,就会出现r不大于l的情况,此时也就说明没有找到。但这里就该注意到判断的边界,如果r=l会怎么样(在这里已经很晕了,但是直觉上来说感觉应该考虑上一次循环,这里的想法我也不知道对不对。上一次循环中,应该只有两个元素,且应该是使用l判断是否与target相等(因为除法向下取整),这样的话,再取一次平均一定会选到l,但一直l和target不相等,所以只要相等的时候就可以判断找不到了?)先写代码尝试一下结果: 

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

 运行时通过了,喜出望外;

 提交时错误了,悲从中来;

检查一下结果发现错完了,出现了预期结果0而出现最后结果为-1的情况,为什么呢,是哪里i想的有问题。实验实例是[5]只有一个元素,这样的话第一时间l=r=mid,如果r=l的时候直接快进到-1就出错了。所以{条件是r大于l(?)}那里肯定出错了 ,应该是r>=l才合理,为什么呢?学一下。

视频学习

两个易错点:
1:while(left<还是<=right)
2:如果nums[middle]>target;那么right<=middle还是(middle-1)
[l,r]和[l,r):

        在其中要坚定一个区间,不能随意改动

1:左闭右闭

        讨论第一个易错点,由区间有效性的角度考虑,l=r时区间有效,因此应该是l<=r;

        讨论第二个易错点,由重复代入的角度考虑,当左闭右闭时,当取mid = (l + r)/ 2时,如果有判断target与nums【mid】不等,则此时已知target与nums【mid】不等,在调整l和r的时候,也不计入mid这个位置了,而是直接选择mid后/前的一个。

        于是容易写出:

class Solution {
public:
    int search(vector<int>& nums, int target) {
        int n;
        n = nums.size();
        int l = 0;
        int r = n-1;
        int result = 0;
        while (r >= l){
            int mid = (r + l)/2;
            if (target > nums[mid]){
                l = mid+1;
            } 
            else if (target < nums[mid]){
                r = mid-1;
            }
            else if (target == nums[mid]){
                return mid;
            }
        }
        cout << result << endl;
        return -1;
    }  
};
2:左闭右开

        此时,区别是什么呢,首先,l和r相等的可能性不再成立了。其次,当左闭右开时,如果target<mid时,此时右边的开区间可以到达mid,如果延伸到mid-1的话就相当于减少了一个可选的位置。

        同样的,也可以很容易的写出:

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

        理清思路还是很好理解的是吧,那我们快进到下一个问题: 

题目2

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

示例 1:

输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。

愚蠢的尝试(该部分都是杂乱想法,不一定正确)

        好像没什么太多的想法,计划是这样。首先获得数组的长度n,之后遍历数组,如果发现再第i个位置有值等于所给的val,则将i+1位置的数赋值给第i个位置,后面也是一样。同时做n--运算,最后输出。

错误的代码,失败了,为什么呢?

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int n = nums.size();
        for (int i = 0 ; i < n ; i++){
            if (nums[i] == val){
                for (int a = i; a < n; a++){
                    nums[a] = nums[a + 1];
                    n--;
                }
            }
        }
        return n;
    }
};

视频学习 

暴力解决

        暴力拆解的方法基本上思路是对的,但是代码写的很混乱,不知道自己要干嘛:很多细节也没有把握好:首先,在第二个循环中, a < n这里就明显不对,i和a都小于n,但是随着每个数往前挪一个位置,下标也要重新回到刚刚覆盖的位置(因为移到前面来的那个数也有可能等于val),所以就造成了两个问题,其一,a应该< n-1;其二,完成内部循环后,i应-1来回到初始位置。

        更新的代码如下:显然该方法比较复杂,复杂度为O(n方)嵌套循环。

class Solution {
public:
    int removeElement(vector<int>& nums, int val) {
        int n = nums.size();
        for (int i = 0 ; i < n ; i++){
            if (nums[i] == val){
                for (int a = i; a < n - 1; a++){
                    nums[a] = nums[a + 1];
                // for (int j = i + 1; j < n; j++ ){
                //     nums[ j - 1 ] = nums[ j ];
                }
                i--;
                n--;
            }
        }
        return n;
    }
};
 双指针法(感觉关键)

        主题思路就是使用快指针和慢指针,其中快指针用来检查每个元素是否是更新后的数组所需要的元素,而慢指针则指向处理过后该元素的实际位置。这样说明之后就很清楚了。给人的主观感受是慢指针先走,但不是一直走;快指针后走,但是保持移动:如果选择的元素不是我们要找的元素,就把该元素给slow的位置(一开始slow和fast在一起,此时没有实际意义),之后呢,如果出现了需要删除的元素,慢指针就停留在原地,表示新数组没有增加该元素,快指针则继续向前寻找。

        代码说明如下,非常的简洁,suki

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

        确实是很简单的方法,大巧不工是这样说的吧。 这也是库函数erase方法,其时间复杂度为O(n),相当于节省了一层for循环就完成了数组的删除,真的很巧妙吧。

总结

        感觉学习这一部分一方面要能想到这种做法,另一方面就是要掌握基本的语法,这样做起来可能会快很多。今天在输出那里其实有很多问题:我想着在最后cout或者return一个数组,这肯定是不对的,那为什么能输出数组,前面也说到了:

问题一

输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

我的理解就是,(我鸡毛理解没有)调用一下别人的理解,说不定我哪天就理解了:


但是数组你已经改了啊
妈的
因为数组是引用
数组里面的数据已经被你改成新的了

 

 (试图理解)数组名就是首元素的地址,所以实际上传入函数的不是数组名,而是首元素的地址。使用函数时都是将数组名作为参数。如果在函数内部对传入的数组进行了修改,该数组本身的值也会改变,有点像引用,这是因为前面提到过传入的是地址,我们是直接对地址上的元素进行修改。

(理解了)我一直迷惑的地方就是为什么return个int会打印出一个数组。意思是leetcode底层封装了代码导致的,可能编译器上做出来ruturn就是一个数字。鉴定为钻死胡同里去了,还是搞清楚基本的方法和语言为主Orz

问题二

关于二分mid溢出问题解答:

mid = (l + r) / 2时,如果l + r 大于 INT_MAX(C++内,就是int整型的上限),那么就会产生溢出问题(int类型无法表示该数)所以写成 mid = l + (r - l) / 2或者 mid = l + ((r - l) >> 1) 可以避免溢出问题,很合理很好理解不是吗?

如有错误,恳请指出,感激不尽!

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值