【數據結構Java實現】映射【鏈表和二叉搜索樹實現】

映射

1. 映射介紹

C++的 mapmap,Python的字典 dictdict,Java的 TreeMap,HashMapTreeMap, HashMap,都是映射。更抽象的說,函數/方法也是映射。對於映射這種數據結構,其實就是存儲 key:valuekey:value 數據對的結構,根據鍵 keykey,尋找值 valuevalue,就像一本字典,能夠根據單詞查找含義。

介紹集合的時候,我們說集合可以用來統計單詞量;而映射,則可以用來統計詞頻。

我們在這裏,可以很簡單的用鏈表或二分搜索樹實現映射,將映射作爲一個接口 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. 基於二分搜索樹的映射

和集合一樣,我們也可以用二分搜索樹實現一個映射類。不過這時 keykey 有一個限制,即 keykey 必須是可比較的——即實現了 ComparableComparable 接口。

樹的結點如下:

class Node {
	K key;
	V value;
	Node left;
	Node right;
}

由於原來的二叉搜索樹的Node只能夠存儲一個值,現在需要存儲 keykeyvaluevalue 了,因此自己寫吧。

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. 性能比較和複雜度分析

性能比較很簡單,和集合的測試幾乎一樣,寫在 TestTest 類中:

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."); 
	} 
} 

在這裏插入圖片描述
LinkedListSetBSTSet 的比較一樣,BSTMap 的速度遠快於 LinkedListMap

LinkedListMap BSTMap BSTMap平均 BSTMap最差
add O(n)O(n) O(h)O(h) O(logn)O(logn) O(n)O(n)
remove O(n)O(n) O(h)O(h) O(logn)O(logn) O(n)O(n)
set O(n)O(n) O(h)O(h) O(logn)O(logn) O(n)O(n)
get O(n)O(n) O(h)O(h) O(logn)O(logn) O(n)O(n)
contains O(n)O(n) O(h)O(h) O(logn)O(logn) O(n)O(n)

5. 有序映射和無序映射

同集合一樣,我們用二叉搜索樹實現的Map,就是一個有序映射;鏈表實現的Map,其實是一個無序映射。不過 LinkedListMap 太慢了,通常,我們會用哈希實現更快的無序映射

Java中的 TreeMapHashMap 就是有序和無序映射的典型代表;C++中的則是 mapunordered_map

6. 多重映射

多重映射中的鍵可以重複,比如:C++的multimap。有時候可能會需要用到。

7. 集合和映射的關係

在實現的時候,我們發現集合與映射有很多相同的地方。實際上,映射就是把 <key, value> 作爲元素的集合;而集合,就是把 <key, null> 作爲元素的映射。因此,有了集合,我們可以實現映射;有了映射,我們也可以反過來實現集合

比如說,基於集合,我們可以重新定義其中的元素爲一種鍵值對,並且要強調, 這種鍵值對在比較的時候,(如果集合基於二叉搜索樹)是以key的值進行比較的,而不管value的值。但是,用集合實現映射,還需要添加新的操作。

所以,更常見的做法是基於映射實現集合。怎樣做到的呢?很簡單,我們就把映射所有的 valuevalue 作爲空,而且只考慮鍵,這時整個Map就是一個集合,當然,這時 getset 方法就沒有意義了。我們實現一個映射類,再對映射進行包裝,就可以實現集合。

甚至於,我們在實現二叉搜索樹的時候,就可以直接以 <key, value> 作爲結點值,這樣會更加簡單,就省去了很多的Map和Set的重複的底層實現。以後實現AVL樹,紅黑樹的時候就是這種做法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章