算法导论 总结索引 | 第三部分 第十二章:二叉搜索树

1、搜索树数据结构 支持 许多动态集合操作,包括 SEARCH、MINIMUM、MAXIMUM、PREDECESSOR、SUCCESSOR、INSERT 和 DELETE 等。使用搜索树 既可以作为一个字典 又可以作为一个优先队列

2、二叉搜索树上的基本操作 所花费的时间 与这棵树的高度成正比。对于有n个结点的 一棵完全的二叉搜索树来说,这些操作的最坏运行时间为 Θ(lgn),如果 这棵树是一条n个结点组成的线性链,那么 同样的操作 就要花费 Θ(n) 的最坏运行时间

1、什么是二叉搜索树

1、除了key和卫星数据之外,每个结点 还包含属性left、night和p,它们分别指向结点的左孩子、右孩子和双亲。根结点是树中唯一父指针为NIL的结点

对任何结点,其左子树中的关键字最大 不超过 x.key,其右子树中的关键字最小 不低于x.key。不同的二叉搜索树 可以代表同一组值的集合

2、中序遍历 就可以 从大到小输出 一棵二叉树T中的 所有元素

INORDER-TREE-WALK(x)
	if x != NIL
		INORDER-TREE-WALK(x.left)
		print x.key
		INORDER-TREE-WALK(x.night)

3、如果x是一棵 有n个结点子树的根,那么调用INORDER-TREE-WALK(x) 需要 Θ(n) 时间

证明:要访问 这棵子树的全部n个结点,所以有 T(n) = Ω(n),下面要证明 T(n) = O(n)
首先有 T(0) = c
当调用 INORDER-TREE-WALK作用在 一个结点x上,x结点左子树 有k个结点 且其右子树有 n - k - 1个结点,时间由 T(n) <= T(k) + T(n - k - 1) +d 的限界其中 常数d > 0。反映了执行的时间的上界,
使用 替换法,通过 证明 T(n) <= (c + d)n + c,可以证得 T(n) = O(n)。对于 n = 0,有 (c + d)*0 + c = c = T(0)。对于 n > 0,有
定理证明
4、二叉搜索树性质与最小堆性质之间 有什么不同?能使用最小堆性质在 O(n) 时间内 按序输出一棵有n个结点树的关键字吗
最小堆性质:设x是最小堆中的一个结点。如果y是x子树中的一个结点,那么y.key≥x.key
二叉搜索树性质:设x是二叉搜索树中的一个结点。如果y是x左子树中的一个结点,那么y.key≤x.key。如果y是x右子树中的一个结点,那么y.key≥x.key

不能使用最小堆性质在时间 O(n) 内按序输出一棵有n个结点树的关键字
证明:假设能使用最小堆性质在时间 O(n) 内按序输出一棵有n个结点树的关键字,这也意味着给定一个最小堆,就能在时间 O(n) 内对其所有结点的关键字进行排序,但堆排序的时间复杂度是 O(lgn) ,所以假设不成立

5、执行 中序追历的非递归算法
一种容易的方法是 使用栈作为辅助数据结构;另一种较复杂但比较简洁的做法是不使用栈,但要假设能测试两个指针是否相等

 使用栈
INORDER-TREE-WALK-ITERATION-WITH-STACK(x)
    let S be a new stack
    p = x
    while p ≠ NIL or !STACK-EMPTY(S)
        if p ≠ NIL
            PUSH(S, p)
            p = p.left
        else p = POP(S)
            print p.key
            p = p.right
 
 不使用栈
INORDER-TREE-WALK-ITERATION-WITHOUT-STACK(x)
    cur = x
    prev = NIL
    while cur ≠ NIL
        if cur.left == NIL
            print cur.key
            cur = cur.right
        else prev = cur.left
            while prev.right ≠ NIL and prev.right ≠ cur
                prev = prev.right
            if prev.right == NIL
                prev.right = cur
                cur = cur.left
            else prev.right = NIL
                print cur.key
                cur = cur.right

1)不使用栈 中序遍历
如果当前节点的左孩子为空,则输出当前节点并将当前节点的右孩子作为“新的当前节点”

如果当前节点的左孩子不为空,在当前节点的左子树中 找到当前节点在中序遍历下的前驱节点

a) 如果前驱节点的右孩子为空,那么将 当前节点设置为前驱节点的右孩子。把当前节点的左孩子设置为“新的当前节点”
b) 如果前驱节点的右孩子为当前节点,将前驱节点的右孩子重新设为空(即恢复树的形状)。然后输出当前节点,并将当前节点的右孩子设置为“新的当前节点”

void inorderMorrisTraversal(TreeNode *root) {
     TreeNode *cur = root, *prev = NULL;
     while (cur != NULL)
     {
         if (cur->left == NULL)         
         {
             printf("%d ", cur->val);
             cur = cur->right;
         }
         else
         {
             // find predecessor
             prev = cur->left;
             while (prev->right != NULL && prev->right != cur)
                 prev = prev->right;

             if (prev->right == NULL)   
             {
                 prev->right = cur;
                 cur = cur->left;
             }
             else                      
             {
                 prev->right = NULL;
                 printf("%d ", cur->val);
                 cur = cur->right;
             }
         }
     }
 }

要使用O(1)空间进行遍历,最大的难点在于,遍历到子节点的时候 怎样重新返回到父节点(假设节点中没有指向父节点的p指针),由于不能用栈作为辅助空间。为了解决这个问题,Morris方法用到了 线索二叉树的概念。在Morris方法中不需要为每个节点额外分配指针指向其前驱 和 后继节点,只需要利用叶子节点中的左右空指针 指向某种顺序遍历下的前驱节点 或 后继节点 就可以了
线索二叉树是一种特殊的二叉树,它通过利用节点的空指针域存储线索,以便在不需要递归或使用栈的情况下实现对二叉树的快速遍历。线索二叉树包括前序线索二叉树、中序线索二叉树、后序线索二叉树等

2)不使用栈 前序遍历(线索二叉树)
与中序遍历相似,代码上只有一行不同,不同就在于输出的顺序

如果当前节点的左孩子为空,则输出当前节点并将其右孩子作为当前节点

如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点 在中序遍历下的前驱节点

a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。输出当前节点(在这里输出,这是与中序遍历唯一一点不同)。当前节点更新为当前节点的左孩子
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空。当前节点更新为当前节点的右孩子

void preorderMorrisTraversal(TreeNode *root) {
     TreeNode *cur = root, *prev = NULL;
     while (cur != NULL)
     {
         if (cur->left == NULL)
         {
             printf("%d ", cur->val);
             cur = cur->right;
         }
         else
         {
             prev = cur->left;
             while (prev->right != NULL && prev->right != cur)
                 prev = prev->right;

             if (prev->right == NULL)
             {
                 printf("%d ", cur->val);  // the only difference with inorder-traversal
                 prev->right = cur;
                 cur = cur->left;
             }
             else
             {
                 prev->right = NULL;
                 cur = cur->right;
             }
         }
     }
 }

3)不使用栈 后序遍历
Morris 后序遍历的步骤(适配后序遍历)
建立临时节点:建立一个临时节点 temp,令 temp.left 指向根节点

遍历节点:初始化 cur 指向 temp

左子树处理:
如果 cur.left 不为空,找到 cur 的左子树中的最右节点(即左子树的前驱节点)
如果前驱节点的右指针为空(即 prev.right == NULL),令 prev.right = cur,然后 cur = cur.left 继续处理左子树
如果前驱节点的右指针指向 cur(形成了一个环路,说明左子树已处理完成),则需要断开环路(prev.right = NULL),处理右子树之前,按逆序打印从 cur.left 到这个前驱之间的所有节点,再令 cur = cur.right

右子树处理:
当 cur.left 为空时,直接移动到 cur.right。
结束遍历:当 cur 为空时,结束遍历

逆序打印
逆序打印是后序遍历中使用 Morris 遍历时特有的一个步骤。从当前节点的左子节点出发到最右子节点的路径上的所有节点需要逆序打印,这模拟了后序遍历中“左右根”的顺序
这种方法避免了使用栈,但通过修改树的结构(临时修改右指针)来保存必要的信息。遍历完成后,树的结构被还原到其原始状态

 void reverse(TreeNode *from, TreeNode *to) // reverse the tree nodes 'from' -> 'to'.
 {
     if (from == to)
         return;
     TreeNode *x = from, *y = from->right, *z;
     while (true)
     {
         z = y->right;
         y->right = x;
         x = y;
         y = z;
         if (x == to)
             break;
     }
 }

 void printReverse(TreeNode* from, TreeNode *to) // print the reversed tree nodes 'from' -> 'to'.
 {
     reverse(from, to);

     TreeNode *p = to;
     while (true)
     {
         printf("%d ", p->val);
         if (p == from)
             break;
         p = p->right;
     }

     reverse(to, from);
 }

 void postorderMorrisTraversal(TreeNode *root) {
     TreeNode dump(0);
     dump.left = root;
     TreeNode *cur = &dump, *prev = NULL;
     while (cur)
     {
         if (cur->left == NULL)
         {
             cur = cur->right;
         }
         else
         {
             prev = cur->left;
             while (prev->right != NULL && prev->right != cur)
                 prev = prev->right;

             if (prev->right == NULL)
             {
                 prev->right = cur;
                 cur = cur->left;
             }
             else
             {
                 printReverse(cur->left, prev);  // call print
                 prev->right = NULL;
                 cur = cur->right;
             }
         }
     }
 }

对于一棵有个结点的树,请设计在 O(n) 时间内完成的先序遍历算法 和 后序遍历算法(上面的总结)

PREORDER-TREE-WALK(x)
    if x ≠ NIL
        print x.key
        PREORDER-TREE-WALK-RECURSIVE(x.left)
        PREORDER-TREE-WALK-RECURSIVE(x.right)
 
PREORDER-TREE-WALK-WITH-STACK(x)
    let S be a new stack
    p = x
    while p ≠ NIL or !STACK-EMPTY(S)
        if p ≠ NIL
            print p.key
            PUSH(S, p)
            p = p.left
        else p = POP(S)
            p = p.right
 
PREORDER-TREE-WALK-WITHOUT-STACK(x)
    cur = x
    prev = NIL
    while cur ≠ NIL
        if cur.left == NIL
            cur = cur.right
        else prev = cur.left
            while prev.right ≠ NIL and prev.right ≠ cur
                prev = prev.right
            if prev.right == NIL
                print cur.key
                prev.right = cur
                cur = cur.left
            else prev.right = NIL
                cur = cur.right
POSTORDER-TREE-WALK(x)
    if x ≠ NIL
        PREORDER-TREE-WALK-RECURSIVE(x.left)
        PREORDER-TREE-WALK-RECURSIVE(x.right)
        print x.key
 
POSTORDER-TREE-WALK-WITH-STACK(x)
    let S be a new stack
    p = x
    while p ≠ NIL or !STACK-EMPTY(S)
        if p ≠ NIL
            p.visited = 1
            PUSH(S, p)
            p = p.left
        else p = POP(S)
            if p.visited == 1
                p.visited = p.visited + 1
                PUSH(S, p)
                p = p.right
            elseif p.visited == 2
                print p.key
                p = NIL
 
PRINT-REVERSE(from, to)
    if from != to
        PRINT-REVERSE(from.right, to)
    print from.key
 
POSTORDER-TREE-WALK-WITHOUT-STACK(x)
    dump.left = x
    cur = dump
    prev = NIL
    while cur ≠ NIL
        if cur.left == NIL
            cur = cur.right
        else prev = cur.left
            while prev.right ≠ NIL and prev.right ≠ cur
                prev = prev.right
            if prev.right == NIL
                prev.right = cur
                cur = cur.left
            else PRINT-REVERSE(cur.left, prev)
                prev.right = NIL
                cur = cur.right

6、证明任何基于比较的算法从n个元素的任意序列中构造一棵二叉搜索树,其最坏情况下需要Ω(n lgn)
证明:
在这里插入图片描述
在这里插入图片描述

2、查询二叉搜索树

1、查找:输入一个指向树根的指针 和 一个关键字k,如果 这个结点存在,TREE-SEARCH 返回一个指向关键字为k的 结点指针;否则返回NIL

TREE-SEARCH(x.k)
if x == NIL or k == x.key
	return x
if k < x.key
	return TREE-SEARCH(x.left, k)
else return TREE-SEARCH(x.right, k)

从树根 开始递归期间遇到的结点 就形成了 一条向下的简单路径,所以 TREE-SEARCH 的运行时间为 O(h),其中 h 是这棵树的高度

采用 while 循环 来展开递归,用一种 迭代方式重写这个过程,效率更高

ITERATIVE-TREE-SEARCH(x.k)
	while x != NIL and k != x.key
		if k < x.key
			x = x.left
		else x = x.right
	return x

2、最大关键字元素和最小关键字元素

TREE-MINIMUM(x)
	while x.left != NIL
		x = x.left
	return x

TREE-MAXIMUM的伪代码是对称的

TREE-MAXIMUM(x)
	while x.right != NIL
		x = x.right
	return x

这个过程 在一棵高度为h的树上 均能在 O(h) 时间内执行完

3、后继和前驱
按中序顺序的次序 查找他的后继,则 一个结点x的后继 是大于 x.key的最小关键字的结点

TREE-SUCCESSOR(x)
	if x.right != NIL
		return TREE-MINIMUM(x.right)
	y = x.p
	while y != NIL and x == y.right // 还是右子树的关系,要找到左子树就停了
		x = y
		y = y.p
	return y

如果 结点x的右子树非空,那么x的后继结点 恰是x右子树中的 最左节点
右子树结点为空
如果结点x(13)的右子树为空 并有一个后继y(15),那么 y就是x的有左孩子的 最底层祖先,而且 它也是x的一个祖先。为了找到y,只需简单地从x开始 沿树而上 直到 遇到一个双亲有左孩子的结点

该过程 或者遵从一条简单路径 沿树而上 或者 遵从简单路径 沿树而下

求前驱跟后继代码对称

TREE-PREDECESSOR(x)
    if x.left ≠ NIL
        return TREE-MAXIMUM(x.left)
    y = x.p
    while y ≠ NIL and x == y.left
        x = y
        y = y.p
    return y

4、在一棵高度为h的二叉搜索树上,动态集合上的操作 SEARCH、MINIMUM、MAXIMUM、SUCCESSOR和PREDECESSOR可以在 O(h) 时间内完成

5、考虑一棵二叉搜索树T,其关键字 互不相同。证明:如果T中 一个结点x的右子树为空,且x有一个后继y,那么 y一定的最底层祖先,并且其左孩子 也是x的祖先
(注意到,每个结点都是它自己的祖先)
证明:因为T中所有的关键字互不相同,所以y是大于x.key的最小关键字的结点。

  1. 如果y不是x的最底层祖先,则y的左孩子的关键字大于x.key小于y.key,此时y不是大于x.key的最小关键字的结点,与条件矛盾,所以y是x的最底层祖先。
  2. 如果y的左孩子不是x的祖先,因为每个结点都是它自己的祖先,则x是y的右子树中的一个结点,此时y的关键字小于x.key,与条件矛盾,所以y的左孩子也是x的祖先。

6、用另一种方法 实现n个结点的二叉搜索树的中序遍历:TREE-MINIMUM找到这棵树中的最小元素,然后再调用 n-1 次的TREE-SUCCESSOR 证明 该算法的运行时间为 Θ(n)
证明:根据算法可知,每条边被遍历了两次,第一次向下,第二次向上。因为二叉搜索树中有 n 个结点,也即有 n-1 条边,共访问了 2(n-1) 次,所以该算法的运行时间为 Θ(n)

7、在一棵高度为h的二叉搜索树中,不论从哪个结点开始,k次连续的TREE-SUCCESSOR调用所需时间为 O(k + h)
证明:在一棵高度为h的树上,TREE-SUCCESSOR的运行时间为 O(h),因为该过程 或者遵从一条简单路径沿树向上 或者遵从简单路径沿树向下。根据6,k次连续的TREE-SUCCESSOR调用的运行时间是 O(k)。结合这两者可得,在一棵高度为h的二叉搜索树中,不论从哪个结点开始,k次连续的TREE-SUCCESSOR调用所需时间为 O(k + h)

8、设T是一棵二叉搜索树,其关键字 互不相同,设x是一个叶结点,y为其父结点。证明:y.key 或者是 T树中大于x.key的最小关键字,或者 是T树中小于x.key的最大关键字
证明:因为T是一棵二叉搜索树,其关键字互不相同,又因为x是一个叶结点(没有左右孩子的),y为其父结点,所以y为x的前驱或后继

  1. 当x是y的左孩子时,y是x的后继,所以y.key是T树中大于x.key的最小关键字
  2. 当x是y的右孩子时,y是x的前驱,所以y.key是T树中小于x.key的最大关键字

因此,y.key或者是T树中大于x.key的最小关键字,或者是T树中小于x.key的最大关键字

3、插入和删除

1、插入一个新结点带来的树修改 要相对简单些,删除的处理 有些复杂
插入:将一个新值v插入到 一棵二叉搜索树中,以结点z作为输入,其中 z.key = v,z.left = NIL,z.right = NIL,只在叶子结点插入

TREE-INSERT(T, z)
	y = NIL
	x = T.root
	while x != NIL // 到叶子结点插入
		y = x
		if z.key < x.key
			x = x.left
		else x = x.right
	z.p = y
	if y == NIL
		T.root = z // 树T为空
	elseif z.key < y.key
		y.left = z
	else y.right = z 
RECURSIVE-TREE-INSERT(x, z)
    y = x
    if z.key < x.key
        x = x.left
    else x = x.right
    if x ≠ NIL
        RECURSIVE-TREE-INSERT(x, z)
    else z.p = y
        if y == NIL
            T.root = z    // tree T was empty
        elseif z.key < y.key
            y.left = z
        else y.right = z

2、删除:

  • 如果 z没有孩子结点,那么 只是简单地将他删除,并修改它的父结点,用 NIL 作为孩子来替换z
  • 如果 z只有一个孩子,那么 将这个孩子 提升到 树中z的位置上,并修改z的父结点,用z的孩子 来替换z
  • 如果 z有两个孩子,那么找z的后继y(一定在z的右子树中),并 让y占据树中z的位置。z的原来右子树部分 变为 y的新的右子树,并且 z的左子树 成为y的新的左子树。还与 y是否为z的右孩子相关

如果 z有两个孩子:
如果y是z的右孩子,用y替换z,并仅留下 y的右孩子(如c)
否则,位于z的右子树中 但并不是z的右孩子。先用 y的右孩子替换y,然后 再用y替换z(看起来做了旋转)(如d)
删除
定义 一个子过程 TRANSPLANT,它是用 另一棵子树 替换 一棵子树并成为 其双亲的孩子结点

TRANSPLANT(T, u, v)
	if u.p == NIL
		T.root = v
	elseif u == u.p.left
		u.p.left = v
	else u.p.right = v
	if v != NIL
		v.p = u.p

u是其双亲的左孩子 或 右孩子。TRANSPLANT 并没有处理 v.left 和 v.right 的更新;这些更新 由TRANSPLANT的调用者来完成

TREE-DELETE(T, x)
	if z.left == NIL
		TRANSPLANT(T, z, z.right)
	elseif z.right == NIL
		TRANSPLANT(T, z, z.left)
	else y = TREE-MINIMUM(z.right) // y是后继,第5行
		if y.p != z
			TRANSPLANT(T, y, y.right)
			y.right = z.right
			y.right.p = y
		TRANSPLANT(T, z, y)
		y.left = z.left
		y.left.p = y

第5行查找结点y,它是z的后继。因为z的右子树非空,这样后继 一定是这个子树中 具有最小关键字的结点
因为y已经是最小的,所以 不会有左孩子。将y移出它的原来位置 进行拼接,并替换树中的z
如果 y是z的右孩子,那么就用 y替换z并成为z的双亲的一个孩子,用z的左孩子 替换y的右孩子
如果 y不是z的右孩子,用y的右孩子 替换y并成为y的双亲的一个孩子,然后 将z的左孩子 替换为 y的左孩子(旋转过程,把没有左孩子的y提上去),最后三行:用y替换z 并成为z的双亲的一个孩子,再用z的左孩子 替换为y的左孩子

除了 第5行调用 TREE-MINIMUM 之外,TREE-DELETE 的每一行,包括调用 TRANSPLANT ,都只花费 常数时间。因此,在一棵高度为h的树上,TREE-DELETE 的运行时间为 O(h)

3、在一棵高度为h的二叉搜索树上,实现动态集合操作INSERT 和 DELETE的运行时间 均为 O(h)

4、删除操作不可交换(可交换的含义是,先删除x再删除y留下的结果树 与 先删除y再删除x 留下的结果树完全一样)。反例如下:删除操作不可交换
也证明了 同样的序列(不同顺序删除后 最后的序列都是一样的)可以对应 不同的二叉搜索树

5、假设 为每个结点换一种设计,将属性x.p(双亲)替换为x.succ(后继)。给出使用这种表示法的二叉搜索树T上SEARCH、INSERT和DELETE操作的伪代码。这些伪代码应在O(h)时间内执行完,其中h为树T的高度
应该设计一个返回某个结点的双亲的子过程

TREE-SEARCH(x, k) // 不变
    if x == NIL or k == x.key
        return x
    if k < x.key
        return TREE-SEARCH(x.left, k)
    else return TREE-SEARCH(x.right, k)
 
TREE-INSERT(T, z)
    y = NIL
    x = T.root
    while x ≠ NIL
        y = x // y是x的父结点
        if z.key < x.key
            x = x.left
        else x = x.right
    if y == NIL
        T.root = z
    elseif z.key < y.key
        y.left = z
        z.succ = y
    else y.right = z
        z.succ = y.succ
        y.succ = z
 
PARENT(x)
    z = x.succ // z是y的父结点
    y = z.left
    while y ≠ x
        z = y
        y = y.right
    return z
 
TRANSPLANT(T, u, v)
    p = PARENT(u)
    if p == NIL
        T.root = v
    elseif u == p.left
        p.left = v
        v.succ = p
    else p.right = v
        p.succ = v
 
TREE-DELETE(T, z)
    if z.left == NIL
        TRANSPLANT(T, z, z.right)
    elseif z.right == NIL
        TRANSPLANT(T, z, z.left)
    else y = TREE-MINIMUM(z.right)
        if z ≠ PARENT(y)
            TRANSPLANT(T, y, y.right)
            y.right = z.right
            y.succ = z.right
        TRANSPLANT(T, z, y)
        y.left = z.left
        y.left.succ = y

6、当 TREE-DELETE 中的结点z有两个孩子时,选择结点y为它的前驱,而不是 后继。如果这样做,对TREE-DELETE应该做些什么必要的修改?一些人提出了一个公平策略,为前驱和后继赋予相等的优先级,这样得到了较好的实验性能。如何对TREE-DELETE进行修改来实现这样一种公平策略(用随机数把前驱 / 后继删除 结合起来)

TREE-DELETE(T, z)
    if z.left == NIL
        TRANSPLANT(T, z, z.right)
    elseif z.right == NIL
        TRANSPLANT(T, z, z.left)
    else y = TREE-MAXIMUM(z.left) // else才有区别
        if y.p ≠ z
            TRANSPLANT(T, y, y.left) // left和right反的
            y.left = z.left
            y.left.p = y
        TRANSPLANT(T, z, y)
        y.right = z.right
        y.right.p = y
TREE-DELETE(T, z)
    if z.left == NIL
        TRANSPLANT(T, z, z.right)
    elseif z.right == NIL
        TRANSPLANT(T, z, z.left)
    elseif RANDOM(1, 2) == 1
        y = TREE-MINIMUM(z.right)
        if y.p ≠ z
            TRANSPLANT(T, y, y.right)
            y.right = z.right
            y.right.p = y
        TRANSPLANT(T, z, y)
        y.left = z.left
        y.left.p = y
    else y = TREE-MAXIMUM(z.left)
        if y.p ≠ z
            TRANSPLANT(T, y, y.left)
            y.left = z.left
            y.left.p = y
        TRANSPLANT(T, z, y)
        y.right = z.right
        y.rignt.p = y

4、随机构建二叉搜索树

1、随着 元素的插入和删除,二叉搜索树的高度 是变化的。例如,如果个关键字 按严格递增的次序被插入,则这棵树 一定是高度为 n-1 的一条链

定义n个关键字 的一棵初始的空树中 而生成的树,这里输入关键字的 n! 个排列中的每个 都是等可能地出现

2、一棵 有n个不同关键字的 随机构建二叉搜索树的期望高度为 O(lg n)
证明:从定义三个随机变量开始,用 Xn 表示一棵 有n个不同关键字的 随即构建二叉树的高度,并定义 指数高度 Yn = 2Xn

选择 一个关键字作为 树根,并设 Rn为一个随机变量,表示 这个关键字在n个关键字排好序后 应占据的位置
如果 Rn = i,那么 根的左子树是一棵 有i - 1个关键字的 随机构建二叉搜索树,右子树 是一棵有 n - i 个关键字的 随机构建二叉搜索树
因为 二叉树的高度 比根的两棵子树较高的 那棵子树大1,因此 二叉树的指数高度是 根的两棵子树较高的 那棵子树的2倍。如果 Rn = i,则有
在这里插入图片描述
设 Yi = 1,1个结点的树的指数高度是 20=1,定义 Y0 = 0
定义 指示器随机变量 Zn,1,Zn,2,…,Zn,n,其中 Zn,i = I{Rn = i}。因为 Rn 对集合 {1, 2, …,n} 中的任何元素 都是等可能的(选择单个元素的位置,而不是整个序列)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
12.3证明12.3证明
有了 E[Yn] 界,但 最终的目标是 要得到 E[Xn] 的界,因为 f(x) = 2x是凸的,使用 Jensen不等式

得到 E[Xn] = O(lgn)
1)Jensen不等式
Jensen不等式
2)凸函数
凸函数
3)判断凸函数的方法
判断凸函数

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/590734.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Android应用程序构成

binder Android应用程序是由Activity、 Service、 Broadcast Receiver和 Content Provider四种类型的组件构成的&#xff0c; 它们有可能运行在同一个进程中&#xff0c; 也有可能运行在不同的进程中。 此外&#xff0c; 各种系统组件也运行在独立的进程中&#xff0c; 例如&a…

如何基于nginx组建多个子目录网站

华子目录 实验要求实验步骤 实验要求 组建多个子目录网站www.openlab.com&#xff0c;该网站有2个子目录www.openlab.com/sxhkt和www.openlab.com/zywww.openlab.com/sxhkt使用http读取www.openlab.com/zy使用https读取 实验步骤 准备工作 [rootserver ~]# setenforce 0[ro…

Matlab|基于多目标粒子群算法的微电网优化调度

目录 1 主要内容 2 部分代码 3 效果图 4 下载链接 1 主要内容 本程序为《基于多目标粒子群算法的微电网优化调度》-王金全文章的方法复现&#xff0c;考虑因素较文章复杂&#xff0c;除了考虑基本机组、储能等的出力&#xff0c;还考虑了弃风和弃光&#xff0c;很值得大家…

Docker Compose如何安装

Docker Compose的安装通常依赖于你的操作系统。以下是在不同操作系统中安装Docker Compose的方法&#xff1a; Linux 系统 //下载最新版本的Docker Compose sudo curl -L "https://github.com/docker/compose/releases/download/v2.5.1/docker-compose-$(uname -s)-$(un…

大厂可视化平台之百度SugarBI:让数据价值一“幕”了然。

百度Sugar是百度智能云推出的敏捷BI和数据可视化平台&#xff0c;可以说是生产力具。 它旨在解决报表和大屏的数据BI分析和可视化问题&#xff0c;同时也是为了解放数据可视化系统的开发人力。百度Sugar基于百度Echarts提供丰富的图表组件&#xff0c;使得用户可以开箱即用、零…

spring框架学习记录(1)

前半个月一直在应付期中考试&#xff0c;快被折磨似了orz 文章目录 SpringIoC(Inversion of Control) 控制反转与DI(Dependency Injection)依赖注入bean相关bean配置bean实例化bean的生命周期 依赖注入相关依赖注入方式依赖自动装配 容器创建容器获取bean Spring IoC(Inversi…

Linux进程——Linux下常见的进程状态

前言&#xff1a;在进程学习这一块&#xff0c;我们主要学习的就是PCB这个进程控制块&#xff0c;而PBC就是用来描述进程的结构体&#xff0c;而进程状态就是PCB结构体中的一个变量。 本篇主要内容&#xff1a; 操作系统中的进程状态Linux下的进程状态 在开始之前&#xff0c;我…

Redis学习笔记(基础)

Redis学习笔记&#xff08;基础&#xff09; 一、Nosql概述1.1、为什么使用Nosql1.2、什么是Nosql1.3、阿里巴巴演进分析1.4、NoSQL的四大分类 二、 Redis入门2.1、概述2.2、Windows使用Redis2.3、linux安装2.4、redis-benchmark性能测试2.5、Redis基础知识 三、五大数据类型3.…

Linux专栏08:Linux基本指令之压缩解压缩指令

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Linux专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Linux基本指令之压缩解压缩指令 编号&#xff1a;08 文章目录 Linu…

初始Java篇(JavaSE基础语法)(7)抽象类和接口(上)

找往期文章包括但不限于本期文章中不懂的知识点&#xff1a; 个人主页&#xff1a;我要学编程(ಥ_ಥ)-CSDN博客 所属专栏&#xff1a;JavaSE 目录 抽象类 抽象类的概念&#xff1a; 抽象类语法 抽象类特性 抽象类的作用 接口 接口的概念&#xff1a; 语法规则 接口…

ssm104园区停车管理系统+jsp

园区停车管理系统的设计与实现 摘 要 网络技术和计算机技术发展至今&#xff0c;已经拥有了深厚的理论基础&#xff0c;并在现实中进行了充分运用&#xff0c;尤其是基于计算机运行的软件更是受到各界的关注。加上现在人们已经步入信息时代&#xff0c;所以对于信息的宣传和管…

Atlassian Jira 信息泄露漏洞(CVE-2019-3403) 排查思路

Atlassian Jira&#xff1a; 企业广泛使用的项目与事务跟踪工具&#xff0c;被广泛应用于缺陷跟踪、客户服务、需求收集、流程审批、任务跟踪、项目跟踪和敏捷管理等工作领域。 简述&#xff1a; 近日发现多个内网IP触发的Atlassian Jira 信息泄露漏洞的告警。 告警的检测规…

C语言:内存操作函数memcpy、memmove、memset和memcpy的使用和模拟实现

一&#xff1a;memcpy的使用和模拟 memcpy使用时需要包含的头文件为#include<string.h> void* memcpy(void* destination,const void* source,size_t num) 函数memcpy从source的位置开始向后复制num个字节的数据到destination指向的内存位置&#xff08;特别注意的是…

Python | Leetcode Python题解之第66题加一

题目&#xff1a; 题解&#xff1a; class Solution:def plusOne(self, digits: List[int]) -> List[int]:n len(digits)for i in range(n - 1, -1, -1):if digits[i] ! 9:digits[i] 1for j in range(i 1, n):digits[j] 0return digits# digits 中所有的元素均为 9retu…

「C++ 内存管理篇 04」动态二维数组

目录 一. 使用calloc/free开辟和释放二维数组 二、 使用new/delete开辟和释放二维数组 一. 使用calloc/free开辟和释放二维数组 让一个二级指针变量存放动态开辟的一级指针数组的起始地址&#xff0c;然后让这些一级指针指向动态开辟的基本类型的数组&#xff1a; // 开辟一个大…

【C++】模拟实现string

文章目录 前言成员变量成员函数构造函数浅拷贝深拷贝构造函数实现 析构函数赋值重载 迭代器begin & endrbegin & rend 空间管理函数元素访问元素修改字符串运算流提取 & 流插入流提取流插入 总结 前言 模拟实现不是为了写得和库里面一样好。而是为了更好的了解底层…

一行代码将文件存储到本地或各种存储平台

一行代码将文件存储到本地或各种存储平台 这里我们介绍的是一个开源项目。 这个是他的官网 简介 (xuyanwu.cn) 下面来看他的一个介绍&#xff1a; 一行代码将文件存储到本地、FTP、SFTP、WebDAV、阿里云 OSS、华为云 OBS、七牛云 Kodo、腾讯云 COS、百度云 BOS、又拍云 USS…

R语言学习—1—将数据框中某一列数据改成行名

将数据框中某一列数据改成行名 代码 结果

【大语言模型LLM】-基于大语言模型搭建客服助手(2)

&#x1f525;博客主页&#xff1a;西瓜WiFi &#x1f3a5;系列专栏&#xff1a;《大语言模型》 很多非常有趣的模型&#xff0c;值得收藏&#xff0c;满足大家的收集癖&#xff01; 如果觉得有用&#xff0c;请三连&#x1f44d;⭐❤️&#xff0c;谢谢&#xff01; 长期不…

408数据结构-树的基本概念与性质 自学知识点整理

树的定义 树是 n n n&#xff08; n ≥ 0 n≥0 n≥0&#xff09;个结点的有限集。当 n 0 n0 n0时&#xff0c;称为空树。 任意一棵非空树应具有以下特性&#xff1a; 有且仅有一个特定的被称为根的结点&#xff08;根结点&#xff09;。当 n &#xff1e; 1 n&#xff1e;1 …