力扣做题笔记(数组篇)

目录

一、数组

题目35.搜索插入位置

总结:

题目27.移除元素

1.暴力解法

代码如下 

2.双指针法

 代码如下

总结:

题目15.三数之和

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

1.暴力解法

2.滑动窗口解法

 C++滑动窗口代码

题目59.螺旋矩阵II

 C++代码

 数组总结:

一、数组理论基础

数组的经典题目

二分法

双指针法

滑动窗口

模拟行为


一、数组

题目35.搜索插入位置

1.二分查找第一种写法

class Solution {
public:
    int searchInsert(vector& nums, int target) {
        int n = nums.size();    //step1,获得数组长度
        int left = 0;
        int right = n - 1;        //定义target在左闭右闭的区间里,[left,right]
        while(left <= right){       //因为我们定义的左闭右闭的区间,所以left = right时仍然有效
            int middle = (left + right)/2;    //确定middle的位置,基本语句1
            if(nums[middle] > target){
                right = middle - 1;         //target在左区间
            }else if(nums[middle] < target){
                left = middle + 1;           
            }else{
                return middle;
            }
           
        }       
         //处理四种情况
         //target在所有元素之前
         //target等于某个元素 return middle
         //target插入数组中  return right + 1
         //target在数组所有元素之后 return right + 1
        return right + 1;
    }
};

时间复杂度: 设基本语句1执行次数为x1,则有2的x1次方等于数组长度n,所以x1=log以2为底n的对数,所以时间复杂度为O(logn)

空间复杂度: O(1):?

2.二分查找第二种写法

class Solution {
public:
    int searchInsert(vector& nums, int target) {
        int n = nums.size();
        int left = 0;
        int right = n; // 定义target在左闭右开的区间里,[left, right)  target
        while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间
            int middle = left + ((right - left) >> 1); //'>> 1'相当于 / 2
            if (nums[middle] > target) {
                right = middle; // target 在左区间,在[left, middle)中
            } else if (nums[middle] < target) {
                left = middle + 1; // target 在右区间,在 [middle+1, right)中
            } else { // nums[middle] == target
                return middle; // 数组中找到目标值的情况,直接返回下标
            }
        }
        // 分别处理如下四种情况
        // 目标值在数组所有元素之前 [0,0)
        // 目标值等于数组中某一个元素 return middle
        // 目标值插入数组中的位置 [left, right) ,return right 即可
        // 目标值在数组所有元素之后的情况 [left, right),return right 即可
        return right;
    }
};

时间复杂度空间复杂度同上一种方法

总结:

        在使用二分查找法时,关键是要确定区间的定义,确定要查找的区间是双闭还是一闭一开,这是变化过程中的不变量,然后在【二分查找的循环中,坚持循环不变量的原则】,其次是对问题的刨解,问题有几种可能,分别怎么解决等等

题目27.移除元素

1.暴力解法

这个题目暴力的解法就是两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组。

删除过程如下:

代码如下 

class Solution {
public:
    int removeElement(vector& nums, int val) {
        int size = nums.size();
        for(int i = 0;i < size;i++){    
            if(nums[i] == val){                     //发现需要移除的元素,就将i之后的元素集体移动
                for(int j = i + 1;j < size;j++){
                    nums[j-1] = nums[j];
                }
                i--;        //因为下表为i以后的数值都向前移动了一位,所以i也应该向前移动一位,再进行外层for循环的时候,才能比较现在i位置的数值和val的关系
                size--;
            }
        }
        return size;
    }
};

时间复杂度:O(n^2) 

2.双指针法

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

 代码如下

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

时间复杂度:O(n) 

总结:

双指针法在数组和链表的操作中非常常见,它的精髓在于将两个for循环幻化为一个for循环,利用一个快指针和一个慢指针,省去了内层for循环的替换赋值操作

题目15.三数之和

大体思想如下:

力扣做题笔记(数组篇)_第1张图片

拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下表0的地方开始,同时定一个下表left 定义在i+1的位置上,定义下表right 在数组结尾的位置上。

依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于  a = nums[i] b = nums[left]  c = nums[right]。

接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0  就说明 此时三数之和大了,因为数组是排序后了,所以right下表就应该向左移动,这样才能让三数之和小一些。

如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。

时间复杂度:O(n^2)。

 双指针法C++代码:

class Solution {
public:
    vector> threeSum(vector& nums) {
        vector> result;
        sort(nums.begin(), nums.end());
        // 找出a + b + c = 0
        // a = nums[i], b = nums[left], c = nums[right]
        for (int i = 0; i < nums.size(); i++) {
            // 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了
            if (nums[i] > 0) {
                return result;
            }
            // 错误去重方法,将会漏掉-1,-1,2 这种情况
            /*
            if (nums[i] == nums[i + 1]) {
                continue;
            }
            */
            // 正确去重方法
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            int left = i + 1;
            int right = nums.size() - 1;
            while (right > left) {
                // 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组
                /*
                while (right > left && nums[right] == nums[right - 1]) right--;
                while (right > left && nums[left] == nums[left + 1]) left++;
                */
                if (nums[i] + nums[left] + nums[right] > 0) {
                    right--;
                } else if (nums[i] + nums[left] + nums[right] < 0) {
                    left++;
                } else {
                    result.push_back(vector{nums[i], nums[left], nums[right]});
                    // 去重逻辑应该放在找到一个三元组之后
                    while (right > left && nums[right] == nums[right - 1]) right--;
                    while (right > left && nums[left] == nums[left + 1]) left++;

                    // 找到答案时,双指针同时收缩
                    right--;
                    left++;
                }
            }

        }
        return result;
    }
};

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

1.暴力解法

class Solution {
public:
    int minSubArrayLen(int target, vector& nums) {
        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 >= target){                  //一旦sum 超过或等于target,更新result
                    subLength = j - i + 1;
                    result = result < subLength ? result : subLength;
                    break;                      //因为找的是最短的子序列,所以一旦找到就退出内层循环,此时的最短,是以i为起点的最短
                }
            }
        }
        //如果result没有被赋值,则返回0,意思是没有满足条件的子序列
        return result == INT32_MAX ? 0 : result;
    }
};

时间复杂度:O(n^2)

空间复杂度:O(1)

2.滑动窗口解法

所谓滑动窗口,「就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果」

这里还是以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程:

最后找到 4,3 是最短距离。

其实从动画中可以发现滑动窗口也可以理解为双指针法的一种!只不过这种解法更像是一个窗口的移动,所以叫做滑动窗口更适合一些。

在本题中实现滑动窗口,主要确定如下三点:

  • 窗口内是什么?

  • 如何移动窗口的起始位置?

  • 如何移动窗口的结束位置?

窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。

窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。

窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,窗口的起始位置设置为数组的起始位置就可以了。

解题的关键在于 窗口的起始位置如何移动,如图所示:

力扣做题笔记(数组篇)_第2张图片

 C++滑动窗口代码

class Solution {
public:
    int minSubArrayLen(int target, vector& nums) {
        int result = INT32_MAX;
        int sum = 0;            //滑动窗口数值之和
        int subLength = 0;      //滑动窗口长度之和
        int i = 0;      //子序列的起点
        for(int j = 0;j < nums.size();j++){     //j是滑动窗口的终端
            sum += nums[j];

            //这里使用while,判断子序列是否符合条件并更新i的值
            while(sum >= target){
                subLength = (j - i + 1);
                result = result < subLength ? result : subLength;
                sum -= nums[i++];   //滑动窗口的精髓,不断变更i
            } 

        }
        //如果result没有被赋值,则返回0,意思是没有符合条件的子序列
        return result == INT32_MAX ? 0 : result;
    }
};

时间复杂度:O(n)

空间复杂度:O(1) 

题目59.螺旋矩阵II

而求解本题依然是要坚持循环不变量原则。

模拟顺时针画矩阵的过程:

  • 填充上行从左到右

  • 填充右列从上到下

  • 填充下行从右到左

  • 填充左列从下到上

由外向内一圈一圈这么画下去。

可以发现这里的边界条件非常多,在一个循环中,如此多的边界条件,如果不按照固定规则来遍历,那就是「一进循环深似海,从此offer是路人」

这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开又闭的原则,这样这一圈才能按照统一的规则画下来。

那么我按照左闭右开的原则,来画一圈,大家看一下:

力扣做题笔记(数组篇)_第3张图片

这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。

这也是坚持了每条边左闭右开的原则。

一些同学做这道题目之所以一直写不好,代码越写越乱。

就是因为在画每一条边的时候,一会左开又闭,一会左闭右闭,一会又来左闭右开,岂能不乱。

 C++代码

class Solution {
public:
    vector> generateMatrix(int n) {
        vector> res(n,vector(n,0));         //使用vector定义一个二维数组
        int startx = 0,starty = 0;  //循环开始的位置
        int loop = n / 2;   //循环的次数,如n等于3,则循环的次数为2
        int count = 1;      //用来给矩阵的每一个格赋值
        int mid = n / 2;    //如果n是奇数,需要单独给中间的值进行赋值
        int offest = 1;     //控制遍历的列表的长度
        int i,j;
        while(loop--){
             i = startx;
             j = starty;
            //以下四个for循环,分别对应了四个方向
            //上行,从左到右
            for(;j < starty + n - offest;j++){
                res[i][j] = count++;
            }
            
            //右列,从上到下
            for(;i < startx + n - offest;i++){
                res[i][j] = count++;
            }

            //下行,从右到左
            for(;j > starty;j--){
                res[i][j] = count++;
            }

            //左列,从下到上
            for(;i > startx;i--){
                res[i][j] = count++;
            }

            //第二圈开始的时候,起始位置各自加一,例如第一圈是(0,0),第二圈就是(1,1)
            startx++;
            starty++;
            //offest控制遍历的长度
            offest += 2;
        }
        //如果n是奇数,则需要单独给矩阵中间位置的值赋值
        if(n % 2){
            res[mid][mid] = count;
        }
        return res;
    }
};

 数组总结:

一、数组理论基础

首先要知道数组在内存中的存储方式,这样才能真正理解数组相关的面试题

「数组是存放在连续内存空间上的相同类型数据的集合。」

数组可以方便的通过下表索引的方式获取到下表下对应的数据。

举一个字符数组的例子,如图所示:

力扣做题笔记(数组篇)_第4张图片

 需要两点注意的是:

  • 「数组下表都是从0开始的。」

  • 「数组内存空间的地址是连续的」

正是「因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。」

例如删除下表为3的元素,需要对下表为3的元素后面的所有元素都要做移动操作,如图所示:

力扣做题笔记(数组篇)_第5张图片

而且大家如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。

「数组的元素是不能删的,只能覆盖。」

那么二维数组直接上图,大家应该就知道怎么回事了

力扣做题笔记(数组篇)_第6张图片

「那么二维数组在内存的空间地址是连续的么?」

我们来举一个例子,例如:int[][] rating = new int[3][4]; , 这个二维数据在内存空间可不是一个 3*4 的连续地址空间

看了下图,就应该明白了:

力扣做题笔记(数组篇)_第7张图片

所以「二维数据在内存中不是 3*4 的连续地址空间,而是四条连续的地址空间组成!」

数组的经典题目

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

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

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

二分法

数组:每次遇到二分法,都是一看就会,一写就废

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

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

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

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

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

双指针法

  • 数组:就移除个元素很难么?

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

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

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

  • 数组在内存中是连续的地址空间,不能释放单一元素,如果要释放,就是全释放(程序运行结束,回收内存栈空间)。

  • C++中vector和array的区别一定要弄清楚,vector的底层实现是array,所以vector展现出友好的一些都是因为经过包装了。

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

滑动窗口

  • 数组:滑动窗口拯救了你

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

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

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

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

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

模拟行为

  • 数组:这个循环可以转懵很多人!

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

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

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

说明:本篇主要来自《代码回忆录》,不用于商业,纯属个人积累

你可能感兴趣的:(leetcode,散列表,算法)