Hot 100 刷题笔记 · 数组与哈希专题(3 题)

Hot 100 刷题笔记 · 数组与哈希专题(3 题)

收录:1. 两数之和、238. 除自身以外数组的乘积、41. 缺失的第一个正数;每题按 ① 分析思路 → ② 解法讲解 → ③ 知识点总结 组织。


一、1. 两数之和

① 题目分析 & 解题思路

给定 nums 与 target,找两个相加等于 target 的元素并返回下标;不能重复使用同一元素,保证只有一组答案。

  • 暴力:两层循环枚举组合判断和,能通过但不满足进阶。
  • 哈希表(优化) :找互补数 complement = target - nums[i];哈希表存【数值→下标】,先查互补数,命中即返回下标,否则存入当前数与下标。

② 常见解法及讲解

解法 1:暴力双循环(O(n2)O(n^2)O(n2),仅用于理解)

java 复制代码
class Solution {
    public int[] twoSum(int[] nums, int target) {
        for(int i = 0;i < nums.length ; i++){
            for(int j = i + 1; j < nums.length; j++){
                if(nums[i] + nums[j] == target){
                    return new int[] {i,j};
                }
            }
        }
        return new int[0];
    }
}

好理解、不占额外空间;大数据下慢,非最优解。

解法 2:HashMap 哈希表(最优,面试首选)

java 复制代码
class Solution {
    public int[] twoSum(int[] nums, int target) {
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int complement = target - nums[i];
            if (map.containsKey(complement)) {
                // HashMap 哈希表 (数值,下标)
                //// 先看互补数是否已经存在map中
                return new int[] { map.get(complement), i };
            }

            // 不存在,把当前数和下标放入map
            map.put(nums[i], i);
        }

        return new int[0];

    }
}

推演 nums=[3,2,4], target=6:i=0 互补数 3 不在表中 → put(3,0);i=1 互补数 4 不在 → put(2,1);i=2 互补数 2 在表中(下标 1)→ 返回 [1,2]。

先查再 put ,避免自己匹配自己([3,3] target=6:先存入第一个 3,第二个 3 查到它,返回 0,1)。

复杂度总结

  • 解法 1(暴力双循环):时间 O(n2)O(n^2)O(n2),空间 O(1)O(1)O(1);
  • 解法 2(HashMap):时间 O(n)O(n)O(n),空间 O(n)O(n)O(n)。

③ 知识点总结

常见易错变种:

  1. j 从 0 开始并写 i==j 判断,容易重复使用同一元素;
  2. 返回 {nums[i], nums[j]}(返回数值而非下标);
  3. 忘记 return,或返回 null。

补充:不推荐排序 + 双指针------排序打乱下标,需额外保存原始下标;该法适合返回值而非下标的题目。


二、238. 除自身以外数组的乘积

① 题目分析 & 解题思路

返回 answer,answer[i] = 除 nums[i] 外所有元素的乘积; 不能用除法(含 0 时总乘积 / numsi 会出错),⏱️ 时间 O(n)O(n)O(n),进阶额外空间 O(1)O(1)O(1)(输出数组不计)。

前缀积 + 后缀积 :prefix[i] 为 i 左边元素乘积,suffix[i] 为 i 右边元素乘积,answer[i] = prefix[i] × suffix[i]。进阶:复用输出数组存前缀,用变量滚动记录后缀,从后往前相乘,不开 suffix 数组。

② 常见解法及讲解

解法 1:前缀 + 后缀数组(空间 O(n)O(n)O(n))

java 复制代码
class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int[] prefix = new int[n];
        int[] suffix = new int[n];
        int[] ans = new int[n];

        // 前缀:prefix[i] = i左边所有乘积
        // 原数组里第一个的前缀记为1
        prefix[0] = 1;
        // 从前往后累积
        for (int i = 1; i < n; i++) {
            prefix[i] = prefix[i - 1] * nums[i - 1];

        }

        // 后缀:suffix[i] = i右边所有乘积
        // 原数组最后一个的后缀记为1
        suffix[n - 1] = 1;
        // 从后往前累积
        for (int j = n - 2; j >= 0; j--) {
            suffix[j] = suffix[j + 1] * nums[j + 1];
        }

        // 左右相乘
        for (int k = 0; k < n; k++) {
            ans[k] = prefix[k] * suffix[k];

        }
        return ans;
    }
}

逻辑直观、不易写错;缺点是开两个辅助数组,空间 O(n)O(n)O(n)。

解法 2:进阶版 O(1)O(1)O(1) 额外空间(面试优先写这个)

java 复制代码
class Solution {
    public int[] productExceptSelf(int[] nums) {
        int n = nums.length;
        int[] ans = new int[n];

        // 第一步:ans数组先存前缀乘积
        ans[0] = 1;
        for (int i = 1; i < n; i++) {
            ans[i] = ans[i - 1] * nums[i - 1];
        }

        // 第二步:从后往前,用一个变量保存后缀乘积,直接乘进ans
        int suffix = 1;
        for (int j = n - 1; j >= 0; j--) {
            ans[j] = ans[j] * suffix;
            suffix = suffix * nums[j];// 更新后缀,给左边下一个用
        }

        return ans;
    }
}

推演 [1,2,3,4]:前缀遍历后 ans=[1,1,2,6],suffix 数组 [24,12,4,1];从后往前:i=3 ans=6×1=6、suffix=4;i=2 ans=2×4=8、suffix=12;i=1 ans=1×12=12、suffix=24;i=0 ans=1×24=24,结果 [24,12,8,6]。

复杂度总结

  • 解法 1(前后缀数组):时间 O(n)O(n)O(n),空间 O(n)O(n)O(n);
  • 解法 2(进阶版):时间 O(n)O(n)O(n),空间 O(1)O(1)O(1)(不计输出数组)。

③ 知识点总结

易错点:

  1. 前缀第一个、后缀最后一个初始值是 1,不是 0;
  2. 前缀用 nums[i-1]、后缀用 nums[i+1],下标容易写反;
  3. 不能用除法,遇到 0 的样例除法方案直接失效;
  4. 数组长度最大 10510^5105,暴力双重循环 O(n2)O(n^2)O(n2) 会直接超时。

三、41. 缺失的第一个正数

① 题目分析 & 解题思路

给无序整数数组,找未出现的最小正整数 ;时间 O(n)O(n)O(n)、额外空间 O(1)O(1)O(1),不能开哈希表。

关键结论 :长度为 n,答案必在 1,n+11, n+11,n+1:1~n 全出现则返回 n+1,否则返回最小缺失数;负数、0、大于 n 的数可忽略。

原地哈希 (把数组本身当哈希表):合法正数 xxx(1≤x≤n1\le x\le n1≤x≤n)放到下标 x−1x-1x−1(3→2,1→0)。流程:① 遍历交换归位;② 再遍历,第一个不满足 nums[i]==i+1 的位置,i+1 即答案;③ 全部满足返回 n+1。

② 常见解法及讲解

解法 1:原地哈希(推荐,面试标准,O(n)O(n)O(n) 时间、O(1)O(1)O(1) 空间)

java 复制代码
class Solution {
    public int firstMissingPositive(int[] nums) {
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            // nums[i]值 的目标位置上的值是 nums[nums[i]-1]
            // 满足三个条件才交换:是合法正数、不在正确位置、目标位置的值和它不一样
            while (nums[i] >= 1 && nums[i] <= n && nums[nums[i] - 1] != nums[i]) {
                // 交换 nums[i] 和 nums[nums[i]-1]
                int temp = nums[nums[i] - 1];
                nums[nums[i] - 1] = nums[i];
                nums[i] = temp;
            }
        }

        // 找第一个 nums[i] != i+1
        for (int i = 0; i < n; i++) {
            if (nums[i] != i + 1) {
                return i + 1;
            }
        }

        // 1~n全都有
        return n + 1;
    }
}

推演 nums=[3,4,-1,1],n=4:i=0,3 合法,交换下标 0、2 → [-1,4,3,1],-1 退出 while;i=1,4 合法,交换下标 1、3 → [-1,1,3,4],1 继续交换到下标 0 → [1,-1,3,4];i=2 的 3、i=3 的 4 已就位。第二轮:i=0 的 1==1 ✔,i=1 的 -1≠2 → 返回 2 ✔。

解法 2:标记法(原地哈希另一种实现,不交换)

先把负数、0 改成 n+1;再把下标 x-1 的值改成负数标记;最后找第一个正数下标。

java 复制代码
class Solution {
    public int firstMissingPositive(int[] nums) {
        int n = nums.length;
        // 第一步:把<=0的数都改成n+1
        for (int i = 0; i < n; i++) {
            if (nums[i] <= 0) nums[i] = n + 1;
        }
        // 第二步:标记
        for (int i = 0; i < n; i++) {
            int x = Math.abs(nums[i]);
            if (x <= n) {//限定范围<=n里的整数,结合上一步排除了<=0的值
                // 把下标x-1的值nums[x - 1] 都改成负数做标记
                nums[x - 1] = -Math.abs(nums[x - 1]);
            }
        }
        // 第三步:找第一个正数
        for (int i = 0; i < n; i++) {
            // nums[i] > 0 说明值(i+1)不属于原数组里的正数,nums[i]没在上一步被改为负数
            if (nums[i] > 0) return i + 1;
        }
        return n + 1;
    }
}

不用 while 交换、不易死循环;缺点是理解稍绕。

解法 3:HashSet(简单但不满足空间要求,适合初学理解)

java 复制代码
class Solution {
    public int firstMissingPositive(int[] nums) {
        Set<Integer> set = new HashSet<>();
        for (int x : nums) set.add(x);
        int res = 1;
        // 从正数1开始依次判断出第一个不在原数组里的正数
        while (set.contains(res)) {
            res++;
        }
        return res;
    }
}

好写好理解;空间 O(n)O(n)O(n),面试会被追问优化。

复杂度总结

  • 原地交换 / 标记法:O(n)O(n)O(n) 时间、O(1)O(1)O(1) 额外空间;
  • HashSet:O(n)O(n)O(n) 时间、O(n)O(n)O(n) 空间。

③ 知识点总结

常见出错点:

  1. 只交换一次不用 while:[3,4,-1,1] 中 3 归位后,该位置的 -1 还要处理;
  2. 不判断 nums[i]==nums[nums[i]-1] 会死循环(重复数字);
  3. 漏判 nums[i]<=n,大于 n 的数不应交换;
  4. 下标换算错:x 对应下标是 x-1,不能直接用 x;
  5. 让负数、0 也参与交换;
  6. 用 HashSet 空间 O(n)O(n)O(n),不满足进阶要求。
相关推荐
Code_Solitude8 分钟前
C语言动态内存 + 柔性数组 + 数组名退化
c语言·算法·柔性数组
上海潭龙东海科技有限公司15 分钟前
客运站管控之动态孪生技术方案(新增一屏管控模块
数据仓库·人工智能·算法·重构·动态规划
文丑颜不良啊1 小时前
LeetCode2821~2840题目解析
java·leetcode
apcipot_rain2 小时前
C++转Java日记(10)
java·开发语言
简 洁 冬冬2 小时前
泛型方法写法全面辨析:从「能编译」到「能跑」到「能用」
java·泛型
YLCHUP3 小时前
【思维&图论建模】题解:债权(debt)_图论_建模_思路转化_思维题目_C++算法竞赛
c++·笔记·算法·数学建模·贪心算法·深度优先·图论
LGL6030A3 小时前
Java学习历程31——初识Spring Boot
java·spring boot·学习
思麟呀3 小时前
背包DP(01背包+完全背包)
c++·算法
笑容掩饰我的失落4 小时前
区块链从小白到精通(七):Solidity 语法精讲——数据类型、函数、修饰器与事件
算法·区块链·php·智能合约·哈希算法
旖旎夜光5 小时前
【C++ 基础入门】C++11 新特性(中):右值引用、移动语义与完美转发
开发语言·c++·笔记·学习