Java 集合框架帮你搞通哈希表,掌握 Map 和 Set 的使用(内含哈希表源码解读及面试常考题)

Posted 吞吞吐吐大魔王

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java 集合框架帮你搞通哈希表,掌握 Map 和 Set 的使用(内含哈希表源码解读及面试常考题)相关的知识,希望对你有一定的参考价值。

文章目录

1. 搜索

1.1 场景引入

在学习编程时,我们常见的搜索方式有:

  • 直接遍历:时间复杂度为 O(N),元素如果比较多效率会非常慢
  • 二分查找:时间复杂度为 O(logN),搜索前必须要求序列有序

但是上述排序比较适合静态类型的查找,即一般不会对区间进行插入和删除操作。而现实中的查找如:

  • 根据姓名查询考试成绩
  • 通讯录(根据姓名查询联系方式)

可能在查找时需要进行一些插入和删除操作,即动态查找。因此上述排序的方式就不太适合。

本章将会介绍 Map 和 Set,它们是一种适合动态查找的集合容器或者数据结构。

1.2 模型

一般会把要搜索的数据称为关键字(Key),和关键字对应的称为值(Value),这两者又能组合成为 Key-Value 的键值对。

因此动态查找的模型有下面两种:

  1. 纯 Key 模型: Set 的存储模式,比如:

    • 有一个英文词典,快速查找一个单词是否在词典中
    • 快速查找某个名字在不在通讯录中
  2. Key-Value 模型: Map 的存储模式,比如:

    • 统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词, 单词出现的次数>
    • 梁山好汉的江湖绰号,每个好汉都有自己的江湖绰号:<好汉, 江湖绰号>

2. Map

2.1 关于 Map 的介绍

简单介绍:

Map 是一个接口类,没有继承自 Collection。该类中存储的是 <K, V> 结构的键值对,并且 K 一定是唯一的,不能重复

注意:

  • Map 接口位于 java.util 包中,使用前需要引入它
  • Map 是一个接口类,故不能直接实例化对象。如果要实例化对象只能实例化其实现类 TreeMap 或 HashMap
  • Map 中存放的键值对的 Key 是唯一的,Value 是可以重复的

文档信息:

2.2 关于 Map.Entry<K, V> 的介绍

简单介绍:

Map.Entry<K, V> 是 Map 内部实现的用来存放 <Key, Value> 键值对映射关系的内部类。该内部类中主要提供了 <Key, Value> 的获取,Value 的设置以及 Key 的比较方式

常用方法:

方法说明
K getKey()返回 entry 中的 key
V getValue()返回 entry 中的 value
V setValue(V value)将键值对中的 value 替换为指定的 value

注意:

Map.Entry<K, V> 并没有提供设置 Key 的方法

文档信息:

2.3 Map 的常用方法说明

方法说明
V get(Object key)返回 key 的对应 value
V getOrDefault(Object key, V defaultValue)返回 key 对应的 value,若 key 不存在,则返回默认值 defaultValue
V put(K key, V value)设置 key 的对应值为 value
V remove(Object key)删除 key 对应的映射关系
Set<K> keySet()返回所有 key 的不重复集合
Collection<V> values()返回所有 value 的可重复集合
Set<Map.Entry<K, V>> entrySet()返回所有的 key-value 映射关系
boolean containsKey(Object key)判断是否包含 keys
boolean containsValue(Object value)判断是否包含 value

注意:

  • 往 Map 中存储数据时,会先根据 key 做出一系列的运算,再存储到 Map 中
  • 如果 key 一样,那么新插入的 key 的 value,会覆盖原来 key 的 value
  • 在 Map 中插入键值对时,key 和 value 都可以为 null
  • Map 中的 key 可以全部分离出来,存储到 Set 中来进行访问(因为 key 是不能重复的)
  • Map 中的 value 可以全部分离出来,存储到 Collection 的任何一个子集中(注意 value 可能有重复)
  • Map 中的键值对的 key 不能直接修改,value 可以修改。如果要修改 key,只能先将 key 删掉,然后再进行重新插入(或者直接覆盖)

2.4 关于 HashMap 的介绍

简单介绍:

  • HashMap 是一个散列表,它存储的内容是键值对(key-value)映射
  • HashMap 实现了 Map 接口,根据键的 HashCode 值存储数据,具有很快的访问速度,最多允许一条记录的键为 null,不支持线程同步
  • HashMap 是无序的,即不会记录插入的顺序
  • HashMap 继承于 AbstractMap,实现了 Map、Cloneable、java.io.Serializable 接口

注意:

HashMap 类位于 java.util 包中,使用前需要引入它

文档信息:

2.5 关于 TreeMap 的介绍

简单介绍:

  • TreeMap 是一个能比较元素大小的 Map 集合,会对传入的 key 进行大小排序。其中可以使用元素的自然顺序,也可以使用集合中自定义的比较器来进行排序。

  • 不同于 HashMap 的哈希映射,TreeMap 底层实现了树形结构,即红黑树的结构。

注意:

TreeMap 类位于 java.util 包中,使用前需要引入它

文档信息:

2.6 HashMap 和 TreeMap 的区别

Map 底层结构TreeMapHashMap
底层结构红黑树哈希桶(链表+数组+红黑树)
插入/删除/查找时间复杂度O(logN)O(1)
是否有序关于 Key 有序无序
线程安全不安全安全
插入/删除/查找区别需要进行元素比较通过哈希函数计算哈希地址
比较覆写Key 必须能够比较,否则会抛出 ClassCastException 异常自定义类型需要覆写 equals 和 hashCode 方法
应用场景需要 Key 有序不关心 Key 是否有序,满足于需要更高的时间性能

2.7 Map 使用示例代码

注意:Map 是一个接口类,故不能直接实例化对象,以下以 HashMap 作为实现类为例

示例一: 创建一个 Map 实例

Map<String,String> map=new HashMap<>();

示例二: 插入 key 及其对应值为 value

map.put("惠城","法师");
map.put("晓","刺客");
map.put("喵班长","盗剑客");
System.out.println(map);
// 结果为:惠城=法师, 晓=刺客, 喵班长=盗剑客

示例三: 返回 key 的对应 value

String str1=map.get("晓");
System.out.println(str1);
// 结果为:刺客

示例四: 返回 key 对应的 value,若 key 不存在,则返回默认值 defaultValue

String str2=map.getOrDefault("弥惠","冒险者");
System.out.println(str2);
// 结果为:冒险者

示例五: 删除 key 对应的映射关系

String str3=map.remove("喵班长");
System.out.println(str3);
System.out.println(map);
// 结果为:盗剑客 和 惠城=法师, 晓=刺客

示例六: 除了上述直接打印 map,也可以通过 Set<Map.Entry<K, V>> entrySet() 这个方法进行遍历打印

Set<Map.Entry<String,String>> set=map.entrySet();
for(Map.Entry<String,String> str: set)
    System.out.println("Key="+str.getKey()+" Value="+str.getValue());

/** 结果为:
Key=惠城 Value=法师
Key=晓 Value=刺客
Key=喵班长 Value=盗剑客
*/

示例七: 判断是否包含 key

System.out.println(map.containsKey("惠城"));
// 结果为:true

3. Set

3.1 关于 Set 的介绍

简单介绍:

Set 是一个继承于 Collection 的接口,是一个不允许出现重复元素,并且无序的集合,主要有 HashSet 和 TreeSet 两大实现类。

注意:

Set 接口位于 java.util 包中,使用前需要引入它

文档信息:

3.1 Set 的常用方法说明

方法说明
boolean add(E e)添加元素,但重复元素不会被添加成功
void clear()清空集合
boolean contains(Object o)判断 o 是否在集合中
Iterator<E> iterator()返回迭代器
boolean remove(Object o)删除集合中的 o
int size()返回 set 中元素的个数
boolean isEmpty()检测 set 是否为空,空返回 true,否则返回 false
Object[] toArray()将 set 中的元素转换为数组返回
boolean addAll(Collection<? extends E> c)将集合 c 中的元素添加到 set 中,可以达到去重效果
boolean containsAll(Collection<?> c)集合 c 中的元素是否在 set 中全部存在,是返回 true,否则返回 false

注意:

  • Set 中只继承了 Key,并且要求 Key 唯一
  • Set 的底层是使用 Map 来实现的,其使用 Key 与 Object 的一个默认对象作为键值对插入插入到 Map 中
  • Set 的最大功能就是对集合中的元素进行去重
  • 实现 Set 接口的常用类有 TreeSet 和 HashSet,还有一个 LinkedHashSet,LinkedHashSet 是在 HashSet 的基础上维护了一个双向链表来记录元素的插入次序
  • Set 中的 Key 不能修改,如果修改,要先将原来的删除
  • Set 中可以插入 null

3.3 关于 TreeSet 的介绍

简单介绍:

TreeSet 实现类 Set 接口,基于 Map 实现,其底层结构为红黑树

注意:

TreeSet 类位于 java.util 包中,使用前需要引入它

文档信息:

3.4 关于 HashSet 的介绍

简单介绍:

  • HashSet 实现了 Set 接口,底层由 HashMap 实现,是一个哈希表结构
  • 新增元素时,新增的元素,相当于 HashMap 的 key,value 则默认为一个固定的 Object

注意:

HashSet 类位于 java.util 包中,使用前需要引入它

文档信息:

3.5 TreeSet 和 HashSet 的区别

Set 底层结构TreeSetHashSet
底层结构红黑树哈希桶
插入/删除/查找时间复杂度O(log(N))O(1)
是否有序关于 Key 有序不一定有序
线程安全不安全不安全
插入/删除/查找区别按照红黑树的特性来进行插入和删除先计算 key 哈希地址,再进行插入和删除
比较与覆写key 必须能够比较,否则会抛出 ClassCastException 异常自定义类型需要覆写 equals 和 hashCode 方法
应用场景需要 Key 有序场景Key 是否有序不关心,需要更高的时间性能

3.6 Set 使用示例代码

注意:Set 是一个接口类,故不能直接实例化对象,以下以 HashSet 作为实现类为例

示例一: 创建一个 Set 实例

Set<Integer> set=new HashSet<>();

示例二: 添加元素(重复元素无法添加成功)

set.add(1);
set.add(2);
set.add(3);
set.add(4);
set.add(5);
System.out.println(set);
// 结果为:[1, 2, 3, 4, 5]

示例三: 判断 1 是否在集合中

System.out.println(set.contains(1));
// 结果为:true

示例四: 删除集合中的元素

System.out.println(set.remove(3));
// 结果为:true

示例五: 返回 set 中集合的个数

System.out.println(set.size());
// 结果为:4

示例六: 检测 set 是否为空

System.out.println(set.isEmpty());
// 结果为:false

示例七: 返回迭代器,进行遍历

Iterator<Integer> it=set.iterator();
while(it.hasNext())
    System.out.println(it.next());

// 结果为:1 2 4 5

hashNext() 方法是判断当前元素是否还有下一个元素,next() 是获取当前元素,并且指向下一个元素

示例八: 清空集合

set.clear();
System.out.println(set);
// 结果为:[]

4. 编程练习题

4.1 找出第一个重复数据

题目:

从一些数据中,打印出第一个重复数据

代码:

public static void findNum(int[] array)
    Set<Integer> set=new HashSet<>();
    for(int i=0;i<array.length;i++)
        if(set.contains(array[i]))
            System.out.println(array[i]);
            break;
        
        set.add(array[i]);
    

4.2 去除重复数据

题目:

去除一些数据当中重复的数据

代码:

public static int[] removeSample(int[] array)
    Set<Integer> set=new HashSet<>();
    for(int i=0;i<array.length;i++)
        set.add(array[i]);
    
    Object[] arr=set.toArray();
    return array;

4.3 统计重复数据的出现次数

题目:

统计重复的数据出现的次数

代码:

public static Map count(int[] array)
    Map<Integer,Integer> map=new HashMap<>();
    for(int i=0;i<array.length;i++)
        if(map.containsKey(array[i]))
            int val=map.get(array[i]);
            map.remove(array[i]);
            map.put(array[i],val+1);
        else 
            map.put(array[i], 1);
        
    
    return map;

4.4 只出现一次的数字

题目(OJ 链接):

给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素

代码1: 异或法

public int singleNumber(int[] nums) 
    int sum=0;
    for(int i=0;i<nums.length;i++)
        sum=sum^nums[i];
    
    return sum;

代码2: 使用 HashSet

public int singleNumber(int[] nums) 
    Set<Integer> set=new HashSet<>();
    for(int val: nums)
        if(set.contains(val))
            set.remove(val);
        else
            set.add(val);
        
    
    for(int val: nums)
        if(set.contains(val))
            return val;
        
    
    return -1;

4.5 复制带随机指针的链表

题目(OJ 链接):

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。

代码:

public static Node copyRandomList(Node head) 
    Map<Node,Node> map=new HashMap<>();
    Node cur=head;
    while(cur!=null)
        Node node=new Node(cur.val);
        map.put(cur,node);
        cur=cur.next;
    
    cur=head;
    while(cur!=null)
        Node node=map.get(cur);
        node.next=map.get(cur.next);
        node.random=map.get(cur.random);
        cur=cur.next;
    
    return map.get(head);

4.6 宝石与石头

题目(OJ 链接):

给你一个字符串 jewels 代表石头中宝石的类型,另有一个字符串 stones 代表你拥有的石头。 stones 中每个字符代表了一种你拥有的石头的类型,你想知道你拥有的石头中有多少是宝石。

字母区分大小写,因此 “a” 和 “A” 是不同类型的石头。

代码:

    public int numJewelsInStones(String jewels, String stones) 
        Set<Character> set=new HashSet<>();
        for(int i=0;i<jewels.length();i++)
            set.add(jewels.charAt(i));
        
        int count=0;
        for(int i=0;i<stones.length();i++)
            if(set.contains(stones.charAt(i)))
                count++;
            
        
        return count;
    

4.7 旧键盘

题目(OJ 链接):

旧键盘上坏了几个键,于是在敲一段文字的时候,对应的字符就不会出现。现在给出应该输入的一段文字、以及实际被输入的文字,请你列出
肯定坏掉的那些键。

代码:

import java.util.*;

public class Main
    public static void main(String[] args)
        Set<Character> set=new HashSet<>();
        List<Character> list=new ArrayList<>();
        Scanner scanner=new Scanner(System.in);
        String str1=scanner.next();
        String str2=scanner.next();
        char[] s1=str1.toUpperCase().toCharArray();
        char[] s2=str2.toUpperCase().toCharArray();
        for(int i=0;i<s2.length;i++)
            set.add(s2[i]);
        
        for(int i=0;i<s1.length;i++)
            if(!set.contains(s1[i]))
                set.add(s1[i]);
                System.out.print(s1[i]);
            
        
    

4.8 前 K 个高频单词

题目(OJ 链接):

给一非空的单词列表,返回前 k 个出现次数最多的单词。返回的答案应该按单词出现频率由高到低排序。如果不同的单词有相同出现频率,按字母顺序排序。

代码:

public List<String> topKFrequent(String[] words, int k) 
    Map<String,Integer> map=new HashMap<>();
    for(String s: words)
        if(map.containsKey(s))以上是关于Java 集合框架帮你搞通哈希表,掌握 Map 和 Set 的使用(内含哈希表源码解读及面试常考题)的主要内容,如果未能解决你的问题,请参考以下文章

Java集合框架中的Hashtable、HashMap、HashSet、哈希表概念

Java集合框架

Java学习笔记31(集合框架五:set接口哈希表的介绍)

Java集合框架中的Hashtable,HashMap,HashSet,哈希表概念

JAVA-初步认识-常用对象API(集合框架-Map集合常见子类对象)

Java学习笔记32(集合框架六:Map接口)