代码随想录训练营第2天|LeetCode 977.有序数组的平方、LeetCode 209.长度最小的子数组、LeetCode 59.螺旋矩阵II

参考

代码随想录


题目一:LeetCode 977.有序数组的平方

一、按照自己的思路求解

如果不用已知的非递减信息,最简单的实现方法是遍历整个数组求平方,然后用快排进行排序,具体没有尝试过,不知道知否会超时,但能想到的还有更优的解法。

注意题目中的信息,给出的数组是非递减的,求平方之后的数组要求也是非递减的。数组中的数据有正有负,分种情况讨论:

  • 第一种:数组中的元素全为非负数。这种情况下,从前往后遍历数组平方之后得到的数据仍然是非递减的,因此这种情况是最简单的。
  • 第二种:数组中的元素全为非正数。这种情况下,从后往前遍历数组求平方得到的数据是非递减的,也很简单。
  • 第三种:数组中的元素有正有负。这种情况下,平方后最小的数对应原来数组中绝对值最小的数,这个数在数组的中间位置(不一定是正中间)。因此可以用双指针法来求解,首先找到绝对值最小的数,然后向两边扩散。

上面的第三种情况更具有普遍性,第一种和第二种可以看作是第三种的特殊情况。在代码实现上把第一种和第二种归入到第三种中进行处理。具体的实现步骤如下:
(1)遍历数组,寻找绝对值最小的数(假设绝对值最小的数为abs_min,该数在数组中的下标为index);
(2)令left = index,right = index + 1(或者left = index - 1, right = index)。在具体实现的时候要注意边界的处理,比如index指向最左边或最右边的情况。
(3)计算。取left指针还是right指针指向的数取决于哪一个指向的数绝对值更小,当left指针指向最左边和right指针指向非法区域的时候停止循环(非法区域是指数组越界)。

代码实现如下:

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        vector<int> res;
        int tmp;
        /* 查找绝对值最小的数的位置 */
        size_t size = nums.size();
        int abs_min = abs(nums[0]); //这里必须初始化为第一个数的绝对值
        int index = 0;
        for(int i=1;i<size;i++)
            if(abs(nums[i]) < abs_min)  
            {
                abs_min = abs(nums[i]);
                index = i;
            }
        /* 初始化指针 */
        int left = index;
        int right = index + 1;
        /* 从index往两边遍历 */
        while(left >= 0 && right <= size - 1)  // 有一个指针越界就退出循环
        {
            if(abs(nums[left]) < abs(nums[right])) //也可以是 <= 
            {
                tmp = nums[left] * nums[left];
                --left;
            }
            else
            {
                tmp = nums[right] * nums[right];
                ++right;
            }
            res.push_back(tmp);
        }
        /* left */
        while(left >= 0)
        {
            tmp = nums[left] * nums[left];
            --left;
            res.push_back(tmp);
        }
        /* right */
        while(right <= size - 1)
        {
            tmp = nums[right] * nums[right];
            ++right;
            res.push_back(tmp);
        }
        return res;
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)
    虽然这个是个简单题,但是能够按照自己的想法写出来还是挺开心的。上面的代码写的时候在指针初始化和循环条件上出问题,最后的解决方法是初始化时候不考虑边界条件(及不判断是否越界),因为如果在初始化的时候作越界判断的话在某些情况下会导致重复计算。上面的代码中有三个while循环,第一个while循环中,当left和right中任意一个越界之后就会退出循环,第二个和第三个计算左边或者右边剩余的数据,且这两个循环只能执行一个。

二、参考代码随想录的求解方法

代码随想录中提供了两种方法,暴力求解和双指针法,其实两种方法自己都已经想到了,只是双指针法的实现在代码上可以进一步简化。
代码随想录求解方法

(一)暴力求解

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        int size = nums.size();
        for(int i=0;i<size;i++)
            nums[i] *= nums[i];
        sort(nums.begin(),nums.end());
        return nums;
    }
};
  • 时间复杂度:O(nlogn)
  • 空间复杂度:O(n)

(二)双指针法

自己想到的方法要先寻找绝对值最小的数,然后向两边扩散,但其实可以从两边向中间遍历,只不过得到的是从大到小的数据序列,从结果数组的最后向前开始存储就可以了。代码实现如下:

class Solution {
public:
    vector<int> sortedSquares(vector<int>& nums) {
        int size = nums.size();
        vector<int> res(size,0);
        int tmp;
        /* 初始化指针 */
        int left = 0;
        int right = size - 1;
        int index = size - 1; //指向存储位置
        while(left <= right) //当 left > right的时候退出循环
        {
            if(abs(nums[left]) > abs(nums[right]))
            {
                tmp = nums[left] * nums[left];
                left++;
            }    
            else
            {
                tmp = nums[right] * nums[right];
                right--;
            }
            res[index] = tmp;
            index--;
        }   
        return res;
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(n)

小结

注意边界的处理,循环条件的设定与条件变量的初始化以及循环体内的变量的更新位置与数据处理的先后位置有关,怎么理解第二个点呢?简单说就是使得每个条件变量对应的数据都要被处理一次。例如上面的双指针法中,left = 0和 right = size - 1,这没有什么问题,关键是while循环条件中包不包含等号的问题,在这个问题中循环体内先处理数据(即求平方),再改变条件变量的值,因此应该包含等号,当left == right的时候还剩最后一个数据,需要再执行一次循环体。


题目二:LeetCode 209.长度最小的子数组

一、按照自己的想法求解

这个题之前已经做过了,还有些印象,第一感觉是双指针,但是今天的作业提醒中已经明确指出应该用滑动窗口,于是就有个疑问:滑动窗口和双指针有什么区别?搜了一下,这里搬运过来:
滑动窗口与双指针的区别

双指针

  • 计算过程仅与两端点相关的称为双指针。
  • 不固定大小。
  • 双指针是解决问题的一种方法。
  • 双指针可以同向移动可以双向移动。
  • 同向移动的双指针和滑动窗口没有任何联系

滑动窗口

  • 计算过程与两端点表示的区间相关的称为滑动窗口。
  • 默认固定大小的窗口,在一些条件触发的情况下,可能会将其大小进行修改。
  • 滑动窗口本身并不是解决问题的一种方法(或者说算法),它其实就是问题本身。
  • 滑动窗口一定是同向移动的。
  • 滑动窗口是一类问题,不同的问题需要使用不同的算法和数据结构来解决。

简单理解,双指针只是两个指针,而滑动窗口是两个指针组成的一个窗口。

方法一:滑动窗口

同样需要两个指针,当窗口内的和sun < target时,移动右指针right,直到满足sum >= target,然后移动左指针left来缩小窗口,直到不满足条件,注意每种满足条件的情况都要和最小窗口长度作比较,即满足条件时要更新窗口的最小长度。当right移动到最右边且不满足sum >= target时结束移动。代码如下:

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int left = 0, right = 0;  //left指向窗口左端点,right指向窗口右端点的下一个点
        int size = nums.size();
        int sum = 0,minLen = INT_MAX;
        //当right == size - 1 且 sun < target的时候退出循环
        while(right < size) 
        {
            //right
            while(sum < target && right < size) sum += nums[right++];
            //更新窗口长度
            if(sum >= target)
                minLen = minLen > (right - left) ? (right - left) : minLen;
            //left
            while(sum >= target && left < size) 
            {
                sum -= nums[left++];
                //更新窗口长度
                if(sum >= target)
                    minLen = minLen > (right - left) ? (right - left) : minLen;
            }
        }
        return minLen == INT_MAX ? 0 : minLen;
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)
    出错点:
    (1)更新窗口的最小长度minLen时要判断是否满足sum >= target条件,因为从while循环退出可能是因为指针已经到了最右边;
    (2)minLen不能初始化为nums的大小,因为可能整个数组的和也无法满足sum >= target,此时应该返回0,但是如果初始化为size就会返回size,所以这里应该初始化为一个比size大的数(比如INT_MAX),最后返回的时候需要判断minLen是否被更新,如果没有说明整个数组的和也小于target,所以应该返回0。

方法二:暴力求解

两层循环,外层循环变量i作为左端点,内层循环变量j作为右端点。代码实现如下:

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int size = nums.size();
        int sum;
        int minLen = INT_MAX;
        for(int i=0;i<size;i++)
        {
            sum = 0;
            for(int j=i;j<size;j++)
            {
                sum += nums[j];
                if(sum >= target)
                    minLen = minLen > (j-i+1) ? (j - i + 1) : minLen;
            }
        }
        return minLen == INT_MAX ? 0 : minLen;
    }
};
  • 时间复杂度:O(n^2)
  • 空间复杂度:O(1)
    暴力求解也可以求解,但是在LeetCode上提交超时了。。

二、参照代码随想录的求解

代码随想录实现
代码随想录中提供了两种方法,暴力求解和滑动窗口,其实这两种方法都已经实现,但是滑动窗口求解的代码还可以简化,在看了代码随想录给出的代码之后,自己再重新实现一遍。

class Solution {
public:
    int minSubArrayLen(int target, vector<int>& nums) {
        int minLen = INT_MAX;
        int sum = 0,i = 0, j = 0;
        int size = nums.size();
        for(j = 0; j < size; j++)
        {
            sum += nums[j];
            while(sum >= target)
            {
                int len = j - i + 1;
                minLen = len < minLen ? len : minLen;
                sum -= nums[i++];
            }
        }
        return minLen == INT_MAX ? 0 : minLen;
    }
};
  • 时间复杂度:O(n)
  • 空间复杂度:O(1)

相比于自己写的代码,这种思路更清晰,更简洁。

小结

滑动窗口要注意窗口的左右端点以及窗口大小的确定,以及什么情况下改变左端点,什么情况下改变右端点。


题目三:LeetCode 59.螺旋矩阵II

这个题之前没有做过,看了题之后没有什么思路,想着用代码模拟旋转过程,但是不知道怎么实现。直接抄一遍代码随想录给出的代码:

class Solution {
public:
    vector<vector<int>> generateMatrix(int n) {
        vector<vector<int>> res(n,vector<int>(n,0));
        int startx = 0, starty = 0; //定义每循环一圈的起始位置
        int loop = n / 2; //循环多少圈
        int mid = n / 2; //矩阵中间位置
        int count = 1; //用来给矩阵的每一个空格赋值
        int offset = 1; //每一圈都要控制每一条边遍历的长度
        int i,j;
        while(loop--)
        {
            i = startx;
            j = starty;
            //下面开始的4个for循环就是模拟转了一圈
            for(j = starty; j < starty + n - offset; j++)
                res[startx][j] = count++;
            for(i = startx; i < startx + n - offset; i++)
                res[i][j] = count++;
            for(;j > starty; j--)
                res[i][j] = count++;
            for(;i > startx; i--)
                res[i][j] = count++;
            //第二圈开始的时候,起始位置各自要加1
            startx++;
            starty++;

            offset += 2;
        }
        //如果n为奇数,则需要单独给矩阵最中间位置赋值
            if(n % 2)
                res[mid][mid] = count;
        return res;
    }
};

以一圈作为一个大循环,一行或一列作为一个小循环,每个大循环的起点为从(0,0)开始,往右下角偏移。

今日小结

前面两个题之前做过,现在还能做出来,但是第三题没做过就不会了,还得继续努力啊。

  • 循环条件的确定很重要,要结合初始化和数据处理位置与变量更新位置的先后来确定
  • 滑动窗口:要清楚什么时候移动左端点,什么时候移动右端点

你可能感兴趣的:(代码随想录训练营,leetcode,算法)