数据结构之map和set
主要的数据结构:
二分搜索树:在Java标准库中对应TreeMap(基于红黑树-二分平衡搜索树RBTree的实现)/TreeSet
哈希表:在Java标准库中对应HashMap(基于哈希表的实现)/HashSet
一、Set集合和Map结合的特点
set:其实就是披着Set外衣的Map,也是Collection接口的子接口,一次保存一个元素,和List集合最大的区别在于:
Set集合保存的元素不能重复,List可以重复,Collection接口一次只能保存一个元素
经常使用Set集合来进行去重处理
这个两个结合一般是用来进行查找操作
Map接口和Collection没有任何关系
Map接口就是最顶层的父接口,表示保存的是一对键值对对象
接口示意图
二、Map接口和Set集合的基础使用
Set集合常用方法
方法 | 含义 |
---|---|
boolean add(E e) | 添加一个元素,若要元素不存在则添加成功,返回True;若添加失败,返回False |
boolean contains(Object o) | 判断一个元素o是否在当前set存在 |
boolean remove(Object o) | 在Set集合中删除指定元素,若该元素不存在,删除失败返回false;否则删除该元素,返回True |
在Set集合中没有提供修改的方法,若需要修改元素,只能先把要修改的元素删除,在添加新元素
遍历Set集合:使用for-each循环
public class SetTest {
public static void main(String[] args) {
Set<Integer> set = new HashSet<>();
System.out.println(set.add(1));
System.out.println(set.add(1));
System.out.println(set.add(2));
System.out.println(set.add(3));
System.out.println(set.contains(3));
System.out.println(set.contains(4));
System.out.println(set.remove(2));
for (int i : set) {
System.out.print(i + " ");
}
}
}
输出:
true
false
true
true
true
false
true
1 3
Map接口的常用方法:保存的是一对键值对元素 <key,value>
方法 | 含义 |
---|---|
V put(K key, V value) | 添加一对键值对 |
boolean containsKey(Object key) | 判断元素在当前Map的key中是否存在 |
boolean containsValue(Object value) | 判断元素在当前Map的value中是否存在 |
Set<Map.Entry<K, V>> entrySet(); | 返回Map中所有的键值对对象Map.Entry[]对象 |
Set keySet(); | 返回当前Map接口中所有的key值集合,因为是不重复的,所以使用了Set集合 |
Collection values(); | 返回当前Map接口中所有的value值集合,因为是可以重复的,所以使用collection接口接收 |
V get(Object key); | 根据相应的key值取得相应的value值,若Key不存在,返回null |
default V getOrDefault(Object key, V defaultValue) | 根据相应的key值取得相应的value值,若key不存在,返回默认值defaultVal |
V remove(Object key); | 删除指定k值的一对键值对,返回value值 |
Map中键值对的类型是: Map.Entry ,它是一个对象,其中包含两个方法:
entry.getKey(): 获取当前键值对的Key
entry.getValue(): 获取当前键值对的value
Map集合的遍历
public static void main(String[] args) {
// key不重复,value可以重复
Map<Integer,Integer> map = new HashMap<>();
// 1 = 10
map.put(1,10);
map.put(2,20);
// value可以重复,用一个key会得到一个value
map.put(3,10);
System.out.println(map.containsKey(3));
System.out.println(map.containsValue(40));
// Map.Entry[],遍历
for (Map.Entry<Integer,Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + " = " + entry.getValue());
}
}
Map的put()方法说明
即是新增又是修改
添加一个新的键值对:
若key不存在,就新增一个Map.Entry对象,保存到Map中。若Key已存在,修改原来的value为新的value,返回修改前的value值
Map接口中,元素添加的顺序和保存的顺序没有必然联系
public static void main(String[] args) {
Map<String,Integer > map = new HashMap<>();
map.put("王五",10);
map.put("李四",12);
map.put("孙武",13);
map.put("张三",14);
System.out.println(map.put("王五",22));
//取出所有的key
Set<String> keys = map.keySet();
System.out.println("当前所有的key为");
for (String str : keys){
System.out.print(str + " ");
}
System.out.println();
//取出所有的value
Collection<Integer> values = map.values();
for (Integer age : values){
System.out.print(age + " ");
}
}
输出:
10
当前所有的key为
李四 张三 孙武 王五
12 14 13 22 //乱序
三、Map接口常见子类的添加问题
- Map接口中元素的添加顺序和元素的保存顺序没有必然的联系
- HashMap中保存的元素顺序由hash函数来决定
- TreeMap中保存的元素顺序由TreeMap中的comparTo方法决定
- 要使用TreeMap保存元素,该类要么实现Comparable接口要么传入一个比较器
public static void main(String[] args) {
Map<String,Integer > map = new HashMap<>();
map.put("王五",10);
map.put("李四",12);
map.put("孙武",13);
map.put("张三",14);
System.out.println(map.toString());
}
输出:
{李四=12, 张三=14, 孙武=13, 王五=10} //乱序
TreeMap不覆写ComparTo方法的结果 ----报错
HashMap不会报错-----这两个用的是不同的底层结构
public class MapTest {
public static void main(String[] args) {
Map<ZhangSan,String> map = new TreeMap<>();
map.put(new ZhangSan(),"test");
}
}
class ZhangSan {}
报错:
jcl.ZhangSan cannot be cast to java.lang.Comparable
四、Map保存Null值的说明
HashMap是可以保存Null值的,HashMap的key和value都能为null,但是key为null的时候,有且只有一个
TreeMap中key不能为空,value可以为空
HashMap
public static void main(String[] args) {
Map<String ,String > map = new HashMap<>();
map.put(null,null);
map.put(null,"test");
System.out.println(map.toString());
}
输出:
{null=test}
看一个代码
public static void main(String[] args) {
Map<String,String> map = new TreeMap<>();
try {
map.put(null,null);
}catch (NullPointerException e) {
System.out.println("key为空");
}
map.put("张三",null);
System.out.println(map);
}
输出:
key为空
{张三=null}
五、练习题
只出现一次的数字
给定一个非空整数数组,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
输入: [2,2,1]
输出: 1
思路:利用Map集合保存每个元素和出现的频次,然后遍历map,道道频次为一的那个元素。
public int singleNumber(int[] nums) {
// 1.先扫描原数组,将每个元素以及出现的频次保存到Map中
Map<Integer,Integer> map = new HashMap<>();
for (int i : nums) {
map.put(i,map.getOrDefault(i,0) + 1);
}
// 2.遍历这个map,找到频次为1的那个元素
int ret = 0;
for (Map.Entry<Integer,Integer> entry : map.entrySet()) {
if (entry.getValue().equals(1)) {
ret = entry.getKey();
}
}
return ret;
}
出现一次的数字 II
给你一个整数数组
nums
,除某个元素仅出现 一次 外,其余每个元素都恰出现 **三次 。**请你找出并返回那个只出现了一次的元素。输入:nums = [2,2,3,2]
输出:3
这道题和上面的题目一样,代码都不用换,只要找到频次为1的元素就可以了
复制带随机指针的链表
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n 个全新节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对e应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。
思路:利用Map集合-它保存的是key和value的映射关系
- 遍历原链表构造新链表,在Map集合中维护原链表和新链表的映射关系
- 再次遍历原链表,通过映射关系维护新链表的next和random
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) {
//1.遍历原链表,构造Map集合维护原链表结点和新链表结点的映射关系
//原1 = 新1
Map<Node,Node> map = new HashMap<>();
for (Node x = head; x != null;x = x.next){
Node node = new Node(x.val);
map.put(x,node);
}
//2.再次遍历原链表,通过映射关系维护新链表的next和random
for (Node x = head; x != null;x = x.next ){
//新的结点的next = 原结点的next
map.get(x).next = map.get(x.next);
//新节点的random = 原节点的random
map.get(x).random = map.get(x.random);
}
//原链表的头结点对应新链表的头结点
return map.get(head);
}
}
单词规律
给定一种规律 pattern 和一个字符串 s ,判断 s 是否遵循相同的规律。
这里的 遵循 指完全匹配,例如, pattern 里的每个字母和字符串 str 中的每个非空单词之间存在着双向连接的对应规律。
输入: pattern = “abba”, str = “dog cat cat dog”
输出: true输入: pattern = “aaaa”, str = “dog cat cat dog”
输出: false
思路:这种题目是模式匹配问题,都可以采用Map映射结构解决
图下图所示,将其翻译成数字保存到Map集合中。
public boolean wordPattern(String pattern, String s) {
//按照空格分隔
String[] str = s.split(" ");
//两个字符长度不一样,肯定不是匹配的
if (str.length != pattern.length()) {
// pattern = "abba" str = "dog cat cat"
return false;
}
// 建立pattern和str关于数字的映射关系
int count = 1;
Map<String,Integer> map1 = new HashMap<>();
StringBuilder sb1 = new StringBuilder();
for (String temp : str) {
if (map1.containsKey(temp)) {
sb1.append(map1.get(temp));
}else {
map1.put(temp,count);
sb1.append(count ++);
}
}
count = 1;
Map<Character,Integer> map2 = new HashMap<>();
StringBuilder sb2 = new StringBuilder();
// pattern = "abba"
for (int i = 0; i < pattern.length(); i++) {
char c = pattern.charAt(i);
if (map2.containsKey(c)) {
sb2.append(map2.get(c));
}else {
map2.put(c,count);
sb2.append(count ++);
}
}
// 1221 1221
return sb1.toString().equals(sb2.toString());
}
宝石与石头
给你一个字符串 jewels 代表石头中宝石的类型,另有一个字符串 stones 代表你拥有的石头。 stones 中每个字符代表了一种你拥有的石头的类型,你想知道你拥有的石头中有多少是宝石。
字母区分大小写,因此 “a” 和 “A” 是不同类型的石头。
输入:jewels = “aA”, stones = “aAAbbbb”
输出:3
思路:使用Set集合保存jewels中的字符
扫描stones字符串,判断出现的每个字符是否在set中已经存在,若存在就说明是个宝石
public int numJewelsInStones(String jewels, String stones) {
// 使用Set集合保存jewels中的字符
Set<Character> set = new HashSet<>();
for (int i = 0; i < jewels.length(); i++) {
set.add(jewels.charAt(i));
}
// 扫描stones字符串,判断出现的每个字符是否在set中已经存在,若存在就说明是个宝石
int ret = 0;
for (int i = 0; i < stones.length(); i++) {
if (set.contains(stones.charAt(i))) {
// 是个宝石
ret ++;
}
}
return ret;
}
旧键盘
旧键盘上坏了几个键,于是在敲一段文字的时候,对应的字符就不会出现。现在给出应该输入的一段文字、以及实际被输入的文字,请你列出肯定坏掉的那些键。
输入描述:
输入在2行中分别给出应该输入的文字、以及实际被输入的文字。每段文字是不超过80个字符的串,由字母A-Z(包括大、小写)、数字0-9、
以及下划线“_”(代表空格)组成。题目保证2个字符串均非空。
输出描述:
按照发现顺序,在一行中输出坏掉的键。其中英文字母只输出大写,每个坏键只输出一次。题目保证至少有1个坏键。
思路:因为Set和Map保存元素的顺序和添加顺序无关,所以实际的字符串去扫描期望的字符串,才能保证按照发现顺序输出
public class BadKey {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
String expectedStr = null;
String actualStr = null;
while (scanner.hasNextLine()) {
expectedStr = scanner.nextLine();
actualStr = scanner.nextLine();
}
// 输出的字母需要大写,都转为大写处理
expectedStr = expectedStr.toUpperCase();
actualStr = actualStr.toUpperCase();
// 遍历实际输入的字符串,存入到Set中
Set<Character> set = new HashSet<>();
for (int i = 0; i < actualStr.length(); i++) {
set.add(actualStr.charAt(i));
}
// 拿着实际输入的set去遍历期望输入的字符串
// 所谓的坏键就是期望中有,而实际中不存在的字符
// 因为期望中存在而实际不存在的字符有可能有多个,因此输出时需要过滤重复元素
Set<Character> single = new HashSet<>();
for (int i = 0; i < expectedStr.length(); i++) {
char c = expectedStr.charAt(i);
// 坏键就是期望中存在,但是实际不存在
if (!set.contains(c)) {
if (single.add(c)) {
System.out.print(c);
}
}
}
}
}