1. 程式人生 > >字典樹-大量字串字首及出現次數是否存在統計(Trie樹-java)演算法實現

字典樹-大量字串字首及出現次數是否存在統計(Trie樹-java)演算法實現

前言

       字典樹又稱單詞查詢樹,它是一種樹形結構,是一種雜湊樹的變種,典型應用是用於統計,儲存大量的字串(但不僅限於字串),統計以是否有以某字串最為字首的字串,有的話有多少,某字串出現了多少次等,所以經常被搜尋引擎系統用於文字詞頻統計。

       它與字典很相似,當你要查一個單詞是不是在字典樹中,首先看單詞的第一個字母是不是在字典的第一層,如果不在,說明字典樹裡沒有該單詞,如果在就在該字母的孩子節點裡找是不是有單詞的第二個字母,沒有說明沒有該單詞,有的話用同樣的方法繼續查詢.字典樹不僅可以用來儲存字母,也可以儲存數字等其它資料。它的優勢是,利用字串的公共字首來節約儲存空間,最大限度地減少無謂的字串比較,查詢效率比雜湊表還高,當資料足夠龐大時,會發現她比傳統的字串統計

要快很多。


       它有三個基本性質:
(1)根節點不儲存字元
(2)除根節點外每一個節點都只儲存一個字元
(3)從根節點到某一節點,路徑上經過的字元連線起來,為該節點對應的字串,每個節點的所有子節點包含的字元都不相同。

       下邊我將把我實現的程式碼和大家分享一下,程式碼幾乎每行都有詳細註釋,大家一看就清除明瞭了,時間原因,就不再用多餘的文字再追加詳述了。

建立字典樹

package Trie;

import org.apache.commons.lang3.StringUtils;

import com.google.common.base.CharMatcher;

/**
 * 字典樹類
 * 
 * @author chenleixing
 */
public class Trie {

	//各個節點的子樹數目即字串中的字元出現的最多種類
	private final int SIZE = 26;
	//除根節點外其他所有子節點的數目
	private int numNode;
	//樹的深度即最長字串的長度
	private int depth;
	//字典樹的根
	private TrieNode root;

	/**
	 * 初始化字典樹
	 */
	public Trie() {
		this.numNode=0;
		this.depth=0;
		this.root = new TrieNode();
	}

	/**
	 * 字典樹節點類,為私有內部類
	 */
	private class TrieNode {

		// 所有的兒子節點或者一級子節點
		private TrieNode[] son;
		// 有多少字串經過或到達這個節點,即節點字元出現的次數
		private int numPass;
		// 有多少字串通過這個節點併到此結束的數量
		private int numEnd;
		// 是否有結束節點
		private boolean isEnd;
		// 節點的值
		private char value;

		/**
		 * 初始化節點類
		 */
		public TrieNode() {
			this.numPass=0;
			this.numEnd=0;
			this.son = new TrieNode[SIZE];
			this.isEnd = false;
		}
	}

首先各種方法操作方法的字串驗證或非法判斷

       一般字典樹常用儲存單詞類字串,當然也可以儲存數字或者其他字元只是一個耗費記憶體較多的問題,不管怎麼最好在操作方法之前做個驗證和判斷,以更加的提高操作效率,程式碼健壯性更強,如插入,查詢是否存在等,如果字串中存在“非法”的字元,那麼可以直接返回false來結束操作。程式碼如下邊所示:

	/**
	 * 對操作的字串進行一個非法的判斷,是否為字母構成的字串
	 */
	private boolean isStrOfLetter(String str){
		//null或者空白字串,則插入失敗
		if (StringUtils.isBlank(str)){
			return false;
		}
		//如果字串中有非字母字元,則插入失敗
		if(!CharMatcher.JAVA_LETTER.matchesAllOf(str)){
			return false;
		}
		return true;
	}

       其中程式碼中我用到了common-lang工具包裡的StringUtils工具類和Guava裡的CharMatcher,當然大家可以用迴圈啊用正則表示式或者其他工具來實現相同的功能,這裡就不再一一詳述了,如果有想深入瞭解它們用法的話或jar包下載,可參考我的以下博文:commons-lang中常用方法StringUtils方法全集介紹JavaScript、Java正則表示式詳解打了興奮劑的CharMatcherStrings字串判斷工具

字典樹儲存字串方法實現

	/**
	 * 插入方法,插入字串,不區分大小寫
	 */
	public boolean insertStr(String str) {
		//插入的字元為非法字元,則插入失敗
		if(!isStrOfLetter(str)){
			return false;
		}
		//插入字串
		str=str.toLowerCase();//不區分大小寫,轉為小寫
		char[] letters = str.toCharArray();//轉成字元陣列
		TrieNode node=this.root;//先從父節點開始
		for (char c:letters) {
			int pos = c - 'a';//得到應存son[]中的索引
			if (node.son[pos] == null) {//此字元不存在
				node.son[pos] = new TrieNode();
				node.son[pos].value = c;
				node.son[pos].numPass=1;
				this.numNode++;
			} else {//此字元已經存入
				node.son[pos].numPass++;
			}
			node = node.son[pos];//繼續為下一下字元做準備
		}
		node.isEnd = true;//標記:有字串到了此節點已結束
		node.numEnd++;//這個字串重複次數
		if(letters.length>this.depth){//記錄樹的深度
			this.depth=letters.length;
		}
		
		return true;//插入成功
	}

查詢是否存以某字首開頭的字串方法實現

	/**
	 * 在字典樹中查詢是否存在某字串為字首開頭的字串(包括字首字串本身),不區分大小寫
	 */
	public boolean isContainPrefix(String str) {
		//查詢的字元是否非法字元,則失敗
		if(!isStrOfLetter(str)){
			return false;
		}
		//查詢字串
		str=str.toLowerCase();//不區分大小寫,轉為小寫
		char[] letters = str.toCharArray();//轉成字元陣列
		TrieNode node=this.root;//先從父節點開始
		for (char c:letters) {
			int pos = c - 'a';//得到應存son[]中的索引
			if (node.son[pos] != null) {
				node=node.son[pos];//此字元存在繼續查詢下一個字元
			} else {//此字元不存在
				return false;
			}
		}
		return true;
	}

查詢是否存在某字串(不為字首)方法實現

	/**
	 * 在字典樹中查詢是否存在某字串(不為字首),不區分大小寫
	 */
	public boolean isContainStr(String str) {
		//查詢的字元是否非法字元,則失敗
		if(!isStrOfLetter(str)){
			return false;
		}
		//查詢字串
		str=str.toLowerCase();//不區分大小寫,轉為小寫
		char[] letters = str.toCharArray();//轉成字元陣列
		TrieNode node=this.root;//先從父節點開始
		for (char c:letters) {
			int pos = c - 'a';//得到應存son[]中的索引
			if (node.son[pos] != null) {
				node=node.son[pos];//此字元存在繼續查詢下一個字元
			} else {//此字元不存在
				return false;
			}
		}
		return node.isEnd;
	}

統計以指定字串為字首的字串數量方法實現

	/**
	 * 統計以指定字串為字首的字串數量,不區分大小寫
	 */
	public int countPrefix(String str) {
		//統計的字元是否非法字元,則返回0
		if(!isStrOfLetter(str)){
			return 0;
		}
		//查詢字串
		str=str.toLowerCase();//不區分大小寫,轉為小寫
		char[] letters = str.toCharArray();//轉成字元陣列
		TrieNode node=this.root;//先從父節點開始
		for (char c:letters) {
			int pos = c - 'a';//得到應存son[]中的索引
			if (node.son[pos] == null) {
				return 0;//沒有以此字串為字首開頭
			} else {//此字元存在,繼續遍歷
				node=node.son[pos];
			}
		}
		return node.numPass;
	}
	

統計某字串出現的次數方法實現

	/**
	 * 統計以指定字串為字首的字串數量,不區分大小寫
	 */
	public int countPrefix(String str) {
		//統計的字元是否非法字元,則返回0
		if(!isStrOfLetter(str)){
			return 0;
		}
		//查詢字串
		str=str.toLowerCase();//不區分大小寫,轉為小寫
		char[] letters = str.toCharArray();//轉成字元陣列
		TrieNode node=this.root;//先從父節點開始
		for (char c:letters) {
			int pos = c - 'a';//得到應存son[]中的索引
			if (node.son[pos] == null) {
				return 0;//沒有以此字串為字首開頭
			} else {//此字元存在,繼續遍歷
				node=node.son[pos];
			}
		}
		return node.numPass;
	}
	

前序遍歷字典樹方法實現

	/**
	 * 統計以指定字串為字首的字串數量,不區分大小寫
	 */
	public int countPrefix(String str) {
		//統計的字元是否非法字元,則返回0
		if(!isStrOfLetter(str)){
			return 0;
		}
		//查詢字串
		str=str.toLowerCase();//不區分大小寫,轉為小寫
		char[] letters = str.toCharArray();//轉成字元陣列
		TrieNode node=this.root;//先從父節點開始
		for (char c:letters) {
			int pos = c - 'a';//得到應存son[]中的索引
			if (node.son[pos] == null) {
				return 0;//沒有以此字串為字首開頭
			} else {//此字元存在,繼續遍歷
				node=node.son[pos];
			}
		}
		return node.numPass;
	}
	

       這裡是通過遞迴打印出所有的節點的值,如果想存入一個List或者追加StringBuilder或者StringBuffer中,需要建立一個全域性變數或者方法裡建立然後以引數形式傳到遞迴方法中,這裡不再進行詳述,因為字典樹的主要用途不在這裡,此方法一般不需要。

返回字典樹的根節點

	
	/**
	 * 返回根節點,根節點不存值
	 */
	public TrieNode getRoot() {
		return this.root;
	}

返回字典樹的深度

	/**
	 * 返回字典樹的深度
	 */
	public int getDept() {
		return this.depth;
	}

返回字典樹的所有子節點的數目

	/**
	 * 返回字典樹的所有子節點的數目(不包含子節點)
	 */
	public int getNumNode() {
		return this.numNode;
	}

測試字典樹的所有方法

package Trie;

import org.junit.Test;

public class TrieTest {
	
	/**
	 * 測試字典樹
	 * 
	 * @author chenleixing
	 */
	@Test
	public void testTrie(){
		//建立一個字典樹(其實可以在建立時指定字典樹各節點的大小,大小根據存入字元種類的數量)
		Trie trie=new Trie();
		//測試字串(當然越龐大越能展現它的優勢)
		String[] testStrs=new String[]{"chefsd","chen","hahi","ch","cxing","hahha","my","home"};
		for(String s:testStrs){//向字典樹中存入字串
			trie.insertStr(s);
		}
		
		//測試是否包含指定字首的字串
		boolean isCont=trie.isContainPrefix("ch");
		System.out.println(isCont);//輸出true
		
		//測試包含指定字首的字串的數量
		int countPrefix=trie.countPrefix("ch");
		System.out.println(countPrefix);//輸出3
		
		//測試包含指定字串的數量
		int countStr=trie.countStr("ch");
		System.out.println(countStr);//輸出1
		
		//測試包含指定字首的字串的數量
		int countPre=trie.countPrefix("chee");
		System.out.println(countPre);//輸出0
		
		//測試子節點的數量和樹的深度
		int numNode=trie.getNumNode();//為22
		int dept=trie.getDept();//為6
		System.out.println("字典樹子節點的數量:"+numNode+"  樹的深度:"+dept);
	}
}

測試結果

true
3
1
0
字典樹子節點的數量:22  樹的深度:6


over了!

最後,認真看過的網友們,大神們,如有感覺我這個程式猿有哪個地方說的不對或者不妥或者你有很好的議或者建議或點子方法,還望您大恩大德施捨n秒的時間留下你的寶貴文字(留言),以便你,我,還有廣大的程式猿們更快地成長與進步.......