程序员的数学基础课
黄申
LinkedIn资深数据科学家
立即订阅
23478 人已学习
课程目录
已完结 57 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 作为程序员,为什么你应该学好数学?
免费
导读 (1讲)
导读:程序员应该怎么学数学?
基础思想篇 (18讲)
01 | 二进制:不了解计算机的源头,你学什么编程
02 | 余数:原来取余操作本身就是个哈希函数
03 | 迭代法:不用编程语言的自带函数,你会如何计算平方根?
04 | 数学归纳法:如何用数学归纳提升代码的运行效率?
05 | 递归(上):泛化数学归纳,如何将复杂问题简单化?
06 | 递归(下):分而治之,从归并排序到MapReduce
07 | 排列:如何让计算机学会“田忌赛马”?
08 | 组合:如何让计算机安排世界杯的赛程?
09 | 动态规划(上):如何实现基于编辑距离的查询推荐?
10 | 动态规划(下):如何求得状态转移方程并进行编程实现?
11 | 树的深度优先搜索(上):如何才能高效率地查字典?
12 | 树的深度优先搜索(下):如何才能高效率地查字典?
13 | 树的广度优先搜索(上):人际关系的六度理论是真的吗?
14 | 树的广度优先搜索(下):为什么双向广度优先搜索的效率更高?
15 | 从树到图:如何让计算机学会看地图?
16 | 时间和空间复杂度(上):优化性能是否只是“纸上谈兵”?
17 | 时间和空间复杂度(下):如何使用六个法则进行复杂度分析?
18 | 总结课:数据结构、编程语句和基础算法体现了哪些数学思想?
概率统计篇 (14讲)
19 | 概率和统计:编程为什么需要概率和统计?
20 | 概率基础(上):一篇文章帮你理解随机变量、概率分布和期望值
21 | 概率基础(下):联合概率、条件概率和贝叶斯法则,这些概率公式究竟能做什么?
22 | 朴素贝叶斯:如何让计算机学会自动分类?
23 | 文本分类:如何区分特定类型的新闻?
24 | 语言模型:如何使用链式法则和马尔科夫假设简化概率模型?
25 | 马尔科夫模型:从PageRank到语音识别,背后是什么模型在支撑?
26 | 信息熵:如何通过几个问题,测出你对应的武侠人物?
27 | 决策树:信息增益、增益比率和基尼指数的运用
28 | 熵、信息增益和卡方:如何寻找关键特征?
29 | 归一化和标准化:各种特征如何综合才是最合理的?
30 | 统计意义(上):如何通过显著性检验,判断你的A/B测试结果是不是巧合?
31 | 统计意义(下):如何通过显著性检验,判断你的A/B测试结果是不是巧合?
32 | 概率统计篇答疑和总结:为什么会有欠拟合和过拟合?
线性代数篇 (13讲)
33 | 线性代数:线性代数到底都讲了些什么?
34 | 向量空间模型:如何让计算机理解现实事物之间的关系?
35 | 文本检索:如何让计算机处理自然语言?
36 | 文本聚类:如何过滤冗余的新闻?
37 | 矩阵(上):如何使用矩阵操作进行PageRank计算?
38 | 矩阵(下):如何使用矩阵操作进行协同过滤推荐?
39 | 线性回归(上):如何使用高斯消元求解线性方程组?
40 | 线性回归(中):如何使用最小二乘法进行直线拟合?
41 | 线性回归(下):如何使用最小二乘法进行效果验证?
42 | PCA主成分分析(上):如何利用协方差矩阵来降维?
43 | PCA主成分分析(下):为什么要计算协方差矩阵的特征值和特征向量?
44 | 奇异值分解:如何挖掘潜在的语义关系?
45 | 线性代数篇答疑和总结:矩阵乘法的几何意义是什么?
综合应用篇 (6讲)
46 | 缓存系统:如何通过哈希表和队列实现高效访问?
47 | 搜索引擎(上):如何通过倒排索引和向量空间模型,打造一个简单的搜索引擎?
48 | 搜索引擎(下):如何通过查询的分类,让电商平台的搜索结果更相关?
49 | 推荐系统(上):如何实现基于相似度的协同过滤?
50 | 推荐系统(下):如何通过SVD分析用户和物品的矩阵?
51 | 综合应用篇答疑和总结:如何进行个性化用户画像的设计?
加餐 (3讲)
数学专栏课外加餐(一) | 我们为什么需要反码和补码?
数学专栏课外加餐(二) | 位操作的三个应用实例
数学专栏课外加餐(三):程序员需要读哪些数学书?
结束语 (1讲)
结束语 | 从数学到编程,本身就是一个很长的链条
程序员的数学基础课
登录|注册

11 | 树的深度优先搜索(上):如何才能高效率地查字典?

黄申 2019-01-07
你好,我是黄申。
你还记得迭代法中的的二分查找吗?在那一讲中,我们讨论了一个查字典的例子。如果要使用二分查找,我们首先要把整个字典排个序,然后每次都通过二分的方法来缩小搜索范围。
不过在平时的生活中,咱们查字典并不是这么做的。我们都是从单词的最左边的字母开始,逐个去查找。比如查找“boy”这个单词,我们一般是这么查的。首先,在 a~z 这 26 个英文字母里找到单词的第一个字母 b,然后在 b 开头的单词里找到字母 o,最终在 bo 开头的单词里找到字母 y。
你可以看我画的这种树状图,其实就是从树顶层的根结点一直遍历到最下层的叶子结点,最终逐步构成单词前缀的过程。对应的数据结构就是前缀树(prefix tree),或者叫字典树(trie)。我个人更喜欢前缀树这个名称,因为看到这个名词,这个数据结构的特征就一目了然。
那前缀树究竟该如何构建呢?有了前缀树,我们又该如何查询呢?今天,我会从图论的基本概念出发,来给你讲一下什么样的结构是树,以及如何通过树的深度优先搜索,来实现前缀树的构建和查询。

图论的一些基本概念

前缀树是一种有向树。那什么是有向树?顾名思义,有向树就是一种树,特殊的就是,它的边是有方向的。而树是没有简单回路的连通图。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《程序员的数学基础课》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(19)

  • Handongyang
    老师讲解得非常好,谈下个人对前缀树的了解,前缀树主要应用于以下场景:
    1.预测文本输入功能:搜索引擎的输入框中输入搜索关键词的预测文本提示、IDE代码编辑器和浏览器网址中输入时的预测文本提示,借助老师讲的前两节动态规划实现预测文本的纠错功能;
    2.使用前缀树结构对字符串或单词按字母顺序实现排序并进行输出的功能;
    3.前缀树也可用于实现近似匹配,包括拼写检查和连字软件使用的算法;

    前缀树的主要优劣对比:
    1.使用前缀树可以高效地进行前缀搜索字符串和插入字符串,时间复杂度为O(length);
    2.使用前缀树可以按字母顺序轻松打印所有字符串,该功能是散列表不易实现的,但前缀树的搜索效率可能不如散列表快;
    3.前缀树的缺点在于,需要大量的内存来存储字符串,对于每个节点需要太多的节点指针。倘若关注内存空间上的优化,可以考虑使用三元搜索树。三元搜索树是实现字典的首选结构,其搜索字符串的时间复杂度是O(height);

    作者回复: 总结的很好,还有实用的场景

    2019-01-07
    1
    21
  • Handongyang
    下面是构建前缀树和查询的代码实现:
    package com.string.test;

    /**
     * Created by hanyonglu on 2019/1/7.
     */
    public class TrieTest {
        private static final int CHARACTER_SIZE = 26;

        private TrieNode root;

        private static class TrieNode {
            private TrieNode[] children;

            private boolean isWordEnd;

            public TrieNode() {
                isWordEnd = false;

                children = new TrieNode[CHARACTER_SIZE];
                for (int index = 0; index < CHARACTER_SIZE; index++) {
                    children[index] = null;
                }
            }
        }

        public void insert(String key) {
            TrieNode newNode = root;
            int index;

            for (int i = 0; i < key.length(); i++) {
                index = key.charAt(i) - 'a';

                if (newNode.children[index] == null) {
                    newNode.children[index] = new TrieNode();
                }

                newNode = newNode.children[index];
            }

            newNode.isWordEnd = true;
        }

        public boolean search(String key) {
            TrieNode searchNode = root;
            int index;

            for (int i = 0; i < key.length(); i++) {
                index = key.charAt(i) - 'a';

                if (searchNode.children[index] == null) {
                    return false;
                }

                searchNode = searchNode.children[index];
            }

            return (searchNode != null && searchNode.isWordEnd);
        }

        public static void main(String args[]) {
            String[] keys = {"my", "name", "is", "hanyonglu", "the", "son", "handongyang", "home", "near", "not", "their"};

            TrieTest trieTest = new TrieTest();
            trieTest.root = new TrieNode();

            for (int index = 0; index < keys.length ; index++) {
                trieTest.insert(keys[index]);
            }

            System.out.println("home result : " + trieTest.search("home"));
            System.out.println("their result : " + trieTest.search("their"));
            System.out.println("t result : " + trieTest.search("t"));
        }
    }

    作者回复: 实现很简洁👍

    2019-01-07
    13
  • 产品助理
    从起始点到终止点所经过的边之数量,就是通路的长度。这里我画了...

    极客时间版权所有: https://time.geekbang.org/column/article/76481

    这里的通路的长度应该是3啊。

    边有:v1——v2,v2——v3,v3——v4 三条,所以边的长度是3对吗?

    作者回复: 对 应该是3,我稍后该一下

    2019-01-07
    1
    5
  • 是我
    觉得二叉树跟正则表达式好像

    作者回复: 具体是哪种正则表达式?

    2019-05-16
    1
    1
  • qinggeouye
    查找到一个 python 版本,分别用迭代循环和递归的方法,实现在树中添加和查找单词。动手实现了一遍:~
    https://github.com/qinggeouye/GeekTime/blob/master/MathematicProgrammer/11_deepFirstSearch/lesson11_1.py

    2019-02-20
    1
  • asc
    老师讲的很好,非常容易理解。😁这次就不做作业了
    2019-01-09
    1
  • Being
    关于思考题的一点想法:其实可以将单词以及字母抽象出来,利用组合模式,形成树形关系,实现递归调用。

    作者回复: 如果使用前缀树,对给定的一些单词进行处理,就无需排列组合了

    2019-01-07
    1
  • 建强
    用Python写了一个简单的程序:
    # 前缀树构造与查询

    # 前缀树节点
    class PrefixNode:
        def __init__(self, letter):
            self.letter = letter # 每个节点中存储一个单词的字母
            self.childnode = {} # 子节点用字典存储
            self.isend = False # isend表示单词是否结束

        def addchild(self, child):
            self.childnode[child.letter] = child

        # 寻找字母为letter的子节点
        def findchild(self, letter):
            return self.childnode.get(letter)

    # 前缀树
    class PrefixTree:
        def __init__(self):
            self.root = PrefixNode('#') # 用#号代表根节点

        # 向字典树中加入一个单词
        def addWord(self, word):
            p = self.root
            for w in word:
                childnode = p.findchild(w)
                if childnode is None:
                    # 前缀树中没有找到对应的字母,则重新构造一个新的子节点
                    childnode = PrefixNode(w)
                    p.addchild(childnode)

                p = childnode
                
            p.isend = True

        # 在字典树中查找一个单词
        def searchWord(self, word):
            p = self.root
            for w in word:
                childnode = p.findchild(w)

                if childnode is None:
                    # 前缀树中没有找到对应的字母,则直接返回False
                    return False

                p = childnode
                
            return p.isend

    # 主程序
    def main():
        # 构造字典树
        dict_tree = PrefixTree()
        
        # 向字典树中加入一些单词
        for word in ['beautiful','student','computer','desk','table','design','book','bookshelf']:
            dict_tree.addWord(word)

        print('prefix tree is maded')

        # 在字典树中查找单词
        for word in ['beau', 'book','student', 'conputer', 'bookshelf', 'desks']:
            if dict_tree.searchWord(word):
                print('"{}" was founded.'.format(word))
            else:
                print('"{}" was not founded.'.format(word))
            
    if __name__ == '__main__':
        main()
    2019-12-07
  • aoe
    这次通俗易懂,感谢老师

    作者回复: 很高兴对你有价值

    2019-12-02
  • Eleven
    老师,这篇概念有点多啊,文章中说的有向边该怎么理解呢?

    作者回复: 有向边是指从某个节点到另一个节点存在方向,顺向可以走通,逆向就不行

    2019-11-25
  • 花指令
    发现大家用循环遍历确定节点,既然是字母,大小写最多就52个,那应该用ascii做二分搜索更快,分布均匀的话还可以直接用数组下标

    作者回复: 如果只限定在英文,这是个好方法👍

    2019-11-04
  • AlphaZero
    Golang 版:

    package main

    import "fmt"

    func main() {
    dict := NewDictionary()
    dict.Insert("hello", "你好")
    dict.Insert("世界", "word")

    fmt.Println(*dict.Explain("hello"))
    fmt.Println(*dict.Explain("世界"))
    }

    type trieNode struct {
    char rune
    children []*trieNode
    explanation *string
    }

    type Dictionary struct {
    root *trieNode
    }

    // Insert adds the word to the dictionary
    func (dict *Dictionary) Insert(word string, explanation string) {
    current := dict.root
    for _, ch := range []rune(word) {
    found := false

    if len(current.children) > 0 {
    for _, node := range current.children {
    if node.char == ch {
    current = node
    found = true
    break
    }
    }
    }

    if !found {
    child := &trieNode{char: ch}
    current.children = append(current.children, child)
    current = child
    }
    }

    if current.explanation == nil || *current.explanation != explanation {
    current.explanation = &explanation
    }
    }

    // Explain returns the explanation of the given word
    func (dict *Dictionary) Explain(word string) *string {
    current := dict.root
    for _, ch := range []rune(word) {
    found := false
    for _, node := range current.children {
    if node.char == ch {
    current = node
    found = true
    break
    }
    }

    if !found {
    return nil
    }
    }

    return current.explanation
    }

    // NewDictionary create a empty Trie
    func NewDictionary() *Dictionary {
    return &Dictionary{
    root: &trieNode{
    char: ' ',
    },
    }
    }
    2019-09-30
  • Paul Shan
    树是一种递归结构,由根节点和若干子树构成。在建树和查找上也会充分利用这种递归结构。
    2019-08-22
  • Better me
    想问下老师最后小结中对应的左子树为0右子树为1对应的二进制数有什么实际应用场景吗

    作者回复: 其实二进制表示的本身就是最好的应用场景。当然,二分法也是一种对应的实现

    2019-07-28
  • 梦倚栏杆
    对于二叉树而言,深度优先遍历是不是就是二叉树的先序遍历啊

    作者回复: 确切的说,先序、中序和后序是深度优先遍历的三种方式

    2019-03-26
  • pyhhou
    简单实现了下,跑了几个例子是可以work的,这里build的时候用Map是考虑到字典里面的单词和解释以key-value的形式出现,search函数输入是单词输出是解释,没有该单词的话返回null

    public class Trie {
         private class TrieNode {
              private TrieNode[] children = new TrieNode[26];
              private String word = null;
              private String explanation = null;
         }

         private TrieNode root = new TrieNode();

              public void buildTrie(Map<String, String> dictionary) {
                   if (dictionary == null || dictionary.size() == 0) {
                        return;
                   }

                   for (String word : dictionary.keySet()) {
                        char[] wordArr = word.toCharArray();
                        TrieNode tmp = root;

                         for (char c : wordArr) {
                              if (tmp.children[c - 'a'] == null) {
                                   tmp.children[c - 'a'] = new TrieNode();
                              }
                              tmp = tmp.children[c - 'a'];
                         }

                         tmp.word = word;
                         tmp.explanation = dictionary.get(word);
                    }
               }

              public String searchTrie(String targetWord) {
                   if (targetWord == null || targetWord.isEmpty()) {
                        return null;
                   }

                   char[] targetWordArr = targetWord.toCharArray();
                   TrieNode tmp = root;

                   for (char c : targetWordArr) {
                        if (tmp.children[c - 'a'] == null) {
                             return null;
                        }

                        tmp = tmp.children[c - 'a'];
                    }

               return tmp.explanation;
          }

    作者回复: 逻辑上没问题,使用26个英文字母对于字典而言足够了👍

    2019-01-18
  • 李尧
    老师好,根节点的高度可以随意设置吗,假如设置为1,子节点的高度是不是都需要加1

    作者回复: 是的 通常根结点高度只会设置为0或1

    2019-01-07
  • alic
    哪位大佬实现了放上去看一下
    2019-01-07
  • WL
    不错不错, 又把大学忘掉的知识复习了一遍
    2019-01-07
收起评论
19
返回
顶部