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

今日主要内容:数组基础(977.有序数组的平方 ,209.长度最小的子数组 ,59.螺旋矩阵II)

前置准备

1:复习昨天的博客

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

内容学习

1:简单复习

        感觉写代码的时候头脑并不很清楚,就有时候写到下面不知道自己上面那一段是怎么想的了,然后就出现很逆天的代码,今日计划写代码的时候注意注释的写。

题目1

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

示例 1:

输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]

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

        首先能想到暴力解法,首先将每个元素进行平方,不改变其位置。然后再对元素们进行比较排序,虽然肉眼可见的复杂,但感觉基本可以实现。

        另一种写法可能考虑双指针,毕竟才学习的思想,先想一想快慢指针分别会代表什么比较合适。(好吧完全没想到这里会以什么方式使用双指针,先尝试一下暴力方法)                                         先写代码尝试一下结果: 

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        for (int i = 0; i < nums.size(); i++){
            nums[i] *= nums[i];
        }//这部分首先把每个元素平方,不改变其具体位置
        int a = 0; // 方便一会交换元素位置
        for (int i = 0; i < nums.size(); i++){
            for (int j = i+1; j < nums.size(); j++){
                if (nums[i] >= nums[j]){
                    a = nums[j];
                    nums[j] = nums[i];
                    nums[i] = a;
                    //这里使用双重循环,如果当前元素大于后面的任意一个
                    //元素,就把他们交换一下
                    //疑点:if否可以取等号,第二重循环是否有可能越界
                }
            }
        }
        return nums;

    }
};

 运行时通过了,但是提交时用时过长了,悲从中来;

 肉眼可见的使用了大量的资源,但是从132/137的通过率感觉,应该大体上的问题不是很大,但是还有一些显而易见的疑点需要处理:if否可以取等号,第二重循环是否有可能越界(以及算法的复杂度过高了,应该使用快速排序算法改善暴力求解的速度)

视频学习

双指针思路

        双指针也即取两个指针,从头和尾分别逼近中心元素。新建一个数组。其实看到这里思路就已经很明确了,于是暂停视频回来写代码,一次通过。

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        int n = nums.size();
        for (int i = 0; i < n; i++){
            nums[i] *= nums[i];
        }//这部分首先把每个元素平方,不改变其具体位置
        vector<int> result = vector(n,0); // 定义新的数组来排序
        for (int i = 0,j = nums.size()-1 ; i <= j ; ){
            //这里控制i和j从两个方向逼近。在i和j相遇的时候结束
            if (nums[i] >= nums[j]){
                result[n-1] = nums[i];
                i++;
                n--;
                //谁大谁就排到后面去,然后这边的指针向前(后)移动,
                //这里新数组的n-1位置已经被占用了,要往后走一步,下同
            }
            else {
                result[n-1] = nums[j];
                j--;
                n--;
            }
        }
        return result;

    }
};

        其中有两个小地方,写的时候竟然想清楚了,帅的一:

         for (int i = 0,j = nums.size()-1 ; i <= j ; ) 第一部分是这里,i<=j,如果去掉等号的话会丢掉最后相等时的一个元素,显然就错误了。其次是例如i++,j--这些内容都是经过判断时候才会去做的,因此不写在for循环里。主要就是这两个问题。

快速排序

        再次补充一下快速排序的内容:

class Solution {
public:
    vector<int> sortedSquares(vector<int>& A) {
        for (int i = 0; i < A.size(); i++) {
            A[i] *= A[i];
        }
        sort(A.begin(), A.end()); // 快速排序
        return A;
    }
};

        这个时间复杂度是 O(n + nlogn), 可以说是O(nlogn)的时间复杂度,但为了和下面双指针法算法时间复杂度有鲜明对比,我记为 O(n + nlog n)。但是这里也没有说明快速排序的具体流程,因此我再去寻找一下资源:

基本思想:

采用“分治”的思想,对于一组数据,选择一个基准元素(base),通常选择第一个或最后一个元素,通过第一轮扫描,比base小的元素都在base左边,比base大的元素都在base右边,再有同样的方法递归排序这两部分,直到序列中所有数据均有序为止。

一趟快速排序的算法步骤是:

1.设置两个变量i、j,排序开始的时候:i=0,j=N-1;
2.以第一个数组元素作为关键数据(通常是第一个),赋值给key,即key=A[0];
3.从j开始向前搜索,即由后开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]和A[i]的值交换; (进行交换的时候i, j指针位置不变)
4.从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]的值交换; (进行交换的时候i, j指针位置不变)
5.重复第3、4步,直到i==j;
        注意:
        在3, 4步中,没找到符合条件的值,即3中A[j]不小于key, 4中A[i]不大于key的时候也要改变j、i的值,使得j--,i++,直至找到为止。找到符合条件的值。
另外,i==j这一过程一定正好是i++或j--完成的时候,此时令循环结束。

        代码如下

void QuickSort(int a[],int low,int high)
{
    if(low>=high) return;//此时已经完成排序,直接返回
    int i = low;
    int j = high;
    int key = a[low];
    while(i<j)//实现第一趟排序
    {
        while(i<j&&key<a[j]) j--;//从右向左找比key小的值
        a[i] = a[j];
        while(i<j&&key>a[i]) i++;//从左向右找比key大的值
        a[j] = a[i];
    }
    a[i] = key;//将关键数据填入low=high的位置
    QuickSort(a,low,i-1);//左边子序列递归排序
    QuickSort(a,i+1,high);//右边子序列递归排序
}
int main()
{
    int a[20];
    QuickSort(a,0,19);
    for(int i = 0;i<20;i++)
        cout<<a[i]<<" ";
    return 0;

题目2

给定一个含有 n 个正整数的数组和一个正整数 target 。

找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度如果不存在符合条件的子数组,返回 0 。

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

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

        我日你哥,看到题目一脸懵b,啥想法没有啊?

        也不能说完全没有吧,首先可以知道如果不存在符合条件的子数组,返回 0 。这个也即遍历数组,计算其总加和,如果总加和都不及target,可以直接返回0。那么如何计算最小的子数组呢,到这里想到可能是根据总和挨个去除一些元素?比如把所有的元素从小到大排序,用总和依次减去累加和,当差值小于target时,再加上刚刚去掉的那个元素就够了。

(呆逼,审题哦,连续子数组你还在这重新排序还在这问答案为什么不对,笑拥了。)

        想到这好像能解释这个问题了,尝试写一下代码吧。(错误代码)

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int sum = 0;
        int n = nums.size();
        int r = 0; //用来输出结果
        for(int i = 0 ; i < n ; i++){
            sum += i;
        }//计算加和
        if(sum<target){
            r=0;//不够直接返回0
        }
        else if(sum == target){
            r=n;//相等直接返回总数
        }
        else{
            sort(nums.begin(), nums.end()); //不相同先排序
            for (int i=0;i<n;i++){
                sum -= nums[i];
                if(sum<target){
                    r=n-i;
                    break;
                }//如果小了,说明刚刚那个只要不减去就够了,
                //一共n个,去掉了i+1个元素,再加一
                else if(sum == target){
                    r=n-i;
                    break;//如果相等,说明现在正好够了,
                //一共n个,去掉了i+1个元素   
                }
            }
        }
        return r;
    }
};

        但是结果是错的,为什么呢,我感觉写的还蛮好的呢(不是)。还是让我们来看看具体该如何解决吧。(你是弱智吧还写得好好钩子)

视频学习 

超级暴力解决

       啊这个也没想到吗===

        超级暴力方法,使用for循环,写出所有的区间可能性并加和比较,选出其中最小的一个。(又写了个错的代码,为什么呢?) 后面再看,先暂缓一下。

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int result;
        int n = nums.size();
        int sum = 0;
        for (int i=0;i<n;i++){
            sum+=nums[i];
        }
        if (sum<target){
            return 0;
        }
        // else if (sum == target){
        //     return n;
        // }
        for (int i=0;i<n;i++){
            int sum = 0;
            for(int j=i;j<n;j++){
                sum += nums[j];
                if(sum >= target){
                    result = j-i+1;
                    break;
                }
            }
        }
        return result;
    }
};

        正确代码 :(虽然正确但是也会有部分会超时)

int result = INT32_MAX; // 最终的结果
        int sum = 0; // 子序列的数值之和
        int subLength = 0; // 子序列的长度
        for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i
            sum = 0;
            for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j
                sum += nums[j];
                if (sum >= s) { // 一旦发现子序列和超过了s,更新result
                    subLength = j - i + 1; // 取子序列的长度
                    result = result < subLength ? result : subLength;
                    break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break
                }
            }
        }
        // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列
        return result == INT32_MAX ? 0 : result;
 双指针法(滑动窗口思想)

       主要就是将j指针初始作为最终位置(不是起始就在最后,for循环遍历得到的),然后移动处在起始位置的i指针来调整位置,调整区间大小。条件是:目前的区间元素和已经大于等于s了,就要移动i。整体思想比较好理解,也是一些小地方需要处理好。

        首先,在判断sum是否大于等于target时,由于要不断地进行判断推进i的位置,应该使用while而不是if;其次,在最后那里需要判断,原始集合是否足够大,若本来就不够,没有进入循环并操作,就应该直接输出0。这两个地方需要注意。

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int result = INT32_MAX; // 最终的结果
        int sum = 0; // 子序列的数值之和
        int subLength = 0; // 子序列的长度
        int i = 0;
        for(int j = 0; j < nums.size();j++){
            sum += nums[j];
            while(sum >= target){
                subLength = j-i+1;
                // result = min(result,subLength);
                result = result < subLength ? result : subLength;
                sum -= nums[i];
                i++;
            }
        }
        return result == INT32_MAX ? 0 : result;
        // return result;
    }
};

实际上,在return的时候也可以提前判断是否参与了循环,也即:

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int result = INT32_MAX; // 最终的结果
        int sum = 0; // 子序列的数值之和
        int subLength = 0; // 子序列的长度
**       
        for(int i = 0;i<nums.size();i++){
            sum += nums[i];
        }
        if (sum < target){

            return 0;
        }
        else{
            sum = 0;
        }
        int i = 0;
**
        for(int j = 0; j < nums.size();j++){
            sum += nums[j];
            while(sum >= target){
                subLength = j-i+1;
                result = min(result,subLength);
                // result = result < subLength ? result : subLength;
                sum -= nums[i];
                i++;
            }
        }
        // return result == INT32_MAX ? 0 : result;
        return result;
    }
};

        

题目3

       

给你一个正整数 n ,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix 。

示例 1:

输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]

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

        重击我的大脑反正是:(((((((

        感觉只有一些零散的想法吧:1~n²这些数一起可以写成一个矩阵,考虑i和j分别作为横纵坐标,先横着打印,然后如果i到达n,就换成j继续行动,j到达n就换成i继续行动这样,然后把输出的结果放入数组中即可。但是个人感觉好像编程能力很难达到这个水平。快进到学习视频,就不胡乱尝试了。

视频学习

        学完了,实际上思路还是比较简单,把握好每一次读哪些结束就可以了。然后用来控制每次跑多远的就设计成全局变量,随着一圈的完成增加就可以了。这里在思路上注意一点,画矩阵要想着二维数组里是【1】【1】→【1】【2】的增长,而不是类似于x轴坐标的增长,第一次写错主要就是这个原因。另外,for循环中如果没有要调整的元素,就空着不写,不要写个变量在里面,类似于这样

for(i;i<n;i++);

        以下是正确的代码:这个题目反而比第二个要好想不少:(

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        int x = 0;
        int y = 0;
        int cha = 1;
        int time = n/2;
        vector<vector<int>> res(n, vector<int>(n, 0));//定义二维数组
        int count = 1;
        while(time--){
            int i;
            int j;
            for (j = y; j < n - cha; j++) {
                res[x][j] = count++;
            }
            // 模拟填充右列从上到下(左闭右开)
            for (i = x; i < n - cha; i++) {
                res[i][j] = count++;
            }
            // 模拟填充下行从右到左(左闭右开)
            for (; j > y; j--) {
                res[i][j] = count++;
            }
            // 模拟填充左列从下到上(左闭右开)
            for (; i > x; i--) {
                res[i][j] = count++;
            }
            x+=1;
            y+=1;
            cha+=1;  
        }
        if(n%2!=0){
            res[x][y] = count;
        }
        return res;
    }
};

总结 

数组的经典题目

在面试中,数组是必考的基础数据结构。

其实数组的题目在思想上一般比较简单的,但是如果想高效,并不容易。

之前一共讲解了四道经典数组题目,每一道题目都代表一个类型,一种思想。

#二分法

这道题目呢,考察数组的基本操作,思路很简单,但是通过率在简单题里并不高,不要轻敌。

可以使用暴力解法,通过这道题目,如果追求更优的算法,建议试一试用二分法,来解决这道题目

  • 暴力解法时间复杂度:O(n)
  • 二分法时间复杂度:O(logn)

在这道题目中我们讲到了循环不变量原则,只有在循环中坚持对区间的定义,才能清楚的把握循环中的各种细节。

二分法是算法面试中的常考题,建议通过这道题目,锻炼自己手撕二分的能力

个人认为,二分法算比较基础的想法,只要弄清楚边界(也很好想,记不得的时候用数学定义就能很好的理解),基本上实现比较容易。

#双指针法

双指针法(快慢指针法):通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。

  • 暴力解法时间复杂度:O(n^2)
  • 双指针时间复杂度:O(n)

这道题目迷惑了不少同学,纠结于数组中的元素为什么不能删除,主要是因为以下两点:

  • 数组在内存中是连续的地址空间,不能释放单一元素,如果要释放,就是全释放(程序运行结束,回收内存栈空间)。
  • C++中vector和array的区别一定要弄清楚,vector的底层实现是array,封装后使用更友好。

双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。

个人认为,双指针法主要要清楚每一个指针的具体作用是什么,这样就很容易理解每一个步骤要做什么,自己写的时候才能写清楚。

#滑动窗口

本题介绍了数组操作中的另一个重要思想:滑动窗口。

  • 暴力解法时间复杂度:O(n^2)
  • 滑动窗口时间复杂度:O(n)

本题中,主要要理解滑动窗口如何移动 窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合条件的长度。

滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。

如果没有接触过这一类的方法,很难想到类似的解题思路,滑动窗口方法还是很巧妙的。

这部分内容掌握的还不是很好,甚至暴力解法也没完全理解在result的取值方面的问题,准备借助文本资料和代码注释再理解一下,之后复现。

#模拟行为

模拟类的题目在数组中很常见,不涉及到什么算法,就是单纯的模拟,十分考察大家对代码的掌控能力。

在这道题目中,我们再一次介绍到了循环不变量原则,其实这也是写程序中的重要原则。

相信大家有遇到过这种情况: 感觉题目的边界调节超多,一波接着一波的判断,找边界,拆了东墙补西墙,好不容易运行通过了,代码写的十分冗余,毫无章法,其实真正解决题目的代码都是简洁的,或者有原则性的,大家可以在这道题目中体会到这一点。

这个实际上难度并不大,就是搞明白边界,然后把用来调整循环次数、循环长度的全局变量控制好就行了。

总体来说,数组方面入门确实不是很难,但一些新的思想还是很值得多次学习反复思考的。其次,数组的操作也没有很熟练,这一定程度上也影响到了做题的速度和效率,例如在二维数组那里,我还是用array来处理,无法输出。马上要开始链表的题目,而链表我的理解和认识,以及对基本语法的掌握还处于很低的水平,因此今晚还要把链表的知识多看一看,多写一些代码热热身。

这两天也挺高强度的学习和了解算法的这样一些比较有趣的思想,提升认识之余也对后面的学习满怀希望,虽然目前是个残旧的半成品,写出来的代码常常依托沟狮不能运行,但我们说:“没有任何一件事情是一定怎么怎么样的。”在工作闲暇时间好好学吧。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值