哈希算法理解

哈希表本质其实就是一个数组,但在java里面有另外两种哈希的容器:HashMap和HashSet

哈希算法:哈希算法适用于查找一个元素是否在一个集合中。可以设想一个在一个数组里面,你想查找一个值,那时间复杂度就是O(n),哈希表就是用键值对(key,value)的方法,将查找一个值的时间效率提高到O(1)。

哈希表的理解:其实随着我写算法题的增多,哈希表基本上是用来当成一个工具,就不会说有什么题目专门可以用哈希算法来写

下面是用map来做哈希表的情况:

leetcode 1 两数之和
public int[] twoSum(int[] nums, int target) {
        Map <Integer,Integer> table = new HashMap<>();
        table.put(nums[0],0);
        for(int i=1;i<nums.length;++i){
            if(table.containsKey(target-nums[i])){
                int[] ans = new int[2];
                ans[0] = table.get(target-nums[i]);ans[1] = i;
                return ans;
            }
            table.put(nums[i],i);
        }
        return new int[2];
    }


给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

这道题也算是一个哈希表入门的经典题目了,这道题目用暴力的方法肯定也能过。

但是用哈希表时间复杂度就能降低成O(n),

仔细想一想哈希表的作用,查找一个数据是否在集合中,那运用到这一题又怎么理解呢?

我们对这个数组进行遍历:当我们遍历到一个数的时候,我们先判断这个(tar-这个数)是否在这个哈希表中,如果不在,那我们就把这个数放到哈希表中,假设这个数它是我们想找的“两数”中的一个,那当我们遍历到这个数的时候,另一个“两数”肯定就已经在哈希表里面了。这不正是哈希表的应用嘛,在一个集合中查找一个元素。

leetcode 454. 四数相加 II
public int fourSumCount(int[] A, int[] B, int[] C, int[] D) {
        Map<Integer, Integer> countAB = new HashMap<Integer, Integer>();
        for (int u : A) {
            for (int v : B) {
                countAB.put(u + v, countAB.getOrDefault(u + v, 0) + 1);
            }
        }
        int ans = 0;
        for (int u : C) {
            for (int v : D) {
                if (countAB.containsKey(-u - v)) {
                    ans += countAB.get(-u - v);
                }
            }
        }
        return ans;
    }

分析:map应该是做哈希的最常用的数据结构了:
map是一种<key, value>的结构,本题可以用key保存数值,用value在保存数值所在的下标。所以使用map最为合适
根据map的结构:map中有一对键值对,有一个对应关系。
对第一道题的两数之和分析:这题我们不仅仅要找到这个数,而且我们需要返回这个数组的下标。这里很明显我们就有两个值。
对第二题的四数相加分析:这道题map中的key值很明显,就是数组的和,不过这题的value的值很值得考究:这个key出现的次数,

这是我一开始写得n3的时间复杂度:

public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        int i,j,k,l;
        int n = nums1.length;
        int count=0;
        Map <Integer,Integer> table = new HashMap<>();
        //把nums4数组元素放入哈希表中
        for(i=0;i<n;++i){
            table.put(nums4[i],table.getOrDefault(nums4[i],0)+1);
        }
        for(i=0;i<n;++i){
            for(j=0;j<n;++j){
                for(k=0;k<n;++k){
                    if(table.containsKey(-(nums1[i]+nums2[j]+nums3[k]))){
                        count += table.get(-(nums1[i]+nums2[j]+nums3[k]));
                    }
                }
            }
        }
        return count;
    }

如果你不去记录这个key出现的次数会发生什么情况呢,就是当你的第四个数组(因为我一开始的想法就是把nums4存到哈希表里去),当nums4数组中有重复的值的时候,这个时候还得分析一下hashmap的特点,里面存储的规则也是,不允许有相同的值,如果你不记录出现的次数,就会导致次数少

举个例子把:比如前三个数组中某三个数的和是-2,然后nums4中有两个2,本来这种情况答案是要+2的,但是你不去记录次数的话,因为hashmap会吃掉一个2,所以导致结果少一个。

下面是用数组做哈希表的情况
 leetcode 349两个数组的交集
public int[] intersection(int[] nums1, int[] nums2) {
        int[] hash = new int[1001];
        Set<Integer> table = new TreeSet<>();
        int i;
        for (i = 0; i < nums1.length; ++i) {
            hash[nums1[i]] = 1;
        }
        for (i = 0; i < nums2.length; ++i) {
            if (hash[nums2[i]] == 1) {
                table.add(nums2[i]);
            }
        }
        int[] res = new int[table.size()];
        int m = 0;
        for(int temp:table){
            res[m++] = temp;
        }
        return res;
    }

题目:给定两个数组 nums1 和 nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序 。


输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]

写着题的题解主要是有两个原因:

1:首先这是一道用数组来当哈希表的题目

2:我在leetcode上注意到这题的数据nums[i]的大小只到1000,所以我就开了一个1001大小的数组,这也算是个小技巧把

leetcode 242:有效的字母异位词
public boolean isAnagram(String s, String t) {
        int[] record = new int[26];
        int i;
        if(s.length()!=t.length()){
            return false;          
        }
        for(i=0;i<s.length();++i){
            record[s.charAt(i)-'a']++;
        }
        for(i=0;i<t.length();++i){
            record[t.charAt(i)-'a']--;
        }
        for(i=0;i<26;++i){
            if(record[i]!=0){
                return false;
            }
        }
        return true;
    }

给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。
注意:若 s 和 t 中每个字符出现的次数都相同,则称 s 和 t 互为字母异位词。

分析:这道题也算是个很简单的用数组来当哈希表的题目了,很容易想到字母的个数只有26个,所以我们开一个长度为26的数组record就行了。

leetcode 383. 赎金信
public boolean canConstruct(String ransomNote, String magazine) {
        if (ransomNote.length() > magazine.length()) {
            return false;
        }
        // 定义一个哈希映射数组
        int[] record = new int[26];

        // 遍历
        for(char c : magazine.toCharArray()){
            record[c - 'a'] += 1;
        }

        for(char c : ransomNote.toCharArray()){
            record[c - 'a'] -= 1;
        }
        
        // 如果数组中存在负数,说明ransomNote字符串总存在magazine中没有的字符
        for(int i : record){
            if(i < 0){
                return false;
            }
        }

        return true;
    }

给你两个字符串:ransomNote 和 magazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。

如果可以,返回 true ;否则返回 false 。

magazine 中的每个字符只能在 ransomNote 中使用一次。

数组做哈希表的总结:

根据数组的特点:长度不可以改变,然后我们可以得出:数组做哈希表的场景主要是对应题目中某些量是定的,比如这两道题,题目所给的都是小写字母,小写字母的个数只有26个,所以,我们开一个26个长度的数组就可以很简单做为这个题目了。

题外话:

至于为什么不用map:因为map的效率比较低,为什么低呢,这个我说实话我现在看别人的理解我有点看不懂,所以等以后我回来再解释把。

下面是用set做哈希表的情况

什么时候要用到set呢,这里就不得不分析set的特点了,set集合是有一个自动去重的功能,所以当你分析题目返回的元素不能重复的时候,就可以考虑用set了。

值得注意的有一道题目:leetcode 三数之和:
给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != j、i != k 且 j != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请
你返回所有和为 0 且不重复的三元组。
注意:答案中不可以包含重复的三元组。


题目中提到了去重,也可以用这样的结构来试一下,虽然这道题用这个结构会超时。

Set<List<Integer>> res = new HashSet<>();

leetcode 217. 存在重复元素
public boolean containsDuplicate(int[] nums) {
        Set<Integer> table = new HashSet<>();
        for(int i=0;i< nums.length;++i){
            if(!table.add(nums[i])){
                return true;
            }
        }
        return false;
    }

给你一个整数数组 nums 。如果任一值在数组中出现 至少两次 ,返回 true ;如果数组中每个元素互不相同,返回 false 。

分析:这道题本身不难,我想把这题记下来的原因是:这道题的哈希表用的是set集合

set集合的add方法返回值是一个boolean类型的值。表示set中如果有这个值就会返回false,没有就会返回true,并且将这个值添加进去。

leetcode 220. 存在重复元素 III
public boolean containsNearbyAlmostDuplicate(int[] nums, int k, int t) {
        int n = nums.length;
        TreeSet<Long> set = new TreeSet<Long>();
        for (int i = 0; i < n; i++) {
            Long ceiling = set.ceiling((long) nums[i] - (long) t);
            if (ceiling != null && ceiling <= (long) nums[i] + (long) t) {
                return true;
            }
            set.add((long) nums[i]);
            if (i >= k) {
                set.remove((long) nums[i - k]);
            }
        }
        return false;
    }

给你一个整数数组 nums 和两个整数 indexDiff 和 valueDiff 。
找出满足下述条件的下标对 (i, j):

  • i != j,
  • abs(i - j) <= indexDiff
  • abs(nums[i] - nums[j]) <= valueDiff

如果存在,返回 true ;否则,返回 false 

这里先补充几个set集合的常用方法:
1:删除指定元素,set集合删除元素的时候是按照元素删的,不是按照下标
set.remove((long) nums[i - k]);

2:
floor(E e) 方法返回在这个集合中小于或者等于给定元素的最大元素,如果不存在这样的元素,返回null.
ceiling(E e) 方法返回在这个集合中大于或者等于给定元素的最小元素,如果不存在这样的元素,返回null.

分析:
这道题题目就是要求我们需要在大小为 k 的滑动窗口所在的「有序集合」中找到与 u 接近的数。
在k个滑动窗口:abs(i - j) <= indexDiff,的有序集合,有序集合的话,只要保证我们一直往前遍历,那i肯定!=j(这个条件我也想到了)
找到与u接近的数字:abs(nums[i] - nums[j]) <= valueDiff 。


这道题根据题目给的大小关系:
abs(nums[i] - nums[j]) <= valueDiff  ----->   nums[i]+valueDiff<nums[j]
这个时候,我们可以这么理解  我们创建的set集合就是用来存放nums[i],然后i++往前跑的是nums[j],所以我们需要ceil比那两个值的差值大
也就可以得出:Long ceiling = set.ceiling((long) nums[i] - (long) t);

以上是这个题目的第一个注意点:
if (i >= k) {
                set.remove((long) nums[i - k]);
            }

这一段代码表示什么意思呢:表示我们只需要在set集合中维护k个元素就好,怎么理解呢:
abs(i - j) <= indexDiff,根据题目所给的条件,就算你给我的元素的值满足要求,但是下标不满足要求,我们也不能要。
这就是一个条件的转化,以后碰到类似的情况也能这么去想。


还有第三个需要知道的就是set的treeset集合本质是红黑树。

原地哈希

原地哈希是一种提升空间时间复杂度的方法:

leetcode 41:缺失的第一个正数

给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。


这道题首先介绍自己的一开始的想法:

写一个哈希表,将这个数组放到哈希表里面去,然后从1遍历到1000001(因为这道题目的范围是这么多)
接着查找这个时候的i是否在这个哈希表中存在,如果不存在,因为i是从小遍历到大的,所以,我们直接返回就行
如果遍历完了都没有,说明,缺失的最小正整数就是这个数组的长度+1;
注意:有可能出现负数的情况,在返回的时候做一个特判。

class Solution {
   public int firstMissingPositive(int[] nums) {
        Map<Integer,Integer> table = new HashMap<>();
        for(int i=0;i<nums.length;++i){
            table.put(nums[i],i);
        }
        int max = Integer.MIN_VALUE;
        for(int i=0;i<nums.length;++i){
            if(nums[i]>max){
                max = nums[i];
            }
        }
        for(int i=1;i<=max;++i){
            if(!table.containsKey(i)){
                return i;
            }
        }
        return (max<=0?1:max+1);
    }
}

ok,下面来介绍原地哈希的办法:这道题要求不能开额外的空间,那我们的想法就是在数组上直接修改值,怎么修改呢,我们可以将nums[i]放在(i-1)上
比如什么呢将num[i] = 2,这个数放在数组(i-1)的位置上,我们对这个数组进行一定的操作之后,我们直接再遍历一次数据,我们从左往右遍历,第一个不符合位置上的数,就是我们要找的缺失的最小正整数。如果遍历到数组的末尾了,我们还是没找到那个正整数,那我们直接把数组的长度+1放回就行。
这里说一下,理论上来说,我们可以把nums[i]放在(i-1)上,但是如果比如说,这个数组的长度是5,然后第一个数字就是7,那这个数字根本没地方放怎么办呢,这个时候,我们就可以忽略这个7这个数字,随便放哪里,反正在我们遍历的时候,我们是根据这个时候的i值来进行比较的。

class Solution {
   public int firstMissingPositive(int[] nums) {
        int len = nums.length;
        for(int i=0;i<len;++i){
            while(nums[i]>=1&&nums[i]<=len&&nums[nums[i]-1]!=nums[i]){
                swap(nums,i,nums[i]-1);
            }
        }
        for(int i=0;i<len;++i){
            if(nums[i]!=i+1){
                return (i+1);
            }
        }
        return len+1;
    }
    public void swap(int[] nums,int a,int b){
        int t = nums[a];
        nums[a] = nums[b];
        nums[b] = t;
    }
}


注意点:for循环中套了一个while循环:为什么要来一个while循环呢:比如[3,4,-1,1]这个案例,我们第一次交换完之后变成[-1,4,3,1],
然后我们再交换变成[-1,1,3,4],注意看这个数组,里面3,4是到了正确的位置,可是这个时候的1呢,我们原来的想法是把这个1放到数组的第一个元素上去
可是,如果我们只交换一次(用if)的话,我们可以保证这个4能交换到正确的位置,但我们交换回来的这个1不行,所以,我们还得再交换(while)
把这个数组变成[1,-1,3,4],这样才能获得答案。
 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值