映射基础与映射实现
一、映射(Map)基础
定义:定义域中的每一个值在止于中都有一个值与它对应;存储(键、值)【Key、Value】数据对的数据结构;根据键,来快速的寻找值,可以非常容易的使用二分搜索树来实现它
实现机制:
Map<K,V> 通过泛型来实现,向 Map 中传入 Key 和 Value 这两个环境变量;
void add(K,V) 添加一个新的元素(键值数据对)
V remove(K) 删除一个元素,对映射来说,键充当索引的作用;可以不考虑键对应的值是谁,只要指定删除键对应的数据,它相应的值就跟着删除了;删除了 K ,告诉用户 K 对应的 Value 是什么,以便用户日后使用;
boolean contains(K) 查找元素,只要看 Key 是否存在于 Map 的数据结构中;
V get(K) 获取具体元素,get 方法中传入的 Key ,也就是 键值;查询的是键(key),返回的是值(Value);
void set(K,V) 修改元素,给定一个新的键(key)和值(value),修改键(key) 在 Map 中对应的值(value)
int getSize() 映射中存储的元素的数量,每一个元素都是一个键值对
boolean isEmpty() 判断映射是否为空
二、以链表来实现映射
代码实现:
Map.java
public interface Map<K, V> { //定义接口
void add(K key, V value); //定义方法
V remove(K key);
boolean contains(K key);
V get(K key);
void set(K key, V newValue);
int getSize();
boolean isEmpty();
}
测试:统计单词 PRIDE 和 PRIEJUDICE其出现的频率是怎样的
LinkedListMap.java
import java.util.ArrayList;
public class LinkedListMap<K, V> implements Map<K, V> {
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;
}
public static void main(String[] args){
System.out.println("Pride and Prejudice");
ArrayList<String> words = new ArrayList<>();
if(FileOperation.readFile("pride-and-prejudice.txt", words)) {
System.out.println("Total words: " + words.size());
LinkedListMap<String, Integer> 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();
}
}
FileOperation.java
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Locale;
import java.util.Scanner;
// 文件相关操作
public class FileOperation {
// 读取文件名称为filename中的内容,并将其中包含的所有词语放进words中
public static boolean readFile(String filename, ArrayList<String> words){
if (filename == null || words == null){
System.out.println("filename is null or words is null");
return false;
}
// 文件读取
Scanner scanner;
try {
File file = new File(filename);
if(file.exists()){
FileInputStream fis = new FileInputStream(file);
scanner = new Scanner(new BufferedInputStream(fis), "UTF-8");
scanner.useLocale(Locale.ENGLISH);
}
else
return false;
}
catch(IOException ioe){
System.out.println("Cannot open " + filename);
return false;
}
// 简单分词
// 这个分词方式相对简陋, 没有考虑很多文本处理中的特殊问题
// 在这里只做demo展示用
if (scanner.hasNextLine()) {
String contents = scanner.useDelimiter("\\A").next();
int start = firstCharacterIndex(contents, 0);
for (int i = start + 1; i <= contents.length(); )
if (i == contents.length() || !Character.isLetter(contents.charAt(i))) {
String word = contents.substring(start, i).toLowerCase();
words.add(word);
start = firstCharacterIndex(contents, i);
i = start + 1;
} else
i++;
}
return true;
}
// 寻找字符串s中,从start的位置开始的第一个字母字符的位置
private static int firstCharacterIndex(String s, int start){
for( int i = start ; i < s.length() ; i ++ )
if( Character.isLetter(s.charAt(i)) )
return i;
return s.length();
}
}
输出:
三、基于二分搜索树的映射实现
代码实现:BSTMap.java(其余同上)
import java.util.ArrayList;
public class BSTMap<K extends Comparable<K>, V> implements Map<K, V> {
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;
}
}
public static void main(String[] args){
System.out.println("Pride and Prejudice");
ArrayList<String> words = new ArrayList<>();
if(FileOperation.readFile("pride-and-prejudice.txt", words)) {
System.out.println("Total words: " + words.size());
BSTMap<String, Integer> 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();
}
}
输出;
四、映射的复杂度分析和更多映射相关问题
代码实现:Main.java
import java.util.ArrayList;
public class Main {
private static double testMap(Map<String, Integer> map, String filename){
long startTime = System.nanoTime();
System.out.println(filename);
ArrayList<String> 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<String, Integer> bstMap = new BSTMap<>();//声明基于二分搜索树的映射
double time1 = testMap(bstMap, filename);
System.out.println("BST Map: " + time1 + " s");
System.out.println();
LinkedListMap<String, Integer> 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是键值数据对<K,V>,对键值数据对进行比较时,是以键值 Key 进行比较的,而不在意 Value 的值。
但更常见的操作是基于映射的实现包装出集合来,若有了映射E的底层实现,集合<K,V>就可以理解为<K,V>中 V 为空的情况,无论什么键(key),其对于的值都是空的,只考虑键即可,当只考虑键key时,映射Map就是Set<K>的集合,但get 与 set 方法就没有意义了,只要对映射Map包装,就可以得到集合Set 这种数据结构了。
五、解决集合与映射实际问题
问题1 集合 Set 问题
代码:
import java.util.ArrayList;
import java.util.TreeSet;
class Solution349 {
public int[] intersection(int[] nums1, int[] nums2) {
TreeSet<Integer> set = new TreeSet<>();
for(int num: nums1)
set.add(num);
ArrayList<Integer> 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;
}
}
问题2 映射 Map 问题
代码:
/// Leetcode 350. Intersection of Two Arrays II
/// https://leetcode.com/problems/intersection-of-two-arrays-ii/description/
import java.util.ArrayList;
import java.util.TreeMap;
public class Solution350 {
public int[] intersect(int[] nums1, int[] nums2) {
TreeMap<Integer, Integer> map = new TreeMap<>();//第一个Integer代表数组中的元素,第二个Integer代表数组中出现的频次
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<Integer> 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;
}
}