Carl代码随想录算法训练营-Day 6-哈希表理论基础、242.有效的字母异位词、349.两个数组的交集、202.快乐数、1.两数之和

本文详细介绍了哈希表的基础理论,包括哈希表的原理、JavaHashMap的实现以及如何解决哈希碰撞。并通过LeetCode题目实例(有效字母异位词、两个数组的交集、快乐数和两数之和)展示了哈希表在算法中的应用,强调了哈希法的时间优势。
摘要由CSDN通过智能技术生成

摘要

本文是参加算法学习的日常记录,分析总结了哈希表基础理论的一些要点,详细阐述了四道基础的哈希表leetcode算法题目(有效字母异位词、两个数组的交集、快乐数、两数之和)的解题思路,并给出了对应的Java代码实现。

哈希表基础理论

哈希表的理论思维其实就在于将Key作为数组下标进行搜索。
我们知道数组的物理空间在内存中是连续的,因而访问任意数组下标,Java虚拟机仅需计算数组起始地址+索引*数组内元素占用空间大小,就可以获得需要访问的索引的物理地址,这个过程显然是非常快的,可以达到O(1)的时间复杂度。
而哈希表的一般实现方式是数组+链表,其中链表是为了解决哈希碰撞而使用的内层数据结构。因而,哈希表的随机存取时间复杂度也可以达到O(1)
哈希表的核心,当然是哈希算法,它接收任何一组任意长度的输入信息,通过哈希算法变换成固定长度的数据指纹输出形式,如字母和数字的组合,该输出就是“哈希值”。这个哈希值通过一些计算之后,就能作为数组的下标。
Java中最常见的哈希表实现是HashMap,它的每一个元素都是一个key-value键值对Entry,存储在一个Entry数组当中,存入之前先通过以下算式计算其应该存入的数组位置:

(h = key.hashCode()) ^ (h >>> 16)

这样计算出的数组下标出现重复,也就是哈希碰撞,的概率是极低的。只有当存入元素的增多,特别是变态多(甚至超过数组下标范围的元素个数),其存取效率才会变低。
当然,因为底层是数组实现的原因,哈希表实际上是用了空间换时间,在空间复杂度方面有一定的牺牲。
在实际的算法学习中,能用哈希法实现的算法题目,还是要具体问题具体分析的。如果key的取值范围有限的,或许定义一个定长的数组作为哈希表即可;如果key的取值范围特别大的,只能直接用HashMap或者HashSet这种库数据结构。
另外一方面,我们需要清楚,哈希表的一个重要特性,那就是无序,如果算法要求有序,那么就不要使用哈希表,否则只会浪费扣脑袋的时间。另一个重要特性是,不重复,key在哈希表中是不能重复出现的,会被覆盖。

242、有效的字母异位词

LeetCode题目链接

思路分析

这道题显然有两种解法。
一种是排序法。如果是字母异位词,排序后将相等。所以对两个字符串进行排序,逐字符进行比较即可。
另一种是计数法。更为直观,要求每个字符出现次数一样,所以对一个字符串中各字符的出现次数进行统计,然后去比较另一个字符串中各个字符的出现字数即可。
排序法,最快的排序法时间复杂度O(2nlogn),加上之后的字符遍历比较O(n),是O(nlogn)级别。由于没有用额外的空间,空间复杂度O(1)
计数法,需要一个哈希表来存储字符出现次数,空间复杂度O(n);时间复杂度方面,需要遍历两次字符串,一次是累加计数O(n),一次是累减计数O(n),最后还要遍历一次哈希表最快O(26),因此时间复杂度O(n)
而这道题哈希表的key范围是固定的,就是小写字母集,因此不需要HashMap,仅需一个长度为26的字符数组作为哈希表即可。

代码实现

//计数法
public boolean isAnagram(String s, String t) {
            if (s.length() != t.length()) {
                return false;
            }
            int[] map = new int[26];
            for (int i = 0; i < s.length(); i++) {
                map[s.charAt(i) - 97]--;//s字符次数计数
            }
            for (int i = 0; i < t.length(); i++) {
                int i1 = t.charAt(i) - 97;
                if (map[i1] == 0) {//剪枝操作
                    return false;
                }
                map[i1]++;//t字符次数反计数
            }
            for (int i : map) {
                if (i != 0) return false;
            }
            return true;
        }
//排序法
public boolean isAnagram1(String s, String t) {
            if (s.length() != t.length()) {
                return false;
            }
            char[] sCharArray = s.toCharArray();
            char[] tCharArray = t.toCharArray();
            Arrays.sort(sCharArray);
            Arrays.sort(tCharArray);
            for (int i = 0; i < sCharArray.length; i++) {
                if (sCharArray[i] != tCharArray[i]) {
                    return false;
                }
            }
            return true;
        }

349、两个数组的交集

LeetCode题目链接

思路分析

根据这道题的题目要求,显然只要一个数字在nums1nums2中都出现了,就可以直接加入输出答案中。因此,我们需要一个额外空间来统计nums1中各元素的出现情况。由于要求要去重,因此我们不需要计数,只需要关心是否出现过就行了。
所以,可以采用一个哈希表,key存储nums1中的数字,value存储对应的key是否出现过。另一方面,我们注意到条件1 <= nums1.length, nums2.length <= 1000,这意味着我们可以采用一个长度为1001的布尔数组map即可。
因此,第一步是将map中下标为nums1中的元素的位置设为
第二步是遍历nums2中的元素,查询map中对应下标的位置是否为。如果是,就将该元素加入输出答案中,并且将map对应位置设为

代码实现

public int[] intersection(int[] nums1, int[] nums2) {
            ArrayList<Integer> ans = new ArrayList<>();
            boolean[] map = new boolean[1001];
            for (int i : nums1) {
                map[i] = true;//记录nums1[i]出现过
            }
            for (int i : nums2) {
                if (map[i]) {//nums2[i]在nums1中出现过
                    ans.add(i);
                    map[i] = false;//记录nums2[i]加过了
                }
            }
            int[] res = new int[ans.size()];//转化为int[]
            for (int i = 0; i < res.length; i++) {
                res[i] = ans.get(i);
            }
            return res;
        }

202、快乐数

LeetCode题目链接

思路分析

这道题首先要实现的就是将给的数n按照个位十位百位分解,并且计算每一位的平方之和n1
接着是将n1进行相同的分解操作得到n2···,直到得到的nm1
那么无限循环始终不到1的情况是什么样呢?如果将nn1n2···nm看作一条“链表”,那么无限循环就是“链表”成环了,也就是说出现过的nm,又被计算得到了。如果不循环,这条“链表”的尾部节点就是1
因此,我们仍然可以使用哈希法来做这道题,将出现过的元素记录一下,每次将计算得到nm时比较是否被记录过,如果是,则不是快乐数字,否则继续计算直到1出现。
最后,我们发现这道题的条件是1 <= n <= 2^31- 1,因此无法直接使用数组作为哈希表,只能用HashMap或者HashSet

代码实现

public boolean isHappy(int n) {
            Map<Integer, Boolean> map = new HashMap<>();
            int count = 0;
            while (map.getOrDefault(n, true)) {
                map.put(n, false);
                n = fact(n);
                count++;
                if (n == 1) {
                    return true;
                }
            }
            return false;
        }
        private int fact(int n) {
            int sum = 0;
            while (n != 0) {
                int tmp = n / 10;
                int num = n - 10 * tmp;
                sum += num * num;
                n = tmp;
            }
            return sum;
        }

1、两数之和

LeetCode题目链接

思路分析

这道题虽然放在第1题,而且是easy,但是如果不清楚哈希法这种做法,还是很难想到的。
哈希法解这道题的精髓在于,从哈希表map中搜索target - nums[i],如果不存在这个下标就将nums[i]加入其中,相对于暴力搜索法的O(n^2),这种哈希表搜索法的时间复杂度O(1)是碾压性的。
对于第一次刷leetcode算法题的初学者来说,这道题在算法思维的塑造上有重大意义,不愧是梦开始的地方!

代码实现

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

总结和思考

哈希表的三大特性:无序无重复O(1)。这三点,是限制也是优势,只要合理运用,就可以降低我们的算法复杂度。
哈希法在计数等方面有很明显的时间优势,用空间换时间,现代的计算机技术已经使得空间不再那么有限、宝贵,对时间的需求远大于对空间的需求,因此哈希表的广泛应用符合计算机发展规律。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值