【Java数据结构】Map&Set的理解与应用(附面试题加深理解)

简介: 搜索、Map的使用、Set的说明、面试题练习

搜索

概念及场景

Map和set是一种专门用来进行搜索的容器或者数据结构,其搜索的效率与其具体的实例化子类有关。


以前常见的搜索方式有:

直接遍历,时间复杂度为O(N),元素如果比较多效率会非常慢

二分查找,时间复杂度为O(log2 N) ,但搜索前必须要求序列是有序的

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

根据姓名查询考试成绩

通讯录,即根据姓名查询联系方式

不重复集合,即需要先搜索关键字是否已经在集合中

可能在查找时进行一些插入和删除的操作,即动态查找,那上述两种方式就不太适合了,本文介绍的Map和Set是一种适合动态查找的集合容器。


模型

一般把搜索的数据称为关键字(Key),和关键字对应的称为值(Value),将其称之为Key-value的键值对,所以模型会有两种:


纯 key 模型,比如:

有一个英文词典,快速查找一个单词是否在词典中

快速查找某个名字在不在通讯录中

Key-Value 模型,比如:

统计文件中每个单词出现的次数,统计结果是每个单词都有与其对应的次数:<单词,单词出现的次数>

梁山好汉的江湖绰号:每个好汉都有自己的江湖绰号

而Map中存储的就是key-value的键值对,Set中只存储了Key。

Map的使用

b1.png


关于Map的说明

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


Map 的常用方法说明

b2.png


注意:


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

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

在Map中插入键值对时,key可以为空,value可以为空

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

Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。

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

TreeMap和HashMap的区别


b3.png


关于Map.Entry<K, V>的说明

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

b4.png


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


Set的说明

Set与Map主要的不同有两点:

  • Set是继承自Collection的接口类
  • Set中只存储了Key。


Set常见方法说明

image.png


注意:


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

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

3.在Map中插入键值对时,key可以为空,value可以为空

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

5.Map中的value可以全部分离出来,存储在Collection的任何一个子集合中(value可能有重复)。

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

7.TreeMap和HashMap的区别

b6.png

关于Map.Entry<K, V>的说明

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


image.png


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


Set的说明

Set与Map主要的不同有两点:

  • Set是继承自Collection的接口类
  • Set中只存储了Key。


Set常见方法说明

image.png


注意:


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.TreeSet和HashSet的区别

b7.png


面试题练习

只出现一次的数字

b8.png


思路:

我们知道Set的特点就是key不会重复,所以这题第一时间想到的就是用HashSet,每读取一个数据,就将其存入我们的set里,如果set已经有这个数据了,说明这是第二次出现了,就把原来存在set里的这个数据给删除掉,遍历完全部数据后,还存在于set里的就是只出现一次的数据了


代码:

class Solution {

   public int singleNumber(int[] nums) {

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


       for(int i = 0 ; i < nums.length ; i++){

           if(set.contains(nums[i])){

               set.remove(nums[i]);

           }else{

               set.add(nums[i]);

           }

       }


       for(int key:set){

           return key;

       }

       return -1;

   }

}


复制带随机指针的链表

b9.png


思路:

题目要求进行深拷贝,所以就是说,我们要返回一个新的链表,新链表和原链表里的东西是一模一样的,但是互不相干,由于存在一 一对应的关系,首先想到的就是用Map,原链表的节点就是key–value里的key,而新链表的节点就是value,两者形成了一 一对应的关系,然后再利用map将原节点对应的next和random依次复制给新节点,具体看代码注释


代码:

/*

// Definition for a Node.

class Node {

   int val;

   Node next;

   Node random;


   public Node(int val) {

       this.val = val;

       this.next = null;

       this.random = null;

   }

}

*/


class Solution {

   public Node copyRandomList(Node head) {

           if(head==null) return null;

           Node cur = head;//创建一个cur节点遍历原链表

           HashMap<Node,Node> map = new HashMap<>();//建一个HashMap

           while(cur!=null){//依次遍历原链表

               Node copyNode = new Node(cur.val);//创建新节点,值和原链表中节点值一样

               map.put(cur,copyNode);//将原链表的节点和新创建的值一样的节点放入Map中

               cur = cur.next;

           }

           //已经将复制好的新节点和原节点按对应方式存入了map中

           //最后需要利用map处理新节点的next和random

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

     

   }

}


宝石与石头

b10.png


思路:

遍历字符串 jewels,使用HashSet存储其中的字符,然后遍历字符串 stones,对于其中的每个字符,如果其在哈希集合(HashSet)中,则是宝石。

代码:

class Solution {

   public int numJewelsInStones(String jewels, String stones) {

       HashSet<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;

   }

}


坏键盘打字

b11.png


思路:


将实际输入的字符存入一个Set里,Set里的值不会重复,也就是说这个Set(setActul)里代表的就是好的键

遍历期望输入的字符串,如果setActual里边没有期望字符串里的字符,说明这个键是坏的,因为题目要求只输出一次坏的键,所以还需要用一个setBroken来记录坏的键,这样就不会重复输出坏的键了

代码:

import java.util.*;

public class 坏键盘 {

   public static void main(String[] args) {

       Scanner scan = new Scanner(System.in);

       String str1 = scan.nextLine();//期望的

       String str2 = scan.nextLine();//实际的


       HashSet<Character> setActual = new HashSet<>();

       for(char ch : str2.toUpperCase().toCharArray()) {

           setActual.add(ch);//把实际的字符串放到一个set里

       }


       HashSet<Character> setBroken = new HashSet<>();

       //再把坏了的键放到一个set里

       for(char ch : str1.toUpperCase().toCharArray()) {//遍历期望的字符串

           if(!setActual.contains(ch) && !setBroken.contains(ch)) {//在对比已经存在setActul里实际输入的字符串

               setBroken.add(ch);

               //这个ch就是坏了的

               //setBroken里只存一次这个坏了的键,存的时候同时打印一次

               System.out.print(ch);

           }

       }

   }

}



10w个数据去除重复数据

思路:

  • 原理就是利用Set的性质,set里的值不会重复,只要去重就想到用Set
  • 遍历生成的10w个随机数,这里假设随机数是1~100之间的,每遍历一个数据就判断一下set里是否已经存有,若没有则存入set,若有则进行下一个数据的检查

代码:

import java.util.ArrayList;

import java.util.HashSet;

import java.util.Random;


public class 去除10w个数据中的重复数据 {

   public static void main(String[] args) {

       Random random = new Random();

       ArrayList<Integer> list = new ArrayList<Integer>();//将随机生成的10w个数据存到顺序表中

       for (int i = 0; i < 100000; i++) {

           list.add(random.nextInt(100));//生成0~100之间的随机数

       }


       HashSet<Integer> set = new HashSet();//创建一个HashSet容器,存放数据

       //由于Set的性质,里面的元素不会重复,将顺序表里的数据存进这个容器就可以达到去除重复数据的目的了

       for (int i = 0 ; i< list.size(); i++){

           set.add(list.get(i));

       }

       System.out.println("不重复的数据有:"+set.size()+"个");

       System.out.println(set);

   }

}



运行结果:

b12.png


在10w个数据中找到第一个重复的数据

思路:

  • 和去重方法一样,用set,每遍历一个数据就检查set里是否已经存在这个数据,若不存在,则存入set,若已经存在,则说明已经找到第一次出现重复的数据了

代码:import java.util.ArrayList;

import java.util.HashSet;

import java.util.Random;


public class 在10w个数据中找到第一个重复的数据 {

   public static void main(String[] args) {

       Random random = new Random();

       ArrayList<Integer> list = new ArrayList<Integer>();//将随机生成的10w个数据存到顺序表中

       for (int i = 0; i < 100000; i++) {

           list.add(random.nextInt(100));//生成0~100之间的随机数

       }


       HashSet<Integer> set = new HashSet<Integer>();//还是用set


       for (int i = 0; i < list.size(); i++) {

           if (set.contains(list.get(i))) {//如果set里已经有此数据

               System.out.println(list.get(i));//输出,这就是第一个重复的数据

               break;//找到第一个从重复数据就可以退出了

           }else {

               set.add(list.get(i));//否则把新数据加入到set里

           }

       }

   }

}


运行结果:

b13.png


统计10w个数据重复出现的次数

思路:


统计重复次数的题,首先想到就是map,利用map的性质,键值对,一 一对应的关系

遍历数据,若map里没有这个数据,则加入到map里,并给 key–value里 的value初始化为1,代表出现了1次

后续再次遍历到这个数据的时候,value值+1,表示重复次数+1

代码:

import java.util.ArrayList;

import java.util.HashMap;

import java.util.Map;

import java.util.Random;


public class 统计10w个数据重复出现的次数 {

   public static void main(String[] args) {

       Random random = new Random();

       ArrayList<Integer> list = new ArrayList<Integer>();//将随机生成的10w个数据存到顺序表中

       for (int i = 0; i < 10; i++) {

           list.add(random.nextInt(10));//生成0~100之间的随机数

       }


       //        数据    出现次数

       HashMap<Integer, Integer> map = new HashMap<Integer, Integer>();

       for (Integer key : list) {//循环遍历list

           if (map.get(key)==null){//如果map里找不到key(数据)对应的值(次数)

               map.put(key, 1);

           }else{//说明之前存过一次了

               int count = map.get(key);//count记录原来出现的次数

               map.put(key, count+1);//更新key出现的次数

           }

       }


       //输出每个数据的重复次数

       System.out.println(map.entrySet());//entrySet()返回的是一个set,里面存放的是键值对(key和value的映射关系)


       //  Map.Entry<Integer, Integer>是一个内部类    map.entrySet()返回的是一个set集合

       for (Map.Entry<Integer, Integer> entry : map.entrySet()){

           System.out.println("数据:"+entry.getKey()+"   "+"出现次数:"+entry.getValue());

       }


   }

}


运行结果:

数据已经简化,方便观看

b14.png



相关文章
|
1月前
|
监控 Java 应用服务中间件
高级java面试---spring.factories文件的解析源码API机制
【11月更文挑战第20天】Spring Boot是一个用于快速构建基于Spring框架的应用程序的开源框架。它通过自动配置、起步依赖和内嵌服务器等特性,极大地简化了Spring应用的开发和部署过程。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,特别是spring.factories文件的解析源码API机制。
76 2
|
26天前
|
Java 程序员
Java社招面试题:& 和 && 的区别,HR的套路险些让我翻车!
小米,29岁程序员,分享了一次面试经历,详细解析了Java中&和&&的区别及应用场景,展示了扎实的基础知识和良好的应变能力,最终成功获得Offer。
66 14
|
1月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
1月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
27天前
|
存储 缓存 算法
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式
在C语言中,数据结构是构建高效程序的基石。本文探讨了数组、链表、栈、队列、树和图等常见数据结构的特点、应用及实现方式,强调了合理选择数据结构的重要性,并通过案例分析展示了其在实际项目中的应用,旨在帮助读者提升编程能力。
53 5
|
26天前
|
并行计算 算法 测试技术
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面
C语言因高效灵活被广泛应用于软件开发。本文探讨了优化C语言程序性能的策略,涵盖算法优化、代码结构优化、内存管理优化、编译器优化、数据结构优化、并行计算优化及性能测试与分析七个方面,旨在通过综合策略提升程序性能,满足实际需求。
57 1
|
1月前
|
Java 编译器 程序员
Java面试高频题:用最优解法算出2乘以8!
本文探讨了面试中一个看似简单的数学问题——如何高效计算2×8。从直接使用乘法、位运算优化、编译器优化、加法实现到大整数场景下的处理,全面解析了不同方法的原理和适用场景,帮助读者深入理解计算效率优化的重要性。
35 6
|
1月前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
43 5
|
1月前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
62 4
|
2月前
|
存储 Java 开发者
Java中的Map接口提供了一种优雅的方式来管理数据结构,使代码更加清晰、高效
【10月更文挑战第19天】在软件开发中,随着项目复杂度的增加,数据结构的组织和管理变得至关重要。Java中的Map接口提供了一种优雅的方式来管理数据结构,使代码更加清晰、高效。本文通过在线购物平台的案例,展示了Map在商品管理、用户管理和订单管理中的具体应用,帮助开发者告别混乱,提升代码质量。
34 1