映射
1. 映射介紹
C++的 ,Python的字典 ,Java的 ,都是映射。更抽象的說,函數/方法也是映射。對於映射這種數據結構,其實就是存儲 數據對的結構,根據鍵 ,尋找值 ,就像一本字典,能夠根據單詞查找含義。
介紹集合的時候,我們說集合可以用來統計單詞量;而映射,則可以用來統計詞頻。
我們在這裏,可以很簡單的用鏈表或二分搜索樹實現映射,將映射作爲一個接口 Map<K, V>
,定義如下:
public interface Map<K, V> {
void add(K key, V value);
V remove(K key); //用鍵作爲索引, 進行刪除和查找
void set(K key, V newValue);
V get(K key);
int getSize();
boolean isEmpty();
boolean contains(K key);
}
2. 基於鏈表實現的映射
這是鏈表的結點的定義:
class Node {
K key;
V value;
Node next;
}
下面是代碼,由於原來的鏈表類只有一個參數化的類型,因此我們必須自己寫一個,而不能直接重用。雖然麻煩,但是沒有多難,主要是一些設計上的習慣要知道:
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) {
this.key = key;
this.value = value;
this.next = next;
}
public Node() { this(null, null, null); }
public Node(K key) { this(key, null, null); }
@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; }
//查找key對應的Node
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) { //是否含有以key爲鍵的鍵值對
return getNode(key) != null;
}
@Override
public V get(K key) { //查找鍵對應的值
Node node = getNode(key);
return node == null ? null : node.value; //key存在則返回值; 不存在則返回NULL
}
@Override
public void add(K key, V value) { //添加一個鍵值對, 不存在則添加; 存在key則覆蓋value
Node node = getNode(key);
if (node == null) { //沒有這個key
dummyHead.next = new Node(key, value, dummyHead.next);
++size;
} else node.value = value; //不拋出異常; 設計慣例, 更新原來的值
}
@Override
public void set(K key, V value) {
Node node = getNode(key);
if (node == null) //用戶指定要更新一個鍵的值, 因此鍵必須存在, 不存在則拋出異常, 以和add方法區分開來
throw new IllegalArgumentException(key + " doesn't exist!");
else node.value = value; //存在則更新
}
@Override
public V remove(K key) { //刪除key對應的鍵值對, 存在則刪除返回null; 不存在則直接返回null
Node prev = dummyHead;
while (prev.next != null) {
if (prev.next.key.equals(key))
break;
prev = prev.next;
}
if (prev.next != null) { //存在key則刪除該結點
Node delNode = prev.next;
prev.next = delNode.next;
delNode.next = null;
return delNode.value;
}
return null;
}
public static void main(String[] args) {
System.out.println("Pride and Prejudice");
Array<String> words = new Array<>();
if (FileOperation.readFile("pride-and-prejudice.txt", words)) {
System.out.println("Total words: " + words.getSize());
LinkedListMap<String, Integer> map = new LinkedListMap<>();
for (String word : words) { //對書中的每個單詞
if (map.contains(word)) //集合中存在該單詞
map.set(word, map.get(word) + 1); //將該單詞對應的鍵值對的值+1
else
map.add(word, 1); //不存在則設置該單詞出現的頻數爲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")); //偏見出現的次數
}
}
}
3. 基於二分搜索樹的映射
和集合一樣,我們也可以用二分搜索樹實現一個映射類。不過這時 有一個限制,即 必須是可比較的——即實現了 接口。
樹的結點如下:
class Node {
K key;
V value;
Node left;
Node right;
}
由於原來的二叉搜索樹的Node只能夠存儲一個值,現在需要存儲 和 了,因此自己寫吧。
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; }
//向二分搜索樹中添加新的元素
@Override
public void add(K key, V value) {
root = add(root, key, value);
}
//向以root爲根的二叉搜索樹中插入元素(key, value), 遞歸算法
//返回插入新結點的二分搜索樹的根
private Node add(Node root, K key, V value) {
if (root == null) {
++size;
return new Node(key, value);
}
if (key.compareTo(root.key) < 0)
root.left = add(root.left, key, value);
else if (key.compareTo(root.key) > 0)
root.right = add(root.right, key, value);
else root.value = value; //我們認爲存在key的時候傳入value是要更新value
return root;
}
//返回以root爲根的二分搜索樹中, key所在的結點
private Node getNode(Node root, K key) {
if (root == null)
return null;
if (key.compareTo(root.key) == 0) //key.equals(root.key)
return root;
else if (key.compareTo(root.key) < 0)
return getNode(root.left, key);
else return getNode(root.right, key);
}
@Override
public boolean contains(K key) {
return getNode(root, key) != null;
}
@Override
public V get(K key) {
Node node = getNode(root, key);
return node == null ? null : 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;
}
@Override
public V remove(K key) {
Node node = getNode(root, key);
if (node != null) { //如果使用原來的刪除函數, 我們不知道key是否存在, 因此要先搜索一次
root = remove(root, key);
return node.value;
}
return null;
}
//刪除掉以root爲根的二分搜索樹中鍵爲key的結點, 遞歸算法
//返回刪除結點後新的二分搜索樹的根
private Node remove(Node root, K key) {
if (root == null)
return null;
if (key.compareTo(root.key) < 0) {
root.left = remove(root.left, key);
return root;
}
else if (key.compareTo(root.key) > 0) {
root.right = remove(root.right, key);
return root;
}
else { //如果待刪除結點的左子樹爲空的情況
if (root.left == null) {
Node rightNode = root.right;
root.right = null; //方便垃圾回收
--size;
return rightNode;
} // 待刪除結點的右子樹爲空
if (root.right == null) {
Node leftNode = root.left;
root.left = null; //方便垃圾回收
--size;
return leftNode;
}
//待刪除結點左右子樹均不爲空的情況
//找到比待刪除結點大的最小結點, 即待刪除結點右子樹的最小結點
//用這個結點替代待刪除結點的位置
Node successor = minimum(root.right);
successor.right = removeMin(root.right);
successor.left = root.left;
root.left = root.right = null; //方便垃圾回收
return successor;
}
}
//返回以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;
}
public static void main(String[] args) {
System.out.println("Pride and Prejudice");
Array<String> words = new Array<>();
if (FileOperation.readFile("pride-and-prejudice.txt", words)) {
System.out.println("Total words: " + words.getSize());
BSTMap<String, Integer> map = new BSTMap<>();
for (String word : words) { //對書中的每個單詞
if (map.contains(word)) //集合中存在該單詞
map.set(word, map.get(word) + 1); //將該單詞對應的鍵值對的值+1
else
map.add(word, 1); //不存在則設置該單詞出現的頻數爲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")); //偏見出現的次數
}
}
}
4. 性能比較和複雜度分析
性能比較很簡單,和集合的測試幾乎一樣,寫在 類中:
import java.util.Random;
public class Test {
private static double testMap(Map<String, Integer> map, String filename) { //詞頻統計
long startTime = System.nanoTime();
System.out.println(filename);
Array<String> words = new Array<>();
if (FileOperation.readFile(filename, words))
System.out.println("Total words: " + words.getSize());
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")); //偏見出現的次數
long endTime = System.nanoTime();
return (endTime - startTime) / 1000000000.0;
}
public static void main(String[] args) {
String filename = "pride-and-prejudice.txt";
System.out.println("BSTMap Test:");
BSTMap<String, Integer> bstMap = new BSTMap<>();
double time1 = testMap(bstMap, filename);
System.out.println(time1 + "s.\n");
System.out.println("LinkedListMap Test:");
LinkedListMap<String, Integer> listMap = new LinkedListMap<>();
double time2 = testMap(listMap, filename);
System.out.println(time2 + "s.");
}
}
和 LinkedListSet
與 BSTSet
的比較一樣,BSTMap
的速度遠快於 LinkedListMap
。
LinkedListMap | BSTMap | BSTMap平均 | BSTMap最差 | |
---|---|---|---|---|
add |
||||
remove |
||||
set |
||||
get |
||||
contains |
5. 有序映射和無序映射
同集合一樣,我們用二叉搜索樹實現的Map,就是一個有序映射;鏈表實現的Map,其實是一個無序映射。不過 LinkedListMap
太慢了,通常,我們會用哈希實現更快的無序映射。
Java中的 TreeMap
和 HashMap
就是有序和無序映射的典型代表;C++中的則是 map
和 unordered_map
。
6. 多重映射
多重映射中的鍵可以重複,比如:C++的multimap
。有時候可能會需要用到。
7. 集合和映射的關係
在實現的時候,我們發現集合與映射有很多相同的地方。實際上,映射就是把 <key, value>
作爲元素的集合;而集合,就是把 <key, null>
作爲元素的映射。因此,有了集合,我們可以實現映射;有了映射,我們也可以反過來實現集合。
比如說,基於集合,我們可以重新定義其中的元素爲一種鍵值對,並且要強調, 這種鍵值對在比較的時候,(如果集合基於二叉搜索樹)是以key的值進行比較的,而不管value的值。但是,用集合實現映射,還需要添加新的操作。
所以,更常見的做法是基於映射實現集合。怎樣做到的呢?很簡單,我們就把映射所有的 作爲空,而且只考慮鍵,這時整個Map就是一個集合,當然,這時 get
和 set
方法就沒有意義了。我們實現一個映射類,再對映射進行包裝,就可以實現集合。
甚至於,我們在實現二叉搜索樹的時候,就可以直接以 <key, value>
作爲結點值,這樣會更加簡單,就省去了很多的Map和Set的重複的底層實現。以後實現AVL樹,紅黑樹的時候就是這種做法。