数据结构 Map与Set
前言
Map与Set是数据结构中重要的两个容器,它们是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。
对于搜索方式,我们可以将它们分为两种,一种静态搜索,即:数据集合一旦确定后,后续只进行 “查找” 操作,几乎不做插入、删除元素的修改。简单说就是 “数据不动,只查不改”,典型的例子是查找一本固定的字典(字典内容不会临时增减),而另一种是动态搜索,它指的是数据集合在查找过程中,可能随时进行插入、删除元素的修改。简单说就是 “边查边改,数据动态变化”,典型的例子是我们手机中的通讯录,它不仅要查联系人电话,还会随时新增朋友、删除旧联系人。
Map与Set就是一种适合动态查找的集合容器。
在Java的集合框架中:
Map并不属于Collection这个体系中,它是独立于Collection的另一个根接口体系,而Set属于Collection这个体系。
1.纯Key模型和Key-Value模型
一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以模型会有两种:纯Key模型和Key-Value模型,Map与Set与它们密不可分。
1.纯Key模型,就好像:
- 在一本英文词典中,快速查找一个单词是否在词典中
- 快速查找某个名字在不在通讯录中
2.Key-Value模型,就好像:
- 统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数
- 梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号
简单来说,纯Key模型可以理解为一个鞋柜,它存放的是一双双鞋子,可以根据鞋子的号码直接找到这双鞋,而Key-Value模型可以理解为一个衣柜,存放的是一套衣服,包括上衣和裤子,可以根据上衣找到与它配对的裤子。
而Map中存储的就是key-value的键值对,Set中只存储了Key。下图更加形象:
2.Map
2.1 关于Map的说明
Map是一个接口类,该类没有继承自Collection,该类中存储的是结构的键值对,并且K一定是唯一的,不能重复。那么它是如何储存键值对的呢?它是这样储存的:在Map这个接口中,定义了一个内部类 Map.Entry<K,V>,它是Map内部实现的用来存放键值对映射关系的内部类,它主要提供了键值对<K,V>的获取方法、V的设置方法和K的比较方式,如下表所示:
方法 | 解释 |
---|---|
K getKey() | 返回 entry 中的 key |
V getValue() | 返回 entry 中的 value |
V setValue(V value) | 将键值对中的value替换为指定value |
需要注意的是:Map.Entry<K,V>并没有提供设置K的方法!
在Java的集合框架中可以看到,Map底下有两个实现了它的类,分别是TreeMap和HashMap,这两个类最根本的区别是它们的底层结构不同,TreeMap的底层结构是红黑树(红黑树可以理解为是二叉搜索树的升级版,它能够避免二叉搜索树的极端情况出现,从而避免性能下降的问题),HashMap的底层结构是哈希桶(在 JDK 7 及以前,HashMap
的底层数据结构是数组 + 链表 ;在 JDK 8 及以后,HashMap
的底层数据结构是数组 + 链表 + 红黑树。)。
如果不知道二叉搜索树和哈希桶,请跳转:
数据结构 实现二叉搜索树与哈希表-CSDN博客
TreeMap和HashMap的具体区别如下:
Map底层结构 | TreeMap | HashMap |
---|---|---|
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间 复杂度 | ||
是否有序 | 关于Key有序 | 无序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找区别 | 需要进行元素比较 | 通过哈希函数计算哈希地址 |
比较与覆写 | key必须能够比较,否则会抛出 ClassCastException异常 | 自定义类型需要覆写equals和 hashCode方法 |
应用场景 | 需要Key有序场景下 | Key是否有序不关心,需要更高的 时间性能 |
2.2 Map的常用方法及说明
方法 | 解释 |
---|---|
V get(Object key) | 返回 key 对应的 value |
V getOrDefault(Object key, V defaultValue) | 返回 key 对应的 value,key 不存在,返回默认值 |
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) | 判断是否包含 key |
boolean containsValue(Object value) | 判断是否包含 value |
【注意事项】
- Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap。
- Map中存放键值对的Key是唯一的,value是可以重复的。
- 在TreeMap中插入键值对时,key不能为空,否则就会抛NullPointerException异常,value可以为空。但是HashMap的key和value都可以为空。
- Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。
- Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。
- Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。
2.3 使用案例
这里拿TreeMap来进行举例:
import java.util.*;public class Test {public static void main(String[] args) {Map<String,String> map = new TreeMap<>();// put(key, value):插入key-value的键值对// 如果key不存在,会将key-value的键值对插入到map中,返回nullmap.put("林冲", "豹子头");map.put("鲁智深", "花和尚");map.put("武松", "行者");map.put("宋江", "及时雨");String str = map.put("李逵", "黑旋风");System.out.println(str);System.out.println(map.size());System.out.println(map);System.out.println("====================");// put(key,value): 注意key不能为空,但是value可以为空// key如果为空,会抛出空指针异常//map.put(null, "花名");str = map.put("无名", null);System.out.println(map.size());System.out.println("====================");// put(key, value):// 如果key存在,会使用value替换原来key所对应的value,返回旧valuestr = map.put("李逵", "铁牛");System.out.println(str);System.out.println(map);System.out.println("====================");// get(key): 返回key所对应的value// 如果key存在,返回key所对应的value// 如果key不存在,返回nullSystem.out.println(map.get("鲁智深"));System.out.println(map.get("史进"));System.out.println("====================");//getOrDefault(): 如果key存在,返回与key所对应的value,如果key不存在,返回一个默认值System.out.println(map.getOrDefault("李逵", "铁牛"));System.out.println(map.getOrDefault("史进", "九纹龙"));System.out.println(map.size());System.out.println("====================");//containKey(key):检测key是否包含在Map中,时间复杂度:O(logN)// 按照红黑树的性质来进行查找// 找到返回true,否则返回falseSystem.out.println(map.containsKey("林冲"));System.out.println(map.containsKey("史进"));System.out.println("====================");// containValue(value): 检测value是否包含在Map中,时间复杂度: O(N)// 找到返回true,否则返回falseSystem.out.println(map.containsValue("豹子头"));System.out.println(map.containsValue("九纹龙"));System.out.println("====================");// 打印所有的key,使用foreach打印Set<String> keys = map.keySet();for (String s1:keys) {System.out.println(s1);}System.out.println("====================");// 打印所有的Value,使用foreach打印Collection<String> values = map.values();for (String s2:values) {System.out.println(s2);}System.out.println("====================");// 打印所有的键值对,使用foreach打印Set<Map.Entry<String, String>> key_value = map.entrySet();for (Map.Entry<String, String> s3:key_value) {System.out.println(s3);}}
}//运行结果null
5
{宋江=及时雨, 李逵=黑旋风, 林冲=豹子头, 武松=行者, 鲁智深=花和尚}
====================
6
====================
黑旋风
{宋江=及时雨, 无名=null, 李逵=铁牛, 林冲=豹子头, 武松=行者, 鲁智深=花和尚}
====================
花和尚
null
====================
铁牛
九纹龙
6
====================
true
false
====================
true
false
====================
宋江
无名
李逵
林冲
武松
鲁智深
====================
及时雨
null
铁牛
豹子头
行者
花和尚
====================
宋江=及时雨
无名=null
李逵=铁牛
林冲=豹子头
武松=行者
鲁智深=花和尚
可以发现Map接口的实现类重写了toString()方法,同理Set接口的实现类也重写了toString()方法。
3.Set
3.1 关于Set的说明
当我们充分了解了Map接口,那么对于Set接口,也就很容易理解了。通过Java的集合框架可以知道,Set接口同Map接口一样,它也有两个实现类,分别是TreeSet和HashSet,它们的底层结构与TreeMap和HashMap相似。需要注意的是,Set与Map主要的不同有两点:Set是继承自Collection的接口类,Set中只存储了Key。
3.2 Set的常用方法及说明
方法 | 解释 |
---|---|
boolean add(E e) | 添加元素,但重复元素不会被添加成功 |
void clear() | 清空集合 |
boolean contains(Object o) | 判断 o 是否在集合中 |
Iterator iterator() | 返回迭代器 |
boolean remove(Object o) | 删除集合中的 o |
int size() | 返回set中元素的个数 |
boolean isEmpty() | 检测set是否为空,空返回true,否则返回false |
Object[] toArray() | 将set中的元素转换为数组返回 |
boolean containsAll(Collection <?> c) | 集合c中的元素是否在set中全部存在,是返回true,否则返回 false |
boolean addAll(Collection <? extends E> c) | 将集合c中的元素添加到set中,可以达到去重的效果 |
【注意事项】
- Set是继承自Collection的一个接口类。
- Set中只存储了key,并且要求key一定要唯一
- TreeSet的底层是使用Map来实现的,其使用key与Object的一个默认对象作为键值对插入到Map中的。
- Set最大的功能就是对集合中的元素进行去重。
- 实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入次序。
- Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入。
- TreeSet中不能插入null的key,HashSet可以。
3.3 使用案例
这里拿TreeSet来进行举例:
import java.util.*;public class Test {public static void main(String[] args) {Set<String> set = new TreeSet<>();// add(key): 如果key不存在,则插入,返回ture// 如果key存在,返回falseboolean isIn = set.add("apple");set.add("orange");set.add("peach");set.add("banana");boolean flag = set.add("apple");System.out.println(flag);System.out.println(set.size());System.out.println(set);// add(key): key如果是空,抛出空指针异常// set.add(null);System.out.println("=================");// contains(key): 如果key存在,返回true,否则返回falseSystem.out.println(set.contains("apple"));System.out.println(set.contains("watermelen"));System.out.println("=================");// remove(key): key存在,删除成功返回true// key不存在,删除失败返回false// key为空,抛出空指针异常set.remove("apple");System.out.println(set);System.out.println("=================");//可以通过迭代器去遍历Iterator<String> it = set.iterator();while (it.hasNext()){System.out.println(it.next());}System.out.println("=================");//对一个集合进行去重,这个集合容器要实现Collection接口List<String> list = new ArrayList<>();list.add("a");list.add("a");list.add("hello");list.add("world");list.add("you");list.add("i");list.add("you");System.out.println(list);Set<String> set1 = new TreeSet<>();set1.addAll(list);System.out.println(set1);}
}//运行结果false
4
[apple, banana, orange, peach]
=================
true
false
=================
[banana, orange, peach]
=================
banana
orange
peach
=================
[a, a, hello, world, you, i, you]
[a, hello, i, world, you]
4.一些关于Java的Map与Set的知识
- Java中HashMap与HashSet会在冲突链表(哈希桶内的链表)长度大于一定阈值后,将链表转变为搜索树(红黑树)。
- java 中计算哈希值实际上是调用的类的 hashCode 方法,进行 key 的相等性比较是调用 key 的 equals 方法。所以如果要用自定义类作为 HashMap 的 key 或者 HashSet 的值,必须覆写 hashCode 和 equals 方法,而且要做到 equals 相等的对象,hashCode 一定是一致的。
到此,关于数据结构中的Map与Set内容到此完结!谢谢您的阅读,如有不对,还请指出。