首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

漫画:如何用字典树进行 500 万量级的单词统计?

  • 24-03-05 01:20
  • 3471
  • 9217
blog.csdn.net

640?wx_fmt=gif

640?wx_fmt=jpeg

作者 | channingbreeze

责编 | 胡巍巍

640?wx_fmt=jpeg

小史是一个应届生,虽然学的是电子专业,但是自己业余时间看了很多互联网与编程方面的书,一心想进BAT互联网公司。

640?wx_fmt=jpeg

今天小史去了一家在线英语培训公司面试了。简单的自我介绍后,面试官给了小史一个问题。


640?wx_fmt=jpeg


640?wx_fmt=png

面试现场


640?wx_fmt=jpeg

题目:我有500w个单词,你帮忙设计一个数据结构来进行存储,存好之后,我有两个需求。

1、来了一个新的单词,需要判断是否在这500w个单词中

2、来了一个单词前缀,给出500w个单词中有多少个单词是该前缀

小史这次没有不假思索就给出回答,他学会了深沉。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

小史回忆起吕老师之前教他的Bitmap算法。

640?wx_fmt=jpeg

小史心想:Bitmap可以判断一个数是否在40亿个int32数中,其核心是每一个数映射成一个位,同时申请的bit位数覆盖了整个int32的值域。

小史在纸上算了半天,终于开口了。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

小史:好的,我用Bitmap来做第一问。我把每一个字符串映射成一个位。比如,a是第1位,b是第2位,z是第26位,aa是第27位,ab是第28位,以此类推。英文一共26个字母,我算了一下,6个字符长度的单词总共有26的6次方个,需要占26的6次方个位,大概300M。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

小史:建立数据结构的时候,排序需要花掉nlg(n),排序时字符串比较花掉m,时间一共mnlg(n)。查找的话用二分,就是mlg(n)了,空间是mn。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

一分钟过去了。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg


640?wx_fmt=png

请教大神


回到学校,小史把面试情况和吕老师说了一下。


640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

吕老师:你想想,a到z这26个字母中,可能只有a和i两个是单词,其他都不是,所以你的Bitmap大量空间都被浪费了。这种情况你搞个Hashset没准还更省一点。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg


640?wx_fmt=png

树形结构解难题


640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

小史:哦,这确实是节省了空间,如果要找单词interest,那么就找根节点了,如果是找单词interesting,那么就从根节点往下走,再把沿路的字母们都拼起来就行了。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

(注:这里说的in不是单词,指的是in不是500w单词中的单词。)

吕老师还没说完,小史就打断了他。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

找单词interest:

640?wx_fmt=jpeg

找前缀为inter的所有单词:

640?wx_fmt=jpeg

遍历以前缀节点为根结点的一棵树,就能统计出前缀为inter的所有单词有多少个。

640?wx_fmt=png

字典树


640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

小史:节点中增加一个变量用于计数,在添加节点的时候,就把相应的计数+1。

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

理解了算法之后,小史的代码写起来也是非常快,不一会儿就写好了:

DictionaryTree.java:

import java.util.HashMap;import java.util.Map;/** * @author xiaoshi on 2018/10/5. */public class DictionaryTree {    // 字典树的节点    private class Node {        // 是否是单词        private boolean isWord;        // 单词计数        private int count;        // 字串        private String str;        // 子节点        private Map childs;        // 父节点        private Node parent;        public Node() {            childs = new HashMap();        }        public Node(boolean isWord, int count, String str) {            this();            this.isWord = isWord;            this.count = count;            this.str = str;        }        public void addChild(String key, Node node) {            childs.put(key, node);            node.parent = this;        }        public void removeChild(String key) {            childs.remove(key);        }        public String toString() {            return "str : " + str + ", isWord : " + isWord + ", count : " + count;        }    }    // 字典树根节点    private Node root;    DictionaryTree() {        // 初始化root        root = new Node();    }    // 添加字串    private void addStr(String word, Node node) {        // 计数        node.count++;        String str = node.str;        if(null != str) {            // 寻找公共前缀            String commonPrefix = "";            for(int i=0; i i && word.charAt(i) == str.charAt(i)) {                    commonPrefix += word.charAt(i);                } else {                    break;                }            }            // 找到公共前缀            if(commonPrefix.length() > 0) {                if (commonPrefix.length() == str.length() && commonPrefix.length() == word.length()) {                    // 与之前的词重复                } else if(commonPrefix.length() == str.length() && commonPrefix.length() < word.length()) {                    // 剩余的串                    String wordLeft = word.substring(commonPrefix.length());                    // 剩余的串去子节点中继续找                    searchChild(wordLeft, node);                } else if(commonPrefix.length() < str.length()) {                    // 节点裂变                    Node splitNode = new Node(true, node.count, commonPrefix);                    // 处理裂变节点的父关系                    splitNode.parent = node.parent;                    splitNode.parent.addChild(commonPrefix, splitNode);                    node.parent.removeChild(node.str);                    node.count--;                    // 节点裂变后的剩余字串                    String strLeft = str.substring(commonPrefix.length());                    node.str = strLeft;                    splitNode.addChild(strLeft, node);                    // 单词裂变后的剩余字串                    if(commonPrefix.length() < word.length()) {                        splitNode.isWord = false;                        String wordLeft = word.substring(commonPrefix.length());                        Node leftNode = new Node(true, 1, wordLeft);                        splitNode.addChild(wordLeft, leftNode);                    }                }            } else {                // 没有共同前缀,直接添加节点                Node newNode = new Node(true, 1, word);                node.addChild(word, newNode);            }        } else {            // 根结点            if(node.childs.size() > 0) {                searchChild(word, node);            } else {                Node newNode = new Node(true, 1, word);                node.addChild(word, newNode);            }        }    }    // 在子节点中添加字串    public void searchChild(String wordLeft, Node node) {        boolean isFind = false;        if(node.childs.size() > 0) {            // 遍历孩子            for(String childKey : node.childs.keySet()) {                Node childNode = node.childs.get(childKey);                // 首字母相同,则在该子节点继续添加字串                if(wordLeft.charAt(0) == childNode.str.charAt(0)) {                    isFind = true;                    addStr(wordLeft, childNode);                    break;                }            }        }        // 没有首字母相同的孩子,则将其变为子节点        if(!isFind) {            Node newNode = new Node(true, 1, wordLeft);            node.addChild(wordLeft, newNode);        }    }    // 添加单词    public void add(String word) {        addStr(word, root);    }    // 在节点中查找字串    private boolean findStr(String word, Node node) {        boolean isMatch = true;        String wordLeft = word;        String str = node.str;        if(null != str) {            // 字串与单词不匹配            if(word.indexOf(str) != 0) {                isMatch = false;            } else {                // 匹配,则计算剩余单词                wordLeft = word.substring(str.length());            }        }        // 如果匹配        if(isMatch) {            // 如果还有剩余单词长度            if(wordLeft.length() > 0) {                // 遍历孩子继续找                for(String key : node.childs.keySet()) {                    Node childNode = node.childs.get(key);                    boolean isChildFind = findStr(wordLeft, childNode);                    if(isChildFind) {                        return true;                    }                }                return false;            } else {                // 没有剩余单词长度,说明已经匹配完毕,直接返回节点是否为单词                return node.isWord;            }        }        return false;    }    // 查找单词    public boolean find(String word) {        return findStr(word, root);    }    // 统计子节点字串单词数    private int countChildStr(String prefix, Node node) {        // 遍历孩子        for(String key : node.childs.keySet()) {            Node childNode = node.childs.get(key);            // 匹配子节点            int childCount = countStr(prefix, childNode);            if(childCount != 0) {                return childCount;            }        }        return 0;    }    // 统计字串单词数    private int countStr(String prefix, Node node) {        String str = node.str;        // 非根结点        if(null != str) {            // 前缀与字串不匹配            if(prefix.indexOf(str) != 0 && str.indexOf(prefix) != 0) {                return 0;            // 前缀匹配字串,且前缀较短            } else if(str.indexOf(prefix) == 0) {                // 找到目标节点,返回单词数                return node.count;            // 前缀匹配字串,且字串较短            } else if(prefix.indexOf(str) == 0) {                // 剩余字串继续匹配子节点                String prefixLeft = prefix.substring(str.length());                if(prefixLeft.length() > 0) {                    return countChildStr(prefixLeft, node);                }            }        } else {            // 根结点,直接找其子孙            return countChildStr(prefix, node);        }        return 0;    }    // 统计前缀单词数    public int count(String prefix) {        // 处理特殊情况        if(null == prefix || prefix.trim().isEmpty()) {            return root.count;        }        // 从根结点往下匹配        return countStr(prefix, root);    }    // 打印节点    private void printNode(Node node, int layer) {        // 层级递进        for(int i=0; i

(友情提示:本文所有代码均可左右滑动))

Main.java:

 
 

/**
 * @author xiaoshi on 2018/10/5.
 */

public class Main {

    public static void main(String[] args) {

        DictionaryTree dt = new DictionaryTree();

        dt.add("interest");
        dt.add("interesting");
        dt.add("interested");
        dt.add("inside");
        dt.add("insert");
        dt.add("apple");
        dt.add("inter");
        dt.add("interesting");

        dt.print();

        boolean isFind = dt.find("inside");
        System.out.println("find inside : " + isFind);

        int count = dt.count("inter");
        System.out.println("count prefix inter : " + count);

    }

}

运行结果:

str : null, isWord : false, count : 8    str : apple, isWord : true, count : 1    str : in, isWord : false, count : 7        str : ter, isWord : true, count : 5            str : est, isWord : true, count : 4                str : ing, isWord : true, count : 2                str : ed, isWord : true, count : 1        str : s, isWord : false, count : 2            str : ert, isWord : true, count : 1            str : ide, isWord : true, count : 1find inside : truecount prefix inter : 5


640?wx_fmt=png

字典树的应用


640?wx_fmt=jpeg

640?wx_fmt=jpeg

小史:我想想啊,大量字符串的统计和查找应该就可以用字典树吧?字符串前缀的匹配也可以用,像咱们搜索常见的AutoComplete控件是不是就可以用?

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

640?wx_fmt=jpeg

作者简介:channingbreeze,国内某互联网公司全栈开发。

声明:本文为作者投稿,版权归对方所有。作者独立观点,不代表 CSDN 立场。

推荐阅读:

  • Android 之父裁员 30%:开发者如何避免“被离职”?

  • 僵尸化的支付宝小程序,将在 BAT 的厮杀中出局 | 畅言

  • 码农30岁后的体检——你最需要的是直面的勇气

  • 港中大、商汤开源目标检测工具包mmdetection,对比Detectron如何?

  • 这届程序员要做苦日子的准备了!

  • 程序员们,快来找漏洞啊!找到就赏15ETH

  • 关于这道填空题,你会如何回答?(附带学习链接)

640?wx_fmt=gif

640?wx_fmt=gif

CSDN
微信公众号
成就一亿技术人
注:本文转载自blog.csdn.net的CSDN资讯的文章"https://blog.csdn.net/csdnnews/article/details/83189934"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top