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

什么时候想到用哈希法,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法

基础知识

基础知识参考代码随想录:文章讲解

charAt()

Java String类中 charAt() 方法用于返回指定索引处的字符。索引范围为从 0 到 length() - 1。

for(int a:i)

这种有冒号的for循环叫做 foreach循环,foreach 语句是 java5 的新特征之一。

1 for(元素类型t 元素变量x : 遍历对象obj){
2   引用了x的java语句;
3 }

哈希表

哈希表 / 散列表(hash table)是根据关键码的值而直接进行访问的数据结构。

直白来讲其实数组就是一张哈希表。哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:

一般哈希表都是用来快速判断一个元素是否出现集合里。

例如要查询一个元素是否在数组里,要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。只需要在查询的时候通过索引直接就可以知道元素在不在数组里了。

将元素映射到哈希表上就涉及到了hash function ,也就是哈希函数

哈希函数

哈希函数如下图所示,通过 hashCode 把列表元素转化为数值,一般 hashcode 是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把元素映射为哈希表上的索引数字了。

有两种哈希函数方法比较普遍:

直接定制法
  • 取关键字的某个线性函数为散列地址:Hash(Key)= A*Key + B

  • 优点:简单、均匀 ;

  • 缺点:需要事先知道关键字的分布情况

  • 使用场景:适合查找比较小且连续的情况

除留取余法
  • 设散列表中允许的地址数为m,取一个不大于m,但最接近或者等于m的质数p作为除数,按照哈希函数: Hash(key) = key% p(p<=m),将关键码转换成哈希地址。

为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作。

但如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置,即发生哈希碰撞现象。

哈希碰撞

如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞

一般哈希碰撞有两种解决方法, 拉链法和线性探测法。

拉链法

将发生冲突的元素都存储在链表中。

(数据规模是 dataSize, 哈希表的大小为 tableSize )

拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。

线性探测法

使用线性探测法,一定要保证 tableSize 大于 dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题

例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放冲突的数据了。如图所示:

常见的三种哈希结构

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

  • 数组

  • set (集合)

  • map (映射)

总结

总结一下,当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法

但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。

Java 补充知识

集合

学习参考文章1文章2

概念

集合是 java 中提供的一种容器,可以用来存储多个数据。

集合中的元素实际上是对象,一些常见的基本类型可以使用它的包装类。

基本类型对应的包装类表如下:

基本类型引用类型
booleanBoolean
byteByte
shortShort
intInteger
longLong
floatFloat
doubleDouble
charCharacter
集合与数组区别
  • 数组的长度是固定的。集合的长度是可变的。

  • 数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象。而且对象的类型可以不一致。在开发中一般当对象多的时候,使用集合进行存储。

分类

集合按照其存储结构可以分为两大类,分别是单列集合java.util.Collection双列集合java.util.Map

Collection

单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是 java.util.List 和java.util.Set,在使用前记得引入。其中,List 的特点是元素有序、元素可重复。Set 的特点是元素无序,而且不可重复。List 接口的主要实现类有 java.util.ArrayList 和 java.util.LinkedList,Set 接口的主要实现类有 java.util.HashSet和java.util.TreeSet。

Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(List 和 Set)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:

  • public boolean add(E e): 把给定的对象添加到当前集合中 。

  • public void clear() :清空集合中所有的元素。

  • public boolean remove(E e): 把给定的对象在当前集合中删除。

  • public boolean contains(E e): 判断当前集合中是否包含给定的对象。

  • public boolean isEmpty(): 判断当前集合是否为空。

  • public int size(): 返回集合中元素的个数。

  • public Object[] toArray(): 把集合中的元素,存储到数组中。

List 集合

java.util.List 接口继承自 Collection 接口,习惯性地会将实现了 List 接口的对象称为 List 集合。

这类集合具有以下特点:

  • List集合中的元素是以线性方式进行存储的,基于不同的实现类有不同的表现形式(数组/链表)

  • List集合中的元素是带索引的,可以通过索引来访问集合中的指定元素

  • List集合中的元素是有序的,即元素的存入顺序和取出顺序一致。

  • List 集合中的元素是可重复的,允许出现重复的元素,可以通过元素的 equals() 方法比较两个元素是否重复

List集合特有方法均与索引相关:

  • public void add(int index, E element):将指定的元素,添加到该集合中的指定位置上。

  • public E get(int index):返回集合中指定位置的元素。

  • public E remove(int index):移除列表中指定位置的元素, 返回的是被移除的元素。

  • public E set(int index, E element):用指定元素替换集合中指定位置的元素,返回值的更新前的元素。

List 接口的实现类

  • java.util.ArrayList 底层采用数组实现,查询快,增删慢

    List<String> list = new ArrayList<String>();//(多态表示法)
  • java.util.LinkedList 底层采用双向链表实现,查询慢,增删快

    LinkedList<String> link = new LinkedList<String>();
set 集合

java.util.Set 接口和 java.util.List 接口一样,同样继承自 Collection 接口,它与 Collection 接口中的方法基本一致,并没有对 Collection 接口进行功能上的扩充,只是比 Collection 接口更加严格了。

与 List 接口不同的是, Set 接口中元素无序,并且都会以某种规则保证存入的元素不出现重复

Set 集合有多个子类,这里我们介绍其中的 java.util.HashSet 、 java.util.LinkedHashSet 这两个集合。

HashSet 集合

创建:

HashSet<String> set = new HashSet<String>();

java.util.HashSet 是 Set 接口的一个实现类,HashSet 允许为null,但仍不允许有重复的值,并且元素都是无序的(即存取顺序不一致)。 java.util.HashSet 底层的实现其实是一个 java.util.HashMap 支持

HashSet 是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存取和查找性能。保证元素唯一性 的方式依赖于: hashCode 与 equals 方法。

HashSet 底层是由一个哈希表结构 -> (数组+链表+红黑树(JDK1.8 增加了红黑树部分))实现的,当出现哈希冲突时,相同哈希值的元素采用链表结构存储,当同一哈希值的冲突元素超过 8 个时,改为红黑树存储。

在HashSet下面有一个子类 java.util.LinkedHashSet ,它是链表和哈希表组合的一个数据存储结构。多出一个链表来保证元素有序。

  • 创建对象LinkedHashSet<String> linked = new LinkedHashSet<String>();

  • 使用 add() 方法添加元素;

  • 使用 contains() 方法来判断元素是否存在于集合当中;

  • 使用 remove() 方法来删除集合中的元素;

  • 使用 clear 方法删除集合中所有元素

  • 使用 size() 方法计算 HashSet 中的元素数量

  • 使用 for-each 来迭代 HashSet 中的元素

Map

学习参考文章1文章2

Map集合是双列集合的顶层接口,它是用来存储键值对的,其中键具有唯一性,而值是可以重复的。即:Map集合的数据结构只针对于键有效。

因为Map是接口,不能通过new关键字直接创建它的对象,为我们可以通过多态的形式,创建其子类对象,从而实现创建Map集合对象的这个需求,常用子类主要有两个,分别是:HashMap 和 TreeMap

创建对象示例:

Map<String,String> map = new HashMap<String,String>();
Map集合的成员方法
  • V put (K key, V value):添加元素

  • V remove(Object key):根据键删除键值对元素

  • void clear():移除所有的键值对元素

  • boolean containsKey(Object key):判断集合是否包含指定的键

  • boolean containsValue(Object value):判断集合是否包含指定的值

  • boolean isEmpty():判断集合是否为空

  • int size():集合的长度,也就是集合中键值对的个数据

Map集合的获取功能
  • V get(Object key):根据键获取值

  • Set keySet():获取所有键的集合

  • Collection values():获取所有值的集合

刷题

242.有效的字母异位词

题目链接 | 文章讲解 | 视频讲解

题目:给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。

示例 1: 输入: s = "anagram", t = "nagaram" 输出: true

示例 2: 输入: s = "rat", t = "car" 输出: false

说明: 你可以假设字符串只包含小写字母。

思路及实现

这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。

  • 定一个数组叫做 record,大小为26 ,初始化为0,因为字符 a 到字符 z 的ASCII 也是26个连续的数值。

  • 遍历字符串 s 的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符 a 的ASCII,只要求出一个相对数值即可。

  • 遍历字符串 t 的时候,对 t 中出现的字符映射哈希表索引上的数值再做-1的操作。

  • 最后检查一下 record数组如果有的元素不为零0,说明字符串 s 和 t 一定是谁多了字符或者谁少了字符,return false;如果 record 数组所有元素都为零0,说明字符串 s 和 t 是字母异位词,return true。

代码如下:

class Solution {
    public boolean isAnagram(String s, String t) {
        int[] record = new int[26];
​
        for (int i = 0; i < s.length(); i++) {
            record[s.charAt(i) - 'a']++;// 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了
        }
​
        for (int i = 0; i < t.length(); i++) {
            record[t.charAt(i) - 'a']--;
        }
        
        for (int count: record) {
            if (count != 0) {// record数组如果有的元素不为零0,说明字符串s和t 字符不同。
                return false;
            }
        }
        return true;  // record数组所有元素都为零0,说明字符串s和t是字母异位词
    }
}

349. 两个数组的交集

题目链接 | 文章讲解 | 视频讲解

题目:给定两个数组,编写一个函数来计算它们的交集。

示例 1:

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

示例 2:

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的
思路及实现
1.使用HashSet
  • 将数组1转化为 HashSet

  • 遍历数组2的过程中判断哈希表中是否存在该元素

  • 最后将结果集合转为数组,或另外申请一个数组存放setRes中的元素,最后返回数组

代码如下:

import java.util.HashSet;
import java.util.Set;
​
class Solution {
    public int[] intersection(int[] nums1, int[] nums2) {
        if (nums1 == null || nums1.length == 0 || nums2 == null || nums2.length == 0) {
            return new int[0];
        }
        Set<Integer> set1 = new HashSet<>();
        Set<Integer> resSet = new HashSet<>();
        //遍历数组1
        for (int i : nums1) {
            set1.add(i);
        }
        //遍历数组2的过程中判断哈希表中是否存在该元素
        for (int i : nums2) {
            if (set1.contains(i)) {
                resSet.add(i);
            }
        }
      
        //方法1:将结果集合转为数组
​
        return resSet.stream().mapToInt(x -> x).toArray();
        
        //方法2:另外申请一个数组存放setRes中的元素,最后返回数组
        int[] arr = new int[resSet.size()];
        int j = 0;
        for(int i : resSet){
            arr[j++] = i;
        }
        
        return arr;
    }
}
2.使用hash数组

题目限制了数值的大小,所以可以使用数组来做哈希的题目。

题目要求:

  • 1 <= nums1.length, nums2.length <= 1000

  • 0 <= nums1[i], nums2[i] <= 1000

所以我们可定义两个长度大于1000的哈希数组,分别记录各数组元素,再遍历这两个哈希数组,当 hash1[i] > 0 && hash2[i] > 0 时,认为数组相交,并用 resList 记录,最后申请一个数组存放resList中的元素,最后返回数组。

代码如下:

class Solution {
    public int[] intersection(int[] nums1, int[] nums2) {
        int[] hash1 = new int[1002];
        int[] hash2 = new int[1002];
        for(int i : nums1)
            hash1[i]++;
        for(int i : nums2)
            hash2[i]++;
        List<Integer> resList = new ArrayList<>();
        for(int i = 0; i < 1002; i++)
            if(hash1[i] > 0 && hash2[i] > 0)
                resList.add(i);
        int index = 0;
        int res[] = new int[resList.size()];
        for(int i : resList)
            res[index++] = i;
        return res;
    }
}
拓展

直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。

不要小瞧这个耗时,在数据量大的情况,差距是很明显的。

202. 快乐数

题目链接 | 文章讲解

题目:编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为 1,那么这个数就是快乐数。

如果 n 是快乐数就返回 True ;不是,则返回 False 。

示例:

输入:19 输出:true 解释: 1^2 + 9^2 = 82 8^2 + 2^2 = 68 6^2 + 8^2 = 100 1^2 + 0^2 + 0^2 = 1

思路及实现

题目中说了会 无限循环,那么也就是说求和的过程中,sum会重复出现,这对解题很重要!

当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。

所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。判断sum是否重复出现就可以使用HashSet。

我们知道1肯定是快乐数,所以我们可以以sum是否为1或sum重复出现来判断是否为快乐数。

求和中,可以通过取余得到末位数,再对数 n 进行 n / 10 操作,通过循环相加就可得到它它每个位置上的数字的平方和。

代码如下:

import java.util.Set;
import java.util.HashSet;
class Solution {
    public boolean isHappy(int n) {
        Set<Integer> recoder = new HashSet<>();
        while(n != 1 && !recoder.contains(n)){
            recoder.add(n);
            n = getNextNumber(n);
       }
        return n == 1;
    }
    private int getNextNumber(int n){
       int sum = 0;
       while(n > 0){
            int temp = n % 10;
            sum += temp * temp;
            n = n / 10;
        }
       return sum;
    }
}

1. 两数之和

题目链接 | 文章讲解 | 视频讲解

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

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。

示例:

给定 nums = [2, 7, 11, 15], target = 9

因为 nums[0] + nums[1] = 2 + 7 = 9

所以返回 [0, 1]

思路及实现

本题,我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,需要使用 key value结构来存放,使用map正合适

map目的用来存放我们访问过的元素,因为遍历数组的时候,需要记录我们之前遍历过哪些元素和对应的下标,这样才能找到与当前元素相匹配的(也就是相加等于target)。

接下来是map中 key 和 value 分别表示什么。

这道题我们需要给出一个元素,判断这个元素是否出现过,如果出现过,返回这个元素的下标。

那么判断元素是否出现,这个元素就要作为 key,所以数组中的元素作为 key,有 key 对应的就是 value,value用来存下标。

所以 map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。

在遍历数组的时候,只需要向 map 去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。

过程如下:

代码如下:

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

总结

今天学了好多新知识,哈希表,java中的集合,Conllection,HashSet,Map等,下来一定要好好复习呀!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值