记一道未能答出的算法面试题

作者: 工程师milter | 来源:发表于2018-03-16 05:19 被阅读2144次

    昨天晚上,参加了一场面试,有道算法题当时没答出来,痛心疾首!刚刚起床给娃娃换尿布的空当,突然间就想清楚了实现的办法,当时没答出来就是卡在构建多叉树这一点!本文会给出这个问题的解答,同时反思为什么没答出来,以期为以后的面试提供一些借鉴。

    一、题目

    任务:查词典
    描述:有一个词典文件,每行一个词。编写程序在用户输入的一段文本中,找到所有在字典中的词,优先匹配最长的词,
    并在句子中标记出来。要求尽量少的使用内存,速度尽量快。

    输入:

    1. 词典文件,假设有这些词:杭州 西湖 西湖博物馆 博物馆
    2. 用户输入的一段文字,如“我在杭州西湖边的西湖博物馆里面。”

    输出:一个字符串,词典中的词单独分出来,并在后面带上标记,如:“我在 杭州/W 西湖/W 边的 西湖博物馆/W 里面。”

    二、分析与作答

    这道题要分两步来解答,首先,利用词典文件,在内存中构建一个单词查找树;其次,遍历用户输入,实时在单词查找树中查找,匹配到一个最长的单词时,按照格式输出。所以,首先来构建树,第一步自然是定义Node类:

    class Node {
            public Character ch = null;
            public boolean isWordEnd = false;
            public Map<Character, Node> next;
    
            public Node() {
                this.next = new HashMap<>();
            }
    
            public Node(char ch) {
                this.ch = ch;
                this.next = new HashMap<>();
            }
    
            public Node(char ch, boolean isWordEnd) {
                this.ch = ch;
                this.isWordEnd = isWordEnd;
                this.next = new HashMap<>();
            }
        }
    /*以上是定义了一个节点类,
    在一个节点类中,首先要存储这个节点中的字符,
    所以定义了ch这个成员变量;同时要存储这个节点
    下面的节点们,为了提高查找效率,使用了Map,
    其key即是该节点下面的一个节点中的字符,value就是这个下面的节点。
    看起来,下面的节点中的字符数据在key和value中都存在,有点浪费空间,
    但这是典型的以空间换时间的策略,后续在使用查找树时会有感觉。
    */
    

    下一步,我们要定义词典类,该类根据单词文件,利用上面的Node类,构建一个单词查找树,并使用它进行查询。

    import java.util.HashMap;
    import java.util.Map;
    
    public class Dictionary {
      
        public Node root = new Node();
    
        public void generateDic(String[] dicFile) {
    
            for (int i = 0; i < dicFile.length; i++) {
                Map<Character, Node> cur = root.next;
                Node curNode = null;
                char[] chs = dicFile[i].toCharArray();
                for (int j = 0; j < chs.length; j++) {
                    if (cur.containsKey(chs[j])) {
                        curNode = cur.get(chs[j]);
                        cur = cur.get(chs[j]).next;
    
                    } else {
                        Node addNode = new Node(chs[j]);
                        cur.put(chs[j], addNode);
                        curNode = addNode;
                        cur = addNode.next;
                    }
                    if(j == chs.length-1){
                        curNode.isWordEnd = true;
                    }
                }
    
            }
        }
    
       public static void main(String[] args) {
            Dictionary dic = new Dictionary();
            String[] dicFile ={"杭州", "西湖", "西湖博物馆"};
            dic.generateDic(dicFile);
            char[] input = "我在杭州西湖博边的西湖博物馆里面".toCharArray();
            Map<Character, Node> cur = dic.root.next;
            boolean inWord = false;
            int wordHead = -1;
            int wordEnd = -1;
            for (int i = 0; i < input.length; i++) {
                if (inWord == false) {
                    cur = dic.root.next;
                    wordHead = -1;
                    wordEnd = -1;
                    if (cur.containsKey(input[i])) {
                        inWord = true;
                        wordHead  = i;
                        if(cur.get(input[i]).isWordEnd == true)
                            wordEnd = i;
                        cur = cur.get(input[i]).next;
                    } else {
                        System.out.print(input[i]);
                    }
                } else {
                    if (cur.containsKey(input[i])) {
                        if(cur.get(input[i]).isWordEnd == true)
                            wordEnd = i;
                        cur =cur.get(input[i]).next;
                    } else {
                        if(wordEnd == -1){
                            i = wordHead; 
                            inWord = false;
                            continue;
                        }else{
                            System.out.print(" ");
                            printWord(input, wordHead, wordEnd);
                            System.out.print("/W");
                            i = wordEnd;
                            inWord = false;
                            continue;
    
                        }
    
                    }
    
                }
                if(i == input.length-1 && inWord == true){
                    if(wordEnd != -1){
                        System.out.print(" ");
                        printWord(input, wordHead, wordEnd);
                        System.out.print("/W");
                        i = wordEnd;
                        inWord = false;
    
                    }else{
                        i = wordHead;
                        inWord = false;
                    }
                }
            }
    
        }
    
        private static void printWord(char[] input, int wordHead, int wordEnd) {
            for(int i = wordHead; i <= wordEnd; i++)
                System.out.print(input[i]);
        }
    }
    //该程序的输出为:
    //我在 杭州/W 西湖/W博边的 西湖博物馆/W里面
    

    在上面的main方法中,使用单词查找树时,我们大量使用了containsKey这一方法,由于该方法属于Map,所以其查找效率为O(1)。这使得我们处理N个字符的字符串时,时间复杂度降到了O(N)。

    三、总结与反思

    认真看看上面的解答,其实并不算难。自己为什么没有答出来呢?分析来分析去,还是因为练得少,手生。面试前,近半年时间,虽然在工作,但是算法类的题目几乎没有练习过,中午得知晚上面试时,才在下午工作空隙中将排序动规之类粗略回忆了一下。
    真正动起手来开始写代码,才发现手生的不行,看到这个题目,虽然大致知道用单词查找树,可是却无法创建出来,导致无法完成解答。
    以后面试,尤其是代码类的面试,一定要留出几天,认真找几道题练练手,达到warm up的效果后再去面试,不能就这样赤膊上阵了。

    相关文章

      网友评论

      • 七叶林:https://blog.csdn.net/handsonn/article/details/78997512
        之前写过类似,不知对楼主是否有帮助,欢迎批评指正😬
      • Paulmark:楼主厉害,加油!
        工程师milter:@Paulmarkye_54b2 感谢鼓励!
      • 737f54bc9f63:大哥,明显的前缀树匹配。跟你的ML文档比起来low爆了。
        工程师milter:@李成金 好吧,被你刺激到了,我要更加努力学习:sweat:
      • 布谷鸟也会编程:谢谢前辈分享~之前我一直不知道算法的重要性...想问下,提高算法是只有在类似于LeetCode这类网站刷题吗?
        工程师milter:看书也可以哈
      • 错乱的比特流:查询写在main里面,代码没有注释
        工程师milter:@错乱的比特流 欢迎给予完善啊
      • 跌跌撞撞小红豆:问一下前辈,您觉得面试的时候,问的这些问题,工作了能用到吗?
        跌跌撞撞小红豆:@跌跌撞撞小红豆 谢谢
        工程师milter:@跌跌撞撞小红豆 能用到啊,虽然不是要写出这些代码,但用到类似的功能,你起码知道基本原理,如果性能不够,你知道应该怎么优化。
      • zsir94:你这个判断写的有点复杂啊,精简了下

        public static void match(Node lib, String target) {
        char[] chars = target.toCharArray();
        int matched = 0; //匹配到的字数
        int preIndex = 0;
        int sufIndex = 0;
        Map<Character, Node> curMap = lib.next;
        Node curNode;
        StringBuilder result = new StringBuilder();

        while (preIndex < chars.length) {
        System.out.printf("pre %d suf %d matched %s result %s\n", preIndex, sufIndex, matched, result);
        if (sufIndex < chars.length && curMap.containsKey(chars[sufIndex])) {
        curNode = curMap.get(chars[sufIndex]);
        if (curNode.wordEnd) {
        matched = sufIndex - preIndex + 1;
        }
        sufIndex++;
        curMap = curNode.next;
        } else {
        if (matched > 0) {
        result.append(' ').append(target.substring(preIndex, preIndex + matched)).append("/W ");
        preIndex += matched;
        sufIndex = preIndex;
        matched = 0;
        } else {
        result.append(chars[preIndex]);
        preIndex++;
        sufIndex = preIndex;
        }
        curMap = lib.next;
        }
        }

        System.out.println(result);
        }
        工程师milter:@zsir94 赞!
        zsir94:@milter 关于 abc 和 bcdf 的问题,改进了下

        public class SmartWordMatch {
        private static String[] wordLib = { "bcd", "cdef" };
        private static String target = "abcdefg";

        public static void main(String[] args) {
        Node root = Node.initNode(wordLib);
        System.out.println(root);

        int index = 0;
        char[] chars = target.toCharArray();
        int length = target.length();
        StringBuilder result = new StringBuilder();

        int offset = 0;
        int matched = 0;
        while (index < length) {
        offset = 0; // index偏移量
        matched = match(root, chars, index); // 匹配到的词元长度
        if (matched > 0) {
        // 匹配到词元后,继续往后查找词元中的字
        for (int i = 1; i <= matched; i++) {
        int smartMatched = match(root, chars, index + i);
        // 匹配到的词元长度更大时,取最大匹配
        if (smartMatched > matched) {
        offset = i;
        matched = smartMatched;
        }
        }
        result.append(target.substring(index, index + offset)).append(' ')
        .append(target.substring(index + offset, index + offset + matched)).append("/W ");
        System.out.printf("index %d offset %d matched %d result %s\n", index, offset, matched, result);
        index = index + offset + matched;
        } else {
        result.append(chars[index]);
        System.out.printf("index %d offset %d matched %d result %s\n", index, offset, matched, result);
        index++;
        }
        }

        System.out.println(result);
        }

        public static int match(Node lib, char[] target, int index) {
        int matched = 0; // 匹配到的字数
        int preIndex = index;
        int sufIndex = index;
        Map<Character, Node> curMap = lib.next;
        Node curNode;

        while (preIndex < target.length) {
        if (sufIndex < target.length && curMap.containsKey(target[sufIndex])) {
        curNode = curMap.get(target[sufIndex]);
        if (curNode.wordEnd) {
        matched = sufIndex - preIndex + 1;
        }
        sufIndex++;
        curMap = curNode.next;
        } else {
        break;
        }
        }

        return matched;
        }
        }
        工程师milter:@zsir94 感谢指点!这样确实精简不少!
      • 叮宕:Trie树行不?它是每个节点一个字,从根到叶子就是一个词。
        工程师milter:@叮宕 神之评论
        叮宕:@milter 我没看你写的源码……:sweat:
        工程师milter:@叮宕 就是这个哈
      • 工程师milter:这道题还有一个点需要推敲。输入字符串是 abcde ,词典里有两个词:abc和bcde,那么这个题目的要求是应该输出 abc/W de 呢?还是 a bcde/W 呢?按说后边这个匹配更长,但这就有点麻烦了
      • b386af8e7915:看了文章的分析,感觉很不错!
        有个小问题要提出来一下:
        如果字典里面有 “Hello”这个单词,当读入的串中包含“Hello”的子串诸如“Hell”,“He”,都会在单词后面输出/W,是不是会不太合理?是否是否需要再加个判断条件,判断是否已经匹配到了字典单词的尽头?
        工程师milter:@Xiaofeng_85e6 代码在一位高人的指导下修改了两次。第二次修改是在节点中标记该节点是否是一个单词的结尾。这样的好处可以通过例子来说明,单词查找树中有 西---->湖---->博---->物---->馆分支。字符串中故意加入了“西湖博边的”字样,程序利用节点中isWordEnd标记,就会准确识别出“西湖"一词,而不会识别成“西湖博”
        b386af8e7915:@milter 嗯嗯,我之前看到的代码可能是旧版本的。学习了:+1:
        工程师milter:@Xiaofeng_85e6 不会出现你说的情况,在匹配到字串结束时,会进行判断如果词典文件已到一个单词的末尾,那么输出/W,否则,将指针回溯到开始匹配的地方继续进行循环。按你的例子He,会发现匹配结束时,词典中单词还没有结束,所以会回退,继续从e开始匹配。此时未能匹配上词典,循环结束。

      本文标题:记一道未能答出的算法面试题

      本文链接:https://www.haomeiwen.com/subject/qizkqftx.html