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

news2024/10/6 8:39:53

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)判断凸函数的方法
判断凸函数

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

发卡盗u源码系统搭建ZHU

2024最新UI发卡盗U/支持多语言/更新UI界面/支持多个主流钱包去除后门板&#xff0c;最好是部署智能合约后用合约地址来授权包含转账支付页面盗U授权源码。 完美提U&#xff0c;教程包含如何提u 。功能完美。 1.Php静态 2.目录puicta 3.扩sal 4.ssl不强https

Messari 报告摘要 :Covalent Network(CQT)2024 年第一季度表现

摘要&#xff1a; 尽管 CQT 代币流通供应量增加了 20%&#xff08;新增 1.04 亿枚 CQT&#xff09;&#xff0c;但 CQT 的质押百分比仅从 2023 年第一季度的 22% 增长到了 2024 年第一季度的 29%。 CQT 的市值季度环比增长了 28%&#xff0c;多次达到 2.75 亿美元&#xff0c…

QT5之事件——包含提升控件

事件概述 信号就是事件的一种&#xff0c;事件由用户触发&#xff1b; 鼠标点击窗口&#xff0c;也可以检测到事件&#xff1b;产生事件后&#xff0c;传给事件处理&#xff0c;判断事件类型&#xff0c;后执行事件相应函数&#xff1b; 类似单片机的中断&#xff08;中断向量…

STL 标准模板库

以下是一些常用的STL容器&#xff1a; vector&#xff1a;动态数组&#xff0c;提供快速的随机访问。list&#xff1a;双向链表&#xff0c;支持快速插入和删除操作。set&#xff1a;有序集合&#xff0c;存储唯一的元素。map&#xff1a;有序映射&#xff0c;存储键值对。sta…

数据库(MySQL)—— DQL语句(基本查询和条件查询)

数据库&#xff08;MySQL&#xff09;—— DQL语句&#xff08;基本查询和条件查询&#xff09; 什么是DQL语句基本查询查询多个字段字段设置别名去除重复记录 条件查询语法条件 我们今天进入MySQL的DQL语句的学习&#xff1a; 什么是DQL语句 MySQL中的DQL&#xff08;Data Q…

【Docker第一课】docker的基本命令和试启动容器(详细图解)

目录 知识梗概 docker的初步了解 了解docker常用命令 试开启容器&#xff08;这里演示nginx、python3和mysql&#xff09; 1、nginx容器的启动 2、python3容器的启动 docker的作用 虚拟机与容器的区别 写在前面&#xff1a; 本专栏你将了解docker一些入门知识&#xff…

【解决方案】Can‘t exec “locale”: No such file or directory

【解决方案】Cant exec “locale”: No such file or directory 还可能出现的错误&#xff1a; 1. 报错原因&#xff1a; 缺少ldconfig 2. 解决方案&#xff1a; sudo apt-get download libc-bin dpkg -x libc-bin*.deb unpackdir/ sudo cp unpackdir/sbin/ldconfig /sbin/ s…

机器学习:深入解析SVM的核心概念【三、核函数】

核函数 **问题一&#xff1a;为什么说是有限维就一定存在高维空间可分呢&#xff1f;**原始空间与特征空间为什么映射到高维空间可以实现可分核函数的作用 **问题二&#xff1a;最终怎么得到函数**从对偶问题到决策函数的步骤&#xff1a;结论 **问题三&#xff1a;为什么说特征…

Coursera: An Introduction to American Law 学习笔记 Week 04: Constitutional Law

An Introduction to American Law 本文是 https://www.coursera.org/programs/career-training-for-nevadans-k7yhc/learn/american-law 这门课的学习笔记。 文章目录 An Introduction to American LawInstructors Week 04: Constitutional LawKey Constitutional Law TermsSup…

Golang chan 实现原理

Golang&#xff1a;context基于go1.22版本 chan的作用和使用方法共享内存的优缺点 chan 的使用方法 chanel 的底层结构channel 结构体创建channel写流程异常处理写时有阻塞读流程写时无阻塞读流程&#xff0c;缓冲区有空间写时无阻塞读流程&#xff0c;缓冲区无空间写流程整体架…

PC通过串口发送指令控制LED+串口中断

如何让单片机接收数据&#xff1f; 首先要打开SCON中的串行接收控制位REN。当REN1时为允许接收状态&#xff0c;可以接收信息。 因此令SCON 0x50&#xff1b; 怎么知道收到数据&#xff1f; 利用RI接收中断请求标志位。当串行接收到第8位结束时由内部硬件自动置为RI1&#…

Matlab各个版本介绍、区别分析及推荐

MATLAB&#xff0c;由美国MathWorks公司出品&#xff0c;是一款广泛应用的商业数学软件。自其诞生之初&#xff0c;MATLAB便以其强大的矩阵计算能力、灵活的编程环境以及广泛的应用领域&#xff0c;赢得了全球科研工作者和工程师的青睐。本文将详细介绍MATLAB的各个版本&#x…

基于springboot+vue+Mysql的学生毕业离校系统

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

牛客美团2024年春招第一场笔试【技术】解题

1.小美的平衡矩阵 小美拿到了一个n∗n的矩阵&#xff0c;其中每个元素是 0 或者 1。 小美认为一个矩形区域是完美的&#xff0c;当且仅当该区域内 0 的数量恰好等于 1 的数量。 现在&#xff0c;小美希望你回答有多少个i∗i的完美矩形区域。你需要回答1≤i≤n的所有答案 输出…

ArcGIS+ChatGPT双剑合璧:从数据读取到空间分析,一站式掌握GIS与AI融合的前沿科技!

目录 专题一 AI大模型应用 专题二 ArcGIS工作流程及功能 专题三 prompt的使用技巧 专题四 AI助力工作流程 专题五 AI助力数据读取 专题六 AI助力数据编辑与处理 专题七 AI助力空间分析 专题八 AI助力遥感分析 专题九 AI助力二次开发 专题十 AI助力科研绘图 专题十一…

chrome extension插件替换网络请求中的useragent

感觉Chrome商店中的插件不能很好的实现自己想要的效果,那么就来自己动手吧。 本文以百度为例: 一般来说网页请求如下: 当前使用的useragent是User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safar…

【模板】差分

原题链接&#xff1a;登录—专业IT笔试面试备考平台_牛客网 目录 1. 题目描述 2. 思路分析 3. 代码实现 1. 题目描述 2. 思路分析 差分模板。 b[0]a[0]; b[1]a[1]-a[0]; b[2]a[2]-a[1]; ...... b[n-1]a[n-1]-a[n-2]; b[n]a[n]-a[n-1]; 差分标记&#xff1a;b[l]k,b…

Springboot+mybatis升级版(Postman测试)

一、项目结构 1.导入依赖 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apach…

Vue3+ts(day05:ref、props、生命周期、hook)

学习源码可以看我的个人前端学习笔记 (github.com):qdxzw/frontlearningNotes 觉得有帮助的同学&#xff0c;可以点心心支持一下哈&#xff08;笔记是根据b站上学习的尚硅谷的前端视频【张天禹老师】&#xff0c;记录一下学习笔记&#xff0c;用于自己复盘&#xff0c;有需要学…

生成gitee公钥

1、打开设置 2、设置SSH公钥 3、生成公钥 4、复制终端输出的公钥&#xff0c;放到这里&#xff0c;标题随便取。 5、测试 ssh -T gitgitee.com 最后用这个测试