什么是字典树
在计算机科学中,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。
Trie这个术语来自于retrieval。根据词源学,trie的发明者Edward Fredkin把它读作/ˈtriː/ “tree”。[1][2]但是,其他作者把它读作/ˈtraɪ/ “try”。[1][2][3]
在图示中,键标注在节点中,值标注在节点之下。每一个完整的英文单词对应一个特定的整数。Trie可以看作是一个确定有限状态自动机,尽管边上的符号一般是隐含在分支的顺序中的。
键不需要被显式地保存在节点中。图示中标注出完整的单词,只是为了演示trie的原理。
trie中的键通常是字符串,但也可以是其它的结构。trie的算法可以很容易地修改为处理其它结构的有序序列,比如一串数字或者形状的排列。比如,bitwise trie中的键是一串位元,可以用于表示整数或者内存地址。
– 摘自维基百科
java实现
package TestNode.BinaryTree;
public class DictTree {
Node root = new Node();
public static void main(String[] args) throws Exception {
for (int i = 0; i < 100; i++) {
test();
}
}
public static void test() throws Exception{
DictTree dictTree = new DictTree();
Thread t1 = new Thread(new Runnable() {
@Override
public void run(){
try {
dictTree.insert("ab");
}catch (Exception e){
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run(){
try {
dictTree.insert("abc");
}catch (Exception e){
e.printStackTrace();
}
}
});
t1.start();
t2.start();
Thread.sleep(100);
System.out.println(dictTree.find("ab") +" " + dictTree.find("abc"));
System.out.println();
}
public void insert(String word) throws Exception{
if (!isLeagal(word)){
throw new Exception("InLeagal Input");
}
Node cur = root;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
if (cur.children[idx] == null){
cur.children[idx] = new Node();
}
cur = cur.children[idx];
}
cur.isEnd = true;
}
public Boolean find(String word){
if (!isLeagal(word)){
return false;
}
Node cur = root;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
if (cur.children[idx]==null){
return false;
}
cur = cur.children[idx];
}
return cur.isEnd==true;
}
public Boolean isLeagal(String word){
if (word==null)return false;
for (int i = 0; i < word.length(); i++) {
if (!(word.charAt(i)>='a' && word.charAt(i)<='z'))
return false;
}
return true;
}
}
class Node{
boolean isEnd;
Node[] children;
public Node(){
isEnd = false;
children = new Node[26];
}
}
高并发场景中存在的问题
可以看出在上述代码中,已经实现了字典树的基本功能。大家可以看一下代码中的test方法,test方法中创建了两个线程,分别用来插入"ab"和"abc",正常逻辑下,肯定是可以输出两个true的。但是,
在我的100次的测试中,出现了两个false,这是怎么回事呢
我们试想一种场景,两个线程分别为t1,t2.
step1:
t1首先执行到51行
t2比t2稍微落后一些
step2:
t1创建了a对应的节点
t2读到了这一节点,并决定不再创建新节点,
step3:
t1,t2同时发现a的children中不包括b
此时,t1要插入的word为"ab",所以t1会把b节点的isEnd设为true。
t2要插入的节点为"abc",因为t2读到的a.children也不包括b,所以t2这时会新建一个node b,把t1中创建的b节点的覆盖掉了,所以最终的trie中,就不包含ab这个单词了。
高并发场景下的问题解决
直接在insert方法上加锁
并发度太低,类似于hashtable,这里不建议这样加锁
这里可以使用类似阻塞队列的方式来完成,在node类中,增加同步代码
直接上代码,通过修改Node类就可以提高并发度。
class Node{
boolean isEnd;
Node[] children;
public Node(){
isEnd = false;
children = new Node[26];
}
public synchronized void addChild(char c) {
children[c-'a'] = new Node();
}
public synchronized Node getChild(int idx){
return children[idx];
}
}
但这里要明确一点,就是我们是通过在给Node添加子节点的时候,获取node对象的锁来完成的,当有两个线程,分别试图插入"ab"和"abc"时,为a节点添加b节点这一操作其实是可以并发的,但我们目前的设计,似乎无法做到这一点。
再次改进
先上代码
class Node {
boolean isEnd;
Node[] children;
ReentrantLock[] locks = new ReentrantLock[26];
public Node(){
isEnd = false;
children = new Node[26];
for (int i = 0; i < 26; i++) {
locks[i] = new ReentrantLock();
}
}
public void addChild(char c) {
int idx = c-'a';
locks[idx].lock();
try{
if (children[idx]!=null){
return;
}
children[idx] = new Node();
}finally {
locks[idx].unlock();
}
}
public Node getChild(int idx){
Node res;
locks[idx].lock();
try{
res = children[idx];
}finally {
locks[idx].unlock();
}
return res;
}
}
这次Node节点,对每个可能的子节点都设置了一把锁,如果两个线程同时给a节点增加b节点,是可以并发的,因为锁在子节点b上而不是a节点上,再一次提高了并发度。
增加读写锁
在某些场景下,可能读写都非常频繁,多个线程同时读的操作不会带来什么不好的影响,所以我们可以把锁换成读写锁。
package TestNode.BinaryTree;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class DictTree {
Node root = new Node();
public static void main(String[] args) throws Exception {
int falseNum = 0;
for (int i = 0; i < 10000; i++) {
if (!test()){
falseNum++;
}
}
System.out.println("falaseNum = " + falseNum + " in 1000 times test.");
}
public static boolean test() throws Exception{
System.out.println();
System.out.println();
DictTree dictTree = new DictTree();
Thread t1 = new Thread(new Runnable() {
@Override
public void run(){
try {
dictTree.insert("ab");
}catch (Exception e){
e.printStackTrace();
}
}
}, "abTread");
Thread t2 = new Thread(new Runnable() {
@Override
public void run(){
try {
dictTree.insert("abc");
}catch (Exception e){
e.printStackTrace();
}
}
}, "abcThread");
t1.start();
t2.start();
t1.join();
t2.join();
boolean res;
if (!(res = dictTree.find("ab") && dictTree.find("abc")))
System.out.println(dictTree.find("ab") +" " + dictTree.find("abc"));
return res;
}
public void insert(String word) throws Exception{
System.out.println(Thread.currentThread().getName() + " -- 进入");
if (!isLeagal(word)){
throw new Exception("InLeagal Input");
}
Node cur = root;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
cur.addChild(word.charAt(i));
cur = cur.getChild(idx);
}
cur.isEnd = true;
System.out.println(Thread.currentThread().getName() + " -- 离开");
}
public Boolean find(String word){
if (!isLeagal(word)){
return false;
}
Node cur = root;
for (int i = 0; i < word.length(); i++) {
int idx = word.charAt(i) - 'a';
if (cur.getChild(idx)==null){
return false;
}
cur = cur.getChild(idx);
}
return cur.isEnd==true;
}
public Boolean isLeagal(String word){
if (word==null)return false;
for (int i = 0; i < word.length(); i++) {
if (!(word.charAt(i)>='a' && word.charAt(i)<='z'))
return false;
}
return true;
}
}
class Node {
boolean isEnd;
Node[] children;
ReadWriteLock[] locks = new ReentrantReadWriteLock[26];
public Node(){
isEnd = false;
children = new Node[26];
for (int i = 0; i < 26; i++) {
locks[i] = new ReentrantReadWriteLock();
}
}
public void addChild(char c) {
int idx = c-'a';
locks[idx].writeLock().lock();
try{
if (children[idx]!=null){
return;
}
children[idx] = new Node();
}finally {
locks[idx].writeLock().unlock();
}
}
public Node getChild(int idx){
Node res;
locks[idx].readLock().lock();
try{
res = children[idx];
}finally {
locks[idx].readLock().unlock();
}
return res;
}
}
如果取消输入字符串仅包括26个小写字母的限制呢?
目前还未落实在代码上,预计思路如下:
- 直接使用ConcurrentHashMap类型的children
- 模仿java 1.7中的segment,使用分段锁的设计,segment的个数可以根据场景的并发度来确定
- 模仿java 1.8中的CAS + synchronized来设计children的类型