資料結構之Java實現底層Map
Map是一種對映類集合,相比於Set既有鍵也有值,以一對鍵值對形式儲存,不能存在相同元素(鍵不能相同),首先和前面的Set一樣,定義一個Map介面類,分別用連結串列和二分搜尋樹來實現,由於結點元素需要儲存的是一對鍵值對,所以不用前面文章的連結串列和二分搜尋樹,重新定製一下結點資訊和相應的資料結構,下面是實現過程。
1、Map介面
public interface Map<K, V> { //對映Map介面類 void add(K key, V value); //新增元素,需要鍵和值 V remove(K key); //根據鍵來刪除值,並返回該元素 boolean contain(K key); //根據鍵判斷是否存在某元素 V get(K key); //根據鍵獲取相應值 void set(K key, V newValue);//根據輸入更新元素 int getSize(); //獲取大小 boolean isEmpty(); //判斷是否為空 }
2、用連結串列實現Map
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(K key, V value) { this(key, value, null); }
public Node() { this(null, null, null); }
@Override public String toString() { return key.toString() + " : " + value.toString(); } }
private Node dummyHead; //宣告虛擬頭結點 private int size; //宣告尺寸大小
public LinkedListMap() { //無參建構函式 dummyHead = new Node(); //例項化頭結點 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; //遍歷整個Map集合都沒找到,返回空 }
@Override public void add(K key, V value) { //新增一個元素 Node node = getNode(key); //根據此鍵查詢集合中是否存在此鍵 //如果返回為空說明傳入此鍵不存在,則以此鍵值對構建一個結點 if (node == null) { dummyHead.next = new Node(key, value, dummyHead.next); size++; } else //否則,此鍵存在,則更新其值 node.value = value; }
//刪除元素 @Override public V remove(K key) { //建立以個指向虛擬頭結點的前驅結點 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; }
@Override public void set(K key, V newValue) { Node node = getNode(key); if (node == null) throw new IllegalArgumentException(key + "does't exist!"); node.value = newValue; }
@Override public V get(K key) { //根據鍵返回值 Node node = getNode(key); //根據鍵去找相應結點 return node == null ? null : node.value; //根據返回值的不同返回相應結果 }
@Override public int getSize() { //獲取集合大小 return size; }
@Override public boolean isEmpty() { //檢視集合是否為空 return size == 0; }
@Override public boolean contain(K key) { //判斷含有某鍵的結點是否存在 return getNode(key) != null; } }
2、用二分搜尋樹實現Map
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; //初始尺寸為0 }
@Override public int getSize() { //獲取集合大小 return size; }
@Override public boolean isEmpty() { //判斷集合是否為空 return size == 0; }
//私有輔助函式,根據傳入結點和鍵找到與其相應的結點,採取遞迴思想 private Node getNode(Node node, K key) { //遞迴終止條件 if (node == null) return null; //鍵相等則返回結點 if (key.compareTo(node.key) == 0) return node; //如果小於當前結點鍵則在當前結點左子樹尋找 else if (key.compareTo(node.key) < 0) return getNode(node.left, key); else //如果大於當前結點鍵則在當前結點右子樹尋找 return getNode(node.right, key); }
@Override public boolean contain(K key) { //根據鍵判斷相應結點是否存在 return getNode(root, key) != null;//根據私有函式返回值判斷 }
@Override public void add(K key, V value) {//利用一個鍵值對向集合中新增一個元素 root = add(root, key, value); } //真正呼叫的新增元素,根據傳入結點、鍵值對實現新增操作,私有化,遞迴思想 private Node add(Node node, K key, V value) { //遞迴終止條件 if (node == null) { //當結點為空時以相應的鍵值對產生新結點,維護size,並返回 size++; return new Node(key, value); } //如果鍵比當前結點鍵小則在左子樹遞迴尋找 if (key.compareTo(node.key) < 0) { node.left = add(node.left, key, value); //如果鍵比當前結點鍵大則在右子樹遞迴尋找 } else if (key.compareTo(node.key) > 0) { node.right = add(node.right, key, value); } //如果該鍵已存在則以傳入的值更新該結點的值 else { node.value = value; } return node; }
//找到最小鍵的結點的函式,私有化,遞迴思想,也就是一直找到樹中最左邊結點 private Node minimum(Node node) { if (node.left == null) return node; return minimum(node.left); } //刪除最小鍵結點函式,遞迴思想 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; }
//根據鍵刪除該結點 @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 {
// 待刪除結點左子樹為空的情況 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; } } //根據鍵為相應結點設定新值 @Override public void set(K key, V newValue) { Node node = getNode(root, key); if (node == null) throw new IllegalArgumentException(key + "does't exist!"); node.value = newValue; }
//根據鍵獲取相應的值 @Override public V get(K key) { Node node = getNode(root, key); return node == null ? null : node.value; }
}
以上是兩種實現集合的方法,LinkedListMap對於增、查、刪的時間複雜度都是O(n),BSTMap對於增、查、刪平均時間複雜度為O(log n)優於LinkedListMap,但對於二分搜尋樹最壞的情況下是退化成連結串列所以其時間複雜度最壞情況是O(n).下面是與前面文章Set一樣的方式讀取檔案分析詞彙比較消耗時間,下面是整個過程。
首先是檔案操作工具類
public class FileOperation {
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; }
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; }
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(); } }
然後是測試程式
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.contain(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";
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");
} }
下面是測試結果
操作相同的詞彙量的文字,正如預見性的一樣BSTMap優於LinkedListMap,但是需要規避二分搜尋樹出現退化為單鏈表情況,所以有了後面的平衡二叉樹等結構,以上整個過程實現了Map基本功能