字典树的实现与在高并发场景下的改进

什么是字典树

在计算机科学中,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个小写字母的限制呢?

目前还未落实在代码上,预计思路如下:

  1. 直接使用ConcurrentHashMap类型的children
  2. 模仿java 1.7中的segment,使用分段锁的设计,segment的个数可以根据场景的并发度来确定
  3. 模仿java 1.8中的CAS + synchronized来设计children的类型

猜你喜欢

转载自blog.csdn.net/weixin_43857365/article/details/89206981