说在前面的话:
为什么单独拿出来发?
1.由于排版篇幅问题,放一起太长没人愿意看吧。
2.单纯想增加投稿数量。(毕竟今天都没怎么发过文)
15.5 最优二叉搜索树
最优二叉搜索树的问题的形式可以定义如下:给定一个n个不同关键字的已排序的序列K=<K1,K2,…,Kn>(因此k1<k2…<kn),我们希望用这些关键字构造一棵二叉树搜索树。对每个关键字ki,都有一个概率pi表示其搜索频率。**有些搜索值可能不在K中,因此我们还要有n+1个“伪关键字”d0,d1,d2,…,dn表示不在K中的值。**对每个伪关键字di,也都有一个概率qi表示对应的搜索频率。
搜索概率如下表:
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 |
如图显示了对于n=5个关键字的集合构造的两颗二叉搜索树。每个关键字ki是一个内部结点,而每个伪关键字di是一个叶结点。每次搜索要么成功(找到关键字ki)要么失败(找到伪关键字di),因此有如下公式:
∑
i
=
1
n
\displaystyle\sum_{i=1}^{n}
i=1∑n pi +
∑
i
=
0
n
\displaystyle\sum_{i=0}^{n}
i=0∑n qi = 1
由于我们知道每个关键字和伪关键字的搜索概率,因而可以确定在一棵给定的二叉搜索树T中进行一次搜索的期望代价。假定一次搜索的代价等于访问的结点数,即此次搜索找到的结点在T中的深度再加1.那么在T中进行亿次搜索的期望代价为:
E[T中搜索代价] =
∑
i
=
1
n
\displaystyle\sum_{i=1}^{n}
i=1∑n (depthT(ki) + 1) * pi +
∑
i
=
0
n
\displaystyle\sum_{i=0}^{n}
i=0∑n(depthT(di) + 1) * qi
= 1 +
∑
i
=
1
n
\displaystyle\sum_{i=1}^{n}
i=1∑n (depthT(ki) ) * pi +
∑
i
=
0
n
\displaystyle\sum_{i=0}^{n}
i=0∑n(depthT(di) ) * qi
其中depthT表示一个结点在树T中的深度。
对于一个给定的概率集合,我们希望构造一课期望搜索代价最小的二叉搜索树,我们成为最优二叉搜索树。上图右边所示的二叉搜索树就是给定概率集合的最优二叉搜索树,其期望代价为2.75。
代码如下:
public class Program
{
public static void Main(string[] args)
{
var handle = new OPTIMALBST_Handle();
handle.optimal_bst();
}
}
public class OPTIMALBST_Handle
{
public decimal[] p { get; set; }
public decimal[] q { get; set; }
public int amount { get; set; }
public decimal[,] w { get; set; }
public decimal[,] e { get; set; }
public int[,] root { get; set; }
public OPTIMALBST_Handle()
{
Console.WriteLine("请输入结点数:\r");
amount = Convert.ToInt32(Console.ReadLine());
p = new decimal[amount + 1];
q = new decimal[amount + 1];
w = new decimal[amount + 2, amount + 1];
e = new decimal[amount + 2, amount + 1];
root = new int[amount + 1, amount + 1];
for (var i = 1; i <= amount; i++)
{
Console.WriteLine("关键字k" + i + "的概率为:\r");
p[i] = Convert.ToDecimal(Console.ReadLine());
}
for (var i = 0; i <= amount; i++)
{
Console.WriteLine("伪关键字d" + i + "的概率为:\r");
q[i] = Convert.ToDecimal(Console.ReadLine());
}
}
public void optimal_bst()
{
for (var i = 1; i <= amount + 1; i++)
{
w[i, i - 1] = q[i - 1];
e[i, i - 1] = q[i - 1];
}
for (var l = 1; l <= amount; l++)
{
for (var i = 1; i <= amount - l + 1; i++)
{
var j = i + l - 1;
w[i, j] = w[i, j - 1] + p[j] + q[j];
e[i, j] = decimal.MaxValue;
for (var r = i; r <= j; r++)
{
var temp = e[i, r - 1] + w[i, j] + e[r + 1, j];
if (temp < e[i, j])
{
e[i, j] = temp;
root[i, j] = r;
}
}
}
}
}
}
15.5 练习
15.5-1
设计伪代码CONSTRUCT-OPTIMAL-BST(root),输入为表root,输出是最优二叉搜索树的结构。例如,对图上图中的root表,应输出
k2为根
k1为k2的左孩子
d0为k1的左孩子
d1为k1的右孩子
k5为k2的右孩子
k4为k5的左孩子
k3为k4的左孩子
d2为k3的左孩子
d3为k3的右孩子
d4为k4的右孩子
d5为k5的右孩子
与图(b)中的最优二叉搜索树对应
直接上代码和输出结果,用的是不递归前序遍历二叉树的思路。
结果和上面是一样的,这里就不贴输出内容了,有兴趣可以自己复制代码试一下。当然这里用递归前序遍历二叉树肯定简单明了多了,用不递归去写纯粹是一时兴起。
public void Construct_optimal_bst()
{
var s_left = new Stack();
var s_right = new Stack();
var l = 1;
var r = amount;
var parent = -1;
while (s_left.Count != 0
|| r >= l)
{
while (r >= l)
{
if (parent == -1)
{
Console.WriteLine("k" + root[l, r] + "为根");
}
else if (root[l, r] >= parent)
{
Console.WriteLine("k" + root[l, r] + "为k" + parent + "的右孩子");
}
else
{
Console.WriteLine("k" + root[l, r] + "为k" + parent + "的左孩子");
}
s_left.Push(l);
s_right.Push(r);
parent = root[l, r];
r = parent - 1;
if (r == l - 1)
{
Console.WriteLine("d" + r + "为k" + parent + "的左孩子");
}
}
if (s_left.Count != 0 && s_right.Count != 0)
{
l = (int)s_left.Pop();
r = (int)s_right.Pop();
parent = root[l, r];
l = parent + 1;
if (r == l - 1)
{
Console.WriteLine("d" + r + "为k" + parent + "的右孩子");
}
}
}
}
15.5-2
若7个关键字的概率如下图所示,求其最优二叉搜索树的结构和代价。
i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
---|---|---|---|---|---|---|---|---|
pi | / | 0.04 | 0.06 | 0.08 | 0.02 | 0.10 | 0.12 | 0.14 |
qi | 0.06 | 0.06 | 0.06 | 0.06 | 0.05 | 0.05 | 0.05 | 0.05 |
输入题目里面的元素,直接运行代码就能出结果了。
15.5-3
假设OPTIMAL-BST不维护表w[i,j],而是在第9行利用公式直接计算w(i,j),然后在第11行使用此值。如此改动会对渐近时间复杂性有何影响?
公式为w(i,j) =
∑
i
=
1
n
\displaystyle\sum_{i=1}^{n}
i=1∑n pi +
∑
i
=
0
n
\displaystyle\sum_{i=0}^{n}
i=0∑n qi
两个总和项都要花费O(n)的时间,因此时间复杂度上升到O(n^3)。
15.5-4
Knuth已经证明,对所有1<=i<=j<=n,存在最优二叉搜索树,其根满足root[i , j-1] <= root[i , j] <= root[i + 1, j]。利用这一特性修改算法OPTIMAL-BST,使得运行时间减少为Θ(n^2)。
非常强的证明,直接简化了我们的递归公式。
后续会直接贴上修改后的代码。