羊羊刷题笔记Day06/60 | 第三章 哈希表P1 | Java中哈希表理论 242.有效字母异位词、349. 两个数组交集、202. 快乐数、1. 两数之和

134 阅读5分钟

哈希表理论基础

哈希表

首先什么是 哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。 哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示: image.png

哈希碰撞

如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞image.png 一般哈希碰撞有两种解决方法, 拉链法线性探测法

拉链法(使用链表)

刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了 image.png (数据规模是dataSize, 哈希表的大小为tableSize) 其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。

线性探测法(跳位)

使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。 例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示: image.png 其实关于哈希碰撞还有非常多的细节,感兴趣的同学可以再好好研究一下,这里我就不再赘述了。

🔴常见的三种哈希结构

当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。

  • 数组
  • set (集合)
  • map(映射)

简洁版: 在Java中,有

  • 单列集合:Set:HashSet、LinkedHashSet、TreeSet
  • 双列集合:Map:HashMap、LinkedHashMap、TreeMap

即分为普通Hash、Linked以及Tree

⭕Hash

  • 底层基于哈希表实现数组 + 链表 or 红黑树(数组长度到64)
  • 特性:存取不一致、不重复、无索引(没有for)

⭕LinkedHash

  • 在Hash基础上,每个键值对元素又额外多一个**双链表 **记录存储顺序
  • 特性:存取一致、不重复、无索引

⭕Tree

  • 底层基于红黑树实现排序,增删改查性能好
  • 特性:可排序、不重复、无索引

✅使用场景:

  • 元素可重复:ArrayList 基于数组 (用的最多)

  • 元素可重复,且增删明显比查询多:LinkedList 基于链表

  • 元素要去重:HashSet 基于哈希表**(用的最多)**

  • 元素要去重,且要保证存取顺序:LinkedHashSet 基于哈希表和双链表,但效率较低

  • 元素要去重,且要进行排序:TreeSet 基于红黑树

详细版见导图以及Java笔记~

总结

总结一下, **当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法**。 但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。 如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法!

242 有效的字母异位词

自己写

思路:把string一个一个拆掉放进HashMap里计数,对比另一个字符串

public boolean isAnagram(String s, String t) {
            HashMap<Character, Integer> hm = new HashMap<>();
            // 放进map里
            for (int i = 0; i < s.length(); i++) {
                if (hm.containsKey(s.charAt(i))) {
                    Integer num = hm.get(s.charAt(i));
                    num++;
                    hm.put(s.charAt(i), num);
                } else {
                    hm.put(s.charAt(i), 1);
                }
            }

            // 覆盖原Map
            for (int i = 0; i < t.length(); i++) {
                if (hm.containsKey(t.charAt(i))) {
                    Integer num = hm.get(t.charAt(i));
                    num--;
                    hm.put(t.charAt(i), num);
                } else {
                    return false;
                }
            }


            // 检查是否超标
            for (Character character : hm.keySet()) {
                if(hm.get(character) != 0){
                    return false;
                }
            }

            return true;
}

看视频

由于26个字母有限个个数,可以采用数组提高效率 哈希表数据结构:视情况而定,给出大部分适用情况

  • 数组(个数较少且确定情况)
  • Set(无重复) - HashSet
  • Map(有Key Value二元关系) - HashMap

代码:

public boolean isAnagram(String s, String t) {
	// 创造数组,赋值
	int[] letter = new int[26];
	for (int i = 0; i < letter.length; i++) {
	    letter[i] = 0;
	}
	
	// 放s进去
	for (int i = 0; i < s.length(); i++) {
	    // 数组括号内为计算字母在数组里面的映射
	    letter[s.charAt(i) - 'a']++;
	}
	
	// 用t覆盖
	for (int i = 0; i < t.length(); i++) {
	    letter[t.charAt(i) - 'a']--;
	}
	
	// 检查数组
	for (int i : letter) {
	    if (i != 0){
	        return false;
	    }
	}

	return true;
}

349 两个数组的交集

自己写

思路:本题不考虑重复元素,因此选用Set元素不重复效率更高,用Set记录对应数字并选出相同元素

  • 在HashSet的**Value转换到int[]**花费一些时间

✅可以使用流转换 / 笨方法(由于Set没有索引,即没有fori循环,设置计时器控制对数组赋值)

代码:

public int[] intersection(int[] nums1, int[] nums2) {

            // 转成int[]花费一些时间
            HashSet<Integer> hs = new HashSet<>();
            HashSet<Integer> resultHs = new HashSet<>();

            for (int i : nums1) {
                hs.add(i);
            }

            for (int i : nums2) {
                if (hs.contains(i)){
                    resultHs.add(i);
                }
            }

            // 用流将Set转换为Int类型并转换为数组
            return resultHs.stream().mapToInt(x -> x).toArray();
        }

202 快乐数

自己写

思路:将n每个数字抽取出来相乘,如果不是 1 则继续循环。

笨方法:循环结束条件设置为一个较大的数(9999),没能理解题目“无限循环”意思,没想出循环条件

public boolean isHappy(int n) {
            ArrayList<Integer> list = new ArrayList<>();
            int sum = n;
            int count = 0;

            while (sum != 1) {
                count++;
                if (count > 9999) {
                    return false;
                }
                
                while (sum > 0) {
                    list.add(sum % 10);
                    sum = sum / 10;
                }

                for (Integer integer : list) {
                    sum = sum + integer * integer;
                }

                list.clear();
            }
            return true;
}

看视频

循环结束:循环结束条件:有重复的sum - n有重复 - 每次记录n并寻找是否有重复 - HashSet

public boolean isHappy(int n) {
    // 循环结束条件:有重复的sum - 每次sum记录并寻找是否有重复 - n有重复 - HashSet
    HashSet<Object> hs = new HashSet<>();
	while (n != 1 && !hs.contains(n)) {
    	hs.add(n);
   	n = getNextNumber(n);
	}
	return n == 1;
}

private int getNextNumber(int n) {
    int sum = 0;
    while (n > 0) {
        int i = n % 10;
        sum = sum + i * i;
        n = n / 10;
    }
    return sum;
}

1 两数之和(梦开始的地方😭)

自己写

没有思路,只会一开始的双层循环,时间复杂度为O(n2) 结合哈希表特征(是否在集合内存在元素)不能想出本题答案

看视频

在两层循环中最根源的,第一层循环 i 确定一个数,那么第二层循环就是找匹配的数(即 target - i ) 此时就能结合哈希表特征想到寻找之前遍历元素是否存在某个数

选哈希数据结构:由于该题需要对比相加是否target且需要返回对应下标,存在二元关系,选用HashMap

思路:逐个遍历数组元素并记录在HashMap,如果存在符合条件数就返回数组 代码:

public int[] twoSum(int[] nums, int target) {
    HashMap<Integer, Integer> hm = new HashMap<>();

    // 遍历数组
    for (int i = 0; i < nums.length; i++) {
        if (!hm.containsKey(target - nums[i])){
            // 没有匹配则记录到hm
            hm.put(nums[i], i);
        }
        else {
            // 找到匹配则返回数组
            int[] ints = new int[2];
            ints = new int[]{hm.get(target - nums[i]), i};
            return ints;
        }
    }

    return null;

注:学习资料:

哈希表理论基础

242 有效的字母异位词

349 两个数组的交集

202 快乐数

1 两数之和😭梦开始的地方