算法 (十七)字符串:单词查找树(前缀树),实现添加、删除、搜索、统计前缀数目等功能

1、单词查找树(前缀树)

又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。 ----百度

具体图文表述请看别的技术博客,这里我分别用数组和map来表示树的下一个节点,具体代码实现和细节如下:

数组实现:需要一开始就初始化,比如有26个字母,就将下一个节点集合变成大小为26的节点数字

package cn.nupt;

/**
 * @Description: 前缀树(单词查找树),数组表示
 *
 * @author PizAn
 * @Email [email protected]
 * @date 2019年3月6日 下午3:32:33
 * 
 */
public class TrieTree {

	static class TrieNode { //节点类,就跟链表、二叉树一样

		private int path; //经过这个节点的路径 的数目,用来计算前缀数
		private int end;  //以这个节点为尾的字符串的数目
		private TrieNode[] next; //指向下一个节点的指针(就跟链表一样)

		public TrieNode() {
			path = 0;
			end = 0;
			next = new TrieNode[26]; //因为是a~z,所以这里初始化为26
		}

	}

	// 默认初始化一个头节点
	public static class Trie { //这里再来一个内部类来放方法
		private TrieNode root;

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

		/**
		 * @Description: 压入字符串
		 * @param word
		 * 
		 */
		public void insert(String word) {
			if (word == null) {
				return;
			}

			TrieNode node = root;
			int index = 0;
			char[] cha = word.toCharArray();
			for (int i = 0; i < cha.length; i++) {
				index = cha[i] - 'a';
				// 每一个点都可以对应26个方向,而这个步骤是找到这个方向(index)看这个方向有没有东西,没有的话就创建一个
				// 有的话就直接跳到下一个
				if (node.next[index] == null) {
					node.next[index] = new TrieNode();
				}
				node = node.next[index];
				node.path++; // 将路径记录+1
			}
			node.end++; // 在最后一个节点记录一下

		}

		/**
		 * @Description: 查找字符串,并返回字符串个数
		 * @param word
		 * @return
		 * 
		 */
		public int search(String word) {
			if (word == null) {
				return 0;
			}
			TrieNode node = root;
			int index = 0;
			char[] cha = word.toCharArray();
			for (int i = 0; i < cha.length; i++) {
				index = cha[i] - 'a';
				if (node.next[index] == null) {//如果找到下一个节点为空,直接返回,说明没有这个单词
					return 0;
				}
				node = node.next[index];

			}
			return node.end;
		}

		/**
		 * @Description: 删除字符串(就是把沿途的path--,最后一个end--,如果路径中的path减到0了,说明以后没有东西了,直接将这个节点指空)
		 * @param word
		 * 
		 */
		public void delete(String word) {
			// 如果没有输入字符串,返回
			if (word == null) {
				return;
			}
			// 如果没有搜索到字符串,直接返回
			if (search(word) == 0) {
				return;
			} else {
				char[] cha = word.toCharArray(); // 每次一上来先把字符串变成字符数组
				TrieNode node = root;// 每次都是从这个头节点开始操作
				int index = 0; // 每次都要初始化一个索引,来记录这个字符是第几个
				for (int i = 0; i < cha.length; i++) {
					index = cha[i] - 'a';
					if (node.next[index].path == 1) { // 如果路径等于1,表示下面是一条独径(前面已经search过了,肯定有这个字符串)
						node.next[index] = null; // 直接指空
						return;
					} else {
						node.next[index].path--;// 如果不为空,路径减1,往下走
						node = node.next[index];
					}
				}
				node.end--;
			}
		}

		/**
		 * @Description: 计算前缀的个数(找到前缀的最后一个如果是null返回0,否则返回path)
		 * @param pre
		 * @return
		 * 
		 */
		public int prefixNumber(String pre) { 
			//其实查前缀和查单词代码是一样的,都是将单词/前缀拆开,然后一个一个找,中间连不下去了说明没有这个单词/前缀,直接返回0,如果能查到最后,分别返回path/end
			if (pre == null) {
				return 0;
			}
			
			//固定三个步骤
			char[] cha = pre.toCharArray();
			int index = 0;
			TrieNode node = root;

			for (int i = 0; i < cha.length; i++) {
				index = cha[i] - 'a';
				if (node.next[index] == null) {//如果找到下一个节点为空,直接返回,说明没有这个前缀
					return 0;
				}
				node = node.next[index];

			}
			return node.path;
		}

	}

	public static void main(String[] args) {

		Trie trie = new Trie();
		System.out.println(trie.search("zuo"));// 0
		trie.insert("zuo");
		System.out.println(trie.search("zuo"));// 1
		trie.delete("zuo");
		System.out.println(trie.search("zuo"));// 0
		trie.insert("zuo");
		trie.insert("zuo");
		trie.delete("zuo");
		System.out.println(trie.search("zuo"));// 1
		trie.delete("zuo");
		System.out.println(trie.search("zuo"));// 0
		trie.insert("zuoa");
		trie.insert("zuoac");
		trie.insert("zuoab");
		trie.insert("zuoad");
		trie.delete("zuoa");
		System.out.println(trie.search("zuoa"));// 0
		System.out.println(trie.prefixNumber("zuo"));// 3

	}

}


map实现:和数组实现一样,只不过不是预先定义下一个节点的数目,而是用map存起来

package cn.nupt;

import java.util.HashMap;

/**
 * @Description: 前缀树(单词查找树),map表示
 *
 * @author PizAn
 * @Email [email protected]
 * @date 2019年3月6日 下午3:32:33
 * 
 */
public class TrieTree2 {

	static class TrieNode {

		private int path;
		private int end;
		//private TrieNode[] next;
		private HashMap<Integer, TrieNode> next;

		public TrieNode() {
			path = 0;
			end = 0;
			//next = new TrieNode[26];
			next = new HashMap<Integer, TrieNode>();
			
		}

	}

	// 默认初始化一个头节点
	public static class Trie {
		private TrieNode root;

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

		/**
		 * @Description: 压入字符串
		 * @param word
		 * 
		 */
		public void insert(String word) {
			if (word == null) {
				return;
			}

			TrieNode node = root;
			int index = 0;
			char[] cha = word.toCharArray();
			for (int i = 0; i < cha.length; i++) {
				index = cha[i] - 'a';
				// 每一个点都可以对应26个方向,而这个步骤是找到这个方向(index)看这个方向有没有东西,没有的话就创建一个
				// 有的话就直接跳到下一个
				/*if (node.next[index] == null) {
					node.next[index] = new TrieNode();
				}*/
				if(node.next.get(index) == null){
					node.next.put(index, new TrieNode());
				}
				node = node.next.get(index);
				node.path++; // 将路径记录+1
			}
			node.end++; // 在最后一个节点记录一下

		}

		/**
		 * @Description: 查找字符串,并返回字符串个数
		 * @param word
		 * @return
		 * 
		 */
		public int search(String word) {
			if (word == null) {
				return 0;
			}
			TrieNode node = root;
			int index = 0;
			char[] cha = word.toCharArray();
			for (int i = 0; i < cha.length; i++) {
				index = cha[i] - 'a';
				/*if (node.next[index] == null) {
					return 0;
				}
				node = node.next[index];*/
				if(node.next.get(index) == null){
					return 0;
				}
				node = node.next.get(index);

			}
			return node.end;
		}

		/**
		 * @Description: 删除字符串(就是把沿途的path--,最后一个end--,如果路径中的path减到0了,说明以后没有东西了,直接将这个节点指空)
		 * @param word
		 * 
		 */
		public void delete(String word) {
			// 如果没有输入字符串,返回
			if (word == null) {
				return;
			}
			// 如果没有搜索到字符串,直接返回
			if (search(word) == 0) {
				return;
			} else {
				char[] cha = word.toCharArray(); // 每次一上来先把字符串变成字符数组
				TrieNode node = root;// 每次都是从这个头节点开始操作
				int index = 0; // 每次都要初始化一个索引,来记录这个字符是第几个
				for (int i = 0; i < cha.length; i++) {
					index = cha[i] - 'a';
				/*	if (node.next[index].path == 1) { // 如果路径等于1,表示下面是一条独径(前面已经search过了,肯定有这个字符串)
						node.next[index] = null; // 直接指空
						return;
					} else {
						node.next[index].path--;// 如果不为空,路径减1,往下走
						node = node.next[index];
					}*/
					if(node.next.get(index).path == 1){
						node.next.put(index, null);
						return;
					}else {
						node.next.get(index).path--;
						node = node.next.get(index);
					}
				}
				node.end--;
			}
		}

		/**
		 * @Description: 计算前缀的个数(找到前缀的最后一个如果是null返回0,否则返回path)
		 * @param pre
		 * @return
		 * 
		 */
		public int prefixNumber(String pre) {
			if (pre == null) {
				return 0;
			}
			
			//固定三个步骤
			char[] cha = pre.toCharArray();
			int index = 0;
			TrieNode node = root;

			for (int i = 0; i < cha.length; i++) {
				index = cha[i] - 'a';
			/*	if (node.next[index] == null) {
					return 0;
				}
				node = node.next[index];*/
				if(node.next.get(index) == null){
					return 0;
				}
				node = node.next.get(index);

			}
			return node.path;
		}

	}

	public static void main(String[] args) {

		Trie trie = new Trie();
		System.out.println(trie.search("zuo"));// 0
		trie.insert("zuo");
		System.out.println(trie.search("zuo"));// 1
		trie.delete("zuo");
		System.out.println(trie.search("zuo"));// 0
		trie.insert("zuo");
		trie.insert("zuo");
		trie.delete("zuo");
		System.out.println(trie.search("zuo"));// 1
		trie.delete("zuo");
		System.out.println(trie.search("zuo"));// 0
		trie.insert("zuoa");
		trie.insert("zuoac");
		trie.insert("zuoab");
		trie.insert("zuoad");
		trie.delete("zuoa");
		System.out.println(trie.search("zuoa"));// 0
		System.out.println(trie.prefixNumber("zuo"));// 3

	}

}

你可能感兴趣的:(算法)