Map和Set

简介: 在之前我们常见的搜索方式有直接遍历【时间复杂度O(N)】、二分查找【时间复杂度O(log2N)】等静态查找方式,在实际查找中可能会进行一些插入、删除的动态操作,上述的两种方式就不合适了,本文介绍的Map和Set便是一种适合动态查找的集合容器。

Map和Set


在学习这两个接口之前,我们先对以及学习到的类和接口,以及即将要将到的两个接口之间的关系图做个总览。

类与接口示意图:


微信图片_20230111033108.png

1.Map与Set


1.1 Map和Set的说明


Map和set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。我们一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以模型会有两种:


  • 纯Key模型,比如在英文词典中查找一个单词是否存在,对应的就是Set接口
  • Key—Value模型,比如梁山好汉的绰号,一个人有一个自己的绰号,对应的就是Map接口


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


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<>();
    }


微信图片_20230111033103.png


1.2 Map.Entry的说明


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


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


注:Map.Entry并没有提供设置Key的方法


代码实现:


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


1.3 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 keySet() 返回所有 key 的不重复集合
Collection values() 返回所有 value 的可重复集合
Set<Map.Entry<K, V>> entrySet() 返回所有的 key-value 映射关系
boolean containsKey(Object key) 判断是否包含 key
boolean containsValue(Object value) 判断是否包含 value


注意:


1.Map是一个接口,不能直接实例化对象,如果要实例化对象只能实例化其实现类TreeMap或者HashMap。

2.Map中存放键值对的Key是唯一的,value是可以重复的

3.Map中的Key可以全部分离出来,存储到Set中来进行访问(因为Key不能重复)。

4.Map中的value可以全部分离出来,存储在Collection集合中(value可能有重复)。

5.Map中键值对的Key不能直接修改,value可以修改,如果要修改key,只能先将该key删除掉,然后再来进行重新插入。


1.4 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 c) 将集合c中的元素添加到set中,可以达到去重的效果


注意:


1.Set是继承自Collection的一个接口类

2.Set中只存储了key,并且要求key一定要唯一

3.Set的底层是使用Map来实现的,其使用key与Object的一个默认空对象作为键值对插入到Map中

4.Set最大的功能就是对集合中的元素进行去重

5.实现Set接口的常用类有TreeSet和HashSet,还有一个LinkedHashSet,LinkedHashSet是在HashSet的基础上维护了一个双向链表来记录元素的插入次序。

6.Set中的Key不能修改,如果要修改,先将原来的删除掉,然后再重新插入

7.Set中不能插入null的key。


2.HashMap与TreeMap

Map底层结构 TreeMap HashMap
底层结构 红黑树 哈希桶

插入/删除/查找时间复杂度

O(log2N)

O(1)
是否有序 关于Key有序 无序
线程安全 不安全 不安全
插入/删除/查找区别 需要进行元素比较 通过哈希函数计算哈希地址
比较与覆写 key必须能够比较,否则会抛出ClassCastException异常 自定义类型需要覆写equals和hashCode方法
应用场景 需要Key有序场景下 Key是否有序不关心,需要更高的时间性能

3.HashSet与TreeSet


Map底层结构 TreeMap HashMap
底层结构 红黑树 哈希桶

插入/删除/查找时间复杂度

O(log2N)

O(1)
是否有序 关于Key有序 无序
线程安全 不安全 不安全
插入/删除/查找区别 需要进行元素比较 通过哈希函数计算哈希地址
比较与覆写 key必须能够比较,否则会抛出ClassCastException异常 自定义类型需要覆写equals和hashCode方法
应用场景 需要Key有序场景下 Key是否有序不关心,需要更高的时间性能

关于红黑树和哈希桶数据结构的具体详情将会在后续的篇章中介绍。


4.练习


4.1 功能函数


给你10w个数据,设计三个函数完成三个对应的功能


  • 把这10w个数据中相同的元素删除掉
  • 找到这10w个数据中第一个重复的数据
  • 统计这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 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 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 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);
    }


4.2 只出现一次的数字


【力扣题目链接】

微信图片_20230111033054.png

思路:

方法一:

利用异或操作符,相同数值异或为0,0与任何数异或结果仍为该数,所以遍历整个数组进行异或操作,最后的结果就是只出现一次的数字。


方法二:

利用Set集合,遍历数组如果第一次出现则放入集合,如果第二次出现则将该数从集合中移除(此时集合中只剩下只出现一次的元素),再次遍历数组,找到集合中仅存的数字返回。


代码实现:

法一:


class Solution {
    public int singleNumber(int[] nums) {
        int ret=0;
        for(int i=0;i
            ret^=nums[i];
        }
        return ret;
    }
}


法二:


class Solution {
    public int singleNumber(int[] nums) {
        Set set=new HashSet<>();
        for(int i=0;i
            if(!set.contains(nums[i])) {
                set.add(nums[i]);
            }else {
                set.remove(nums[i]);
            }
        }
        for(int i=0;i
            if(set.contains(nums[i]))
            return nums[i];
        }
        return -1;
    }
}


4.3 复制带随机指针的链表

【力扣题目链接】


微信图片_20230111033050.png


微信图片_20230111103125.png


微信图片_20230111033046.png


思路:

深拷贝应该正好由 n 个全新节点组成,复制链表中的指针都不应指向原链表中的节点,可以先遍历一遍链表,用原链表的值先创建出新链表,并在Map中将原节点与复制节点一一对应,再次遍历链表通过Map得到复制链表节点的地址,通过Map中的一一对应关系给复制链表中的next域和random域赋值。


微信图片_20230111033040.png

代码实现:


class Solution {
    public Node copyRandomList(Node head) {
        Map 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);
    }
}


4.4 宝石与石头

【力扣题目链接】


微信图片_20230111033037.png

思路:

先遍历宝石数组,将宝石对应字符存储在Set集合中,再遍历石头数组,设置数值计数,若在遍历石头数组的过程中Set集合含有该字符,计数加1


代码实现:


class Solution {
    public int numJewelsInStones(String jewels, String stones) {
        Set 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;
    }
}


4.5 旧键盘


【牛客题目链接】


微信图片_20230111033033.png

思路:

先将输出的字符数组(都通过toUpperCase()方法转换为大写)存储在Set集合中,再遍历键盘敲击的字符数组,如果集合中不存在则打印(而且是第一次出现,不能出现多次)


代码实现:


public class Main {
    public static void main(String[] args) {
        Scanner scanner=new Scanner(System.in);
        Set set=new HashSet<>();
        String s1=scanner.nextLine();
        String s2=scanner.nextLine();
        //先遍历实际输出字符数组,将其存储到Set集合中(完成去重)
        for (char ch:s2.toUpperCase().toCharArray()) {
             set.add(ch);
        }
        //为了保证相同的字符只出现一次,所以再实例化一个集合用于存储坏掉的键,只打印一次
        Set 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);
            }
        }
    }
}


4.6 前K个高频单词

【力扣题目链接】


微信图片_20230111033029.png

思路:


1.利用Map统计所有单词的词频(在4.1功能函数中涉及到相同的问题)

【(Top—K问题) 详解见【PriorityQueue——优先级队列(堆)3.2】】

2.建立K个结点的小根堆

3.剩余Map中元素依次与堆顶元素比较,进行堆顶元素的替换

4.最后将得到的小根堆中元素依次存到List集合中,利用Collections.reverse()方法完成逆置,按从大到小的顺序依次输出结果。


下边我们按照上述思路来一步一步实现代码:


1.统计词频


Map 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个节点的小根堆


在这一步我们需要注意一个问题,就是在词频一致的情况下,我们应该将新元素插入堆顶还是堆尾。


微信图片_20230111033026.png

假设我们插入堆尾,在Collections完成逆置后,list中的结果为[“d”,“c”,“a”],但我们预期的结果为[“d”,“a”,“c”] (词频一致的情况下,字典序靠前的先输出)


微信图片_20230111033021.png

所以在实例化优先级队列时需要重写比较方法:


PriorityQueue> minHeap=new PriorityQueue<>(new Comparator>() {
            @Override
            public int compare(Map.Entry o1, Map.Entry 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 m: map.entrySet()) {
            if(minHeap.size()
                minHeap.offer(m);
            }else {
                Map.Entry 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 list=new ArrayList<>();
        for (int i = 0; i < k; i++) {
            list.add(minHeap.poll().getKey());
        }
        Collections.reverse(list);


本题代码实现:


class Solution {
    public List topKFrequent(String[] words, int k) {
        Map 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> minHeap=new PriorityQueue<>(new Comparator>() {
            @Override
            public int compare(Map.Entry o1, Map.Entry o2) {
                if(o1.getValue().compareTo(o2.getValue())==0) {
                    return o2.getKey().compareTo(o1.getKey());
                }
                return o1.getValue()-o2.getValue();
            }
        });
        for (Map.Entry m: map.entrySet()) {
            if(minHeap.size()
                minHeap.offer(m);
            }else {
                Map.Entry 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 list=new ArrayList<>();
        for (int i = 0; i < k; i++) {
            list.add(minHeap.poll().getKey());
        }
        Collections.reverse(list);
        return list;
    }
}


相关文章
|
18天前
|
算法
你对Collection中Set、List、Map理解?
你对Collection中Set、List、Map理解?
52 18
你对Collection中Set、List、Map理解?
|
11天前
|
存储 缓存 安全
只会“有序无序”?面试官嫌弃的List、Set、Map回答!
小米,一位热衷于技术分享的程序员,通过与朋友小林的对话,详细解析了Java面试中常见的List、Set、Map三者之间的区别,不仅涵盖了它们的基本特性,还深入探讨了各自的实现原理及应用场景,帮助面试者更好地准备相关问题。
49 20
|
28天前
|
存储 C++ 容器
【C++】map、set基本用法
本文介绍了C++ STL中的`map`和`set`两种关联容器。`map`用于存储键值对,每个键唯一;而`set`存储唯一元素,不包含值。两者均基于红黑树实现,支持高效的查找、插入和删除操作。文中详细列举了它们的构造方法、迭代器、容量检查、元素修改等常用接口,并简要对比了`map`与`set`的主要差异。此外,还介绍了允许重复元素的`multiset`和`multimap`。
30 3
【C++】map、set基本用法
|
28天前
|
存储 算法 C++
【C++】unordered_map(set)
C++中的`unordered`容器(如`std::unordered_set`、`std::unordered_map`)基于哈希表实现,提供高效的查找、插入和删除操作。哈希表通过哈希函数将元素映射到特定的“桶”中,每个桶可存储一个或多个元素,以处理哈希冲突。主要组成部分包括哈希表、哈希函数、冲突处理机制、负载因子和再散列,以及迭代器。哈希函数用于计算元素的哈希值,冲突通过开链法解决,负载因子控制哈希表的扩展。迭代器支持遍历容器中的元素。`unordered_map`和`unordered_set`的插入、查找和删除操作在理想情况下时间复杂度为O(1),但在冲突较多时可能退化为O(n)。
22 5
|
2月前
|
存储 JavaScript 前端开发
Set、Map、WeakSet 和 WeakMap 的区别
在 JavaScript 中,Set 和 Map 用于存储唯一值和键值对,支持多种操作方法,如添加、删除和检查元素。WeakSet 和 WeakMap 则存储弱引用的对象,有助于防止内存泄漏,适合特定场景使用。
|
3月前
|
存储 Java API
【数据结构】map&set详解
本文详细介绍了Java集合框架中的Set系列和Map系列集合。Set系列包括HashSet(哈希表实现,无序且元素唯一)、LinkedHashSet(保持插入顺序的HashSet)、TreeSet(红黑树实现,自动排序)。Map系列为双列集合,键值一一对应,键不可重复,值可重复。文章还介绍了HashMap、LinkedHashMap、TreeMap的具体实现与应用场景,并提供了面试题示例,如随机链表复制、宝石与石头、前K个高频单词等问题的解决方案。
48 6
【数据结构】map&set详解
|
2月前
|
存储 缓存 Java
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
【用Java学习数据结构系列】HashMap与TreeMap的区别,以及Map与Set的关系
43 1
|
3月前
|
算法
你对Collection中Set、List、Map理解?
你对Collection中Set、List、Map理解?
42 5
|
3月前
|
存储 JavaScript 前端开发
js的map和set |21
js的map和set |21
|
3月前
|
存储 前端开发 API
ES6的Set和Map你都知道吗?一文了解集合和字典在前端中的应用
该文章详细介绍了ES6中Set和Map数据结构的特性和使用方法,并探讨了它们在前端开发中的具体应用,包括如何利用这些数据结构来解决常见的编程问题。
ES6的Set和Map你都知道吗?一文了解集合和字典在前端中的应用