了解原地哈希思路,使用其解决LC几道代表性题目。
原地哈希#
概念学习#
我个人对原地哈希的理解:
- 原地:一般题干要求O(1)的空间复杂度,此时我们可以考虑使用入参数组,在不开辟多余空间的情况操作,就是就地、原地算法;
- 哈希:hashmap是我们大多数时候使用哈希数据结构的一种通用实现,内部也是使用了数组进行存储,部分题干中的数据,有比较明确的哈希映射关系,比如[1,n]数据,可以直接存放在n长度的数组中,对应哈希函数:hash(x)=x-1;
可以参考下liweiwei大佬的讲解:原地哈希(哈希函数为:f(nums[i]) = nums[i] - 1)。
本文接下来通过LC上的几道题来感受、理解其特征、运用。
剑指 Offer 03. 数组中重复的数字#
剑指 Offer 03. 数组中重复的数字
这道题之前写过题解:数组中重复的数字 原地部分有序化题解 。
原地哈希解法的核心就是:
- 识别题干已知条件
- 数据范围:[0,n-1]
- 有数字是重复出现的
- 返回任意一个重复数字
 
尝试使用原地哈希的思路,我们尝试把遍历到的每个数据都放到对应位置:nums[nums[i]]=nums[i]。
这种操作重复,重复数据一定会相遇:如果此时nums[i]下标处已经有归位的数据,说明找到了重复数据。
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
 |     public int findRepeatNumber(int[] nums) {
        int n = nums.length;
        // 尝试把遍历到的数字放到 i 的位置上,进行局部有序化
        // 如果往过放的过程,发现i的位置已经存在了i数值,说明重复,即可返回
        // i指向前序已经归位的下一个下标
        int i=0;
        while(true){
            while(nums[i]!=i){
                if(nums[nums[i]]==nums[i]){
                    return nums[i];
                }
                swap(nums,i,nums[i]);
            }
            i++;
        }
    }
    public void swap(int[] nums,int i1,int i2){
        int tmp=nums[i1];
        nums[i1]=nums[i2];
        nums[i2]=tmp;
    }
 | 
 
442. 数组中重复的数据#
442. 数组中重复的数据
分析题干:
- 数据量n,数据范围[1, n]
- 需要找出所有重复数据
我们尝试用解上道题的套路来处理:
- 遍历数组,将数据nums[i]放在下标nums[i]-1处
- 收集所有nums[i]!=i+1的数据
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
 | public List<Integer> findDuplicates(int[] nums) {
    List<Integer> ans= new ArrayList<>();
    int n=nums.length;
    for(int i=0;i<n;i++){
        while(nums[nums[i]-1]!=nums[i]){
            swap(nums,i,nums[i]-1);
        }
    }
    for(int i=0;i<n;i++){
        if(nums[i]!=i+1){
            ans.add(nums[i]);
        }
    }
    return ans;
}
private void swap(int[] nums, int i1, int i2){
    int tmp=nums[i1];
    nums[i1]=nums[i2];
    nums[i2]=tmp;
}
 | 
 
参考下官解的做法:
- 将遍历到的数据对应下标值设置为相反数(一个数既表达了访问状态,也保留了原值)
- 下次碰到对应下标为负数时,说明是重复元素
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
 |     /**
     * 当前值对应下标,访问过后,我们将其设置为相反数,表示已经被访问
     * 下次碰到重复值,发现对应下标值已经是负数,将其相反数塞进ans
     * <p>
     * 时间复杂度 O(n)
     * 空间复杂度 O(1)
     */
    public List<Integer> findDuplicates(int[] nums) {
        // 访问过的数字,在对应下标处标记为负数相反数
        List<Integer> ans = new ArrayList<>();
        int n = nums.length;
        for (int i = 0; i < n; i++) {
            int curr = Math.abs(nums[i]);
            if (nums[curr - 1] < 0) {
                ans.add(curr);
            } else {
                nums[curr - 1] = -nums[curr - 1];
            }
        }
        return ans;
    }
 | 
 
448. 找到所有数组中消失的数字#
448. 找到所有数组中消失的数字
分析题干:
- 数据量n,数据范围[1, n]
- 题目要求出没出现的数字
同样的套路,我们遍历数组,将nums[i]都归位到nums[i]-1下标处。
再遍历一次,i+1!=nums[i]的就是消失的数字。
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 | /**
 * 原地hash,使用入参数组,进行数据还原。
 * 题干限定了数据范围为 [1,n],尝试将当前下标的数替换到对应下标上,nums[i]下标对应:nums[i]-1
 * 碰到下标 nums[i]-1 处 与当前值相同时,进行下一个下标处理
 * <p>
 * 最终,遍历整个数组,下标处值不等于 i+1 的,即为缺失的数据
 * <p>
 * 时间复杂度 O(n)
 * 空间复杂度 O(1)
 */
public List<Integer> findDisappearedNumbers(int[] nums) {
    List<Integer> ans = new ArrayList<>();
    int n = nums.length;
    for (int i = 0; i < n; i++) {
        // 当前位置值与下标不匹配,同时 nums[i]-1 下标处于当前值不等
        // nums[i] != i + 1 &&
        while (nums[nums[i] - 1] != nums[i]) {
            swap(nums, i, nums[i] - 1);
        }
    }
    for (int i = 0; i < n; i++) {
        if (nums[i] != i + 1) {
            ans.add(i + 1);
        }
    }
    return ans;
}
private void swap(int[] nums, int i1, int i2) {
    int tmp = nums[i1];
    nums[i1] = nums[i2];
    nums[i2] = tmp;
}
 | 
 
41. 缺失的第一个正数#
41. 缺失的第一个正数
观察用例:
- [1,2,0]- 
- 将[1,n]范围的数据放到nums[i]-1的位置上
- 遍历到的第一个nums[i]!=i+1的即为所求
 
- [1,2,3]
- [3,4,-1,1]
- [7,8,9,11,12]- 
- 所有数据均不在[1,n]范围内
- 直接从1遍历,1即为所求
 
算法过程:
- 将 nums[i]-1有效下标范围的数据归位,即x放在x-1的位置上,负数、0、大于n的数我们不动
- 依次从1开始遍历,没有归位的i值,即为缺失的最小正数
|  1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 | /**
 * 时间复杂度 O(n)
 * 空间复杂度 O(1)
 */
public int firstMissingPositive(int[] nums) {
    int n = nums.length;
    for (int i = 0; i < n; i++) {
        while (nums[i] - 1 >= 0 && nums[i] - 1 < n && nums[nums[i] - 1] != nums[i]) {
            swap(nums, i, nums[i] - 1);
        }
    }
    for (int i = 1; i <= n; i++) {
        if (nums[i - 1] != i) {
            return i;
        }
    }
    return n + 1;
}
private void swap(int[] nums, int i1, int i2) {
    int tmp = nums[i1];
    nums[i1] = nums[i2];
    nums[i2] = tmp;
}
 | 
 
原地哈希的固定套路:#
- 使用已有的数组空间
- 根据数据特征来映射数据到数组中
找重复:#
找缺失:#
- 先归位(根据题干适当检查数据范围)
- nums[i]!=i+1的即为缺失项