热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

[CLRS][CH15.5]最优二叉查找树

背景铺垫假设我们正在设计一个翻译程序,讲英语翻译成法语,需要用一棵BST存储文章中出现的单词及等价的法语。因为要频繁地查找这棵树,所以我们希望查找时间越短越好。当然我们可以考虑使用红黑树,或者可能

背景铺垫

假设我们正在设计一个翻译程序,讲英语翻译成法语,需要用一棵BST存储文章中出现的单词及等价的法语。因为要频繁地查找这棵树,所以我们希望查找时间越短越好。当然我们可以考虑使用红黑树,或者可能更适用的伸展树,来实现这一操作。但是这仍然不能满足我们的需要:由于单词出现频率不同,如果mycophagist这种奇怪的单词出现在根节点,而the、a、is这些常用单词出现在叶节点,即使O(lgn)的查找速度也极大的浪费了时间。

因此我们需要这样一种BST:频繁出现的单词出现在离根节点较近的地方。假设我们知道每个单词的出现频率,应该如何组织BST使得总搜索访问的节点数目最小呢?

最优二叉查找树 Optimal Binary Search Tree

定义:对于给定的一组概率,构造一棵搜索期望代价最小的BST,称为最优二叉查找树。

介绍:形式地,给定一个由n个互异关键字组成的序列 K = 1, k2, ..., kn>,且关键字有序,即 k1 2 <... n。利用这些关键字构造一棵BST,对于每个关键字ki搜索的概率是pi。某些搜索的值可能不在K内,因此还有n+1个“虚拟键” 0, d1, d2, ..., dn> 代表不在K内的值。具体地,d0代表所有小于k1的值,dn代表所有大于kn的值。对于 i = 1, 2, ..., n-1,虚拟键di代表所有位于ki和ki+1之间的值。对于每个虚拟键di搜索的概率是qi。每个关键字ki是一个内部结点,而di是叶节点。每次搜索或者成功(找到内部结点ki),或者失败(找到叶节点di)。因此有:

  $\sum_{i=1}^{n}p_{i}+\sum_{i=0}^{n}q_{i}=1$

假设一次搜索的实际代价为检查的节点个数,亦即,在T内搜索所发现的节点的深度加上1。所以在T内一次搜索的期望代价为:

  $\begin{align*} E[T]&=\sum_{i=1}^{n}(depth_{T}(k_{i})+1)\cdot{} p_{i}+\sum_{i=0}^{n}(depth_{T}(d_{i})+1)\cdot{} q_{i}\\ &=\sum_{i=1}^{n}depth_{T}(k_{i})\cdot{} p_{i}+\sum_{i=0}^{n}depth_{T}(d_{i})\cdot{} q_{i}+1 \end{align*}$

举个栗子:

如图,根据相同关键字构造的BST,检索概率如下表

i 0 1 2 3 4 5
pi   0.15 0.10 0.05 0.10 0.20
qi 0.05 0.10 0.05 0.05 0.05 0.10

 

 

 

因此容易得出左树搜索代价为2.80,右树搜索代价为2.75,因此这棵树是最优的。

这个例子说明:一棵最优BST不一定是一棵整体高度最小的树,也不一定总是把有最大概率的关键字放在根部。

步骤1:一棵最优二叉树的结构

首先找最优子结构:如果一棵最优二叉查找树T有一棵子树T',包含关键字i, ..., kj>和i-1, ..., dj>,那么树T'的子树T''也必定是一棵最优二叉查找树。这样,我们可以根据最优子结构来构造一棵最优二叉查找树。

给定关键字i, ..., kj>,假设根节点为k(i <= r <= j),则:
左子树包含关键字i, ..., kr-1>和i-1, ..., dr-1>;
右子树包含关键字r+1, ..., kj>和r, ..., dj>。
只要我们检查所有候选根节点k(i <= r <= j),并且确定左右子树的最优子树,就保证能得到一棵最优二叉查找树。

注意一点是空子树的处理。如果一棵子树包含关键字i, ..., ki-1>,则其不含有任何关键字,但是却含有虚拟键di-1。同样的,对于子树j+1, ..., kj>,不含有任何关键字,但含有虚拟键dj

步骤2:一个递归解

选取子问题域为寻找包含关键字i, ..., kj>的最优二叉查找树,其中 i >= 1 且 i-1 <= j <= n(当 j = i-1 时只有虚拟键di-1)。定义e[i, j]为该问题的期望代价,显然,e[1, n]就是原问题的解。

当 j = i-1 时出现简单情况。因为只有一个虚拟键di-1,所以搜索期望代价是e[i, i-1] = qi-1。
当 j >= i 时需要遍历结点i, ..., ki-1>选择一个合适的根k(i <= r <= j),然后分别构造左右子树。同时,当一棵树成为另一棵树的子树时,子树所有结点深度加1。
对一颗有关键字i, ..., kj>的子树,定义概率的总和为:

  $w(i,j) = \sum_{j=i}^{j}pi+\sum_{j=i-1}^{j}ql$

因此,如果kr是一棵包含关键字i, ..., ki-1>的最优子树的根,则有

  $e[i,j]=p_{r}+((e[i,r-1]+w(i,r-1))+(e[r+1,j])+w(r+1,j))$

注意

  $w(i,j)=w(i,r-1)+p_{r}+w(r+1,j)$

将e[i,j]重写为

  $e[i,j]=e[i,r-1]+e[r+1,j]+w(i,j)$

于是我们就能得出递归式。在这里假设我们已知结点kr,我们选择有最低期望搜索代价的结点作为根:

  $e[i,j]=\begin{cases} q_{i-1}&j=i-1\\\textrm{min}_{i\leq r\leq j}{e[i,r-1]+e[r+1,j]+w(i,j)}&i\leq j\end{cases}$

我们还定义root[i,j]为根kr的下标r。

步骤3:计算一棵最优二叉查找树的期望搜索代价

其实我们能发现最优二叉查找树和矩阵链乘法之间有些相似,比如子问题都是由连续下标子范围组成。

正如我们之前所做的,为了储存子问题的结果,我们需要把e[i,j]保存在表e[1..n+1, 0..n]中,其中:
第一维下标需要达到n+1而不是n,原因是为了有一个只包含虚拟键dn的子树,我们需要计算和保存e[n+1,n];
第二纬下标需要从0开始,是为了保存e[1,0]。

使用 j >= i-1 的表项 e[i,j] 和表 root[i,j] 来记录包含关键字i, ..., kj>的子树的根,这个表只用 1 <= i <= j <= n的表项。

为了提高效率,还需要一个表格。不是没当计算e[i,j]时都要从头开始计算w(i,j),而是把这些值保存在表w[1..n+1, 0..n]中。
计算基础情况 w[i,i-1] = qi-1,其中 1 <= i <= n;
计算 w[i,j] = w[i,j-1] + pj + qj,其中 i <= j;
因此,可以计算出O(n2)个w[i,j]的值,每一个值需要O(1)计算时间。

下面的代码中,以概率以及结点数量n为参数,返回表e和root。运行时间为O(n3)。

OPTIMAL-BST(p, q, n)
for i = (1 to n+1)     // 1~3行: 循环初始化e[i, i-1]和w[i, i-1]的值
    e[i, i-1] = q[i-1] //
    w[i, i-1] = q[i-1] // for l = (1 to n)            // 4~13行: 循环利用递归式计算e[i, j]和w[i, j] for i = (1 to n-l+1)    // 第一次迭代时, l=1, 计算e[i,i]和w[i,i], i = 1..n
        j = i+l-1           // 第二次迭代时, l=2, 计算e[i,i+1]和w[i,i+1], i = 1..n-1
        e[i, j] =// 如此往复
        w[i, j] = w[i, j-1] + p[j] + q[j]
        for r = (i to j)                        // 9~13行: 尝试每个下标r, 以确定根节点
            t = e[i, r-1] + e[r+1, j] + w[i, j] // if t < e[i, j]                      // 发现更好的下标值则在root[i,j]中保存当前r
                e[i, j] = t                     //
                root[i, j] = r                  // return e and root

补充:优化OPTMAL-BST过程 (CLRS 15.5-4)

现已被证明,对于所有的 i <= i 利用这个事实我们可以修改OPTMAL-BST过程,使其在O(n2)时间内执行。

将第九行改为:
for r = ( root[i, j-1] to root[i+1, j] )

证明:该算法运行时间为O(n2)。

这一结论可以通过证明每一个e[i, j]的运行时间都是O(n)而得出。我们定义 e[i, j] (其中 j - i = k) 的全部状态集合为第k层集合(显然集合中 n-k 个结点)。所以计算 e[i, j] 需要 root[i+1, j] - root[i, j-1] + 1 次循环。因此,对于所有第k层集合的元素而言,总共需要 root[k, 1] – root[1, k] + n – k 次循环。又因为 1 <= root[k, 1] 且 root[1, k] <= n,所以循环次数为O(n)。

因为 0 <= k <= n-1,所以总循环次数为O(n2)。

该解法由Rip's Infernal Majesty给出。


推荐阅读
  • 本文由编程笔记#小编为大家整理,主要介绍了logistic回归(线性和非线性)相关的知识,包括线性logistic回归的代码和数据集的分布情况。希望对你有一定的参考价值。 ... [详细]
  • 阿里Treebased Deep Match(TDM) 学习笔记及技术发展回顾
    本文介绍了阿里Treebased Deep Match(TDM)的学习笔记,同时回顾了工业界技术发展的几代演进。从基于统计的启发式规则方法到基于内积模型的向量检索方法,再到引入复杂深度学习模型的下一代匹配技术。文章详细解释了基于统计的启发式规则方法和基于内积模型的向量检索方法的原理和应用,并介绍了TDM的背景和优势。最后,文章提到了向量距离和基于向量聚类的索引结构对于加速匹配效率的作用。本文对于理解TDM的学习过程和了解匹配技术的发展具有重要意义。 ... [详细]
  • 在Android开发中,使用Picasso库可以实现对网络图片的等比例缩放。本文介绍了使用Picasso库进行图片缩放的方法,并提供了具体的代码实现。通过获取图片的宽高,计算目标宽度和高度,并创建新图实现等比例缩放。 ... [详细]
  • 本文讨论了如何在不使用SearchBar display controller的情况下,单独使用SearchBar并捕获其textChange事件。作者介绍了实际状况,即左侧SliderMenu中的SearchBar需要在主页TableView中显示搜索结果。然后,作者提供了解决方案和步骤,帮助读者实现这一功能。 ... [详细]
  • 本文介绍了一个在线急等问题解决方法,即如何统计数据库中某个字段下的所有数据,并将结果显示在文本框里。作者提到了自己是一个菜鸟,希望能够得到帮助。作者使用的是ACCESS数据库,并且给出了一个例子,希望得到的结果是560。作者还提到自己已经尝试了使用"select sum(字段2) from 表名"的语句,得到的结果是650,但不知道如何得到560。希望能够得到解决方案。 ... [详细]
  • 本文讨论了Kotlin中扩展函数的一些惯用用法以及其合理性。作者认为在某些情况下,定义扩展函数没有意义,但官方的编码约定支持这种方式。文章还介绍了在类之外定义扩展函数的具体用法,并讨论了避免使用扩展函数的边缘情况。作者提出了对于扩展函数的合理性的质疑,并给出了自己的反驳。最后,文章强调了在编写Kotlin代码时可以自由地使用扩展函数的重要性。 ... [详细]
  • Go语言实现堆排序的详细教程
    本文主要介绍了Go语言实现堆排序的详细教程,包括大根堆的定义和完全二叉树的概念。通过图解和算法描述,详细介绍了堆排序的实现过程。堆排序是一种效率很高的排序算法,时间复杂度为O(nlgn)。阅读本文大约需要15分钟。 ... [详细]
  • 浏览器中的异常检测算法及其在深度学习中的应用
    本文介绍了在浏览器中进行异常检测的算法,包括统计学方法和机器学习方法,并探讨了异常检测在深度学习中的应用。异常检测在金融领域的信用卡欺诈、企业安全领域的非法入侵、IT运维中的设备维护时间点预测等方面具有广泛的应用。通过使用TensorFlow.js进行异常检测,可以实现对单变量和多变量异常的检测。统计学方法通过估计数据的分布概率来计算数据点的异常概率,而机器学习方法则通过训练数据来建立异常检测模型。 ... [详细]
  • 本文详细介绍了如何使用MySQL来显示SQL语句的执行时间,并通过MySQL Query Profiler获取CPU和内存使用量以及系统锁和表锁的时间。同时介绍了效能分析的三种方法:瓶颈分析、工作负载分析和基于比率的分析。 ... [详细]
  • Ihavethefollowingonhtml我在html上有以下内容<html><head><scriptsrc..3003_Tes ... [详细]
  • 第四章高阶函数(参数传递、高阶函数、lambda表达式)(python进阶)的讲解和应用
    本文主要讲解了第四章高阶函数(参数传递、高阶函数、lambda表达式)的相关知识,包括函数参数传递机制和赋值机制、引用传递的概念和应用、默认参数的定义和使用等内容。同时介绍了高阶函数和lambda表达式的概念,并给出了一些实例代码进行演示。对于想要进一步提升python编程能力的读者来说,本文将是一个不错的学习资料。 ... [详细]
  • SpringMVC接收请求参数的方式总结
    本文总结了在SpringMVC开发中处理控制器参数的各种方式,包括处理使用@RequestParam注解的参数、MultipartFile类型参数和Simple类型参数的RequestParamMethodArgumentResolver,处理@RequestBody注解的参数的RequestResponseBodyMethodProcessor,以及PathVariableMapMethodArgumentResol等子类。 ... [详细]
  • 本文讨论了编写可保护的代码的重要性,包括提高代码的可读性、可调试性和直观性。同时介绍了优化代码的方法,如代码格式化、解释函数和提炼函数等。还提到了一些常见的坏代码味道,如不规范的命名、重复代码、过长的函数和参数列表等。最后,介绍了如何处理数据泥团和进行函数重构,以提高代码质量和可维护性。 ... [详细]
  • 背景应用安全领域,各类攻击长久以来都危害着互联网上的应用,在web应用安全风险中,各类注入、跨站等攻击仍然占据着较前的位置。WAF(Web应用防火墙)正是为防御和阻断这类攻击而存在 ... [详细]
  • 本文整理了315道Python基础题目及答案,帮助读者检验学习成果。文章介绍了学习Python的途径、Python与其他编程语言的对比、解释型和编译型编程语言的简述、Python解释器的种类和特点、位和字节的关系、以及至少5个PEP8规范。对于想要检验自己学习成果的读者,这些题目将是一个不错的选择。请注意,答案在视频中,本文不提供答案。 ... [详细]
author-avatar
爱你116564
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有