哈希相关LeetCode真题题解思路

目录

1. 哈希表的概念

2. LeetCode真题之两数之和

01 暴力解法——枚举

02 哈希表

补充知识:HashMap(或哈希表)的大小通常为2^n的原因

3. LeetCode真题之无重复字符的最长子串

01 滑动窗口+哈希表

4. LeetCode真题之字母异位词分组

 01 排序+哈希表

02 计数+哈希表

5. LeetCode真题之存在重复元素

01 哈希表/暴力解法

补充知识:

01 HashSet和HashMap的区别

02 HashSet的优缺点

03 HashMap的优缺点 

04 HashSet和HashMap的适用场景 


1. 哈希表的概念

散列表(Hash table,也叫哈希表),是根据关键码值(key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

即:给每份数据分配一个编号,放入表格(数组)。通过建立编号与表格索引的关系,将来就可以通过编号快速查找数据。

(1)理想情况编号当是唯一的,数组能够容纳所有数据

(2)现实是不能说为了容纳所有数据造一个超大数组,编号也有可能重复

解决:(1)有限长度的数组,以【拉链】(链表)方式存储数据;(2)允许编号适当重复,通过数据自身来进行区分。

2. LeetCode真题之两数之和

热题100——两数之和

题目描述:

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

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

你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]

提示:

  • 2 <= nums.length <= 104
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109
  • 只会存在一个有效答案

进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗?

01 暴力解法——枚举

思路:先枚举一个数x,然后寻找数组中是否存在target-x。使用两层for循环对nums数组进行遍历,第一层for循环固定初始位置,即第一个数x,从左往右进行遍历,第二层for循环用于寻找与第一层for循环对应变量的满足条件的值,即第二个数target-x,如果有就返回,没有则继续进行遍历。

Java代码实现

class Solution {
    public int[] twoSum(int[] nums, int target) {
        int len = nums.length;
        // 定义数组存储要返回的结果
        int[] result = new int[2];
        for (int i = 0; i < len; i++) {
            // 定义要查询的配对变量
            int tar = target - nums[i];
            for(int j = i + 1; j < len; j++){
                if(nums[j] == tar){
                    result[0] = i;
                    result[1] = j;
                    return result;
                }
            }
        }
        return null;
    }
}
02 哈希表

思路:先循环遍历数组,获取每个数字x,然后以target - x作为key到hash表查找,若没找到,将x作为key,它的索引作为value放到hash表中,若找到了,返回x以及与它配对的索引。

对于每一个x,先查询hash表中是否存在target - x,然后再将x插入到哈希表中,这样做可以保证不会让x与自己匹配。

Java代码实现

    class Solution {
        public static int[] twoSum(int[] nums, int target) {
            // 定义一个hash表,键和值都是整数类型
            HashMap<Integer, Integer> map = new HashMap<>();
            // 遍历数组,获取每个数字x
            for (int i = 0; i < nums.length; i++) {
                int x = nums[i];
                if (map.containsKey(target - x)) {
                    // 找到了,返回x以及与它配对的索引
                    return new int[]{i, map.get(target - x)};
                } else {
                    // 没找到,将x作为key,它的索引作为value放到hash表中
                    map.put(x, i);
                }
            }
            return new int[0];
        }
    }

补充知识:HashMap(或哈希表)的大小通常为2^n的原因

主要涉及以下几个方面:

  1. 哈希函数的效率和冲突减少:哈希表通过哈希函数将键映射到一个数组的索引上。当数组的大小是2^n时,哈希函数的计算和索引定位变得非常高效。假设数组大小为2^n,哈希函数生成的哈希值可以通过简单的按位操作来计算索引,例如,对于一个哈希值hash和数组大小N=2^n,索引计算可以是:index=hash&(N-1),这里按位与&操作,比除法和取模运算要高效得多。通过这种方式,可以有效地减少哈希冲突。
  2. 内存对齐和缓存友好:数组大小是2^n可以更好地利用内存对齐和缓存优化,现代计算机的内存和缓存系统通常是按块和对齐方式来设计的。当数组的大小是2^n时,数据结构会更好的符合硬件的内存对齐要求,从而提高内存访问速度。
  3. 简化计算和优化性能:使用2^n作为数组大小使得一些计算更为简单和高效。例如,计算哈希值的哈希表索引只需进行按位与操作,而不是求模,这在硬件上实现起来更快。这种优化有助于提高哈希表的整体性能,特别是在处理大量数据时。
  4. 均匀分布哈希值:哈希表的大小是2^n有助于均匀分布哈希值,减少哈希冲突。当哈希表的大小是2^n时,哈希值的低位比特更能均匀分布,从而减少了哈希冲突的可能性。均匀的哈希值分布有助于保持哈希表的性能稳定。

总结:哈希表的大小通常是2^n的原因在于提高哈希函数的效率、优化内存对齐和缓存友好性、简化计算以及减少哈希冲突。通过这些优化措施,哈希表在处理大量数据时能够提供更好的性能和更高的效率。

3. LeetCode真题之无重复字符的最长子串

热题100——无重复字符的最长子串

题目描述:

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是"abc",所以其长度为 3。

示例 2:

输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是"b",所以其长度为 1。

示例 3:

输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是"wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

  • 0 <= s.length <= 5 * 104
  • s 由英文字母、数字、符号和空格组成
01 滑动窗口+哈希表

思路1:根据题中给出的实例,我们可以从左到右一一列举符合条件的结果,从第一个位置a开始最长不含有重复字符的字符串为abc,从b开始为bca,依次类推,可以发现如果我们依次递增枚举子串的开始位置,那么子串的结束位置也是递增的。原因为,如果选择字符串中的第m个位置为开始位置,得到一个不包含重复字符的最长子串的结束位置为n,那么选择第m+1个字符作为起始位置时,从m+1到n的字符很显然是不重复的,并且由于去掉了原本的第m个字符,可以尝试继续增大n,直到右侧出现了重复字符为止。很显然,可以利用滑动窗口的策略去解题。

  • 可以使用左右指针表示字符串中的某个子串的左右边界,其中左指针代表枚举的开始位置,右指针代表满足条件的最长子串的结束位置。
  • 在每一步的操作中,通过将左指针向右移动一格,表示每次枚举的起始位置,然后不断的移动右指针,但需要保证左右指针的区间中没有重复的字符。在右指针移动结束后,这个子串就对应以左指针开始,不包含重复字符的最长子串,记录其长度。
  • 枚举结束后,找到的最长子串的长度即为答案。

在以上的流程中,还有一个关键的点——判断重复字符 ,使用哈希集合来判断是否有重复字符,java中的HashMap和HashSet。

Java代码实现

 public static int lengthOfLongestSubString1(String s) {
        // 定义hash集合,记录字符是否出现过
        HashMap<Character, Integer> map = new HashMap<>();
        int result = 0;
        // begin 表示枚举的开始位置,end表示每次枚举要寻找的结束位置
        for (int begin = 0, end = 0; begin < s.length(); begin++) {
            // begin不为0,说明移动了初始位置,要将前面存在map集合中的字符删除
            if(begin != 0){
                // 初始指针向右移动一格,删除一个字符
                map.remove(s.charAt(begin - 1), begin - 1);
            }
            // map中不存在重复字符,不断的右移结束指针
            while (end < s.length() && !map.containsKey(s.charAt(end))) {
                map.put(s.charAt(end), end);
                end++;
            }
//            System.out.println(s.substring(begin, end));
            // 由于结束while循环时,end指针的位置是满足条件的子串的后一位,因此不需额外+1
            result = Math.max(result, end - begin);
        }
        return result;
    }

 思路1结果打印

思路2:从左到右依次访问字符串,比如题中的abcabcbb,初始进入这个窗口为abc时满足题目要求,当再进入a,窗口变成了abca。此时不满足要求,所以要对这个窗口进行移动,如何进行移动呢?只要把窗口的左边的元素移出来就可以了,直到满足题目的要求,一直维持这样的窗口,找出队列出现最长的长度即为答案。时间复杂度为O(n)。此时利用滑动窗口+哈希表,来实现,首先定义两个变量表示开始和结束位置,用hash表检查重复字符,然后从左向右查看每个字符,如果:  没遇到重复字符,调整end;遇到重复的字符,调整begin,并将当前字符放入hash表,end - begin + 1是当前子串的长度。

Java代码实现

/*
        实现要点:
            * 使用begin和end表示子串开始和结束位置
            * 用hash表检查重复字符
            * 从左向右查看每个字符,如果:
                - 没遇到重复字符,调整end
                - 遇到重复的字符,调整begin
                - 将当前字符放入hash表
            * end - begin + 1是当前子串的长度
     */
public static int lengthOfLongestSubString(String s){
        int begin = 0;
        HashMap<Character, Integer> map = new HashMap<>();
        int result = 0;
        for (int end = 0; end < s.length(); end++) {
            // 获取每个字符
            char ch = s.charAt(end);
            if(map.containsKey(ch)){
                // 防止begin回退,所以要比较一下
                begin = Math.max(begin, map.get(ch) + 1);
                map.put(ch, end);
            }else{
                map.put(ch, end);
            }
//            System.out.println(s.substring(begin, end + 1));
            result = Math.max(result, (end - begin + 1));
        }
        return result;
    }

 思路2 结果打印

4. LeetCode真题之字母异位词分组

题目描述:

给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。

字母异位词 是由重新排列源单词的所有字母得到的一个新单词。

示例 1:

输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]

输出: [["bat"],["nat","tan"],["ate","eat","tea"]]

示例 2:

输入: strs = [""]

输出: [[""]]

示例 3:

输入: strs = ["a"]

输出: [["a"]]

提示:

  • 1 <= strs.length <= 104
  • 0 <= strs[i].length <= 100
  • strs[i] 仅包含小写字母
 01 排序+哈希表

思路:根据题目可知,字母异位词,指的是字符串中包含的字母相同。最初看到这道题,想到的是获取每个字母的ascii码,然后相加;或者给从a~z的字母进行编号,然后求和,但是这样做会出问题,不同的字母组合之和可能是相同的。

由于互为字母异位词的两个字符串包含的字母相同,因此对两个字符串分别进行排序之后得到的字符串一定是相同的。因此,利用字符串中包含的字母相同的特点,使用哈希表存储每一组字母异位词,哈希表的键为一组字母异位词的标志,哈希表的值为一组字母异位词列表。

Java代码实现

class Solution {
    public List<List<String>> groupAnagrams(String[] strs) {
        // 定义hash集合,存储字母异位词,key为排序后的字母组合String类型,value为字母异位词List集合
        HashMap<String, List<String>> map = new HashMap<>();
        // 遍历字符串数组中的每个元素
        for(String str: strs){
            // 对字符串进行排序,得到hash中的key
            char[] chars = str.toCharArray();
            Arrays.sort(chars);
            // 将排序后的字符转换成String类型
            String key = new String(chars);
            // 获取key对应的values列表
            List<String> list = map.get(key);
            // 如果哈希表中没有对应的list
            if(list == null){
                // 创建一个新的空的集合
                list = new ArrayList<>();
                // 放入哈希表中
                map.put(key, list);
            }
            // 如果已经存在,加到对应的list集合中即可
            list.add(str);
        }
        return new ArrayList<>(map.values());
    }
}
02 计数+哈希表

思路:进一步优化代码,基于互为字母异位词的两个字符串包含相同的字符的特点,可知两个字符串中字母出现的次数应该是完全相同的,可以将每个字母出现的次数使用字符串来表示,作为哈希表的键。根据题目中说仅包含小写字母,可以使用长度为26的数组记录每个字母出现的次数。

Java代码实现

 // 由于hash表中的key,要求要重写hashCode的equals方法,整数数组不能直接作为hash表中的key
    static class ArrayKey {
        // 新建一个类封装整数数组
        int[] key = new int[26];

        // 重写hashCode的equals方法
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            ArrayKey arrayKey = (ArrayKey) o;
            return Arrays.equals(key, arrayKey.key);
        }

        @Override
        public int hashCode() {
            return Arrays.hashCode(key);
        }

        // 把字符串变成整数数组,即key
        public ArrayKey(String str) {
            for (int i = 0; i < str.length(); i++) {
                char ch = str.charAt(i);
                key[ch - 'a']++;
            }
        }

    }

    // 计数加hash表
    public static List<List<String>> groupAnagrams(String[] strs) {
        // 定义hash表,key为定义的类中的ArrayKey类型
        HashMap<ArrayKey, List<String>> map = new HashMap<>();
        // 遍历数组中的每个元素
        for(String str: strs){
            // 获取每个字符串对应的key
            ArrayKey key = new ArrayKey(str);
            // 获取key对应的values列表
            List<String> list = map.get(key);
            if(list == null){
                // 创建一个新的空的集合
                list = new ArrayList<>();
                // 放入哈希表中
                map.put(key, list);
            }
            // 如果已经存在,加到对应的list集合中即可
            list.add(str);
        }
        return new ArrayList<>(map.values());
    }

5. LeetCode真题之存在重复元素

leetCode217——存在重复元素

题目描述:

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

示例 1:

输入:nums = [1,2,3,1]
输出:true

示例 2:

输入:nums = [1,2,3,4]
输出:false

示例 3:

输入:nums = [1,1,1,3,3,4,3,2,4,2]
输出:true

提示:

  • 1 <= nums.length <= 105
  • -109 <= nums[i] <= 109
01 哈希表/暴力解法

思路:这题是leetcode真题中的简单题,思路也比较简单,使用hash表(hashMap和hashSet都可)对整数数组中的重复元素进行判断即可。

暴力解法即使用两层for去判断,逐个比较是否有重复元素,如果存在返回true,不存在返回false,在此就不写实现代码了。官方题解还有一种先对数组进行排序,再判断相邻两个元素是否重复进行解题。

Java代码实现

public static boolean containsDuplicate(int[] nums){
        // 创建hashSet集合,用来判断是否存在重复元素
        HashSet<Integer> set = new HashSet<>();
        // 遍历数组中的元素
        for (int num: nums) {
            // 如果set集合中包含num
            if(set.contains(num)){
                // 存在重复元素返回true
                return true;
            }
            // 将元素添加到set中
            set.add(num);
        }
        // 遍历完成,说明不存在重复元素,返回false
        return false;
    }

 注意:这里还可以对代码进行优化,看以下add的源码,set的add方法是插入成功,返回true,否则返回false,可以利用这个特点替换contains方法进行判断

 更新后的代码实现:

class Solution {
    public boolean containsDuplicate(int[] nums) {
         // 创建hashSet集合,用来判断是否存在重复元素
        HashSet<Integer> set = new HashSet<>();
        // 遍历数组中的元素
        for (int num: nums) {
            // 插入失败说明已经存在重复元素,返回true
            if(!set.add(num)){
                return true;
            }
        }
        // 遍历完成,说明不存在重复元素,返回false
        return false;
    }
}

补充知识:

01 HashSet和HashMap的区别
  • 用途不同

        HashSet是一个基于哈希表的集合,用于存储不重复的元素,它不存储键值对。它实际上是基于HashMap实现的,之存储了键,值都设置为同一个特殊值(默认为null)HashMap也是一个哈希表的集合,用于存储键值对。允许根据键来查找值,因此在存储和检索键值对方面更加灵活。

  • 数据结构不同

        HashSet内部使用哈希表(或哈希集合)来存储元素。哈希表是一个无序的数据结构,元素之间没有特定的顺序。HashMap内部也使用哈希表,但它存储键值对,其中键和值之间有关联关系,HashMap具有键的集合和值的集合,键是唯一的,值可以重复。

  • 元素类型不同

        HashSet存储的是单一的元素类型,如整数、字符串等,用于存储不重复的对象,通过元素的哈希码来判断重复性。HashMap存储键值对,键和值可以是不同类型的对象,键用于检索值。

  • 方法不同

        HashSet提供了添加、删除、查找元素的方法,如add(), remove(), contains()等,没有提供根据键查找值的方法。HashMap提供了添加键值对、删除键值对、根据键查找值的方法,如put(), remove(), get()等,可以根据键来查找对应的值。

02 HashSet的优缺点

        优点:

  1. 唯一性:HashSet确保存储的元素不重复,适合用于去重
  2. 快速查找:HashSet提供了快速的元素查找,因为它使用哈希表
  3. 无序性:HashSet不保证元素的存储顺序,适合不需要关心顺序的场景

        缺点

  • 不支持键值对:HashSet只存储单一的元素类型,不支持键值对的存储
  • 无法存储关联数据:无法将额外的数据与元素关联,智能存储元素本身
03 HashMap的优缺点 

优点:

  1. 键值对存储:HashMap可以存储键值对,允许将关联数据存储在一起
  2. 快速查找:HashMap提供了快速的键查找值的能力,适合需要根据键查找值的场景
  3. 灵活性:HashMap提供了更多的功能,如替换值、遍历键值对等

        缺点

  • 复杂性:相对于HashSet,HashMap的使用可能更加复杂,因为它需要处理键值对的关系
  • 额外的内存消耗:HashMap存储键值对,因此需要额外的内存空间
04 HashSet和HashMap的适用场景 
  • HashSet的适用场景

        数据去重:当需要存储一组数据,但不关心顺序和关联信息,只关心数据是否重复时,使用HashSet是合适的,例如,存储一组唯一的用户名或标签。

        集合运算:HashSet适合用于集合运算,如求交集、并集、差集等。

  •  HashMap的适用场景

        键值存储:当需要将数据与关联的键一起存储时,使用HashMap是合适的。例如,存储学生的成绩,其中学生名是键,成绩是值。

        数据索引:HashMap适合用于构建索引,提供快速查找能力,例如,建立一个电话簿,根据姓名查找电话号码。

        需要键值对的功能:如果你需要存储关联数据,并且需要使用键来查找值、替换值或遍历键值对,那么HashMap是最好的选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值