代码随想录-数组篇

158 阅读3分钟

1 二分查找

关于二分法的详细原理,可以参见文章[双指针技巧]第3.1小节。

T704-二分查找

见LeetCode第704题[二分查找]

题目描述

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

示例

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

题解

public int search(int[] nums, int target) {
    if (nums == null || nums.length <= 0) return -1;
    int l = 0;
    int r = nums.length;
    while (l < r) {
        int mid = l + (r - l) / 2;
        if (target > nums[mid]) {
            l = mid + 1;
        } else if (target == nums[mid]) {
            return mid;
        } else {
            r = mid;
        }
    }
    return -1;
}

注意搜索区间是个左闭右开的区间,即[l,r)[l, r),因此r往左边收缩的时候,赋值语句为r = mid

循环结束条件为l == r

T35-搜索插入位置

见LeetCode第35题[搜索插入位置]

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

示例

输入: nums = [1,3,5,6], target = 5
输出: 2

我的思路

  • 二分法的变种,如果没找到,就返回大于target的最小的那个值的索引
  • 需要在原始二分法中,锁定右指针r

实现代码

public int searchInsert(int[] nums, int target) {
    if (nums == null || nums.length == 0) return 0;
    int l = 0;
    int r = nums.length;
    while (l < r) {
        int mid = l + (r - l) / 2;
        if (target == nums[mid]) {
            r = mid; // 锁定右指针
        } else if (target > nums[mid]) {
            l = mid + 1;
        } else if (target < nums[mid]) {
            r = mid;
        }
    }
    return r;
}

T34-排序数组中寻找目标元素的第一个和最后一个位置

见LeetCode第34题[寻找目标元素]

题目描述

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

我的思路

  • 二分法的改进算法,返回目标元素最左侧的坐标,或者是小于目标元素中最大元素的索引l
  • 判断nums[l] == target,如果相等,往后遍历,找到结束位置r
  • 如果不相等,直接返回[-1, -1]

实现代码

/**
 * 你找出给定目标值在数组中的开始位置和结束位置。
 *
 * @param nums
 * @param target
 * @return 如果数组中不存在目标值 target,返回 [-1, -1]
 */
public int[] searchRange(int[] nums, int target) {

    int[] range = {-1, -1};
    if (nums == null || nums.length == 0) return range;

    int l = 0;
    int r = nums.length;
    while (l < r) {
        int mid = l + (r - l) / 2;
        if (nums[mid] > target) {
            r = mid;
        } else if (nums[mid] < target) {
            l = mid + 1;
        } else if (nums[mid] == target) {
            r = mid; // 相等的时候,往左收缩,寻找到左边界
        }
    }

    // 如果存在 target, l 应该在第一次出现的位置,否则应该是小于目标元素最大的元素的索引
    if (l >= nums.length || target != nums[l]) {
        return range;
    }
    range[0] = l;
    while (l < nums.length && nums[l] == target) {
        l++;
    }
    range[1] = --l;
    return range;

}

T69-x的平方根

见LeetCode第69题[x的完全平方根]

题目描述

给你一个非负整数 x ,计算并返回 x算术平方根

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

**注意:**不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5

我的思路

  • [1,x2][1, \frac{x}{2}]开始进行二分查找
  • 判断mid ^ 2 == target
  • 返回最左边界值
public int mySqrt(int x) {
    if (x <= 1) return x;
    int l = 1;
    int r = x / 2 + 1;
    while (l < r) {
        int mid = l + (r - l) / 2;
        long powMid = mid * mid;
        if (mid * mid > x) { // 溢出风险
            r = mid;
        } else if (mid * mid == x) {
            return mid;
        } else if (mid * mid < x) {
            l = mid + 1;
        }
    }
    return --l;
}
  • 这段代码中,对mid平方会有溢出的风险
  • 解决方案:开long
  • 时间复杂度为:O(log(x/2))O(\log(x/2))

解法二:袖珍计算器

[袖珍计算器]是一种利用指数函数exp\exp和对数函数ln\ln代替平方根函数的方法,如下面公式:

x=x12=(elnx)12=e12lnx\sqrt x = x^{\frac{1}{2}} = (e^{\ln x})^{\frac{1}{2}} = e^{\frac{1}{2}\ln x}

由于计算器无法存储浮点数的精确值,指数函数和对数函数的参数和返回值均为浮点数,因此运算的过程会出现误差,在对据结果进行取整的时候,可能会得出错误的结果。

在得到结果的整数部分ans之后,需要判断ansans + 1那个是正确答案。

/**
* 袖珍计算器
* @param x
* @return
*/
public int mySqrtI(int x) {
    if (x <= 1) return x;
    int ans = (int) Math.exp(0.5 * Math.log(x));
    return (long) (ans + 1) * (ans + 1) <= x ? ans + 1 : ans;
}

时间复杂度为:O(1)O(1)

T367-有效的完全平方根

见LeetCode第367题[有效的完全平方根]

题目描述

给你一个正整数 num 。如果 num 是一个完全平方数,则返回 true ,否则返回 false

完全平方数 是一个可以写成某个整数的平方的整数。换句话说,它可以写成某个整数和自身的乘积。

不能使用任何内置的库函数,如 sqrt

示例 1:

输入:num = 16
输出:true
解释:返回 true ,因为 4 * 4 = 164 是一个整数。

我的思路

  • 和上一题的解法同样

2 双指针

T27-移除元素

见LeetCode第27题[移除元素]

题目描述

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素。元素的顺序可能发生改变。然后返回 nums 中与 val 不同的元素的数量。

假设 nums 中不等于 val 的元素数量为 k,要通过此题,您需要执行以下操作:

  • 更改 nums 数组,使 nums 的前 k 个元素包含不等于 val 的元素。nums 的其余元素和 nums 的大小并不重要。
  • 返回 k

我的思路

  • 快慢指针,慢指针指向下一个有效的位置,即有效数组末尾
  • 快指针用于判断元素是否为目标元素
public int removeDuplicates(int[] nums) {
    if (nums == null || nums.length <= 1) {
        return nums == null ? 0 : nums.length;
    }

    int slow = 0;
    int fast = 0;
    while (fast < nums.length) {
        if (nums[fast] != nums[slow]) {
            nums[++slow] = nums[fast];
        } else {
            fast++;
        }
    }
    return ++slow;
}
  • 解题技巧:快慢双指针
  • 时间复杂度:O(N)O(N),快指针过一遍数组。

T26-删除有序数组中的重复项

题目描述

给你一个 非严格递增排列 的数组 nums ,请你** 原地** 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。

考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:

  • 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
  • 返回 k

示例 1:

输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。

我的思路

  • 使用快慢指针技巧
  • 慢指针指向重复元素的第一个位置
  • 快指针元素和慢指针做判断,
    • 如果相同,快指针往后走,
    • 否则,慢指针往前移动,并获得快指针所指元素

时间复杂度为:O(N)O(N),快指针遍历了一遍数组

T283-移动零

题目描述

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

我的思路

  • 快慢指针
  • 慢指针指向数组的有效位置
  • 快指针用来寻找非零元素
  • 快指针结束循环之后,需要将慢指针之后的元素都赋值为0

时间复杂度分析:快指针遍历了一遍数组,慢指针也遍历了一遍数组,因此时间复杂度为O(N)O(N)

T844-比较含退格的字符串

题目描述

给定 st 两个字符串,当它们分别被输入到空白的文本编辑器后,如果两者相等,返回 true# 代表退格字符。

**注意:**如果对空文本输入退格字符,文本继续为空。

示例 1:

输入:s = "ab#c", t = "ad#c"
输出:true
解释:s 和 t 都会变成 "ac"

我的思路

  • 使用两个栈来模拟退格
  • #字符就入栈,否则的话就弹出
  • 最后比较两个栈的元素是否相同

思路二:快慢指针

  • 快指针用来判断是否为有效字符
  • 慢指针用来表示有效的索引范围
  • 然后比较两个原地修改后的字符数组是否相同
/**
 * 使用 StringBuilder 构建操作之后的字符串
 * @param s
 * @param t
 * @return
 */
public boolean backspaceCompareII(String s, String t) {
    return operation(s).equals(operation(t));
}

private String operation(String s) {
    StringBuilder sb = new StringBuilder();
    int p = 0; // 表示有效位数
    for (char c : s.toCharArray()) {
        if (c == '#') {
            if (p > 0) {
                sb.deleteCharAt(--p);
            }
        } else {
            sb.append(c);
            p++;
        }
    }
    return sb.toString();
}

使用StringBuilder要比操作char[]数组更快

时间复杂度:分别遍历了字符串st,因此时间复杂度为O(m+n)O(m + n)级别,m | n分别表示两个字符串的长度

T977-有序数组的平方

题目描述

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

示例 1:

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

我的思路

  • 找到距离0最近的元素坐标
  • 双指针,从中间向两边扩散,,合并两个升序数组, 加入到新的数组
  • 最后对新的数组做平方
/**
 * 有序数组的平方
 * @param nums
 * @return
 */
public int[] sortedSquares(int[] nums) {
    int minAbs = Integer.MAX_VALUE;
    int start = -1;
    for (int i = 0; i < nums.length; i++) {
        if (Math.abs(nums[i]) < minAbs) {
            minAbs = Math.abs(nums[i]);
            start = i;
        }
    }

    int l = start;
    int r = start + 1;
    int[] res = new int[nums.length];
    int count = 0;
    while (l >= 0 && r < nums.length) {
        if (Math.abs(nums[l]) > Math.abs(nums[r])) {
            res[count++] = (int) Math.pow(nums[r++], 2);
        } else {
            res[count++] = (int) Math.pow(nums[l--], 2);
        }
    }
    if (l == -1) {
        while (r < nums.length) {
            res[count++] = (int) Math.pow(nums[r++], 2);
        }
    } else if (r == nums.length) {
        while (l >= 0) {
            res[count++] = (int) Math.pow(nums[l--], 2);
        }
    }
    return res;

}

优化思路

  • 为什么从中间往两端扩散呢?直接左右指针,往中间走
  • 省去了寻找离0最近元素坐标的复杂度
/**
 * 从两端开始,左右指针
 * @param nums
 * @return
 */
public int[] sortedSquaresI(int[] nums) {
    int[] res = new int[nums.length];
    int tail = nums.length - 1;
    int l = 0;
    int r = tail;
    while (l <= r) {
        if (nums[l] * nums[l] > nums[r] * nums[r]) {
            res[tail--] = nums[l] * nums[l];
            l++;
        } else {
            res[tail--] = nums[r] * nums[r];
            r--;
        }
    }
    return res;
}

时间复杂度:l指针和r指针共同遍历了nums数组,时间复杂度为O(N)O(N)

3 滑动窗口

T209-长度最小的子数组

题目描述

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

找出该数组中满足其总和大于等于 target 的长度最小的

子数组[numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度**。**如果不存在符合条件的子数组,返回 0

示例 1:

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

我的思路

  • 使用滑动窗口
  • 窗口小于目标则右指针往前移动,添加元素
  • 窗口大于目标则左指针往前移动,移出元素
  • 满足目标的,更新最小长度
/**
 * 优化之后的代码
 * @param target
 * @param nums
 * @return
 */
public int minSubArrayLenI(int target, int[] nums) {
    int l = 0;
    int r = 0;
    int minLen = nums.length + 1;
    int winSum = 0;
    while (r < nums.length) {
        winSum += nums[r++];
        while (winSum >= target) {
            minLen = Math.min(r - l, minLen);
            winSum -= nums[l++];
        }
    }
    return minLen == nums.length + 1 ? 0 : minLen;
}

T904-水果成篮

你正在探访一家农场,农场从左到右种植了一排果树。这些树用一个整数数组 fruits 表示,其中 fruits[i] 是第 i 棵树上的水果 种类

你想要尽可能多地收集水果。然而,农场的主人设定了一些严格的规矩,你必须按照要求采摘水果:

  • 你只有 两个 篮子,并且每个篮子只能装 单一类型 的水果。每个篮子能够装的水果总量没有限制。
  • 你可以选择任意一棵树开始采摘,你必须从 每棵 树(包括开始采摘的树)上 恰好摘一个水果 。采摘的水果应当符合篮子中的水果类型。每采摘一次,你将会向右移动到下一棵树,并继续采摘。
  • 一旦你走到某棵树前,但水果不符合篮子的水果类型,那么就必须停止采摘。

给你一个整数数组 fruits ,返回你可以收集的水果的 最大 数目。

翻译为人话,给你一个数组,求出最多有两种元素的子数组的最大长度

示例 3:

输入:fruits = [1,2,3,2,2]
输出:4
解释:可以采摘 [2,3,2,2] 这四棵树。
如果从第一棵树开始采摘,则只能采摘 [1,2] 这两棵树。

我的思路

  • 滑动窗口法,需要有一个HashMap<Integer, Integer> notes记录当前果篮里头水果的种类和个数
  • 当水果种类少于2就右滑,然后多于2,收缩窗口,同时改变notes
/**
 * 寻找最多包含 2 个元素的子数组的最大长度
 * @param fruits
 * @return
 */
public int totalFruit(int[] fruits) {
    int maxLen = 0;
    HashMap<Integer, Integer> notes = new HashMap<>();
    int l = 0;
    int r = 0;
    while (r < fruits.length) {
        notes.put(fruits[r], notes.getOrDefault(fruits[r], 0) + 1);
        r++;
        if (notes.size() <= 2) {
            maxLen = Math.max(maxLen, r - l);
        } else {
            while (notes.size() > 2) {
                // 将左边的水果扔掉
                if (notes.get(fruits[l]) == 1) {
                    notes.remove(fruits[l]);
                } else {
                    notes.put(fruits[l], notes.get(fruits[l]) - 1);
                }
                l++;
            }
        }
    }
    return maxLen;
}
  • 时间复杂度:快指针遍历一遍数组O(N)O(N)
  • 空间复杂度:额外空间哈希表,空间复杂度为O(N)O(N)

T76-最小覆盖字串

题目描述

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

注意:

  • 对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
  • 如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:

输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"
解释:最小覆盖子串 "BANC" 包含来自字符串 t 的 'A''B''C'
  • 什么是涵盖?
    • 就是s的词频表完全大于t
  • 使用new int[128] dict表示字符串的词频
  • 遍历母串,添加元素,然后判断是否涵盖,如果涵盖,返回窗口中所有的字符串,否则返回""
/**
 * 判断 s 中涵盖 t 的最小子串
 * @param s
 * @param t
 * @return
 */
public String minWindow(String s, String t) {

    if (s.length() < t.length()) return "";
    int[] subDict = new int[128];

    // 初始化模式串的词频表
    int[] tDict = new int[128];
    for (char c : t.toCharArray()) {
        tDict[c]++;
    }

    int l = 0;
    int r = 0;
    int[] candidates = {0, s.length() + 1};
    while (r < s.length()) {
        // 将当前字符加入窗口
        subDict[s.charAt(r++)]++;
        while (isCovered(subDict, tDict)) {
            // 如果当前窗口小于已经记录的最大窗口
            if (r - l < candidates[1] - candidates[0]) {
                candidates[0] = l;
                candidates[1] = r;
            }
            // 从里面移出元素
            subDict[s.charAt(l++)]--;
        }
    }
    StringBuilder sb = new StringBuilder();
    if (candidates[1] == s.length() + 1) return sb.toString();
    for (int i = candidates[0]; i < candidates[1]; i++) {
        sb.append(s.charAt(i));
    }
    return sb.toString();
}

/**
 * 判断 subDict 是否涵盖 tDict
 * @param subDict
 * @param tDict
 * @return
 */
private boolean isCovered(int[] subDict, int[] tDict) {
    for (int i = 0; i < subDict.length; i++) {
        if (subDict[i] < tDict[i]) {
            return false;
        }
    }
    return true;
}
  • 时间复杂度:需要遍历字符串s | t以及词频数组,但是词频数组是固定长度的,因此时间复杂度为O(M+N)O(M + N)
  • 空间复杂度:额外空间为两个词频数组,但是固定长度,所以空间复杂度为O(1)O(1)

4 模拟

T54-螺旋矩阵

题目描述

给你一个 mn 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

示例

image-20241025094628706

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

我的思路

  • 何时该转向,往哪个方向转向?
  • 何时转向?需要有一个组boundary记录矩阵的有效边界,到达边界的时候需要转向
  • 往哪个方向转向?
    • 0:从左往右遍历
    • 1:从上往下
    • 2:从右往左
    • 4:从下往上
List<Integer> resList = new ArrayList<>();
int[] boundary;
/**
 * 螺旋矩阵
 * @param matrix
 * @return
 */
public List<Integer> spiralOrder(int[][] matrix) {
    int m = matrix.length;
    int n = matrix[0].length;
    boundary = new int[]{0, n, 0, m};
    spiral(matrix, 0); // 0 代表从左往右遍历
    return resList;
}

/**
 * 螺旋遍历矩阵
 * @param matrix
 * @param direction
 */
private void spiral(int[][] matrix, int direction) {
    if (boundary[0] == boundary[1] || boundary[2] == boundary[3]) return;


    switch (direction) {
        case 0: {
            // 从左往右遍历
            int start = boundary[0];
            int end = boundary[1];
            for (int i = start; i < end; i++) {
                resList.add(matrix[boundary[2]][i]);
            }
            // 上边界加1
            boundary[2]++;
            // 调用从上往下遍历
            spiral(matrix, 1);
            break;
        }
        case 1: {
            // 从上往下遍历
            int start = boundary[2];
            int end = boundary[3];
            for (int i = start; i < end; i++) {
                resList.add(matrix[i][boundary[1] - 1]);
            }
            // 右边界减1
            boundary[1]--;
            // 调用从右往左
            spiral(matrix, 2);
            break;
        }

        case 2: {
            // 从右往左
            int start = boundary[0];
            int end = boundary[1] - 1;
            for (int i = end; i >= start; i--) {
                resList.add(matrix[boundary[3] - 1][i]);
            }
            // 下边界减1
            boundary[3]--;
            // 调用从右往左
            spiral(matrix, 3);
            break;
        }

        case 3: {
            // 从下往上遍历
            int start = boundary[2];
            int end = boundary[3] - 1;
            for (int i = end; i >= start; i--) {
                resList.add(matrix[i][boundary[0]]);
            }
            // 左边界加1
            boundary[0]++;
            // 调用从右往左
            spiral(matrix, 0);
            break;
        }
        default: {
            return;
        }
    }
}
  • 时间复杂度:O(N)
  • 空间复杂度:O(1)

T59-螺旋矩阵II

题目描述

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

我的思路

  • 同样使用模拟的方式,一个一个元素填进去

代码略

  • 时间复杂度:将n个数字填入到矩阵中,所以时间复杂度为O(N)
  • 空间复杂度:用了startNum记录当前数字,direction记录当前遍历方向,空间复杂度为O(1)

5 前缀和

见[前缀和技巧]

6 差分数组

见[差分数组]