leetcode--哈希表专栏

前提知识

哈希表的理论基础C++版

题目速览

242. 有效的字母异位词
349. 两个数组的交集
202. 快乐数
1. 两数之和
454. 四数相加 II
383. 赎金信
15. 三数之和
18. 四数之和

题目详解

242. 有效的字母异位词

整体思路
法一:遍历两个字符串,建立两个哈希表,存进去每一个字符的个数,一次比较。很慢

    public static boolean isAnagram(String s, String t) {
        if (s.length() != t.length()) return false;
        HashMap<Character, Integer> map1 = new HashMap<>();
        HashMap<Character, Integer> map2 = new HashMap<>();
        for (int i = 0; i < s.length(); i++) {
            map1.put(s.charAt(i), map1.getOrDefault(s.charAt(i), 0) + 1);
            map2.put(t.charAt(i), map2.getOrDefault(t.charAt(i), 0) + 1);
        }
        for (Character a : map1.keySet()){
            if (!map1.get(a).equals(map2.get(a))){
                return false;
            }
        }
        return true;
    }

但是上面的时间复杂度和空间复杂度都很高,就解决空间复杂度来讲,可以只用一个map,当在字符串1中遇到字符时就加1,当在字符串2中遇到字符时就减1。

    public static boolean isAnagram(String s, String t) {
        if (s.length() != t.length()) return false;
        HashMap<Character, Integer> map1 = new HashMap<>();
        for (int i = 0; i < s.length(); i++) {
            map1.put(s.charAt(i), map1.getOrDefault(s.charAt(i), 0) + 1);
            map1.put(t.charAt(i), map1.getOrDefault(t.charAt(i), 0) - 1);
        }
        for (Character a : map1.keySet()){
            if (!map1.get(a).equals(0)){
                return false;
            }
        }
        return true;
    }

这样就会减少空间复杂度。
法二:答案是用数组做的,因为哈希表查询是近似O(1),还有自己内部调整的时间,而数组是标准的O(1),且全是小写字母,只用26个字节就可以。

class Solution {
    public boolean isAnagram(String s, String t) {

        int[] record = new int[26];
        for (char c : s.toCharArray()) {
            record[c - 'a'] += 1;
        }
        for (char c : t.toCharArray()) {
            record[c - 'a'] -= 1;
        }
        for (int i : record) {
            if (i != 0) {
                return false;
            }
        }
        return true;
    }
}

注意:
经过自己的实测,转化成s.toCharArray,比直接chatAt()更快

349. 两个数组的交集

整体思路
用一个哈希表去存一个数组中的字符,遍历另一个数组,如有在哈希表中存在就存入set集合中,除去重复
ps:下面的版本其实map直接用set集合就可以

class Solution {
    public int[] intersection(int[] nums1, int[] nums2) {
        HashMap<Integer, Integer> map1 = new HashMap<>();
        for (int i : nums1){
            map1.put(i, map1.getOrDefault(i, 0) + 1);
        }
        Set<Integer> set = new HashSet<>();
        for (int i : nums2){
            if (map1.containsKey(i)){
                set.add(i);
            }
        }
        int[] res = new int[set.size()];
        int count = 0;
        for (int i : set){
            res[count] = i;
            count++;
        }
        return res;
    }
}

202. 快乐数

基本思路:
1.建立一个函数随时更新n
2.建立一个无重复的Set集合,如果n在里面第二次出现,就是进入了死循环
代码:

    public boolean isHappy(int n) {
        Set<Integer> set = new HashSet<>();
        while (n != 1 && !set.contains(n)){
            set.add(n);
            n = getNewNumber(n);
        }
        return n == 1;
    }

    public int getNewNumber(int n){
        int res= 0;
        while (n > 0){
            int temp = n % 10;
            res += temp * temp;
            n = n / 10;
        }
        return res;
    }

1. 两数之和

基本思路:
法一:遍历
法二:自己做得时候用了两次for循环,一次是保存值与索引进入map,一次是遍历数组,查找map中是不是有对应的target - 这个值。但是其实用一次循环就可以了,因为后期如果满足条件的话,target-后期的值已经提前存进去了

    public static int[] twoSum(int[] nums, int target) {
        HashMap<Integer, Integer> map = new HashMap<>();
        int res[] = new int[2];
        for (int i = 0; i < nums.length; i++){
            map.put(nums[i],i);
        }
        for (int i = 0; i < nums.length; i++){
            if (map.containsKey(target - nums[i]) && i != map.get(target - nums[i])){
                res[0] = i;
                res[1] = map.get(target - nums[i]);
                break;
            }
        }
        return res;
    }

法三:用一次循环就可以了,因为后期如果满足条件的话,target-后期的值已经提前存进去了

    public int[] twoSum(int[] nums, int target) {
        HashMap<Integer, Integer> map = new HashMap<>();
        int res[] = new int[2];
        for (int i = 0; i < nums.length; i++){
            if (map.containsKey(target - nums[i])){
                res[0] = i;
                res[1] = map.get(target - nums[i]);
                break;
            }
            map.put(nums[i], i);
        }
        return res;
    }

454. 四数相加 II

基本思路:

  1. 首先定义 一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。
  2. 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。
  3. 定义int变量count,用来统计a+b+c+d = 0 出现的次数。
  4. 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。
  5. 最后返回统计值 count 就可以了
    代码:
    public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        //记录前两个数组a+b之和,和这个和出现的次数
        Map<Integer, Integer> map = new HashMap<>();
        int res = 0;
        int n = nums1.length;
        for (int a : nums1){
            for (int b : nums2){
                map.put((a + b), map.getOrDefault((a + b), 0) + 1);
            }
        }
        //在后两个数组中c+d计算和,看是不是和前两个数组a+b和相加符合题意
        for (int c : nums3) {
            for (int d : nums4) {
                if (map.containsKey(0 - (c +d))){
                    res += map.get(0 - (c + d));
                }
            }
        }
        return res;
    }

383. 赎金信

基本思路:

  1. 如果ran的长度比mag大,则直接返回false
  2. 因为是小写字母,所以可以直接使用数组
  3. 记录在mag中每个字母出现的次数,对应的数组位置++
  4. 遍历ran,遇到对应的数组位置–
  5. 如果有一个位置的值小于0则返回false
    代码:
    public static boolean canConstruct(String ransomNote, String magazine) {
        int len1 = ransomNote.length();
        int len2 = magazine.length();
        if (len1 > len2) return false;
        int[] find = new int[26];
        char[] mag = magazine.toCharArray();
        char[] ran = ransomNote.toCharArray();
        for (char a : mag){
            find[a - 'a']++;
        }
        for (char b : ran){
            find[b - 'a']--;
            if (find[b - 'a'] < 0){
                return false;
            }
        }
        return true;
    }

15. 三数之和

基本思路:
其实这道题目使用哈希法并不十分合适,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。

而且使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在leetcode上通过,但是程序的执行时间依然比较长 。

使用另一个解法:双指针法,这道题目使用双指针法 要比哈希法高效一些
在这里插入图片描述

  1. 拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。

  2. 依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i] b = nums[left] c = nums[right]。

  3. 接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。

  4. 如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。

代码:

    public static List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);
        for (int i = 0; i < nums.length; i++){
            //因为已经排好序了,当开始的时候就大于0,直接可以返回现在的结果
            if (nums[i] > 0) return res;
            //去重,不要nums[i] == nums[i + 1],会遗掉-1,-1,2
            if (i > 0 && nums[i] == nums[i - 1]){
                continue;
            }
            int left = i + 1;
            int right = nums.length - 1;
            while (right > left){
                int sum = nums[i] + nums[left] + nums[right];
                if (sum > 0){
                    right--;
                }else if (sum < 0){
                    left++;
                }else {
                    //先加入,再去重
                    res.add(Arrays.asList(nums[i], nums[left], nums[right]));
                    //去重,遇到原来相同的值移动
                    while (right > left && nums[right] == nums[right - 1]) right--;
                    while (right > left && nums[left] == nums[left + 1]) left++;
                    right--;
                    left++;
                }
            }
        }
        return res;
    }

18. 四数之和

基本思路:
四数之和,和15.三数之和 (opens new window)是一个思路,都是使用双指针法, 基本解法就是在15.三数之和 (opens new window)的基础上再套一层for循环。

四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下表作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n^2),
四数之和的时间复杂度是O(n^3) 。

那么一样的道理,五数之和、六数之和等等都采用这种解法。
代码:

    public static List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(nums);
        for (int i = 0; i < nums.length; i++) {
            //下面这个需要取消,比如遇到 -5 -2 -1   target 为 -8 就会一上来就否了,
            //主要原因是target可能是负值,正数越加越大,负数越加越小
            /*
            if (nums[i] > target) {
                System.out.println(nums[i]);
                return res;
            }
             */
            //去重
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            for (int j = i + 1; j < nums.length; j++) {
                //去重
                if (j > i + 1 && nums[j] == nums[j - 1]){
                    continue;
                }
                int left = j + 1;
                int right = nums.length - 1;
                while (right > left) {
                    int sum = nums[i] + nums[j] + nums[left] + nums[right];
                    if (sum > target) {
                        right--;
                    } else if (sum < target) {
                        left++;
                    } else {
                        //先加入,再去重
                        res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
                        //去重,遇到原来相同的值移动
                        while (right > left && nums[right] == nums[right - 1]) right--;
                        while (right > left && nums[left] == nums[left + 1]) left++;
                        right--;
                        left++;
                    }
                }
            }
        }
        return res;
    }

更进一步,充分的剪枝,时间更快

class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> quadruplets = new ArrayList<List<Integer>>();
        if (nums == null || nums.length < 4) {
            return quadruplets;
        }
        Arrays.sort(nums);
        int length = nums.length;
        for (int i = 0; i < length - 3; i++) {
            if (i > 0 && nums[i] == nums[i - 1]) {
                continue;
            }
            if ((long) nums[i] + nums[i + 1] + nums[i + 2] + nums[i + 3] > target) {
                break;
            }
            if ((long) nums[i] + nums[length - 3] + nums[length - 2] + nums[length - 1] < target) {
                continue;
            }
            for (int j = i + 1; j < length - 2; j++) {
                if (j > i + 1 && nums[j] == nums[j - 1]) {
                    continue;
                }
                if ((long) nums[i] + nums[j] + nums[j + 1] + nums[j + 2] > target) {
                    break;
                }
                if ((long) nums[i] + nums[j] + nums[length - 2] + nums[length - 1] < target) {
                    continue;
                }
                int left = j + 1, right = length - 1;
                while (left < right) {
                    int sum = nums[i] + nums[j] + nums[left] + nums[right];
                    if (sum == target) {
                        quadruplets.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
                        while (left < right && nums[left] == nums[left + 1]) {
                            left++;
                        }
                        left++;
                        while (left < right && nums[right] == nums[right - 1]) {
                            right--;
                        }
                        right--;
                    } else if (sum < target) {
                        left++;
                    } else {
                        right--;
                    }
                }
            }
        }
        return quadruplets;
    }
}

总结

数组作为哈希表:
题目只包含小写字母,那么使用数组来做哈希最合适不过。而这种情况使用map的空间消耗要比数组大一些,因为map要维护红黑树或者符号表,而且还要做哈希函数的运算。所以数组更加简单直接有效。

set作为哈希表:
数组的大小是有限的,受到系统栈空间(不是数据结构的栈)的限制。
如果数组空间够大,但哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。

map作为哈希表:
数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。
set是一个集合,里面放的元素只能是一个key,而例如两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。
map是一种<key, value>的结构,可以用key保存数值,用value在保存数值所在的下表。所以使用map最为合适。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值