代码随想录——数组

目录

一、数组理论基础

二、二分查找

1、题目:704. 二分查找 - 力扣(LeetCode)

2、思路

1)左闭右闭:

2)左闭右开:

3、代码

1)左闭右闭的代码

2)左闭右开的代码

4、复杂度分析

三、移除元素

1、题目:27. 移除元素 - 力扣(LeetCode)

2、思路

1)暴力解法(力扣上也能通过)

2)双指针法(快慢指针法)

3、代码

1)暴力解法

2)双指针法

4、复杂度分析

四、有序数组的平方

1、题目:977. 有序数组的平方 - 力扣(LeetCode)

2、思路

1)暴力排序

2)双指针法

3、代码

1)暴力法

2)双指针法

4、复杂度分析

五、长度最小的子数组

1、题目:209. 长度最小的子数组 - 力扣(LeetCode)

2、思路

1)暴力法

2)滑动窗口

3、代码

1)暴力解法:

2)滑动窗口法

4、复杂度分析

六、螺旋矩阵Ⅱ(面试高频)

1、题目:59. 螺旋矩阵 II - 力扣(LeetCode)

2、思路

3、代码

4、复杂度分析


一、数组理论基础

  • 定义数组是存放在连续内存空间上的相同类型数据的集合。可以方便的通过下标索引的方式获取到下标对应的数据(下标从0开始)。
  • 在删除或增添元素时,需要移动其他元素的地址,以保证内存地址的连续。

  • java中数组的存储方式:
public static void test_arr() {
    int[][] arr = {{1, 2, 3}, {3, 4, 5}, {6, 7, 8}, {9,9,9}};
    System.out.println(arr[0]);
    System.out.println(arr[1]);
    System.out.println(arr[2]);
    System.out.println(arr[3]);
}

输出的地址为:
[I@7852e922
[I@4e25154f
[I@70dea4e
[I@5c647e05

arr这个二维数组包含4个内部数组,每个内部数组有若干元素。打印的是这些内部数组的哈希码。这些内部数组的内存地址是连续的,但它们在外部数组中的引用可以是分散的,因为它们是独立的数组对象。即二维数组的每一行头结点的地址是没有规则的(每一行内部是连续的,但是每一行随机存储)

二、二分查找

视频课:手把手带你撕出正确的二分法 | 二分查找法 | 二分搜索法 | LeetCode:704. 二分查找_哔哩哔哩_bilibili

1、题目:704. 二分查找 - 力扣(LeetCode)

2、思路

用二分法来查找。

难点1:while()里面left是<right还<=?

难点2:更新区间的时候,xx=middle还是middle-1?

二分法有两种思路:左闭右闭即[left, right],或者左闭右开即[left, right)。

1)左闭右闭:

比如想在下面的数组里面查找2,就先定位到数组中间,比完大小后,此时的区间是左边部分。

  • 比如比较后middle>target,已经确定middle不会是目标值了,因为后面是左闭右闭的区间,所以可以直接在对半分的时候,把middle这个值摒除掉,直接把right设为middle-1即可。同样如果middle<targrt的时候,直接把left设为middle+1即可。
  • while里面要left<=right:因为比如到left=2,right=3,实际上还有两个数字都没有比较。执行一轮还是没找到之后,right=2,此时left=right,还有一个数字没有比较,还需要再比一轮。

2)左闭右开:

[ )  所以while里面必须left<right,使得左闭右开有意义,[1,1)是不合法的。

同样道理,更新的时候,因为right是没有包含的。所以在更新左边界的时候,因为左闭,又已经确定了middle不为目标值,所以left=middle+1。在更新右边界的时候因为不会比较right,所以直接right=middle。

  • while里面要left<right:因为比如到left=2,right=3,实际上只有一个数字没有比较了。所以此时就是执行的最后一轮,不需要再执行下一轮了。

3、代码

1)左闭右闭的代码

class Solution {
    public int search(int[] nums, int target) {
        // 避免当 target 小于nums[0] nums[nums.length - 1]时多次循环运算
        if (target < nums[0] || target > nums[nums.length - 1]) {
            return -1;
        }
        int left = 0, right = nums.length - 1;  //计算出区间左右的下标
        while (left <= right) {
            int mid = left + ((right - left) >> 1); // 计算中间下标(要不就向下取整)
            // 然后开始比大小环节
            if (nums[mid] == target) {
                return mid;
            }
            else if (nums[mid] < target) {
                left = mid + 1;  // 即修改左区间的下标,把比较区间放在右边
            }
            else { // nums[mid] > target
                right = mid - 1; // 即修改右区间的下标,把比较区间放在左边
            }
        }
        // 未找到目标值
        return -1;
    }
}

2)左闭右开的代码

class Solution {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length;  //相当于区间右边的下标是无效的
        // 这样可以防止二分到最后
        while (left < right) {
            int mid = left + ((right - left) >> 1);  // 同样找到中间下标
            if (nums[mid] == target) {
                return mid;
            }
            else if (nums[mid] < target) {
                left = mid + 1;
            }
            else { // nums[mid] > target
                right = mid;  // 这里不一样,相当于最右侧的下标是开区间
            }
        }
        // 未找到目标值
        return -1;
    }
}

4、复杂度分析

两种方法的复杂度是一样的。(暴力法的时间复杂度为O(n))

  • 时间复杂度:O(log n)       因为每次都是对半分
  • 空间复杂度:O(1)        不需要额外存储什么

三、移除元素

视频课:数组中移除元素并不容易! | LeetCode:27. 移除元素_哔哩哔哩_bilibili

1、题目:27. 移除元素 - 力扣(LeetCode)

2、思路

因为数组的元素在内存地址中的是连续的,所以不能单独删除元素,只能覆盖。(其实就是实现erase这个库函数的功能,其实可以直接调用)

1)暴力解法(力扣上也能通过)

  • 两层循环:从左到右遍历元素,把每一个不等于目标值的数都赋值给数组的从0开始的每一个顺序位置。(即如果找到了一个target,后面所有的元素就往前覆盖(这个元素就先不管了)

2)双指针法(快慢指针法)

通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。(在一个循环里面就可以做,所以复杂度是O(n))

  • 快指针:寻找新数组里所需要的元素(即不等于目标值的元素)。
  • 慢指针:指向更新新数组下标的位置。(新数组的下标值)

就是在一个数组上进行操作的。(其实和暴力法差不太多)

3、代码

1)暴力解法

class Solution {
    public int removeElement(int[] nums, int val) {
        int slowIndex = 0;
        // for里面从下标为0开始遍历
        for (int fastIndex = 0; fastIndex < nums.length; fastIndex++) {
            if (nums[fastIndex] != val) {
             // 如果不等于目标值,就将该值赋予给数字更新下标对应的位置
                nums[slowIndex] = nums[fastIndex]; 
                slowIndex++;  // 即数字更新下标初始为0,然后每次右移一个
            }
        }
        return slowIndex;  //最后这个更新下标值为多少,就是有多少个不为目标值的数
    }
}

2)双指针法

class Solution {
    public int removeElement(int[] nums, int val) {
        int slow = 0;
        int len = nums.length;
        // 快慢指针都是从0开始
        for (int fast = 0; fast < len; fast++)  // 表示快指针向右移动
            if (nums[fast] != val)  // 如果数不是目标值
                nums[slow++] = nums[fast];  // 就赋予给慢指针所在的位置
               // 注意这里慢指针下标会在复制之后之后增加1。
               // 当然也可以把在下面额外写slow=slow+1;
        return slow;
    }
}

4、复杂度分析

  • 暴力解法的时间复杂度是O(n^2),空间复杂度是O(1)
  • 双指针法的时间复杂度是O(n),空间复杂度是O(1)

四、有序数组的平方

视频课:双指针法经典题目 | LeetCode:977.有序数组的平方_哔哩哔哩_bilibili

1、题目:977. 有序数组的平方 - 力扣(LeetCode)

2、思路

就是要先计算每个数的平方和,再递增排序。

1)暴力排序

简单的每个数平方之后,再用快排排个序。

2)双指针法

因为数组本身其实是递增的,只不过平方后,可能负数的平方和会更大。

  • 但是,该数组平方和的最大值一定在数组两端 (一定是最左边或者最右边,不会是中间)
  • 所以可以只用反复比较数组最左端和最右端的两个数就好,不断从两边往中间比。
  • 就可以有两个指针,一个指向最左边,一个指向最右边。然后左指针右移,右指针左移
  • 再有一个新的数组,来存放每次两端比较后更大的数(从右往左放)。

3、代码

1)暴力法

class Solution {
    public int[] sortedSquares(int[] nums) {
        int[] result = new int[nums.length];
        for (int i = 0; i < nums.length; i++){
            result[i]=nums[i]*nums[i];
        }
        Arrays.sort(result); //用Arrays.sort方法对result数组进行排序
        return result;
    }
}

2)双指针法

class Solution {
    public int[] sortedSquares(int[] nums) {
        int l = 0; //最左边下标
        int r = nums.length - 1;  //最右边下标
        int[] res = new int[nums.length];  //将返回的数组result[],指定长度
        int j = nums.length - 1;
        while(l <= r){  //要小于等于,不然最后一个元素就掉了
            if(nums[l] * nums[l] > nums[r] * nums[r]){
                res[j--] = nums[l] * nums[l++];  
            //但凡左边界的数大于右边界的数,就把左边界数的平方和放到新数组的最右边,
            //然后把新数组的下标左移一个。(原始的左边界指针下标右移)
            }else{
                res[j--] = nums[r] * nums[r--];
            }
            //同样的如果有边界的平方和比左边界更大,就把有边界的平方和给新数组最右侧
            //然后把新数组的下标左移一个。(原始的右边界指针下标左移)
        }
        return res;
    }
}

4、复杂度分析

  • 暴力法:时间复杂度是 O(n + nlogn) 
    • 1、平方操作:O(n),对每个元素进行平方。
    • 2、排序操作:Arrays.sort(result)的时间复杂度为 O(n log n)。
  • 双指针法:时间复杂度为O(n)
    • while 循环从数组的两端开始,向中间遍历,直到 lr 相遇。这个过程只需要遍历数组一次,因此是线性时间的。
    • 在每次迭代中,算法计算 nums[l]nums[r] 的平方,并将结果放入 res 数组的相应位置。比较操作是常数时间的,而赋值操作也是常数时间的

五、长度最小的子数组

视频课:拿下滑动窗口! | LeetCode 209 长度最小的子数组_哔哩哔哩_bilibili

1、题目:209. 长度最小的子数组 - 力扣(LeetCode)

2、思路

1)暴力法

两个for循环,不断寻找符合条件的子序列。(把所有子序列的情况都列出来了)

2)滑动窗口

  • 滑动窗口:就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果
    • 相当于暴力法中一个for循环是滑动窗口的起始位置,一个for循环是滑动窗口的终止位置
    • 滑动窗口用1个for循环就可以解决。
    • 如下图其实也类似于双指针法:不过更像滑动窗口。
      • 一个指针表示子序列的终止位置,从左向右移动:
        • 窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。(相当于从左到右一个个移动)
        • 窗口的起始位置如何移动:如果当前窗口的值大于等于s了,窗口就要向前移动了(也就是该缩小了)。(相当于从左不断逼近结束位置,缩小子序列长度,直到和小于s了,结束位置就要前移了)比如下图s=7
        • 相当于最左边先囊括一个满足条件的序列,再从左压缩序列长度。直到不满足条件后,再往右边扩序列,直到满足条件,再压缩左边界。
  • 滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。

3、代码

1)暴力解法:

class Solution {
    public int minSubArrayLen(int s, int[] nums) {
        int sum = 0;
        int result = Integer.MAX_VALUE;  //初始赋予一个最大值
        int sublength = 0; // // 子序列的长度
        for (int i = 0; i < nums.length; i++) {  //设置子序列起点为i
            sum = 0;
            for (int j=i; j<nums.length; j++){  //设置子序列终止位置为j
               sum += nums[j];
               if(sum>=s){  // 一旦发现子序列和超过了s,更新result
                  sublength = j-i+1;  // 取子序列的长度
                  result = (result < sublength ? result : sublength);
                  break;
               }
            }
        }
        return result == Integer.MAX_VALUE ? 0 : result;
    }
}

2)滑动窗口法

class Solution {

    // 滑动窗口
    public int minSubArrayLen(int s, int[] nums) {
        int left = 0; //子序列七十位置指针
        int sum = 0;
        int result = Integer.MAX_VALUE;  //初始赋予一个最大值
        for (int right = 0; right < nums.length; right++) {  //子序列终止位置指针从左向右移
            sum += nums[right];
            while (sum >= s) { //但凡目前的子序列和>s,就右移起始位置指针
                result = Math.min(result, right - left + 1); // 比较计算出最小的序列长度
                sum -= nums[left++];  //右移起始指针
            }
        }
        return result == Integer.MAX_VALUE ? 0 : result;
    }
}

4、复杂度分析

  • 暴力法:时间复杂度很明显是O(n^2)。空间复杂度:O(1)
  • 滑动窗口:时间复杂度:O(2n)=O(n),空间复杂度:O(1)

六、螺旋矩阵Ⅱ(面试高频)

视频课:一入循环深似海 | LeetCode:59.螺旋矩阵II_哔哩哔哩_bilibili

1、题目:59. 螺旋矩阵 II - 力扣(LeetCode)

2、思路

给定n,生成的矩阵就是n*n的,数字就是从1~n^2螺旋排列的。重点就是要怎么去填充这个矩阵。如下图,顺时针去填充这个矩阵:

  • 关键点其实就是边界上的几个点。所以写边界条件的时候会比较麻烦。这里选择秉持“左闭右开”这样一个固定的原则,去填充每一条边。(每一圈顺序填充4条边)
  1. 首先是转几圈的问题:转n/2圈,如果n是偶数,几整圈就可以全部填充,如果是奇数,那么到最后最中心的位置就要自己再额外处理一下。
  2. 然后是起始位置:因为每一圈的起始位置都不是固定的(不能每一圈一开始就i=0),所以要定义起始位置startX(从左到右的起始)、startY(从上到下的起始),都从0开始。
  3. 然后是终止位置:因为要左闭右开,所以不能包含终止位置(每一圈都会改变),第一圈是n-1,第二圈是n-2,所以还需要一个变量offset
  4. 写代码的时候,4条边怎么填充,都要具体细心的分析(其实不难,画图慢慢分析)

3、代码

class Solution {
    public int[][] generateMatrix(int n) {
        int[][] nums = new int[n][n]; //矩阵是n^2的大小
        int startX = 0, startY = 0;  // 每一圈的起始点
        int offset = 1;
        int count = 1;  // 矩阵中需要填写的数字(从1开始)
        int loop = 1; // 记录当前的圈数
        int i, j; // j 代表列, i 代表行;

        while (loop <= n / 2) {  // 比如n=3,循环1圈就行
        //每一圈,都按照顶部、右列、底部、左列的顺序去填充
            
            // 顶部
            // 左闭右开,所以判断循环结束时, j 不能等于 n - offset
            for (j = startY; j < n - offset; j++) { //j=0、1
                nums[startX][j] = count++; // 填充矩阵的顶部
            }                // 需要填写的数字count要一直自增
            // 右列
            // 左闭右开,所以判断循环结束时, i 不能等于 n - offset
            for (i = startX; i < n - offset; i++) {
                nums[i][j] = count++;  // 固定列j不变,增加i来向下填充
            }
            // 底部
            // 左闭右开,所以判断循环结束时, j != startY
            for (; j > startY; j--) {  //不需要对j进行初始化了
                nums[i][j] = count++;  // 向左填充,所以列j--
            }
            // 左列
            // 左闭右开,所以判断循环结束时, i != startX
            for (; i > startX; i--) {
                nums[i][j] = count++;
            }
            // 一圈结束后,如果要进行下一圈,起始行要向右移1个,起始列也要向下移1个
            startX++;  
            startY++;
            offset++;  // 下一圈左闭右开时边界值也要距离矩阵边界远1格
            loop++; // 记录圈数,是while里的判断
        }
        // n为奇数的时候,单独处理中心位置 (循环到最后[startX][startY]就是中心位置)
        if (n % 2 == 1) { 
            nums[startX][startY] = count;
        }
        return nums;
    }
}

4、复杂度分析

  • 时间复杂度 O(n^2): 模拟遍历二维矩阵的时间(要填充多少个数)
  • 空间复杂度 O(1)

写在最后的话:java里面什么时候用for什么时候用while?

  • while():括号里面跟条件判断,比如left<right
  • for():括号里面用来递增一个值,比如for(i=0;1<10;1++)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值