⭐️前言⭐️
在之前我们常见的搜索方式有直接遍历【时间复杂度O(N)】、二分查找【时间复杂度O( l o g 2 N log_2N log2N)】等静态查找方式,在实际查找中可能会进行一些插入、删除的动态操作,上述的两种方式就不合适了,本文介绍的Map和Set便是一种适合动态查找的集合容器。
🍉博客主页: 🍁【如风暖阳】🍁
🍉精品Java专栏【JavaSE】、【备战蓝桥】、【JavaEE初阶】、【MySQL】、【数据结构】
🍉欢迎点赞 👍 收藏 ⭐留言评论 📝私信必回哟😁🍉本文由 【如风暖阳】 原创,首发于 CSDN🙉
🍉博主将持续更新学习记录收获,友友们有任何问题可以在评论区留言
在学习这两个接口之前,我们先对以及学习到的类和接口,以及即将要将到的两个接口之间的关系图做个总览。
类与接口示意图:
Map和set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。我们一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以模型会有两种:
Map是一个接口类,该类没有继承自Collection,该类中存储的是
Set是继承自Collection的接口类,Set中只存储了Key。
由于Map和Set都是接口,并不能直接实例化,所以需要实例化实现了对应接口的类
Map可以通过HashMap或者TreeMap来实现
Set可以通过HashSet或者TreeSet来实现
(HashMap和HashSet底层通过哈希表来进行存储,TreeMap和TreeSet底层通过搜索树来进行存储)【注:哈希表见文章【哈希表】,搜索树见文章【二叉搜索树】】
代码实现:
public static void main(String[] args) {
Map<String,Integer> map1=new TreeMap<>();
Map<String,Integer> map2=new HashMap<>();
Set<String> set1=new TreeSet<>();
Set<String> set2=new HashSet<>();
}
Map.Entry
方法 | 解释 |
---|---|
K getKey() | 返回 entry 中的 key |
V getValue() | 返回 entry 中的 value |
V setValue(V value) | 将键值对中的value替换为指定value |
注:Map.Entry
代码实现:
public static void main(String[] args) {
Map<String,Integer> map=new HashMap<>();
map.put("abc",10);
map.put("hello",3);
map.put("double",5);
Set<Map.Entry<String,Integer>> entrySet=map.entrySet();
for (Map.Entry<String,Integer> entry:entrySet) {
System.out.println("key:"+entry.getKey()+" val:"+entry.getValue());
}
}
//
key:abc val:10
key:double val:5
key:hello val:3
Process finished with exit code 0
方法 | 解释 |
---|---|
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 keySet() | 返回所有 key 的不重复集合 |
Collection values() | 返回所有 value 的可重复集合 |
Set | 返回所有的 key-value 映射关系 |
boolean containsKey(Object key) | 判断是否包含 key |
boolean containsValue(Object value) | 判断是否包含 value |
注意:
方法 | 解释 |
---|---|
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中,可以达到去重的效果 |
注意:
Map底层结构 | TreeMap | HashMap |
---|---|---|
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间复杂度 | O( l o g 2 N log_2N log2N) | O(1) |
是否有序 | 关于Key有序 | 无序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找区别 | 需要进行元素比较 | 通过哈希函数计算哈希地址 |
比较与覆写 | key必须能够比较,否则会抛出ClassCastException异常 | 自定义类型需要覆写equals和hashCode方法 |
应用场景 | 需要Key有序场景下 | Key是否有序不关心,需要更高的时间性能 |
Set底层结构 | TreeSet | HashSet |
---|---|---|
底层结构 | 红黑树 | 哈希桶 |
插入/删除/查找时间复杂度 | O( l o g 2 N log_2N log2N) | O(1) |
是否有序 | 关于Key有序 | 不一定有序 |
线程安全 | 不安全 | 不安全 |
插入/删除/查找区别 | 按照红黑树的特性来进行插入和删除 | 1. 先计算key哈希地址 2. 然后进行插入和删除 |
比较与覆写 | key必须能够比较,否则会抛出ClassCastException异常 | 自定义类型需要覆写equals hashCode方法 |
应用场景 | 需要Key有序场景下 | Key是否有序不关心,需要更高的时间性能 |
关于红黑树和哈希桶数据结构的具体详情将会在后续的篇章中介绍。
给你10w个数据,设计三个函数完成三个对应的功能
代码实现:
初始化数组与测试:
public static void main(String[] args) {
int[] array=new int[10_0000];
Random random=new Random();
//随机生成十万个数字(给定范围5000,一定有重复的数字)
for (int i = 0; i < array.length; i++) {
array[i]=random.nextInt(5_000);
}
func1(array);
System.out.println(func2(array));
func3(array);
}
功能实现:
/**
* 删除重复元素 直接用Set集合
* @param array
*/
public static void func1(int[] array) {
Set<Integer> set=new HashSet<>();
for (int i = 0; i < array.length; i++) {
set.add(array[i]);
}
System.out.println(set);
}
/**
* 找第一个重复出现的数值
* @param array
*/
public static int func2(int[] array) {
Set<Integer> set=new HashSet<>();
for (int i = 0; i < array.length; i++) {
//第一次出现,就将其添加到集合中;若不是第一次出现,则返回该值
if(!set.contains(array[i])) {
set.add(array[i]);
}else {
return array[i];
}
}
return -1;
}
/**
* 统计每个数值出现的次数
* @param array
*/
public static void func3(int[] array) {
Map<Integer,Integer> map=new HashMap<>();
for (int i = 0; i < array.length; i++) {
int key=array[i];
//第一次出现,Value值为1;再重复出现时,将Value值加1
if(map.get(key)==null) {
map.put(key,1);
}else {
int val=map.get(key);
map.put(key,val+1);
}
}
System.out.println(map);
}
【力扣题目链接】
思路:
方法一:
利用异或操作符,相同数值异或为0,0与任何数异或结果仍为该数,所以遍历整个数组进行异或操作,最后的结果就是只出现一次的数字。
方法二:
利用Set集合,遍历数组如果第一次出现则放入集合,如果第二次出现则将该数从集合中移除(此时集合中只剩下只出现一次的元素),再次遍历数组,找到集合中仅存的数字返回。
代码实现:
法一:
class Solution {
public int singleNumber(int[] nums) {
int ret=0;
for(int i=0;i<nums.length;i++) {
ret^=nums[i];
}
return ret;
}
}
法二:
class Solution {
public int singleNumber(int[] nums) {
Set<Integer> set=new HashSet<>();
for(int i=0;i<nums.length;i++) {
if(!set.contains(nums[i])) {
set.add(nums[i]);
}else {
set.remove(nums[i]);
}
}
for(int i=0;i<nums.length;i++) {
if(set.contains(nums[i]))
return nums[i];
}
return -1;
}
}
【力扣题目链接】
思路:
深拷贝应该正好由 n 个全新节点组成,复制链表中的指针都不应指向原链表中的节点,可以先遍历一遍链表,用原链表的值先创建出新链表,并在Map中将原节点与复制节点一一对应,再次遍历链表通过Map得到复制链表节点的地址,通过Map中的一一对应关系给复制链表中的next域和random域赋值。
代码实现:
class Solution {
public 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) {
map.get(cur).next=map.get(cur.next);
map.get(cur).random=map.get(cur.random);
cur=cur.next;
}
return map.get(head);
}
}
【力扣题目链接】
思路:
先遍历宝石数组,将宝石对应字符存储在Set集合中,再遍历石头数组,设置数值计数,若在遍历石头数组的过程中Set集合含有该字符,计数加1
代码实现:
class Solution {
public int numJewelsInStones(String jewels, String stones) {
Set<Character> set=new HashSet<>();
for(char ch:jewels.toCharArray()) {
set.add(ch);
}
int count=0;
for(char ch:stones.toCharArray()) {
if(set.contains(ch))
count++;
}
return count;
}
}
【牛客题目链接】
思路:
先将输出的字符数组(都通过toUpperCase()方法转换为大写)存储在Set集合中,再遍历键盘敲击的字符数组,如果集合中不存在则打印(而且是第一次出现,不能出现多次)
代码实现:
public class Main {
public static void main(String[] args) {
Scanner scanner=new Scanner(System.in);
Set<Character> set=new HashSet<>();
String s1=scanner.nextLine();
String s2=scanner.nextLine();
//先遍历实际输出字符数组,将其存储到Set集合中(完成去重)
for (char ch:s2.toUpperCase().toCharArray()) {
set.add(ch);
}
//为了保证相同的字符只出现一次,所以再实例化一个集合用于存储坏掉的键,只打印一次
Set<Character> set1=new HashSet<>();
for (char ch:s1.toUpperCase().toCharArray()) {
//若set集合中不存在则说明该键坏掉了;若set1集合中也不存在说明为第一次出现
if(!set.contains(ch)&&!set1.contains(ch)) {
System.out.print(ch);
set1.add(ch);
}
}
}
}
【力扣题目链接】
思路:
1.利用Map统计所有单词的词频(在4.1功能函数中涉及到相同的问题)
【(Top—K问题) 详解见【PriorityQueue——优先级队列(堆)3.2】】
2.建立K个结点的小根堆
3.剩余Map中元素依次与堆顶元素比较,进行堆顶元素的替换
4.最后将得到的小根堆中元素依次存到List集合中,利用Collections.reverse()方法完成逆置,按从大到小的顺序依次输出结果。
下边我们按照上述思路来一步一步实现代码:
1.统计词频
Map<String,Integer> map=new HashMap<>();
for(String s:words) {
if(map.get(s)==null) {
map.put(s,1);
}else {
int count=map.get(s);
map.put(s,count+1);
}
}
2.建立K个节点的小根堆
在这一步我们需要注意一个问题,就是在词频一致的情况下,我们应该将新元素插入堆顶还是堆尾。
假设我们插入堆尾,在Collections完成逆置后,list中的结果为[“d”,“c”,“a”],但我们预期的结果为[“d”,“a”,“c”] (词频一致的情况下,字典序靠前的先输出)
所以在实例化优先级队列时需要重写比较方法:
PriorityQueue<Map.Entry<String,Integer>> minHeap=new PriorityQueue<>(new Comparator<Map.Entry<String, Integer>>() {
@Override
public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
//词频相同时,字典序大的反而被认为小
if(o1.getValue().compareTo(o2.getValue())==0) {
return o2.getKey().compareTo(o1.getKey());
}
//词频不同时,o1-o2默认为小根堆
return o1.getValue()-o2.getValue();
}
});
3.与堆顶元素的比较完成替换
Map中剩余的元素,只有在词频比堆顶元素大或者词频一致,字典序小的情况下才完成替换。
2、3代码实现:
for (Map.Entry<String,Integer> m: map.entrySet()) {
if(minHeap.size()<k) {
minHeap.offer(m);
}else {
Map.Entry<String,Integer> top=minHeap.peek();
if(top.getValue()==m.getValue()&&m.getKey().compareTo(top.getKey())<0) {
minHeap.poll();
minHeap.offer(m);
}
if(m.getValue()>top.getValue()) {
minHeap.poll();
minHeap.offer(m);
}
}
}
4.将小根堆元素存放到集合并完成逆置
List<String> list=new ArrayList<>();
for (int i = 0; i < k; i++) {
list.add(minHeap.poll().getKey());
}
Collections.reverse(list);
本题代码实现:
class Solution {
public List<String> topKFrequent(String[] words, int k) {
Map<String,Integer> map=new HashMap<>();
for(String s:words) {
if(map.get(s)==null) {
map.put(s,1);
}else {
int count=map.get(s);
map.put(s,count+1);
}
}
PriorityQueue<Map.Entry<String,Integer>> minHeap=new PriorityQueue<>(new Comparator<Map.Entry<String, Integer>>() {
@Override
public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
if(o1.getValue().compareTo(o2.getValue())==0) {
return o2.getKey().compareTo(o1.getKey());
}
return o1.getValue()-o2.getValue();
}
});
for (Map.Entry<String,Integer> m: map.entrySet()) {
if(minHeap.size()<k) {
minHeap.offer(m);
}else {
Map.Entry<String,Integer> top=minHeap.peek();
if(top.getValue()==m.getValue()&&m.getKey().compareTo(top.getKey())<0) {
minHeap.poll();
minHeap.offer(m);
}
if(m.getValue()>top.getValue()) {
minHeap.poll();
minHeap.offer(m);
}
}
}
List<String> list=new ArrayList<>();
for (int i = 0; i < k; i++) {
list.add(minHeap.poll().getKey());
}
Collections.reverse(list);
return list;
}
}
⭐️最后的话⭐️
总结不易,希望uu们不要吝啬你们的👍哟(^U^)ノ~YO!!如有问题,欢迎评论区批评指正😁