前言:通过下面的思维导图将Map的学习划分为3部分,分别是映射基础介绍、映射实现、映射的复杂度分析、对比集合与映射的关系以及介绍映射的一些应用。
1 映射基础
1.1映射定义
为了能更好的理解映射的概念,先来看看一个基础的数学题吧,对于函数f(x)=2*x+1
,自变量x
取不同的值因变量f(x)
对应不同的值,例如:x=1,f(x)=2*1+1=3
等,相关情况如下图:
此时我们可以得出一个结论就是:
定义:定义域中的每一个值在值域中都有一个值与它对应;
这种对应关系就是我们常说的映射。
1.2 映射与字典关系
映射是非常抽象的,因此在各种语言中这种映射关系也被称为字典dict
:
字典: 词------->释意
实际字典举例:
(1)定义域中的每一个值在值域中都有一个值与它对应;
(2)存储(键、值)【Key、Value】数据对的数据结构;
(3)根据键,来快速的寻找值
2 映射实现
2.1 实现机制
底层基于链表和二分搜索树实现,结构如下:
2.1.1 基于链表实现
由于映射的值结构与之前实现的链表值结构不同,因此这里需要单独实现Map
相关解释:
(1)
Map
通过泛型来实现,向 Map
中传入Key
和 Value
这两个环境变量;
(2)
void add(K,V)
添加一个新的元素(键值数据对)
(3)
V remove(K)
删除一个元素,对映射来说,键充当索引的作用;可以不考虑键对应的值是谁,只要指定删除键对应的数据,它相应的值就跟着删除了;删除了 K
,告诉用户K
对应的 Value 是什么,以便用户日后使用;
(4)
boolean contains(K)
查找元素,只要看 Key
是否存在于Map
的数据结构中;
(5)
V get(K)
获取具体元素,get
方法中传入的 Key
,也就是 键值;查询的是键(key)
,返回的是值(Value)
;
(6)
void set(K,V)
修改元素,给定一个新的键(key)
和值(value)
,修改键(key)
在Map
中对应的值(value)
(7)
int getSize()
映射中存储的元素的数量,每一个元素都是一个键值对
(8)
boolean isEmpty()
判断映射是否为空
代码实现:
1.Map
接口:
public interface Map {
void add(K key, V value);
V remove(K key);
boolean contains(K key);
V get(K key);
void set(K key, V newValue);
boolean isEmpty();
int getSize();
}
- 以链表为底层的映射类
LinkedListMap
:
public class LinkedListMap implements Map {
private class Node {
public K key; //定义键
public V value; //定义值
public Node next;
public Node(K key, V value, Node next) { //用户传入 key 和 value
this.key = key; //将用户传入的 key 赋值给 this.key
this.value = value; //将用户传入的 value 赋值给 this.value
this.next = next;
}
public Node(K key, V value) { //用户只传入 key
this(key, value, null); //value 默认为空
}
public Node() { //用户 key 和 value 都没有传入
this(null, null, null);//key 和 value 默认为空
}
@Override
public String toString() {
return key.toString() + " : " + value.toString();
}
}
private Node dummyHead; //虚拟头节点
private int size;
public LinkedListMap() {
dummyHead = new Node();
size = 0;
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
private Node getNode(K key) {
Node cur = dummyHead.next;
while (cur != null) {
if (cur.key.equals(key)) {
return cur;
}
cur = cur.next;
}
return null;
}
@Override
public boolean contains(K key) { //查看是否包含键为 K 的值
return getNode(key) != null;
}
@Override
public V get(K key) { //查找键为 K 的对应的值 V
Node node = getNode(key);
return node == null ? null : node.value; //如果 K 为空则返回的 V 也为空,否则返回 node.value
}
@Override
public void add(K key, V value) { //添加元素
Node node = getNode(key); //查询当前映射中是否已存在 key 对应的数据
if (node == null) { //如果 node 为空,
dummyHead.next = new Node(key, value, dummyHead.next); //直接在链表头添加元素即可
size++;
} else
node.value = value; //将用户传入的 value 覆盖掉之前的 value
}
@Override
public void set(K key, V newValue) { //用户指定键,希望这个键在映射中附上新的 Value
Node node = getNode(key);
if (node == null)
throw new IllegalArgumentException(key + " doesn't exist!"); // key 不存在无法赋值
node.value = newValue;
}
@Override
public V remove(K key) { //删除 key 所对应的 value 值
Node prev = dummyHead;
while (prev.next != null) {
if (prev.next.key.equals(key))
break;
prev = prev.next;
}
if (prev.next != null) { //删除节点
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
size--;
return delNode.value;
}
return null;
}
}
3.测试:统计单词 PRIDE
和 PRIEJUDICE
其出现的频率是怎样的
新建一个测试类TestLinkedListMapDemo
:
import java.util.ArrayList;
//测试
public class TestLinkedListMapDemo {
public static void main(String[] args) {
System.out.println("Pride and Prejudice");
ArrayList words = new ArrayList<>();
if (FileOperation.readFile("pride-and-prejudice.txt", words)) {
System.out.println("Total words: " + words.size());
LinkedListMap map = new LinkedListMap<>();
for (String word : words) {
if (map.contains(word))
map.set(word, map.get(word) + 1);
else
map.add(word, 1);
}
System.out.println("Total different words: " + map.getSize());
System.out.println("Frequency of PRIDE: " + map.get("pride"));
System.out.println("Frequency of PREJUDICE: " + map.get("prejudice"));
}
System.out.println();
}
}
4.测试结果为:
2.1.2 基于二分搜索树实现
- 以二分搜索树底层的映射类
BSTMap
:
注意:对于底层是二分搜索树,Key必须是可比较的。
package MapPart;
import java.util.ArrayList;
public class BSTMap, V> implements Map {
private class Node {
public K key;
public V value;
public Node left, right;
public Node(K key, V value) {
this.key = key;
this.value = value;
left = null;
right = null;
}
}
private Node root;
private int size;
public BSTMap() {
root = null;
size = 0;
}
@Override
public int getSize() {
return size;
}
@Override
public boolean isEmpty() {
return size == 0;
}
// 向二分搜索树中添加新的元素(key, value)
@Override
public void add(K key, V value) {
root = add(root, key, value);
}
// 向以node为根的二分搜索树中插入元素(key, value),递归算法
// 返回插入新节点后二分搜索树的根
private Node add(Node node, K key, V value) {
if (node == null) {
size++;
return new Node(key, value);
}
if (key.compareTo(node.key) < 0) //key < node.key
node.left = add(node.left, key, value); //向左子树中插入 key 和 value
else if (key.compareTo(node.key) > 0)//key > node.key
node.right = add(node.right, key, value);//向右子树中插入 key 和 value
else // key.compareTo(node.key) == 0
node.value = value;
return node;
}
// 返回以node为根节点的二分搜索树中,key所在的节点
private Node getNode(Node node, K key) { //递归函数getNode
if (node == null)
return null;
if (key.equals(node.key))
return node;
else if (key.compareTo(node.key) < 0)
return getNode(node.left, key);
else // if(key.compareTo(node.key) > 0)
return getNode(node.right, key);
}
@Override
public boolean contains(K key) {
return getNode(root, key) != null; //从根节点root开始寻找 key
}
@Override
public V get(K key) {
Node node = getNode(root, key);//从根节点root开始寻找 key
return node == null ? null : node.value;//如果 K 为空则返回的 V 也为空,否则返回 node.value
}
@Override
public void set(K key, V newValue) {
Node node = getNode(root, key);
if (node == null)
throw new IllegalArgumentException(key + " doesn't exist!");
node.value = newValue;
}
//(删除节点)
// 返回以node为根的二分搜索树的最小值所在的节点
private Node minimum(Node node) {
if (node.left == null)
return node;
return minimum(node.left);
}
// 删除掉以node为根的二分搜索树中的最小节点
// 返回删除节点后新的二分搜索树的根
private Node removeMin(Node node) {
if (node.left == null) {
Node rightNode = node.right;
node.right = null;
size--;
return rightNode;
}
node.left = removeMin(node.left);
return node;
}
// 从二分搜索树中删除键为key的节点
@Override
public V remove(K key) {
Node node = getNode(root, key);
if (node != null) {
root = remove(root, key);
return node.value;
}
return null;
}
private Node remove(Node node, K key) {
if (node == null)
return null;
if (key.compareTo(node.key) < 0) {
node.left = remove(node.left, key);
return node;
} else if (key.compareTo(node.key) > 0) {
node.right = remove(node.right, key);
return node;
} else { // key.compareTo(node.key) == 0
// 待删除节点左子树为空的情况
if (node.left == null) {
Node rightNode = node.right;
node.right = null;
size--;
return rightNode;
}
// 待删除节点右子树为空的情况
if (node.right == null) {
Node leftNode = node.left;
node.left = null;
size--;
return leftNode;
}
// 待删除节点左右子树均不为空的情况
// 找到比待删除节点大的最小节点, 即待删除节点右子树的最小节点
// 用这个节点顶替待删除节点的位置
Node successor = minimum(node.right);
successor.right = removeMin(node.right);
successor.left = node.left;
node.left = node.right = null;
return successor;
}
}
}
2.测试:统计单词 PRIDE
和 PRIEJUDICE
其出现的频率是怎样的
新建测试类TestBSTMapDemo
:
import java.util.ArrayList;
public class TestBSTMapDemo {
public static void main(String[] args){
System.out.println("Pride and Prejudice");
ArrayList words = new ArrayList<>();
if(FileOperation.readFile("pride-and-prejudice.txt", words)) {
System.out.println("Total words: " + words.size());
BSTMap map = new BSTMap<>();
for (String word : words) {
if (map.contains(word))
map.set(word, map.get(word) + 1);
else
map.add(word, 1);
}
System.out.println("Total different words: " + map.getSize());
System.out.println("Frequency of PRIDE: " + map.get("pride"));
System.out.println("Frequency of PREJUDICE: " + map.get("prejudice"));
}
System.out.println();
}
}
-
测试结果:
结果.png
3.映射的复杂度分析
通过代码执行时间分别对底层基于链表和底层基于二分搜索树的时间差异进行对比,代码如下:
import java.util.ArrayList;
public class testMap {
private static double testMap(Map map, String filename) {
long startTime = System.nanoTime();
System.out.println(filename);
ArrayList words = new ArrayList<>();
if (FileOperation.readFile(filename, words)) {
System.out.println("Total words: " + words.size()); //输出总词汇数
for (String word : words) {
if (map.contains(word))//当前映射中存在word
map.set(word, map.get(word) + 1);//将 word 对应的频率进行+1操作
else
map.add(word, 1); //给map添加词频,初始化时该次出现1次
}
System.out.println("Total different words: " + map.getSize());
System.out.println("Frequency of PRIDE: " + map.get("pride"));
System.out.println("Frequency of PREJUDICE: " + map.get("prejudice"));
}
long endTime = System.nanoTime();
return (endTime - startTime) / 1000000000.0;
}
public static void main(String[] args) {
String filename = "pride-and-prejudice.txt";
BSTMap bstMap = new BSTMap<>();//声明基于二分搜索树的映射
double time1 = testMap(bstMap, filename);
System.out.println("BST Map: " + time1 + " s");
System.out.println();
LinkedListMap linkedListMap = new LinkedListMap<>();
double time2 = testMap(linkedListMap, filename);
System.out.println("Linked List Map: " + time2 + " s");
}
}
结果为:
结论:由上述结果可知:基于二分搜索树所实现的映射时间复杂度远远小于基于链表所实现的映射时间复杂度。
分析基于二分搜索树所实现的映射时间复杂度远远小于基于链表所实现的映射时间复杂度的原因:
基于二分搜索树所实现的映射中时间复杂度是O(h)
,h
是二分搜索树对应的高度,从根节点开始一层一层的向下找,二分搜索树有多少层它就访问了多少节点;基于链表所实现的映射时间复杂度为O(n)
,对应所有的元素都要遍历一遍。
映射相关:
1.有序映射:Map
中的 键(key)
具有顺序性;【基于搜索树实现】
2.无序映射:Map
中的 键(key)
不具有顺序性;【更高效的通过哈希表来实现】
3.多重映射:多重映射中的键可以重复
4. 集合与映射关系
从某种意义上,可以认为映射Map
也是集合Set
,不过是键(key)
这样的集合,而每一个key
都携带了 value
;本质与映射没有太大的区别;二者间可以相互转化,若有了集合的底层实现,通过重定义集合中的元素E
是键值数据对
,对键值数据对进行比较时,是以键值 Key 进行比较的,而不在意 Value 的值。
基于映射的实现包装出集合来:
当已经有了映射e
的底层实现,集合
就可以理解为
中V
为空的情况,不管什么样的键(key)
,其对应的值都是空的,只需考虑键即可,当只考虑键key
时,映射Map
就是Set
的集合,但get 与 set 方法
就没有意义了,只要对映射Map
包装,就可以得到集合Set
这种数据结构了。
5 映射在应用--Leetcode
题目
-
集合 Set 问题
//Leetcode 349号题目
image.png
代码:
import java.util.ArrayList;
import java.util.TreeSet;
class Solution349 {
public int[] intersection(int[] nums1, int[] nums2) {
TreeSet set = new TreeSet<>();
for(int num: nums1)
set.add(num);
ArrayList list = new ArrayList<>();
for(int num: nums2){
if(set.contains(num)){
list.add(num); //记录交集数组
set.remove(num);//删除重复元素
}
}
int[] res = new int[list.size()];
for(int i = 0 ; i < list.size() ; i ++)
res[i] = list.get(i);
return res;
}
}
-
映射 Map 问题
//Leetcode 350号题目
image.png
import java.util.ArrayList;
import java.util.TreeMap;
public class Solution350 {
public int[] intersect(int[] nums1, int[] nums2) {
//第一个Integer代表数组中的元素,第二个Integer代表数组中出现的频次
TreeMap map = new TreeMap<>();
for(int num: nums1){
if(!map.containsKey(num))//不包含key
map.put(num, 1);//添加元素,对num 添加频次,频次是1
else
map.put(num, map.get(num) + 1);//修改为现在num 频次 +1
}
ArrayList res = new ArrayList<>();
for(int num: nums2){
if(map.containsKey(num)){
res.add(num); //存放交集num
map.put(num, map.get(num) - 1); //添加进去后,num频次要 -1
if(map.get(num) == 0) //num 频次为0,则直接删除
map.remove(num);
}
}
int[] ret = new int[res.size()];
for(int i = 0 ; i < res.size() ; i ++)
ret[i] = res.get(i);
return ret;
}
}
点赞是最好的支持,关注是最大的鼓励。亲爱的朋友,很荣幸在遇到您,若有疑问,欢迎探讨~~~。
源码地址 https://github.com/FelixBin/dataStructure/tree/master/src/MapPart