跳转至

数组

参考:代码随想录 数组内容

1. 数组介绍

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

  • 数组下标都是从0开始
  • 数组内存空间的地址都是连续的

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

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

即数组的元素不能删除,只能覆盖

2. 二分查找

使用二分查找的前提: - 数组为有序数组 - 数组中无重复元素(有重复元素的话,寻找到的满足题目要求的元素可能不唯一)

二分查找的边界问题:

遵循循环不变量的原则:在二分查找的过程中,保持不变量,即 在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是循环不变量规则

二分查找中的区间定义有两种方式:

  • 左闭 右闭:[left, right] (说明右边的元素能够被选中,因此应该是 left <= right )
  • 左闭 右开:[left, right) (说明右边的元素无法被选中, 因此应该是 left < right)

2.1 二分查找

link : LeetCode 704

方法一:暴力

方法二:左闭右闭

图示:在数组:1,2,3,4,7,9,10中查找元素2,

class Solution {

    public int search(int[] nums, int target) {

        int left = 0, right = nums.length-1;

        // 采用左闭右闭的写法

        // 即 需要满足题目的条件在一个左闭右闭的区间中

        while(left <= right){

            int mid = (left + right) / 2;



            if (nums[mid] < target) left = mid + 1;

            else if (nums[mid] > target) right = mid - 1;

            else return mid;    
        }
        return -1;
    }

}

方法三:左闭右开

图示:

class Solution {

    public int search(int[] nums, int target) {

        int left = 0, right = nums.length-1;

        // 采用左闭右开的写法

        // 即 需要满足题目的条件在一个左闭右开的区间中

        // 因为左闭右开, right不能被选中

        while(left < right){

            int mid = (left + right) / 2;



            if (nums[mid] < target) left = mid + 1;

            // 因为 nums[mid] 肯定不满足条件, 但是right不能被选中, 所以 righ = mid

            else if (nums[mid] > target) right = mid;

            else return mid;    

        }



        return -1;

    }

}

3. 移除元素(快慢双指针)

link : LeetCode 27

思考:

  • 数组中的元素的内存地址是连续的,不能直接删除某一个元素, 只能覆盖

暴力:

两层for循环,一个for循环遍历数组元素 ,当遇到目标元素后;第二个for循环更新数组(该元素后面的元素统一向前移动一位)。

双指针:

快慢指针法:通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。需要仔细考虑

  • 快指针:寻找新数组的元素 ,即最终的新数组就是不含有目标元素的数组,所以就是寻找 不等于 val 的元素 (存储的也是下标)
  • 慢指针:指向 新数组 的下标(用来存储快指针指向的非val元素)

图示:

class Solution {

    public int removeElement(int[] nums, int val) {

        int fastIndex = 0, slowIndex = 0;

        // 快指针需要遍历 整个原数组, 从而找到所有不等于val的元素

        while(fastIndex < nums.length){

            if (nums[fastIndex] != val){

                nums[slowIndex] = nums[fastIndex];

                fastIndex++;

                slowIndex++;    

            }else{

                fastIndex++;

            }

        }

        return slowIndex;

    }

}

4. 有序数组的平方 (相向双指针)

link: LeetCode 977

暴力:

双指针(相向双指针):

数组一开始是有序的,但是负数平方之后变成了最大数,所以数组平方后的最大值要么在原数组的最左边,要么在原数组的最右边,从而采用相向指针

class Solution {

    public int[] sortedSquares(int[] nums) {

        int n = nums.length;

        int res[] = new int[n];

        int size = n-1;

        // 使用双指针, 相向指针

        // left 指针从左边开始, right指针从右边开始

        int left = 0, right = n-1;

        while(left <= right){

            if (nums[left]*nums[left] > nums[right] * nums[right]) {

                res[size] = nums[left] * nums[left];

                left++;

                size--;

            }else{

                res[size] = nums[right] * nums[right];

                right--;

                size--;

            }

        }



        return res;



    }

}

关于循环中是 \(<=\) 还是 \(<\) : - 可以先模拟一遍,发现最后当 left=right的时候,正好是中间的值,需要进行处理; - 也可以直接运行,然后调试

5. 长度最小的子数组(滑动窗口)

link: LeetCode 59

暴力:

滑动窗口:

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

在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。

滑动窗口如何用一个for循环来完成这个操作?

  1. 首先要思考 如果用一个for循环,那么应该表示 滑动窗口的起始位置,还是终止位置。

如果只用一个for循环来表示 滑动窗口的起始位置,那么如何遍历剩下的终止位置? 此时难免再次陷入 暴力解法的怪圈。

所以 只用一个for循环,那么这个循环的索引,一定是表示 滑动窗口的终止位置。 那么滑动窗口的起始位置如何移动呢?

图示:s=7, 数组是 2,3,1,2,4,3,

实现滑动窗口的要素:

  • 窗口内是什么?
  • 如何移动窗口的起始位置?
  • 如何移动窗口的结束位置?

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

窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。(只要窗口内 满足题目条件, 就需要一直移动起始位置, 即一直缩小窗口, 直到窗口内不满足条件为止, 从而得到最小的窗口)

窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。

class Solution {

    public int minSubArrayLen(int target, int[] nums) {

       // 因为有一个子区间满足题目条件

       // 所以使用滑动窗口

       int left=0, right=0;

       // 定义窗口内的数值和

       int sum = 0;

       // 定义窗口的长度, 因为要求最小长度, 定义一个超大值

       int len = 100010;



       // 滑动窗口需要经过整个数组, 即窗口右边到数组最右端终止

       for (;right<nums.length; right++){

           // 首先窗口是第一个数组

           sum += nums[right];

           // 当窗口满足条件的时候, 持续缩小窗口, 直到不满足条件, 从而找到一个最小的子区间

           while(sum >= target){

               // 取最小值

               len = len< right-left+1?len: right-left+1;

               sum -= nums[left];

               left++;
           }
       }
       return len < 100010?len:0;

    }

}

6. 螺旋矩阵

link: LeetCode 59

思考:

在进行矩阵模拟的时候, 也需要坚持循环不变量的原则

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

  • 填充上行从左到右
  • 填充右列从上到下
  • 填充下行从右到左
  • 填充左列从下到上

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

这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开右闭的原则,这样这一圈才能按照统一的规则画下来。(即在循环中 坚持循环不变量

按照左闭右开的原则,图示:

同时,进行模拟矩阵,需要首先确定的要素:

  1. 每一圈的起始位置的 x 和 y
  2. 能够循环多少圈
  3. 定义 offset 控制每一条边的遍历长度,每循环一次右边界收缩一位

  4. 矩阵中心位置(非必须)

class Solution {

    public int[][] generateMatrix(int n) {

        // 起始位置

        int startx = 0, starty = 0;

        // 圈数

        int loop= n /2;

        // 长度控制

        int offset = 1; // 左开右闭



        // 中心位置

        int mid = n/2;



        int res[][] = new int[n][n];



        int count = 1;



        while(loop-- > 0){

            int i=startx, j=starty;

            // 从左向右

            for (;j < n-offset; j++)

                res[i][j] = count++;

            // 从上到下

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

                res[i][j] = count++;



            // 从右向左

            for (; j > starty; j--)

                res[i][j] = count++;



            for (; i > startx; i--)

                res[i][j] = count++;




            startx++;

            starty++;



            offset++;





        }



        // 如果有正中心

        if ( n % 2 != 0) res[startx][starty] = count;



        return res;

    }

}