Map和Set
概念及使用场景
Map
和Set
是一种专门用来进行搜索的容器(或数据结构);
前面常见的搜索方式
:遍历,二分查找;该种方式适合于静态搜索(搜索过程中不会再进行插入和删除操作了)
而Map
和Set
是一种适合动态查找的集合容器;
两种模型
一般把搜索的数据称为关键字(
Key
),与关键字相对应的称为值(Value
),将其称之为Key-value
的键值对,因此,存在以下两种模型:
纯 K 模型 :
Set
中采用的就是纯 K 这种模型,该模型主要用来判断关键字Key
是否在集合中;
例如: 快速查找某个名字是否在通讯录中,或查找一个英文单词是否在字典中;
K-V 模型 :
Map
中采用的就是 Key-Value 这种模型,该模型需要根据指定的Key
来查找对应的Value
值,并且K
一定是唯一的,不能重复;
例如:梁山好汉的江湖绰号,每个好汉都有自己的江湖绰号(及时雨-宋江);
Map的使用
Map在集合框架中的位置
由上可以看出:Map实质是一个接口,没有继承自 Collection,底层是通过TreeMap、HashMap两个实现类来实现的
;
Map.Entry(K,V)的说明
Map.Entry<K, V>
是Map
内部实现的用来存放<key, value>
键值对映射关系的内部类,该内部类中主要提供了<key,value>
的获取,value
的设置以及Key
的比较方式;
注意:Map.Entry<K,V> 并没有提供设置 Key 的方法
Map的常用方法
方法 | 说明 |
---|---|
V put(K key, V value) | 设置 key 对应的 value |
V getOrDefault(Object key, V defaultValue) | 返回 key 对应的 value,key 不存在,返回默认值 |
V get(Object key) | 返回 key 对应的 value |
V remove(Object key) | 删除 key 对应的映射关系 |
Set keySet() | 返回所有 key 的不重复集合 |
Collection values() | 返回所有 value 的可重复集合 |
Set<Map.Entry<K, V>> entrySet() | 返回所有的 key-value 映射关系 |
boolean containsKey(Object key) | 判断是否包含 key |
boolean containsValue(Object value) | 判断是否包含 value |
注意区别:
boolean containsKey(Object key)
方法时间复杂度:O(log2N)
;
boolean containsValue(Object value)
方法的时间复杂度:O(N)
;
Map的两个实现类的使用(TreeMap、HashMap)
TreeMap相关知识:
TreeMap
底层是红黑树;
红黑树
:红黑树是一棵二叉搜索树
和一些条件限制
组成的结构;
二叉搜索树:二叉搜索树又称为二叉排序树,也可以是一棵空树,若它的左子树不为空,则左子树上所有结点的值都小于根结点的值;若它的右子树不为空,则右子树上所有结点的值都大于根结点的值
它的左右子树也分别为二叉搜索树;
二叉搜索树
条件限制:
- 每个结点都有颜色:要么是红色,要么是黑色;
- 特性约束:最长路径中结点个数不超过最短路径中结点个数的2倍;
红黑树近似为一棵平衡的二叉搜索树
;
特性约束
:
(1) 结点的颜色不是红色就是黑色;
(2)根结点的颜色必须是黑色;
(3) 不能有连在一起的红色结点;
(4)每条路径中黑色结点的个数必须相同;
(5) “叶子结点”(指的是空引用或空结点
)必须是黑色的;
红黑树:
TreeMap 的使用:
public class TestMap {
public static void method1() {
Map<String, String> m = new TreeMap<>();
//设置 Key 对应的 value
m.put("orange", "橙子");
m.put("apple", "苹果");
m.put("banana", "香蕉");
System.out.println(m.size()); // 3
System.out.println(m.get("apple")); //key存在,返回与key对应的value
System.out.println(m.get("pear")); //key不存在,返回null
System.out.println(m.getOrDefault("pear", "梨")); //key不存在,返回默认值
// TreeMap 中的 key 不能为空---抛空指针异常
//m.put(null,"null");
// TreeMap 中的 value可以为空
m.put("grape", null);
System.out.println(m.size()); //4
//key不可以重复,当key不存在时,插入键值对
//key存在,则用新的 value修改之前与 key 对应的 value
System.out.println(m.get("orange"));
System.out.println(m.put("orange", "橘子")); //返回旧value值
System.out.println(m.get("orange")); //返回新的value值--->橘子
//value可以重复
m.put("unknown", "苹果");
System.out.println(m.size()); // 5
//containsKey,检测是否包含key---->O(log2N)
if (m.containsKey("unknown")) {
m.remove("unknown");
} else {
System.out.println("unknown不存在");
}
//containsValue,检测是否包含value--->O(N)
if (m.containsValue("橘子")) {
System.out.println("橘子存在");
} else {
System.out.println("橘子不存在");
}
//拿到m中所有的key---->keySet
//Set的类型必须与Map一致
//结果默认情况是按照字典序排列
Set<String> set = m.keySet();
for (String s : set) {
System.out.print(s + " ");
}
System.out.println(); //apple banana grape orange(有序--默认字典序)
System.out.println("=================");
//拿到m中所有的value---->values
Collection<String> list = m.values();
for(String v:list){
System.out.print(v + " ");
}
System.out.println();
System.out.println("================");
//获取所有的键值对 -->entrySet
Set<Map.Entry<String, String>> kv=m.entrySet();
for(Map.Entry<String ,String> e:kv){
System.out.println(e.getKey()+" :"+e.getValue());
}
System.out.println();
}
打印结果:
注意:
(1) key 必须是能够比较的,否则抛出类型转换异常;---->解决方法:实现 comparable
接口
(2) 打印的 key
是有序的排列;
(3)在 TreeMap
中插入键值对时,key
不能为空,否则就会抛 NullPointerException
异常,但是value
可以为空;
m.put(null,"null");
HashMap 的使用:
public static void method2() {
Map<String, String> m = new HashMap<>();
//设置 Key 对应的 value
m.put("orange", "橙子");
m.put("apple", "苹果");
m.put("banana", "香蕉");
System.out.println(m.size()); // 3
System.out.println(m.get("apple")); //key存在,返回与key对应的value
System.out.println(m.get("pear")); //key不存在,返回null
System.out.println(m.getOrDefault("pear", "梨")); //key不存在,返回默认值
// HashMap中的key可以为空
m.put(null,"null");
// HashMap 中的value可以为空
m.put("grape", null);
System.out.println(m.size()); //5
//key不可以重复,当key不存在时,插入键值对
//key存在,则用新的 value修改之前与 key 对应的 value
System.out.println(m.get("orange"));
System.out.println(m.put("orange", "橘子")); //返回旧value值
System.out.println(m.get("orange")); //返回新的value值--->橘子
//value可以重复
m.put("unknown", "苹果");
System.out.println(m.size()); // 6
//containsKey,检测是否包含key---->O(log2N)树的高度
if (m.containsKey("unknown")) {
m.remove("unknown");
} else {
System.out.println("unknown不存在");
}
//containsValue,检测是否包含value--->O(N)
if (m.containsValue("橘子")) {
System.out.println("橘子存在");
} else {
System.out.println("橘子不存在");
}
//拿到m中所有的key---->keySet
//Set的类型必须与Map一致
//结果不一定有序
Set<String> set = m.keySet();
for (String s : set) {
System.out.print(s + " ");
}
System.out.println(); //orange banana null apple grape (不一定是有序的)
System.out.println("=================");
//拿到m中所有的value---->values
Collection<String> list = m.values();
for(String v:list){
System.out.print(v + " ");
}
System.out.println();
System.out.println("================");
//获取所有的键值对 -->entrySet
Set<Map.Entry<String, String>> kv=m.entrySet();
for(Map.Entry<String ,String> e:kv){
System.out.println(e.getKey()+" :"+e.getValue());
}
System.out.println();
}
打印结果:
注意:
(1)HashMap
中的 key
可以为空;
(2)结果不一定有序;
TreeMap 和 HashMap 的区别
Map使用时的注意事项
-
Map
是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap
或者HashMap
; -
Map
中存放键值对的Key
是唯一的,value
是可以重复的; -
Map
中的Key
可以全部分离出来,存储到Set
中来进行访问(因为Key不能重复) ; -
Map
中键值对的Key
不能直接修改,value
可以修改,如果要修改key
,只能先将该key
删除掉,然后再来进行重新插入;
Set的使用
Set在集合框架中的位置
由上可以看出:Set实质是一个接口,继承自 collection ,底层是通过TreeSet、HashSet两个实现类来实现的
;
Set的常用方法
方法 | 说明 |
---|---|
boolean add(E e) | 添加元素,但重复元素不会被添加成功 |
void clear() | 清空集合 |
boolean contains(Object o) | 判断 o 是否在集合中 |
Iterator iterator() | 返回迭代器 |
boolean containsAll(Collection<?> c) | 集合c中的元素是否在set中全部存在 |
boolean remove(Object o) | 删除集合中的 o |
int size() | 返回set中元素的个数 |
boolean isEmpty() | 检测set是否为空,为空返回true,否则返回false |
Object[] toArray() | 将set中的元素转换为数组返回 |
boolean addAll(Collection<?extends E> c) | 将集合c中的元素添加到set中,可以达到去重的效果 |
Set的两个实现类的使用(TreeSet、HashSet)
TreeSet使用:
import java.util.TreeSet;
import java.util.Iterator;
import java.util.Set;
public class TestSet {
public static void main(String[] args) {
Set<String> s = new TreeSet<>();
// add(key): 如果key不存在,则插入,返回ture
// 如果key存在,返回false
s.add("apple");
s.add("orange");
s.add("peach");
s.add("banana");
System.out.println(s.size());
System.out.println(s); //[apple, banana, orange, peach]
s.add("apple");
System.out.println(s);//添加元素,重复元素不会被添加成功,结果仍是[apple, banana, orange, peach]
// add(key): key如果是空,抛出空指针异常
//s.add(null);
// contains(key): 如果key存在,返回true,否则返回false
System.out.println(s.contains("apple"));//true
System.out.println(s.contains("watermelon"));//false
// remove(key): key存在,删除成功返回true
// key不存在,删除失败返回false
// key为空,抛出空指针异常
s.remove("apple");
System.out.println(s); //[banana, orange, peach]
s.remove("watermelon");
System.out.println(s);
// 抛出空指针异常
//s.remove(null);
//迭代器遍历
Iterator<String> it = s.iterator();
while(it.hasNext()){
System.out.print(it.next() + " ");
}
System.out.println();
}
}
结果打印:
注意:
(1) TreeSet
打印结果是有序的;
(2)不能插入空 key
;
HashSet 的使用:
public class TestSet {
public static void main(String[] args) {
Set<String> s = new HashSet<>();
// add(key): 如果key不存在,则插入,返回ture
// 如果key存在,返回false
s.add("apple");
s.add("orange");
s.add("peach");
s.add("banana");
System.out.println(s.size()); //4
System.out.println(s); //[orange, banana, apple, peach]
s.add("apple");
System.out.println(s);//添加元素,重复元素不会被添加成功,结果仍是[orange, banana, apple, peach]
//key可以为空
s.add(null);
System.out.println(s);//[orange, banana, null, apple, peach]
System.out.println(s.size()); //5
// contains(key): 如果key存在,返回true,否则返回false
System.out.println(s.contains("apple"));//true
System.out.println(s.contains("watermelon"));//false
// remove(key): key存在,删除成功返回true
// key不存在,删除失败返回false
s.remove("apple");
System.out.println(s); //[orange, banana,null, peach]
s.remove("watermelon");
System.out.println(s);//[orange, banana,null, peach]
s.remove(null);
System.out.println(s);//[orange, banana, peach]
Iterator<String> it = s.iterator();
while(it.hasNext()){
System.out.print(it.next() + " ");
}
System.out.println();
}
}
结果输出:
注意:
(1)HashSet
打印结果是无序的;
(2) 可以插入null
;
TreeSet和HashSet的区别
Set使用时的注意事项
Set
中只存储了key
,并且要求key
唯一;Set
的底层是使用Map
来实现的,其使用key
与Object
的一个默认对象作为键值对插入到Map
中;
Java源代码中:
Set
最大的功能就是对集合中的元素进行去重;- 实现
Set
接口的常用类有TreeSet
和HashSet
; Set
中的Key
不能修改,如果要修改,先将原来的删除掉,然后再重新插入;Set
中不能插入null
的key
;
OJ练习
只出现一次的数字
- 采用
Map
统计数组中每个元素出现的次数,因为不用考虑是否有序,所以采用HashMap
; - 通过遍历,找出只出现一次的元素;
代码实现:
class Solution {
public int singleNumber(int[] nums) {
//HashMap统计每个元素出现的次数
HashMap<Integer,Integer> m = new HashMap<>();
for(int i = 0;i<nums.length;i++){
m.put(nums[i],m.getOrDefault(nums[i],0)+1);
}
//遍历找只出现一次的元素
for(Map.Entry<Integer,Integer> entry:m.entrySet()){
if(entry.getValue() == 1)
return entry.getKey();
}
return 0;
}
}
复制带随机指针的链表
- 通过遍历原链表,给遇到的每个结点
cur
创建一个新结点newNode
; - 将新结点往
HashMap
中插入---->put
; - 链接新链表;
m.get(cur).next=m.get(cur.next)
- 给新链表的随机域赋值;
m.get(cur).random=m.get(cur.random)
;
代码实现:
class Solution {
public Node copyRandomList(Node head) {
//链表为空时
if(head==null){
return null;
}
Map<Node,Node> m=new HashMap<>();
Node cur=head;
while(cur!=null){
//插入相同值域的结点
m.put(cur,new Node(cur.val));
cur=cur.next;
}
cur=head;
while(cur!=null){
//将插入的新结点链接起来
m.get(cur).next=m.get(cur.next);
//给插入结点的随机指针域赋值
m.get(cur).random=m.get(cur.random);
cur=cur.next;
}
return m.get(head);
}
}
宝石与石头
leetcode:宝石与石头
思路
:
- 统计石头中每个字符出现的次数;
- 统计宝石中每个字符出现次数
代码实现:
class Solution {
public int numJewelsInStones(String jewels, String stones) {
//统计石头出现的次数
Map<Character,Integer> m=new HashMap<>();
for(int i=0;i<stones.length();i++){
Character ch=stones.charAt(i);
m.put(ch,m.getOrDefault(ch,0)+1);
}
//统计宝石的个数
int count=0;
for(int i=0;i<jewels.length();i++){
Character ch=jewels.charAt(i);
count+=m.getOrDefault(ch,0);
}
return count;
}
}
旧键盘打字
牛客网:旧键盘打字
思路
:
对于IO类型的题目,必须要包含:
(1)创建一个
Main
类;
(2)在写一个main
方法;
(3)将所需要的包导入;
(4)循环接收数据的输入
本题目中:
- 循环接收输入(
in
)、输出(out
)的数据,并将其转换为大写字符; - 将输出的数据放入
set
中;---->不考虑有序,采用HashSet
; - 获取输入中的每个字符,向
set
中插入,插入成功则表明是坏键(set
具有去重的功能),将其删除;
代码实现:
//导入需要的包
import java.util.Scanner;
import java.util.HashSet;
import java.util.Set;
//写一个Main类
public class Main{
//写一个main方法
public static void main(String[] args){
Scanner sc=new Scanner(System.in);
//循环接收用户输入的String,将其转换成大写
//循环接收输出的String,将其转换成大写
while(sc.hasNext()){
String in=sc.nextLine().toUpperCase();
String out=sc.nextLine().toUpperCase();
//将out结果存放在HashSet中
Set<Character> s=new HashSet<>();
for(int i=0;i<out.length();i++){
s.add(out.charAt(i));
}
//将in中的每个字符往set中插入,插入成功---就是坏键,输出
for(int i=0;i<in.length();i++){
if(s.add(in.charAt(i))){
System.out.print(in.charAt(i));
}
}
System.out.println();
}
}
}
前K个高频单词
leetcode:前K个高频单词
思路
:
借助Map
和优先级队列
- 采用
Map
统计每个单词出现的次数; - 利用前
K
个元素来建小堆(需要比较器),将剩余的N-K
个元素与堆顶元素进行比较,大于堆顶的元素往前移;
代码实现:
class Less implements Comparator<Map.Entry<String,Integer>>{
public int compare(Map.Entry<String,Integer> o1,
Map.Entry<String,Integer> o2){
if(o1.getValue()>o2.getValue()){
return 1;
}
if(o1.getValue()==o2.getValue() && o2.getKey().compareTo(o1.getKey())>0){
return 1;
}
if(o2.getValue()==o2.getValue() && o1.getKey().compareTo(o2.getKey())==0){
return 0;
}
return -1;
}
}
class Solution {
public List<String> topKFrequent(String[] words, int k) {
//统计每个单词出现的次数---HashMap
Map<String,Integer> m=new HashMap<>();
for(int i=0;i<words.length;i++){
m.put(words[i],m.getOrDefault(words[i],0)+1);
}
//TOP-K
//用前K个建小堆
Less cmp=new Less();
PriorityQueue<Map.Entry<String,Integer>> p=new PriorityQueue<>(cmp);
//entrySet获取键值对(单词,次数)
Set<Map.Entry<String,Integer>> s=m.entrySet();
int i=0;
for(Map.Entry<String,Integer> kv:s){
if(i<k){
p.offer(kv);
i++;
}else{
//用N-K个与堆顶元素比较,找最大的;
if(cmp.compare(kv,p.peek())>=0){
p.poll();
p.offer(kv);
}
}
}
LinkedList<String> list=new LinkedList<>();
for(i=0;i<k;i++){
list.addFirst(p.poll().getKey());
}
return list;
}
}