Android程式設計師會遇到的演算法(part 5 字典樹)

qing的世界發表於2019-04-07

隔了三個月,終於下定決心繼續更新了,還是想把 關於演算法這部分寫完整,這次我會開始介紹一些資料結構的用法,本來想說更新一個關於並查集的問題的。但是思前想後,還是字典樹的實際用例更多一些,例子也容易讓人理解。所以這次我要詳細的講一下Trie-> AKA 字典樹這個資料結構的用法和一些實際的例子。

首先Trie這個單詞是一個新的詞彙,中文的翻譯一般說是字典樹,或者叫字首樹(因為用這個樹結構可以通過字首搜尋資料),中文的翻譯就比較容易讓人理解。但是有趣的是這個單詞的英文發音卻很有爭議,如果你在Google上搜尋Trie Pronunciation,你會發現大家都在糾結於到底是發Tree的音還是Try的音。

trie1.png

當然,這些都不是重點,隨便哪個我覺得大家都能理解,反正千萬別到時候因為某些同學和你認知的發音不一樣就去鄙視人家。。。

image

華麗麗的分割線


1. 字典樹的原理

那麼說會重點,通俗的來講,字典樹是一種可以方便人們通過字首查詢單詞的一種資料結構,它把具有相同字首的單詞合併到一起,節省了儲存空間(和直接用HashMap存相比),同時也可以做字首查詢或者自動補全。

舉個例子,我就搬來維基百科的例子

假設我們要收錄一下幾個單詞Tea, Ted, Ten,Inn。那麼我們會以以下的方式把這些單詞逐一錄入樹種:

  1. 建立初始節點Root, 當前插入節點為根節點
  2. 先準備錄入Tea,分解Tea的字母T, E, A,先把第一個字母T放入根節點下面。並且把當前插入節點改為T
  3. T也就是當前插入節點下面放入E,當前插入節點改為E
  4. E也就是當前插入節點下面放入A,當前插入節點改為A,但是單詞所有字母都輸入結束了,因此在A上面打個tag,標記該節點是某個單詞的結束字母。

第一個單詞錄入完畢

trie2.jpg

  1. 準備錄入Ted,當前插入節點為根節點。分解單詞Ted為TED.準備插入第一個字母T
  2. 先檢查根節點下面有沒有T這個字母。答案是有,那麼不需要再插入一個T了,直接將插入節點改為T。
  3. 第二個輸入字母為E,同樣的,T下面也有一個E,也不需要再插入E了,直接將插入節點改為E.
  4. 第三個輸入字母為D,這次當前的插入節點E下面沒有D,那麼我們插入D,當前插入節點也改為D
  5. 因為TED輸入完畢,所以在D上面打一個tag,標記標記該節點是某個單詞的結束字母。

trie3.jpg

剩下的字母同理可得

trie4.jpg

在討論程式碼的實現之前,我們先看看字典樹的實際應用:

2.字典樹的應用。

2.1自動補全

字典樹的最經典的應用肯定就是自動補全了,比如你在Google或者百度進行搜尋的時候

trie5.png

谷歌將很多熱門的搜尋單詞用字典樹的方式儲存在後端,當Web網頁搜尋單詞的字首的時候,後端查詢到該字首最後一個節點的位置,將下面的子節點全部排列出來返回給前端。

比如用以上的例子,當使用者搜尋TE的時候,定位到最後一個節點E,將E節點下面的節點返回並且拼湊出一個完整的字串,TED和TEA。

2.2 搜尋檔案

還有比如說當我們在Android Studio按下:alt+control+o,並且搜尋檔案的時候:

trie6.png

當我們搜尋Main的時候,顯示以Main開頭的檔案。

這個功能也可以用字典樹實現(當然這是可以,不一定就是用字典樹實現,事先宣告免得誤人子弟,因為做索引還可以用B樹來做,類似資料庫,這裡不展開,感興趣的可以看阮一峰大神的關於資料庫原理的文章)

2.3 通訊錄

trie7.jpg

請自動忽略我粉紅色的鍵盤。。。。

8_150416141903_13.jpg

比如說我們在通訊錄搜尋185,app會自動返回以185開頭的號碼的聯絡人,這個道理也是一樣的,只不過樹的內容不再是單詞,而是數字罷了。

有同學問,貌似通訊錄app不只可以搜尋號碼吧,應該照理來說搜尋名字也是可以,比如根據姓來搜尋。

這個答案很簡單,再根據名字做另外一個字典樹不就行了。

image


3.字典樹的程式碼。

這裡我用一個Leetcode的題目作為參考,程式碼是我自己實現的。

Leetcode 字典樹




/**
	 * 
	 * 實現字典樹,訣竅就是用什麼資料結構儲存children,個人偏向用hashmap,因為用陣列寫起來也麻煩而且會浪費多餘的空間。26個字元不一定全都用得到。
	 *
	 */
	class TrieNode {
		// Initialize your data structure here.
		char c;
		HashMap<Character, TrieNode> children = new HashMap<Character, TrieNode>();
		boolean hasWord;

		public TrieNode() {

		}

		public TrieNode(char c) {
			this.c = c;
		}
	}

	public class Trie {
		private TrieNode root;

		public Trie() {
			root = new TrieNode();
		}

		// Inserts a word into the trie.
		public void insert(String word) {
			TrieNode cur = root;
            //curChildren 對應我們講的當前插入節點,所有要插入的節點都要放在當前插入節點的子節點裡面
			HashMap<Character, TrieNode> curChildren = root.children;
			char[] wordArray = word.toCharArray();
            //迴圈遍歷這次要插入的單詞,從第一個字母開始
			for (int i = 0; i < wordArray.length; i++) {
				char wc = wordArray[i];
                //如果當前插入節點有這個字元對應的子節點的話,直接把該子節點變成當前插入節點
				if (curChildren.containsKey(wc)) {
					cur = curChildren.get(wc);
				} else {
                //如果沒有的話,建立一個。
					TrieNode newNode = new TrieNode(wc);
					curChildren.put(wc, newNode);
					cur = newNode;
				}
				curChildren = cur.children;
                //如果該節點是插入單詞的最後一個字元,打一個tag,表面這是一個單詞的結尾、
				if (i == wordArray.length - 1) {
					cur.hasWord = true;
				}
			}
		}

		// Returns if the word is in the trie.
        //有時候雖然該單詞作為字首,存在於字典樹中,但是卻沒有這個單詞。比如我搜尋
        //TE。這個字首存在,但是並沒有這個單詞,E字母對應的節點沒有tag
		public boolean search(String word) {
			if (searchWordNodePos(word) == null) {
				return false;
			} else if (searchWordNodePos(word).hasWord)
				return true;
			else
				return false;
		}

		// Returns if there is any word in the trie
		// that starts with the given prefix.
		public boolean startsWith(String prefix) {
			if (searchWordNodePos(prefix) == null) {
				return false;
			} else
				return true;
		}

		//搜尋制定單詞在字典樹中的最後一個字元對應的節點。這個方法是搜尋的關鍵
		public TrieNode searchWordNodePos(String s) {
			TrieNode cur = root;
			char[] sArray = s.toCharArray();
			for (int i = 0; i < sArray.length; i++) {
				char c = sArray[i];
				if (cur.children.containsKey(c)) {
					cur = cur.children.get(c);
				} else {
					return null;
				}
			}
			return cur;
		}
	}
    




複製程式碼

這部分是簡單的搜尋和生成字典樹,但是並沒有實現如何返回字串。其實返回字串就是暴力搜尋的一個過程,把最後一個節點一下的所有子節點組合起來。這裡可以使用我們之前講到的深度優先的方法(其實就相當於找一個樹的所有path的過程,參考Leetcode 257. Binary Tree Paths))。

感興趣的同學可以先自己實現一下。

這裡最後貼上我的實現


public ArrayList<String> getAutoCompletionStrings(String preflix){

	TrieNode lastNode = searchWordNodePos(preflix);
    if( lastNode == null ){
    	return new ArrayList<String>();
    }
    else{
    	ArrayList<String> list = new ArrayList<String>();
        trieHelper(list,"",lastNode);
        return list;
    }
}



private void trieHelper(ArrayList<String> result, String current, TrieNode node) {
		if (node != null && node.hasWord) {
			result.add(current);
		} else if(node != null){
        	Set<Entry<Character,TrieNode>> set = node.children.entrySet();
            for( Entry<Character,TrieNode> entry : set ){
            	trieHelper(result, current+entry.getValue().c , entry.getValue());
            }
		}
        return;
	}


複製程式碼

相關文章