原地哈希解决查找数组缺失重复问题

引言

有这样一类题型,他需要你在给定的数组中找到缺失的值或者重复的值,当然你可以利用HashSet来做,遍历数组,存储每个元素,然后根据题目要求,即可以得到正确的解,但这样做也有一个问题,导致其空间复杂度为 O ( n ) O(n) O(n),能否有一种方法,使得其既能完成题目要求,同时只用常数级别的空间呢,确实有,就是原地哈希

思想

原地哈希的思想可以理解为:把原始的数组当做哈希表来使用,我们自己编写哈希函数来存储值,根据题目的不同,所设置的哈希函数也不同。

下面我们直接以例题分析,从例题中明白其中思想。

实例

剑指 Offer 03. 数组中重复的数字

在这里插入图片描述
有题目可知,数组长度为n,数组里的数都集中在0~n-1的范围,由鸽巢原理可知,必定存在两个数值相同,即数组元素的 索引一对多 的关系,而值是0 ~ n-1,而索引也是0 ~ n-1;
可遍历数组并通过交换操作,使元素的 索引 一一对应(即 nums[i] = i

如果nums[i]!=i,则进行交换,否则说明该索引已和值正确对应;
如果nums[nums[i]]=nums[i],代表索引 nums[i]处和索引 i 处的元素值都为 nums[i],即找到一组重复值,返回此值 nums[i]

代码实现:

class Solution {
    public int findRepeatNumber(int[] nums) {
        int n=nums.length;
        if(n<1||nums==null) return 0;
        for(int i=0;i<n;i++){
            while(i!=nums[i]){//这里用while因为,不一定交换一次,值和索引就正确对应,可能需要多次交换直到正确对应为止,所以用while
                if(nums[nums[i]]==nums[i]) return nums[i];
                swap(nums,i,nums[i]);
            }
        }
        return -1;
    }
    public void swap(int[] nums,int i,int j){
        int temp=nums[i];
        nums[i]=nums[j];
        nums[j]=temp;
    }
}

当然也可以利用HashSet来解决

class Solution {
    public int findRepeatNumber(int[] nums) {
            int n=nums.length;
            Set<Integer> hashset=new HashSet();
            for(int i=0;i<n;i++){
                if(!hashset.add(nums[i])) return nums[i];
            }
            return -1;
    }
}

442. 数组中重复的数据

在这里插入图片描述
这道题同上道题类似,都是去找重复的数字,但这道题因为数组中的元素的值对应在1~n之间,所以对应的哈希函数的写法也略有不同,不能直接下标和元素对应了,因为整体相对下标+1了,我们可以这么构建:如果当前元素出现过,则让这个值-1作为下标,下标对应的元素取负数,如果已经为负数,说明这个值已经重复出现过,这个值是我们要找的值
此外,这个做法还可以用于寻找没出现的值,在执行完一轮后,如果有下标对应的元素值为正,则说明这个下标+1的值根本没有出现过

这个方法具有普适性,在数组元素取值为1~n时,都适用

具体代码:

class Solution {
    public List<Integer> findDuplicates(int[] nums) {
        List<Integer> ans=new ArrayList();
        int n=nums.length;
        if(n==0||nums==null) return ans;

        for(int i=0;i<n;i++){//用绝对值,因为可能数值在前面的遍历中被变为负数
            if(nums[Math.abs(nums[i])-1]>0) nums[Math.abs(nums[i])-1]*=-1;
            else ans.add(Math.abs(nums[i]));
        }
        return ans;
    }
    
}

448. 找到所有数组中消失的数字

在这里插入图片描述
此题是找缺失的数字,可以利用上述的取负的方法来做:

class Solution {
    public List<Integer> findDisappearedNumbers(int[] nums) {
            List<Integer> ans=new ArrayList();
            int n=nums.length;
            if(n==0||nums==null) return ans;

            for(int i=0;i<n;i++){//用绝对值,因为可能数值在前面的遍历中被变为负数
                if(nums[Math.abs(nums[i])-1]>0) nums[Math.abs(nums[i])-1]*=-1;
                
            }
            for(int i=0;i<n;i++){
                if(nums[i]>0) ans.add(i+1);
            }
            return ans;
        }
}

可以利用hash函数:f(nums[i]) = nums[i] - 1去完成,如果出现元素值不等于下标+1的情况的话,则为缺失值

class Solution {
    public List<Integer> findDisappearedNumbers(int[] nums) {
            List<Integer> ans=new ArrayList();
            int n=nums.length;
            if(n==0||nums==null) return ans;

            for(int i=0;i<n;i++){
                while(nums[i]!=nums[nums[i]-1]) swap(nums,i,nums[i]-1);
            }
            for(int i=0;i<n;i++){
                if(nums[i]!=i+1) ans.add(i+1);
            }
            return ans;
    }
    public void swap(int[] nums,int i,int j){
        int temp=nums[i];
        nums[i]=nums[j];
        nums[j]=temp;
    }
}

利用额外空间,使用hash表(手动创建,or利用HashSet)也可以

class Solution {
    public List<Integer> findDisappearedNumbers(int[] nums) {
        int n=nums.length;
        int[] hash=new int[n+1];
        for(int num:nums){
            hash[num]++;
        }
        List<Integer> ans=new ArrayList<>();
        for (int i = 1; i <=n; i++) {
            if(hash[i]==0) ans.add(i);
        }
        return ans;
    }
}

41. 缺失的第一个正数

在这里插入图片描述

  • 按照刚才我们读例子的思路,其实我们只需从最小的正整数 1 开始,依次判断 2、 3 、4 直到数组的长度 N 是否在数组中;

  • 如果当前考虑的数不在这个数组中,我们就找到了这个缺失的最小正整数;

  • 我们要找的数就在 [1, N + 1] 里,最后 N + 1 这个元素我们不用找。因为在前面的 N 个元素都找不到的情况下,我们才返回 N + 1

  • 我们可以采取这样的思路:就把 1 这个数放到下标为 0 的位置, 2 这个数放到下标为 1 的位置,按照这种思路整理一遍数组。然后我们再遍历一次数组,第 11个遇到的它的值不等于下标的那个数,就是我们要找的缺失的第一个正数。

  • 利用hash函数:f(nums[i]) = nums[i] - 1去完成

public class Solution {

    public int firstMissingPositive(int[] nums) {
        int len = nums.length;

        for (int i = 0; i < len; i++) {
            while (nums[i] > 0 && nums[i] <= len && nums[nums[i] - 1] != nums[i]) {
                // 满足在指定范围内、并且没有放在正确的位置上,才交换
                // 例如:数值 3 应该放在索引 2 的位置上
                swap(nums, nums[i] - 1, i);
            }
        }

        // [1, -1, 3, 4]
        for (int i = 0; i < len; i++) {
            if (nums[i] != i + 1) {
                return i + 1;
            }
        }
        // 都正确则返回数组长度 + 1
        return len + 1;
    }

    private void swap(int[] nums, int index1, int index2) {
        int temp = nums[index1];
        nums[index1] = nums[index2];
        nums[index2] = temp;
    }
}

没有测试用变负的方法,但应该没有问题,思路类似,不过要注意这个的取值范围是可能大于数组长度,所以变负需要注意边界条件

也可以利用hashset完成

class Solution {
    public int firstMissingPositive(int[] nums) {
        Set<Integer> set=new HashSet();
        for(int num:nums){
            set.add(num);
        }
        int n=nums.length;
        for(int i=1;i<=n;i++){
            if(!set.contains(i)) return i;
        }
        return n+1;
    }
}

也可以利用二分查找,这个问题其实就是要我们查找一个元素,而查找一个元素,如果是在有序数组中查找,会快一些;我们可以将数组先排序,再使用二分查找法从最小的正整数 11 开始查找,找不到就返回这个正整数;

class Solution {
    public int firstMissingPositive(int[] nums) {
            int n=nums.length;
            Arrays.sort(nums);
            for(int i=1;i<=n;i++){
                if(find(nums,i)==-1) return i;
            }
            return n+1;
    }
    public int find(int[] nums,int target){
        int n=nums.length;
        int l=0,r=n-1;
        while(l<=r){
            int m=l+(r-l)/2;
            if(nums[m]>=target) r=m-1;
            else l=m+1;
        }
        if(l>=nums.length||nums[l]!=target) return -1;
        return l;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值