zoukankan      html  css  js  c++  java
  • 算法(第四版)C# 习题题解——3.2

    写在前面

    整个项目都托管在了 Github 上:https://github.com/ikesnowy/Algorithms-4th-Edition-in-Csharp
    查找更方便的版本见:https://alg4.ikesnowy.com/
    这一节内容可能会用到的库文件有 BinarySearchTree,同样在 Github 上可以找到。
    善用 Ctrl + F 查找题目。

    习题&题解

    3.2.1

    解答

    构造出的树如下图所示:

    总比较次数:0 + 1 + 1 + 2 + 2 + 3 + 1 + 2 + 4 + 3 + 4 + 5 = 28 次

    另请参阅

    BinarySearchTree 库

    3.2.2

    解答

    用这样的序列就可以构造出最坏情况:

    "A X C S E R H",
    "X A S C R E H",
    "A C E H R S X",
    "X S R H E C A",
    "X A S R H E C",
    "A X S R H E C"
    

    构造出来的树看起来像这样:

    3.2.3

    解答

    官方答案:第一个插入的是 H,且 C 在 A 和 E 之前插入,S 在 R 和 X 之前插入的树。
    对序列排序,得到 A C E H R S X
    最优情况需要树两侧平衡,因此 H 为根结点,C 和 S 分别为 H 的子结点。
    同理,A 和 E 为 C 的子结点,R 和 X 为 S 的子结点。

    3.2.4

    解答

    d 是错误的。

    要点是追踪序列中的左右顺序,
    如果向右查找,那么后面的树一定都比它大,反之都比它小。
    例如 d 选项,2->7 向右查找,但后面的 8 比 7 大,应该挂在 7 的右子树上,不可能在 7 的左子树里。

    3.2.5

    解答

    事实上,这个问题可以归结为,如何根据频率来构造一棵最优的BST?
    如果知道树的形状,逆推插入顺序也就不难了(先序遍历)。

    首先我们定义某个结点的查找开销为该结点的深度加一乘以频率,
    (注意根结点的深度为 0,树的高度等于整棵树中最大的深度值)
    所有结点的查找开销加起来就是一整棵树的查找开销了。

    [cost(n)=sum_{i=0}^{n} (depth(i)+1) imes frequency(i) ]

    对于固定的一组键值和频率,$ cost $ 最小的树即为我们要找的树。
    这样的树被称为最优化二叉树,或者 Optimal BST。

    根据二叉树的性质,我们可以将 $ cost $ 的表达式变为:

    [cost(n)=cost(left)+cost(right)+sum_{i=1}^{n} frequency(i) ]

    即左子树的开销和右子树的开销相加,再加上所有结点的频率之和。
    (左子树和右子树开销计算时每个结点的深度都少了1,因此要加上频率和)
    不难得到结论,Optimal BST 的所有子树也都是 Optimal BST。

    我们可以利用一种类似于构造哈夫曼树的方法构造 Optimal BST,
    哈夫曼树中比较的是频率,而构造 Optimal BST 时比较的则是开销。
    由于二叉查找树是有序的,因此我们先对序列排序。
    然后计算所有大小为 1 的子树开销,显然就等于各个节点的频率。
    再计算大小为 2 的子树,注意这里只能按顺序取结点,不能跳着取(例如取第一个和第三个结点),
    每种结点取法都对应有两种根结点选择,计算出最小的开销并记录。
    以此类推,直到计算到大小为 n 的树,此时整棵 BST 就被构造出来了。

    举个例子,例如给出键值和频率如下表所示:

    键值 1 2 3 4 5
    频率 0.4 0.1 0.2 0.1 0.2

    当 $ s=1 $ 时,各子树的开销如下表(根结点不唯一):

    子树 [1,1] [2,2] [3,3] [4,4] [5,5]
    开销 0.4 0.1 0.2 0.1 0.2
    根结点 1 2 3 4 5

    当 $ s=2 $ 时,各个子树的开销如下(根结点不唯一):

    子树 [1,2] [2,3] [3,4] [4,5]
    开销 $ min egin{cases} 0+[2,2]+0.5, \ [1,1]+0+0.5, end{cases} \ =0.6 $ $ min egin{cases} 0+[3,3]+0.3, \ [2,2]+0+0.3, end{cases} \ =0.4 $ $ min egin{cases} 0+[4,4]+0.3, \ [3,3]+0+0.3, end{cases} \ =0.4 $ $ min egin{cases} 0 +[5,5]+0.3, \ [4,4]+0+0.3, end{cases} \ =0.4 $
    根结点 1 3 3 5

    当 $ s=3 $ 时,各个子树的开销如下表(根结点不唯一):

    子树 [1,3] [2,4] [3,5]
    开销 $ min egin{cases} 0+[2,3]+0.7, \ [1,1]+[3,3]+0.7, \ [1,2]+0+0.7, end{cases} \ =1.1 $ $ min egin{cases} 0+[3,4]+0.4, \ [2,2]+[4,4]+0.4, \ [2,3]+0+0.4, end{cases} \ =0.6 $ $ min egin{cases} 0+[4,5]+0.5, \ [3,3]+[5,5]+0.5, \ [3,4]+0+0.5, end{cases} \ =0.9 $
    根结点 1 3 3

    当 $ s=4 $ 时,各个子树的开销如下表(根结点不唯一):

    子树 [1,4] [2,5]
    开销 $ min egin{cases} 0+[2,4]+0.8, \ [1,1]+[3,4]+0.8, \ [1,2]+[4,4]+0.8, \ [1,3]+0+0.8, end{cases} \ = 1.4 $ $ min egin{cases} 0+[3,5]+0.5, \ [2,2]+[4,5]+0.5, \ [2,3]+[5,5]+0.5, \ [2,4]+0+0.5, end{cases} \ = 1.0 $
    根结点 1 3

    当 $ s=5 $ 时,各个子树的开销如下表(根结点不唯一):

    子树 [1,5]
    开销 $ min egin{cases} 0+[2,5]+1.0, \ [1,1]+[3,5]+1.0, \ [1,2]+[4,5]+1.0, \ [1,3]+[5,5]+1.0, \ [1,4]+0+1.0 end{cases} \ = 2.0 $
    根结点 1

    于是最优二叉树为(不唯一):

    另请参阅

    Optimal Binary Search Tree - Wikipedia

    3.2.6

    解答

    官方 BST 实现见:https://algs4.cs.princeton.edu/32bst/BST.java.html

    二叉树的高度=左右子树最大高度+1,叶结点的高度为 0。
    于是我们可以构造如下递归方法:

    private int Height(Node x)
    {
        return x == null ? -1 : 1 + Math.Max(Height(x.Left), Height(x.Right));
    }
    

    x 等于 null 时,说明它是叶子结点的左/右子树,应该返回 0-1=-1。

    也可以在结点中添加一个 Height 属性,记录当前结点的高度,当插入新结点时重新计算高度。
    Put 方法中添加计算高度的代码:

    private Node Put(Node x, TKey key, TValue value)
    {
        if (x == null)
            return new Node(key, value, 1, 0);
        int cmp = key.CompareTo(x.Key);
        if (cmp < 0)
            x.Left = Put(x.Left, key, value);
        else if (cmp > 0)
            x.Right = Put(x.Right, key, value);
        else
            x.Value = value;
        x.Size = 1 + Size(x.Left) + Size(x.Right);
        x.Height = 1 + Math.Max(Height(x.Left), Height(x.Right));
        return x;
    }
    

    由于叶结点的高度为零,因此新插入的结点高度应该初始化为 0。

    3.2.7

    解答

    平均查找次数 = 树所有结点的深度之和 / 结点个数 + 1。

    只要能够获得深度和,就能构造出如下用于计算平均查找次数的方法:

    public int AverageCompares()
    {
        return DepthSum(root) / Size() + 1;
    }
    

    二叉树的深度之和 = 左子树的深度和 + 右子树的深度和 + 左子树的结点个数 + 右子树的结点个数
    (加上根结点会使左右子树所有结点的深度+1)

    于是我们可以获得如下递归方法,用于计算一棵树的深度和:

    private int DepthSum(Node x)
    {
        if (x == null)
            return 0;
        return DepthSum(x.Left) + DepthSum(x.Right) + x.Size - 1;
    }
    

    也可以在结点中直接添加一个 DepthSum 属性,用于记录当前结点的深度和。
    需要在每次插入新结点时重新计算查找路径上所有结点的 DepthSum

    private Node Put(Node x, TKey key, TValue value, int depth)
    {
        if (x == null)
            return new Node(key, value, 1, depth);
        var cmp = key.CompareTo(x.Key);
        if (cmp < 0)
            x.Left = Put(x.Left, key, value, depth + 1);
        else if (cmp > 0)
            x.Right = Put(x.Right, key, value, depth + 1);
        else
            x.Value = value;
        x.Size = 1 + Size(x.Left) + Size(x.Right);
        x.DepthSum = depth;
        if (x.Left != null)
            x.DepthSum += x.Left.DepthSum;
        if (x.Right != null)
            x.DepthSum += x.Right.DepthSum;
        return x;
    }
    

    在插入结点时需要传入一个参数以记录当前深度,新插入结点的默认深度和就是当前深度。

    3.2.8

    解答

    假设输入的完全二叉树结点数目为 (n)

    完全二叉树总是可以分成两部分,一个满二叉树,以及多余的结点。

    于是完全二叉树中满二叉树的部分层数为 (l = lfloor log_2 (n+1) floor)。(根结点位于第一层)
    满二叉树占的结点数量为 (n_1 = 2^l -1),多余结点数量为 $ n_2=n-n_1$。

    又因为深度等于层数 - 1,多余结点正好在满二叉树的下一层,因此多余结点的深度即为 $ l $。
    于是多余结点的深度和 (d_2 = l imes n_2)

    接下来计算满二叉树的深度和。
    一个层数为 $ l $ 的满二叉树,最后一层正好有 $ 2^{l-1} $ 个结点。
    于是深度和为 $ d_1 = 0 imes 1 + 1 imes 2+2 imes 4+cdots+(l-1)2^{l-1} =sum_{i=1}^{l-1} i2^i $。
    用错位相减法,有:

    [egin{eqnarray*} d_1&=&1 imes 2^1 + &2 imes 2^2 + cdots + (l-1)2^{l-1} \ 2d_1&=& &1 imes 2^2 + cdots+(l-2)2^{l-1} +(l-1)2^{l} \ d_1-2d_1&=& 1 imes2^1+ &1 imes2^2+cdots+1 imes2^{l-1}-(l-1)2^l \ d_1 &=&(l-1)2^l &-2^l+2 \ &=&(l-2)2^l &+2 end{eqnarray*} ]

    于是可得总深度和: (d=d_1+d_2=l imes n_2+ (l-2)2^l+2)
    平均查找次数即为:(avgCompare=d / n + 1)

    代码
    private static int OptCompares(int n)
    {
        // 完全二叉树 = 满二叉树 + 多余结点
        // 满二叉树的层数。
        var l = (int)Math.Log(n + 1, 2);
        // 多余结点的个数。
        var extraNodes = n + 1 - (int)Math.Pow(2, l);
    
        var depthSum =
            extraNodes * l + (l - 2) * (int) Math.Pow(2, l) + 2;
        return depthSum / n + 1;
    }
    

    3.2.9

    解答

    比较简单,可以按照如下步骤解决:

    1. 生成 n 个数。
    2. 生成这 n 个数的全排列。
    3. 生成 n! 棵二叉搜索树,取其中结构不同的部分。

    全排列可以通过递归方式生成,方法类似于 DFS。
    开始 pool 中存有所有的数,遍历 pool ,每次取一个数放入 path 中,然后递归选择下一个。

    static void Permutation(List<int> pool, List<int> path, List<int[]> result)
    {
        if (pool.Count == 0)
        {
            result.Add(path.ToArray());
            return;
        }
    
        for (var i = 0; i < pool.Count; i++)
        {
            var item = pool[i];
            path.Add(item);
            pool.RemoveAt(i);
            Permutation(pool, path, result);
            pool.Insert(i, item);
            path.Remove(item);
        }
    }
    

    有了 n! 棵二叉树之后,我们需要过滤掉结构相同的树。
    我们可以把二叉树转换成数组表示(层序遍历即可),然后遍历数组进行比较。

    var treeA = a.ToArray();
    var treeB = b.ToArray();
    
    if (treeA.Length != treeB.Length)
        return false;
    for (var i = 0; i < treeA.Length; i++)
    {
        if (treeA[i] == null && treeB[i] == null)
            continue;
        if (treeA[i] != null && treeB[i] != null)
            continue;
        return false;
    }
    
    return true;
    

    结果如下:

    n=2

    n=3

    n=4

    n=5

    n=6


    另请参阅

    BinarySearchTree 库

    3.2.10

    解答

    官方实现:https://algs4.cs.princeton.edu/32bst/TestBST.java.html

    测试结果:

    size = 10
    min = A
    max = X
    
    Testing keys()
    ---------------------------
    A 8
    C 4
    E 12
    H 5
    L 11
    M 9
    P 10
    R 3
    S 0
    X 7
    
    Testing select
    ---------------------------
    0 A
    1 C
    2 E
    3 H
    4 L
    5 M
    6 P
    7 R
    8 S
    9 X
    
    key rank floor ceil
    ---------------------------
    A 0 A A
    B 1 A C
    C 1 C C
    D 2 C E
    E 2 E E
    F 3 E H
    G 3 E H
    H 3 H H
    I 4 H L
    J 4 H L
    K 4 H L
    L 4 L L
    M 5 M M
    N 6 M P
    O 6 M P
    P 6 P P
    Q 7 P R
    R 7 R R
    S 8 S S
    T 9 S X
    U 9 S X
    V 9 S X
    W 9 S X
    X 9 X X
    Y 10 X
    Z 10 X
    range search
    ---------------------------
    A-Z (11)A C E H L M P R S X
    Z-A (0)
    X-X (1)X
    0-Z (11)A C E H L M P R S X
    B-G (3)C E
    C-L (4)C E H L
    
    After deleting the smallest 3 keys
    ---------------------------
    H 5
    L 11
    M 9
    P 10
    R 3
    S 0
    X 7
    
    After deleting the remaining keys
    ---------------------------
    
    After adding back the keys
    ---------------------------
    A 8
    C 4
    E 12
    H 5
    L 11
    M 9
    P 10
    R 3
    S 0
    X 7
    

    3.2.11

    解答

    树的高度为树中深度最大的点的深度。
    因此 N 个结点最多只能构造出高度为 N-1 的树来,并不能构成高度为 N 的树。

    如果将题目变为用 N 个结点构造高度为 N-1 的二叉搜索树。
    这样的树即为二叉搜索树的最坏情况,除唯一的叶子结点之外,每个结点有且只有一个子树。
    于是除根结点外,每个结点都有两种选择,要么在左,要么在右。
    因此共有 (2 ^ {n - 1}) 种形状。

    接下来证明,对于某一种确定的最坏情况,在 N 个元素各不相同的情况下,输入顺序是唯一的。
    证明:
    就 1 2 3 这三个元素而言,构造这样一棵树:

      2
    1   3
    

    可以有 2 1 3 和 2 3 1 两种序列,因为在插入 2 之后可以选择的位置有两个
    但最坏情况下的二叉搜索树不存在具有两个子结点的结点,因此输入顺序是唯一的。

    反证:
    也可以这样考虑,假设序列 A 可以构造出一棵最坏情况下的二叉树,插入顺序为 (x_1 dots x_n)
    假设存在与 A 顺序不同的序列 B,它构造出的二叉树与 A 的相同。
    由于 A 和 B 的元素相同,因此 A 必然可以通过有限次元素交换得到 B。
    根据最坏情况下的二叉搜索树的性质,A 中的元素 (x_n) 必然满足 (x_1 dots x_{n - 1}) 的所有关系。
    例如,假设 (x_1 > x_2), 则 $ x_3 $ 也必然满足 (x_1 > x_3),即后面的元素必然满足前面元素的关系。
    于是 A 存在关系序列 (r_1 dots r_{n - 1}),只有满足这样序列的输入才能构造出对应的二叉树。
    (可以将 (r) 理解为大于号或者小于号,由于元素各不相同,因此不存在等号)
    显然 A 中的任意两个元素交换会导致至少一个 (r) 倒置,除非进行逆向交换,否则这种倒置不可能消除。
    (大于号和小于号是不满足交换律的)
    因此 A 和 B 必定相同,得证。

    于是每一种最坏情况下的形状都唯一对应一种输入序列,共有 (2 ^ {n - 1}) 种构造方式。

    3.2.12

    解答

    二叉树的大小=左子树的大小+右子树的大小+1

    根据上述表达式可以构造出一个递归的 Size() 方法,并删除结点中的 Size
    Rank()Select() 仍然可以正常工作,但最坏情况下的耗时可能会达到 (O(n))(O(n^2))

    3.2.13

    解答

    官方实现:https://algs4.cs.princeton.edu/32bst/NonrecursiveBST.java.html

    Get 方法可以很方便的改成非递归形式。

    while (cur != null)
    {
        var cmp = key.CompareTo(cur.Key);
        if (cmp < 0)
            cur = cur.Left;
        else if (cmp > 0)
            cur = cur.Right;
        else
            return cur.Value;
    }
    

    Put 方法结构类似,但需要注意更新路径上各个结点的 Size 属性。

    var path = new Stack<Node>();
    var cur = x;
    while (cur != null)
    {
        path.Push(cur); 
        var cmp = key.CompareTo(cur.Key);
        if (cmp < 0)
            cur = cur.Left;
        else if (cmp > 0)
            cur = cur.Right;
        else
        {
            cur.Value = value;
            return x;
        }
    }
    
    var parent = path.Peek();
    var node = new Node(key, value, 1);
    if (parent.Key.CompareTo(key) > 0)
        parent.Left = node;
    else
        parent.Right = node;
    
    while (path.Count > 0)
        path.Pop().Size++;
    

    Keys 方法中,可以用新建一个栈来代替递归栈记录路径。

    while (x != null || stack.Count > 0)
    {
        if (x != null)
        {
            stack.Push(x);
            x = x.Left;
        }
        else
        {
            x = stack.Pop();
            queue.Enqueue(x.Key);
            x = x.Right;
        }
    }
    
    另请参阅

    BinarySearchTree 库

    3.2.14

    解答

    minmaxselect 而言,它们是尾递归的,可以直接转换成迭代形式。
    (简单的说,尾递归就是所有递归操作都出现在 return 语句上,且返回值不需要参与其他运算)

    例如 min,递归形式为:

    if (x.Left == null)
        return x;
    return Min(x.Left); 
    

    递归调用获得的值直接返回,不再参与运算,可以直接转换为递推:

    while (true)
    {
        if (x.Left == null) return x;
        x = x.Left;
    }
    

    ceilingfloor 会略微复杂一些,具体见代码。

    代码

    minmax 比较简单,用一个 while 循环就可以转换为递推形式,例如 min

    while (x != null)
    {
        if (x.Left == null) return x;
        x = x.Left;
    }
    
    return x;
    

    floorceiling 则要稍微复杂一点,需要记录当前找到的最小/最大值,例如 floor

    Node floor = null;
    while (x != null)
    {
        var cmp = key.CompareTo(x.Key);
        if (cmp == 0)
        {
            return x;
        }
    
        if (cmp < 0)
        {
            x = x.Left;
        }
        else
        {
            floor = x;
            x = x.Right;
        }
    }
    
    return floor;
    

    rank 和它们类似,需要用一个变量记录当前排名。

    var rank = 0;
    while (x != null)
    {
        var cmp = key.CompareTo(x.Key);
        if (cmp < 0)
        {
            x = x.Left;
        }
        else if (cmp > 0)
        {
            rank += 1 + Size(x.Left);
            x = x.Right;
        }
        else
        {
            rank += Size(x.Left);
            return rank;
        }
    
    }
    
    return rank;
    

    select 比较简单,不需要维护变量。

    while (x != null)
    {
        var t = Size(x.Left);
        if (t > k)
        {
            x = x.Left;
        }
        else if (t < k)
        {
            x = x.Right;
            k = k - t - 1;
        }
        else
        {
            return x;
        }
    }
    
    return null;
    
    另请参阅

    尾递归-维基百科

    BinarySearchTree 库

    3.2.15

    解答

    比较简单,这里比较/取值都算一次访问,因此 keys 的访问序列会出现重复元素。

    函数 路径
    floor(Q) E Q
    select(5) E Q
    ceiling(Q) E Q
    rank(J) E Q J
    size(D, T) E Q T E D
    keys(D, T) E D E Q J J M Q T S S T

    3.2.16

    解答

    在高德纳的《计算机程序设计艺术》第一卷里出现了这个公式,编号为 (2.3.4.5(3))

    书中的证明简单直接:

    考虑二叉树中的某个叶子结点 (V),设根结点到它的路径长度为 (k),现在将 (V) 删去。
    对于二叉树的内部路径长度 (I) 和外部路径长度 (E)

    由于 (V) 被删去,(E) 将会减少 (2(k+1))(I) 将会减少 (k),但此时 (V) 变成了一个外部结点,(E) 又会加上 (k)
    因此最后 (E) 减少了 (k+2)(I) 减少了 (k),重复 (N) 次操作之后就可以得到 (E = I + 2N)

    另请参阅

    《计算机程序设计艺术:第一卷 基本算法》(第三版)P400

    3.2.17

    解答

    像这样,有一些字母是重复的,因此删除后树形状不变:

    |-------------------------------E-------------------------------|
    A                                               |---------------S---------------|
                                            |-------Q                       |-------Y
                                            I---|                       |---U
                                              |-O                       T
                                              N
    
    |---------------I---------------|
    A                       |-------S-------|
                        |---Q           |---Y
                      |-O             |-U
                      N               T
    
    I---------------|
            |-------S-------|
        |---Q           |---Y
      |-O             |-U
      N               T
    
    I---------------|
            |-------T-------|
        |---Q           |---Y
      |-O               U
      N
    
    I---------------|
            |-------T-------|
        |---Q               U
      |-O
      N
    
    I-------|
        |---T---|
      |-O       U
      N
    
    I-------|
        |---T
      |-O
      N
    
    I-------|
        |---T
      |-O
      N
    
    I-------|
        |---T
      |-O
      N
    
    I---|
      |-O
      N
    
    |-O
    N
    
    N
    

    3.2.18

    解答

    和上一题类似,只是删除顺序不同:

    |-------------------------------E-------------------------------|
    A                                               |---------------S---------------|
                                            |-------Q                       |-------Y
                                            I---|                       |---U
                                              |-O                       T
                                              N
    
    E-------------------------------|
                    |---------------S---------------|
            |-------Q                       |-------Y
            I---|                       |---U
              |-O                       T
              N
    
            |---------------S---------------|
    |-------Q                       |-------Y
    I---|                       |---U
      |-O                       T
      N
    
            |---------------S---------------|
    |-------Q                       |-------Y
    I---|                       |---U
      |-O                       T
      N
    
          |-------S-------|
      |---Q           |---Y
    |-O             |-U
    N               T
    
        |-------S-------|
    |---Q           |---Y
    O             |-U
                  T
    
    |-------S-------|
    Q           |---Y
              |-U
              T
    
    S-------|
        |---Y
      |-U
      T
    
      |---Y
    |-U
    T
    
      |---Y
    |-U
    T
    
    |-Y
    U
    
    Y
    

    3.2.19

    解答

    类似于这样的序列:

    |-------------------------------E-------------------------------|
    A                                               |---------------S---------------|
                                            |-------Q                       |-------Y
                                            I---|                       |---U
                                              |-O                       T
                                              N
    
    |---------------I---------------|
    A                       |-------S-------|
                        |---Q           |---Y
                      |-O             |-U
                      N               T
    
    |---------------N---------------|
    A                       |-------S-------|
                        |---Q           |---Y
                        O             |-U
                                      T
    
    |---------------O---------------|
    A                       |-------S-------|
                            Q           |---Y
                                      |-U
                                      T
    
    |---------------Q---------------|
    A                               S-------|
                                        |---Y
                                      |-U
                                      T
    
    |-------S-------|
    A           |---Y
              |-U
              T
    
    |---T---|
    A     |-Y
          U
    
    |-U-|
    A   Y
    
    |-Y
    A
    
    A
    

    3.2.20

    解答

    勘误:英文版为 keys() 方法(而非 Size() 方法)。

    先来观察一下 keys() 方法的实现:

    public IEnumerable<TKey> Keys(TKey lo, TKey hi)
    {
        var queue = new Queue<TKey>();
        Keys(root, queue, lo, hi);
        return queue;
    }
    
    private void Keys(Node x, Queue<TKey> queue, TKey lo, TKey hi)
    {
        var cmplo = lo.CompareTo(x.Key);
        var cmphi = hi.CompareTo(x.Key);
        if (cmplo < 0)
            Keys(x.Left, queue, lo, hi);
        if (cmplo <= 0 && cmphi >= 0)
            queue.Enqueue(x.Key);
        if (cmphi > 0)
            Keys(x.Right, queue, lo, hi);
    }
    

    简单地说,就是从根结点同时向两侧查找,同时把中间的键加入到队列中(树高的倍数+范围内键的数量)。

    于是 Keys() 的耗时可以分成两部分:
    寻找小于 lo 的最大键值和大于 hi 的最小键值在最坏情况下需要的访问结点数——即树高。
    (例如当 lo 小于树中的最小键或者 hi 大于树中最大键时)
    以及访问 lohi 之间所有结点。

    3.2.21

    解答

    要注意保持每个键的出现概率相等,可以先随机一个排名,然后从树中将对应排名的键取出来。

    private static readonly Random Random = new Random();
    
    public TKey RandomKey()
    {
        var rank = Random.Next(1, Size() + 1);
        return GetKeyWithRank(root, rank);
    }
    
    private TKey GetKeyWithRank(Node x, int rank)
    {
        var left = (x.Left == null ? 0 : x.Left.Size) + 1;
        if (left > rank)
        {
            return GetKeyWithRank(x.Left, rank);
        }
        else if (left == rank)
        {
            return x.Key;
        }
        else
        {
            return GetKeyWithRank(x.Right, rank - left);
        }
    }
    

    可以观察到每个键的出现概率都是差不多的。

    另请参阅

    BinarySearchTree 库

    3.2.22

    解答

    这里的前驱和后继指的是二叉树中序遍历序列里结点的前驱和后继。

    由于二叉搜索树的性质,它的中序遍历序列是递增有序的。
    因此一个结点如果有左子树,要么前驱就是左子树中最大的结点(最右侧);
    同理结点如果有右子树,要么后继就是右子树中最小的结点(最左侧)。

    于是结点的前驱不会有右子节点,后继不会有左子节点,得证。

    3.2.23

    解答

    不满足,反例如下:

        |-------10
    |---5---|
    3     |-8-|
          6   9
    
    Delete 5 then delete 3
    |-------10
    6---|
        8-|
          9
    
    Delete 3 then delete 5
      |---10
    |-8-|
    6   9
    

    这里先删除 3 会使 5 的左子树为空,导致删除 5 的时候采取的策略被改变(尽管 5 的右子树没有任何变化)。

    另请参阅

    Deletion procedure for a Binary Search Tree-Stackoverflow

    3.2.24

    解答

    根据命题 D (英文版 P404,中文版 P255),一次插入所需的比较次数平均为 (1.39lg N)
    (我们这里要求和,因此可以直接使用平均值进行计算)
    于是构造一棵二叉查找树所需的比较次数为:

    [1.39C= 1.39sum_{i=0}^N lg i=1.39 imes(lg 1 + lg2+cdots+lg N) ]

    根据对数恒等式,有:

    [C=lg 1 + lg2+cdots+lg N=lg(1 imes2 imes3 imescdots imes N)=lg(N!) ]

    于是有 (1.39C=1.39lg(N!) > lg(N!)) ,得证。

    3.2.25

    解答

    官方实现:https://algs4.cs.princeton.edu/32bst/PerfectBalance.java.html

    先排序,然后视其为中序序列,每次取中间的键作为根结点,左半数组是左子树,右半数组是右子树,递归构造。

    private Node BuildTree(KeyValuePair<TKey, TValue>[] init, int lo, int hi)// init is sorted
    {
        if (lo > hi)
        {
            return null;
        }
    
        var mid = (hi - lo) / 2 + lo;
        var current = new Node(init[mid].Key, init[mid].Value, 1);
        current.Left = BuildTree(init, lo, mid - 1);
        current.Right = BuildTree(init, mid + 1, hi);
        if (current.Left != null)
        {
            current.Size += current.Left.Size;
        }
    
        if (current.Right != null)
        {
            current.Size += current.Right.Size;
        }
        return current;
    }
    
    另请参阅

    BinarySearchTree 库

    3.2.26

    解答

    在 3.2.9 的代码基础上进行修改,统计每种形状的出现次数,以此获得准确的概率。

    概率如下,基本呈现正态分布。

    原始数据:

    n=2
    50%
    50%
    
    n=3
    16.666666%
    16.666666%
    33.333332%
    16.666666%
    16.666666%
    
    n=4
    8.333333%
    16.666666%
    37.5%
    12.5%
    8.333333%
    16.666666%
    
    n=5
    0.8333333%
    0.8333333%
    1.6666666%
    0.8333333%
    0.8333333%
    2.5%
    2.5%
    2.5%
    2.5%
    0.8333333%
    0.8333333%
    1.6666666%
    0.8333333%
    0.8333333%
    3.3333333%
    3.3333333%
    6.6666665%
    3.3333333%
    3.3333333%
    5%
    5%
    5%
    5%
    3.3333333%
    3.3333333%
    6.6666665%
    3.3333333%
    3.3333333%
    0.8333333%
    0.8333333%
    1.6666666%
    0.8333333%
    0.8333333%
    2.5%
    2.5%
    2.5%
    2.5%
    0.8333333%
    0.8333333%
    1.6666666%
    0.8333333%
    0.8333333%
    
    n=6
    0.2777778%
    0.5555556%
    1.25%
    0.41666666%
    0.2777778%
    0.5555556%
    1.1111112%
    2.2222223%
    1.9444444%
    0.8333333%
    1.9444444%
    0.8333333%
    1.1111112%
    0.2777778%
    0.5555556%
    1.25%
    0.41666666%
    0.2777778%
    0.5555556%
    1.3888888%
    2.7777777%
    6.25%
    2.0833333%
    1.3888888%
    2.7777777%
    4.1666665%
    2.7777777%
    4.1666665%
    4.1666665%
    2.7777777%
    4.1666665%
    1.3888888%
    1.3888888%
    2.7777777%
    2.7777777%
    1.3888888%
    1.3888888%
    1.3888888%
    2.7777777%
    6.25%
    2.0833333%
    1.3888888%
    2.7777777%
    0.2777778%
    0.5555556%
    1.25%
    0.41666666%
    0.2777778%
    0.5555556%
    1.1111112%
    2.2222223%
    1.9444444%
    0.8333333%
    1.9444444%
    0.8333333%
    1.1111112%
    0.2777778%
    0.5555556%
    1.25%
    0.41666666%
    0.2777778%
    0.5555556%
    

    3.2.27

    解答

    二叉查找树的内存开销=对象开销+根结点引用+N个结点
    =对象开销+根结点引用+N×(对象开销+父类型引用+左/右子树引用+键/值引用+结点数)
    =16+8+N×(16+8+16+16+4+4)=24+64N 字节

    BinarySearchST:对象开销+键/值数组引用+键/值数组+计数器(一个 int)。
    =16+16+(16+4+4+8N)×2+4+4=88+16N 字节。

    SequentialSearchST:对象开销+头结点引用+N个结点+计数器
    =对象开销+头结点引用+N×(对象开销+父类型引用+next引用+键/值引用)+计数器
    =16+8+N×(16+8+8+16)+4+4=32+48N 字节

    示意图如下:

    其中,对象开销 16 字节,其他均为引用,各占 8 字节。

    《双城记》中不重复的单词有 26436 个(不包括最后的版权声明),全部是原文的子字符串,每个占 40 字节。
    一个 Integer 占 24 字节,于是估计的内存消耗为:24+(64+40+24)×26436=3383832 字节。

    3.2.28

    解答

    修改一下 PutGet 方法,在实际操作之前先检查缓存是否符合要求,然后在操作之后更新缓存。

    代码
    private Node _cache;
    
    public override TValue Get(TKey key)
    {
        if (_cache != null && _cache.Key.CompareTo(key) == 0)
        {
            return _cache.Value;
        }
    
        return Get(root, key).Value;
    }
    
    protected override Node Get(Node x, TKey key)
    {
        if (key == null)
        {
            throw new ArgumentNullException("calls get() with a null key");
        }
    
        if (x == null)
        {
            return null;
        }
        var cmp = key.CompareTo(x.Key);
        if (cmp < 0)
        {
            return Get(x.Left, key);
        }
        else if (cmp > 0)
        {
            return Get(x.Right, key);
        }
        else
        {
            _cache = x;
            return x;
        }
    }
    
    public override void Put(TKey key, TValue value)
    {
        if (key == null)
        {
            throw new ArgumentNullException("calls Put() with a null key");
        }
    
        if (value == null)
        {
            Delete(key);
            return;
        }
    
        if (_cache != null && _cache.Key.CompareTo(key) == 0)
        {
            _cache.Value = value;
            return;
        }
        root = Put(root, key, value);
    }
    
    protected override Node Put(Node x, TKey key, TValue value)
    {
        if (x == null)
        {
            _cache = new Node(key, value, 1);
            return _cache;
        }
        var cmp = key.CompareTo(x.Key);
        if (cmp < 0)
            x.Left = Put(x.Left, key, value);
        else if (cmp > 0)
            x.Right = Put(x.Right, key, value);
        else
            x.Value = value;
        x.Size = 1 + Size(x.Left) + Size(x.Right);
        return x;
    }
    
    另请参阅

    BinarySearchTree 库

    3.2.29

    解答

    本题在原书后续印刷中已修改,这里仍采用中文版的题目。

    部分解答:https://algs4.cs.princeton.edu/32bst/BST.java.html(isSizeConsistent()

    如果根结点记录的结点数=左子树的结点数+右子树的结点数+1,就符合要求。
    按照这个题意编制递归方法即可。

    先写边界,当输入为 null 时,显然符合要求。
    然后计算左子树的 Size 和右子树的 Size 加起来是否等于根结点的 Size + 1,
    以及左子树和右子树是否符合同样的条件。

    代码
    protected static bool IsBinaryTree(Node x)
    {
        if (x == null)
        {
            return true;    // 空树显然符合二叉树条件。
        }
    
        var size = 1;       // 包括当前结点本身。
        if (x.Left != null)
        {
            size += x.Left.Size;
        }
    
        if (x.Right != null)
        {
            size += x.Right.Size;
        }
    
        return  IsBinaryTree(x.Left) && 
                IsBinaryTree(x.Right) && 
                x.Size == size;
    }
    
    另请参阅

    BinarySearchTree 库

    3.2.30

    解答

    本题在原书后续印刷中已修改,这里仍然采用中文版的题目。

    与上一题非常类似,条件有:
    根结点必须在 minmax 范围内,
    且左右子树要么不存在,要么小于/大于根结点的键,
    左右子树同样满足上述条件。

    代码
    protected static bool IsOrdered(Node x, TKey min, TKey max)
    {
        if (x == null)
        {
            return true;        // 空树显然是满足要求的。
        }
    
        return IsOrdered(x.Left, min, max) &&
               IsOrdered(x.Right, min, max) &&                        // 左右子树都满足要求。
               x.Key.CompareTo(max) <= 0 &&
               x.Key.CompareTo(min) >= 0 &&                           // 当前结点位于范围内。
               (x.Left == null || x.Left.Key.CompareTo(x.Key) < 0) &&
               (x.Right == null || x.Right.Key.CompareTo(x.Key) > 0); // 当前结点与子结点满足大小关系。
    }
    
    另请参阅

    BinarySearchTree 库

    3.2.31

    解答

    本题在原书后续印刷中已删除,这里仍然采用中文版的题目。

    注意这个题并没有递归的要求,直接广度优先搜索即可,随时记录和检查已找到的键。

    代码
    protected static bool HasNoDuplicates(Node x)
    {
        var keys = new List<TKey>();    // 也可以用 HashSet 之类的数据结构提高效率。
        var queue = new Queue<Node>();
        queue.Enqueue(x);
        while (queue.Count > 0)
        {
            var node = queue.Dequeue();
            if (node == null)
            {
                continue;
            }
    
            if (keys.Contains(node.Key))
            {
                return false;
            }
            keys.Add(node.Key);
            queue.Enqueue(node.Left);
            queue.Enqueue(node.Right);
        }
    
        return true;
    }
    
    另请参阅

    BinarySearchTree 库

    3.2.32

    解答

    本题在原书后续印刷中已修改,这里仍然采用中文版的题目。

    官网解答见:https://algs4.cs.princeton.edu/32bst/BST.java.htmlisBST()

    书中已经给出了答案,当然在 Java 和 C# 里,&& 总是从左向右计算的,而且遇到 false 会直接返回结果。

    如果数据结构中存在环,IsOrdered 有可能会陷入无限递归的情况,因此调用顺序比较重要。

    代码
    public static bool IsBST(BST<TKey, TValue> bst)
    {
        return IsBinaryTree(bst) &&
               IsOrdered(bst) &&
               HasNoDuplicates(bst);
    }
    
    另请参阅

    Boolean logical operators (C# reference)

    Equality, Relational, and Conditional Operators

    BinarySearchTree 库

    3.2.33

    解答

    官网解答见:https://algs4.cs.princeton.edu/32bst/BST.java.htmlisRankConsistent()

    按照题目要求实现即可,分为两步进行测试。

    代码
    public static bool IsRankConsistent(BST<TKey, TValue> bst)
    {
        for (var i = 0; i < bst.Size(); i++)
        {
            if (i != bst.Rank(bst.Select(i)))
            {
                return false;
            }
        }
    
        foreach (var key in bst.Keys())
        {
            if (key.CompareTo(bst.Select(bst.Rank(key))) != 0)
            {
                return false;
            }
        }
    
        return true;
    }
    
    另请参阅

    BinarySearchTree 库

    3.2.34

    解答

    其实就是将所有的结点按照中序序列排成了一个双向链表,对树进行修改时要同时更新这个双向链表。

    当插入新结点时,插到左侧的结点会变为父结点的新前驱,同理右侧会变为新后继。
    注意要更新父结点原来的前驱和后继结点(如果有)。

    删除结点时较为简单,只要更新需要删除结点的前驱和后继结点即可。

    原本操作 leftright 的代码不需要更改,只需要加上对 prevnext 做操作的代码即可。

    Prev 方法实现如下(Next 类似),修改了内部的 Get 方法使之返回 Node 而非 TValue

    public TKey Prev(TKey key)
    {
        var node = Get(root, key);
        if (node == null || node.Prev == null)
            return null;
        return node.Prev.Key;
    }
    
    代码

    处理结点关系的几个方法。

    private void DeleteNode(Node x)
    {
        if (x.Prev != null)
            x.Prev.Next = x.Next;
        if (x.Next != null)
            x.Next.Prev = x.Prev;
    }
    private void InsertRight(Node parent, Node newNode)
    {
        parent.Right = newNode;
        InsertBetween(parent, newNode, parent.Next);
    }
    private void InsertLeft(Node parent, Node newNode)
    {
        parent.Left = newNode;
        InsertBetween(parent.Prev, newNode, parent);
    }
    private void InsertBetween(Node prev, Node newNode, Node next)
    {
        newNode.Prev = prev;
        newNode.Next = next;
    
        if (prev != null)
            prev.Next = newNode;
        if (next != null)
            next.Prev = newNode;
    }
    

    Put 方法

    protected virtual Node Put(Node x, TKey key, TValue value)
    {
        if (x == null)
        {
            return new Node(key, value, 1); // 树是空的。
        }
        var cmp = key.CompareTo(x.Key);
        if (cmp < 0)
        {
            if (x.Left == null)
            {
                var newNode = new Node(key, value, 1);
                InsertLeft(x, newNode);
            }
            else
            {
                x.Left = Put(x.Left, key, value);
            }
        }
        else if (cmp > 0)
        {
            if (x.Right == null)
            {
                var newNode = new Node(key, value, 1);
                InsertRight(x, newNode);
            }
            else
            {
                x.Right = Put(x.Right, key, value);
            }
        }
        else
        {
            x.Value = value;
        }
        x.Size = 1 + Size(x.Left) + Size(x.Right);
        return x;
    }
    

    Delete 方法

    protected virtual Node Delete(Node x, TKey key)
    {
        if (x == null)
            return null;
    
        var cmp = key.CompareTo(x.Key);
        if (cmp < 0)
            x.Left = Delete(x.Left, key);
        else if (cmp > 0)
            x.Right = Delete(x.Right, key);
        else
        {
            DeleteNode(x);          // 在中序链表中删除结点。
            if (x.Right == null)
                return x.Left;
            if (x.Left == null)
                return x.Right;
            var t = x;
            x = Min(t.Right);
            x.Right = DeleteMin(t.Right);
            x.Left = t.Left;
        }
        x.Size = Size(x.Left) + Size(x.Right) + 1;
        return x;
    }
    

    DeleteMin 方法,DeleteMax 类似。

    protected virtual Node DeleteMin(Node x)
    {
        if (x.Left == null)
        {
            DeleteNode(x);
            return x.Right;
        }
        x.Left = DeleteMin(x.Left);
        x.Size = Size(x.Left) + Size(x.Right) + 1;
        return x;
    }
    
    另请参阅

    BinarySearchTree 库

    3.2.35

    解答

    根据书中已经给出的归纳关系式(中文版P255/英文版P403):

    [C_N=N-1+(C_0+C_{N-1})/N+(C_1+C_{N-2})/N+cdots+(C_{N-1}+C_0)/N ]

    整理得:

    [C_N=N-1+(C_0+C_1+cdots+C_{N-1})/N+(C_{N-1}+cdots+C_1+C_0)/N ]

    这和快速排序的式子基本一致,只是 $ N+1 $ 变成了 $ N-1 $。

    遵循相同的推导过程,我们可以获得类似的结果,两边同乘以 $ N $:

    [NC_N=N(N-1)+2(C_0+C_1+cdots+C_{N-1}) ]

    用 $ N+1 $ 时的等式减去该式得:

    [(N+1)C_{N+1}-NC_N=2N+2C_N \ (N+1)C_{N+1}=2N+(N+2)C_N \ frac{C_{N+1}}{N+2}=frac{2N}{(N+1)(N+2)} + frac{C_N}{N+1} ]

    令 $ T_N = frac{C_N}{N+1} $,得到:

    [T_{N+1}=frac{2N}{(N+1)(N+2)} + T_N \ T_{N+1}-T_{N} = frac{2N}{(N+1)(N+2)} ]

    归纳得:

    [egin{aligned} T_N &= 2 sum_{i=2}^{N} frac{i-1}{i(i+1)} \ C_N&=2(N+1)sum_{i=2}^{N} frac{i-1}{i(i+1)} \ C_N&=2(N+1)sum_{i=2}^{N}(i-1) (frac{1}{i}-frac{1}{i+1}) \ C_N&=-2(N-1)+ 2(N+1)sum_{i=2}^{N}frac{1}{i}\ C_N&=-2(N-1)+ 2(N+1)(-1+sum_{i=1}^{N}frac{1}{i})\ C_N&=-4N+2(N+1)(ln N+gamma) end{aligned} ]

    于是平均成本为:(1+C_N/N sim 2ln N+2gamma-3)

    3.2.36

    解答

    用一个栈来模拟递归即可,将路径上的结点记录到栈里。
    注意 Queue<TKey> 不算额外空间,因为它在keys执行完毕之后不会被回收。

    代码
    private void Keys(Node x, Queue<TKey> queue, TKey lo, TKey hi)
    {
        var stack = new Stack<Node>();
    
        while (x != null || stack.Count > 0)
        {
            if (x != null)
            {
                var cmpLo = lo.CompareTo(x.Key);
                var cmpHi = hi.CompareTo(x.Key);
                if (cmpHi >= 0)
                    stack.Push(x);
                if (cmpLo < 0)
                    x = x.Left;
                else
                    x = null;
            }
            else
            {
                x = stack.Pop();
                var cmpLo = lo.CompareTo(x.Key);
                var cmpHi = hi.CompareTo(x.Key);
                if (cmpLo <= 0 && cmpHi >= 0)
                    queue.Enqueue(x.Key);
                x = x.Right;
            }
        }
    }
    
    另请参阅

    BinarySearchTree 库

    3.2.37

    解答

    二叉树层序遍历,出队一个结点,打印它,将结点的左右子树入队,循环即可。

    代码
    private void PrintLevel(Node x)
    {
        var queue = new Queue<Node>();
        queue.Enqueue(x);
        while (queue.Count > 0)
        {
            var node = queue.Dequeue();
            if (node.Left != null)
                queue.Enqueue(node.Left);
            if (node.Right != null)
                queue.Enqueue(node.Right);
            Console.Write(node.Key + ", ");
        }
    }
    
    另请参阅

    BinarySearchTree 库

    3.2.38

    解答

    通过层序遍历计算结点的坐标,然后绘制即可。
    先算出最大深度,确定每一层的高度 Y,
    再将每一层的宽度分成 (2^n-1) 份,从左到右依次对结点赋值。

    效果如下:

    代码

    计算坐标的函数。

    public void DrawTree(Graphics pen, RectangleF panel)
    {
        var depth = Depth(root); // 确定最大深度。
        var layerHeight = panel.Height / depth;
        var nowLayer = new Queue<Node>();
        var nextLayer = new Queue<Node>();
        nextLayer.Enqueue(root);
    
        for (var layer = 0; layer != depth; layer++)
        {
            var unitSizeX = (float)(panel.Width / Math.Pow(2, layer));
            var temp = nowLayer;
            nowLayer = nextLayer;
            nextLayer = temp;
    
            var cursorX = 0.0f;
            var cursorY = layer * layerHeight;
            while (nowLayer.Count != 0)
            {
                var node = nowLayer.Dequeue();
    
                if (node != null)
                {
                    nextLayer.Enqueue(node.Left);
                    nextLayer.Enqueue(node.Right);
                }
                else
                {
                    nextLayer.Enqueue(null);
                    nextLayer.Enqueue(null);
                }
    
    
                if (node != null)
                {
                    node.X = cursorX + unitSizeX / 2.0f;
                    node.Y = cursorY;
                }
                
                cursorX += unitSizeX;
            }
        }
    }
    

    3.2.39

    解答

    测试结果:

    可以看到和公式给出的结果十分一致。

    测试时先生成 0~2n 顺序序列,奇数插入二叉树中,偶数用于测试查找失败的情况。

    代码
    static void Main(string[] args)
    {
        var n = 10000;
        var trial = 100;
        for (var i = 0; i < 3; i++)
        {
            var odds = new int[n];
            var evens = new int[n];
            var bst = new BSTAnalysis<int, int>();
            for (var j = 100; j < n; j++)
            {
                evens[j] = j;
                odds[j] = j + 1;
            }
            Shuffle(odds);
            foreach (var item in odds)
            {
                bst.Put(item, item);
            }
    
            Console.WriteLine("n:" + n);
            // hit
            Shuffle(odds);
            Test(bst, odds, trial, "hit");
    
            // miss
            Shuffle(evens);
            Test(bst, evens, trial, "miss");
    
            n *= 10;
        }
    }
    
    static void Test(BSTAnalysis<int, int> bst, int[] testCases, int trials, string label)
    {
        var testRecords = new long[trials];
        for (var j = 0; j < trials; j++)
        {
            bst.CompareTimes = 0;             // reset
            bst.Get(testCases[j]);            // test
            testRecords[j] = bst.CompareTimes; // record
        }
    
        var testAverage = 0d;        // 'd' for double
        foreach (var record in testRecords)
        {
            testAverage += record;
        }
    
        testAverage /= testRecords.Length;
    
        var testStandardDeviation = 0d;
        foreach (var record in testRecords)
        {
            testStandardDeviation += (record - testAverage) * (record - testAverage);
        }
    
        testStandardDeviation /= testRecords.Length;
        testStandardDeviation = Math.Sqrt(testStandardDeviation);
        // 2lnN + 2γ - 3
        var expect = 2 * Math.Log(testCases.Length) + 2 * 0.5772156649 - 3;
        Console.WriteLine(label + ": ActualAverage: " + testAverage + "	ExpectAverage: " + expect + "	StandardDevitation:" + testStandardDeviation);
    }
    
    static void Shuffle<T>(T[] a)
    {
        var random = new Random();
        for (var i = 0; i < a.Length; i++)
        {
            var r = i + random.Next(a.Length - i);
            var temp = a[i];
            a[i] = a[r];
            a[r] = temp;
        }
    }
    
    另请参阅

    BinarySearchTree 库

    3.2.40

    解答

    书中的结论是 1986 年 L. Devroye 给出的,原式为 (H_n ightarrow clog(n))

    其中 (c) 是方程 (clog frac{2e}{c}=1) 的最大解,约为 (4.31107)

    2002 年 Michael Drmota 给出了一个跟精确的公式:(mathrm{E}(H_n)=clog n-frac{3c}{2(c-1)}log log n + O(1))

    测试结果如下,误差基本稳定在一个常数。

    代码
    static void Main(string[] args)
    {
        var n = 10000;
        var trials = 100;
    
        for (var i = 0; i < 3; i++)
        {
            var items = new int[n];
            
            for (var j = 0; j < n; j++)
            {
                items[j] = j;
            }
    
            var aveHeight = 0d;
            for (var j = 0; j < trials; j++)
            {
                var bst = new BST<int, int>();
                Shuffle(items);
                foreach (var item in items)
                {
                    bst.Put(item, item);
                }
    
                aveHeight += bst.Height();
            }
    
            aveHeight /= trials;
            var c = 4.31107d;
            var expectHeightLuc = c * Math.Log(n);
            var expectHeightMichael = c * Math.Log(n) - (3 * c / (2 * (c - 1))) * Math.Log(Math.Log(n));
            Console.WriteLine("n:" + n);
            Console.WriteLine("Actual Height:" + aveHeight);
            Console.WriteLine("Devroye: " + expectHeightLuc + "	Diff: " + (float)(expectHeightLuc - aveHeight));
            Console.WriteLine("Michael: " + expectHeightMichael + "	Diff: " + (float)(expectHeightMichael - aveHeight));
    
            n *= 10;
        }
    }
    
    static void Shuffle<T>(T[] a)
    {
        var random = new Random();
        for (var i = 0; i < a.Length; i++)
        {
            var r = i + random.Next(a.Length - i);
            var temp = a[i];
            a[i] = a[r];
            a[r] = temp;
        }
    }
    
    另请参阅

    A note on the height of binary search tree

    Note The Variance of the height of binary search trees

    BinarySearchTree 库

    3.2.41

    解答

    大体上和标准 BST 实现差不多,做如下变换即可:

    x.Key => _nodes[x].Key;
    x.Value => _nodes[x].Value;
    x.Left => _left[x];
    x.Right => _right[x];
    

    由于使用了数组,在正常「删除」二叉树结点之后,还需要手工「垃圾回收」,如下图所示:

    性能比较:

    可见数组实现在删除节点时有巨大的性能差距。

    代码
    private readonly Node[] _nodes;
    private readonly int[] _left;
    private readonly int[] _right;
    private int _size;
    private int _root;
    
    /// <summary>
    /// 二叉搜索树的结点。
    /// </summary>
    private class Node
    {
        public TKey Key { get; set; }
        public TValue Value { get; set; }
    }
    
    /// <summary>
    /// 建立一个以数组为基础的二叉搜索树。
    /// </summary>
    /// <param name="maxSize">二叉搜索树中的结点数。</param>
    public BSTArray(int maxSize)
    {
        _nodes = new Node[maxSize];
        _left = new int[maxSize];
        _right = new int[maxSize];
        for (var i = 0; i < maxSize; i++)
        {
            _left[i] = -1;
            _right[i] = -1;
        }
        _size = 0;
        _root = 0;
    }
    
    /// <summary>
    /// 向符号表插入键值对。
    /// </summary>
    /// <param name="key">键。</param>
    /// <param name="value">值。</param>
    public void Put(TKey key, TValue value)
    {
        if (_size == _nodes.Length)
        {
            throw new InvalidOperationException("BST is full");
        }
    
        if (IsEmpty())
        {
            _nodes[_size] = new Node{Key = key, Value = value};
            _size++;
            return;
        }
    
        Put(key, value, null, _root);
    }
    
    /// <summary>
    /// 向二叉树插入键值对。
    /// </summary>
    /// <param name="key">键。</param>
    /// <param name="value">值。</param>
    /// <param name="treeSide">子树数组。</param>
    /// <param name="parent">父结点下标。</param>
    private void Put(TKey key, TValue value, int[] treeSide, int parent)
    {
        int now;
        if (treeSide == null)               // init
        {
            now = parent;
        }
        else if (treeSide[parent] == -1)    // finish
        {
            _nodes[_size] = new Node { Key = key, Value = value };
            treeSide[parent] = _size;
            _size++;
            return;
        }
        else
        {
            now = treeSide[parent];
        }
    
        var cmp = _nodes[now].Key.CompareTo(key);
        if (cmp > 0)
        {
            Put(key, value, _left, now);
        }
        else if (cmp < 0)
        {
            Put(key, value, _right, now);
        }
        else
        {
            _nodes[now].Value = value;
        }
    }
    
    /// <summary>
    /// 获取键 <paramref name="key"/> 对应的值,不存在则返回 <c>default(Value)</c>。
    /// </summary>
    /// <param name="key">键。</param>
    /// <returns>键 <paramref name="key"/> 对应的值,不存在则返回 <c>default(Value)</c>。</returns>
    public TValue Get(TKey key)
    {
        var indices = Get(key, _root);
        if (indices == -1)
        {
            return default;
        }
    
        return _nodes[indices].Value;
    }
    
    /// <summary>
    /// 获取 <paramref name="key"/> 对应的下标,不存在则返回 -1。
    /// </summary>
    /// <param name="key">键。</param>
    /// <param name="start">起始搜索下标。</param>
    /// <returns>找到则返回对应下标,否则返回 -1。</returns>
    private int Get(TKey key, int start)
    {
        var now = start;
        while (now != -1)
        {
            var cmp = _nodes[now].Key.CompareTo(key);
            if (cmp > 0)
            {
                now = _left[now];
            }
            else if (cmp < 0)
            {
                now = _right[now];
            }
            else
            {
                return now;
            }
        }
    
        return -1;
    }
    
    /// <summary>
    /// 从表中删去键 <paramref name="key"/> 及其对应的值。
    /// </summary>
    /// <param name="key">要删除的键。</param>
    public void Delete(TKey key)
    {
        var toDelete = Get(key, _root);
        if (toDelete == -1)
        {
            throw new InvalidOperationException("No Such Key in BST");
        }
    
        _root = Delete(key, _root);
        RemoveNode(toDelete);
    }
    
    /// <summary>
    /// 从根结点为 <paramref name="root"/> 的二叉搜索树中删除键为 <paramref name="key"/> 的结点。
    /// 返回删除结点后树的根结点下标。
    /// </summary>
    /// <param name="key">要删除的键。</param>
    /// <param name="root">根结点。</param>
    /// <returns>删除结点后树的根结点下标。</returns>
    private int Delete(TKey key, int root)
    {
        if (root == -1 || _nodes[root] == null)
        {
            return -1;
        }
    
        var cmp = _nodes[root].Key.CompareTo(key);
        if (cmp > 0)
        {
            _left[root] = Delete(key, _left[root]);
        }
        else if (cmp < 0)
        {
            _right[root] = Delete(key, _right[root]);
        }
        else
        {
            if (_left[root] == -1)
            {
                return _right[root];
            }
    
            if (_right[root] == -1)
            {
                return _left[root];
            }
    
            var toReplace = Min(_right[root]);
            _right[toReplace] = DeleteMin(_right[root]);
            _left[toReplace] = _left[root];
            root = toReplace;
        }
    
        return root;
    }
    
    /// <summary>
    /// 键 <paramref name="key"/> 在表中是否存在对应的值。
    /// </summary>
    /// <param name="key">键。</param>
    /// <returns>如果存在则返回 <c>true</c>,否则返回 <c>false</c>。</returns>
    public bool Contains(TKey key)
    {
        return Get(key, _root) > -1;
    }
    
    /// <summary>
    /// 符号表是否为空。
    /// </summary>
    /// <returns>为空则返回 <c>true</c>,否则返回 <c>false</c>。</returns>
    public bool IsEmpty()
    {
        return _size == 0;
    }
    
    /// <summary>
    /// 获得符号表中键值对的数量。
    /// </summary>
    /// <returns>符号表中键值对的数量。</returns>
    public int Size()
    {
        return _size;
    }
    
    /// <summary>
    /// [<paramref name="lo"/>, <paramref name="hi"/>] 之间键的数量。
    /// </summary>
    /// <param name="lo">范围起点。</param>
    /// <param name="hi">范围终点。</param>
    /// <returns>[<paramref name="lo"/>, <paramref name="hi"/>] 之间键的数量。</returns>
    public int Size(TKey lo, TKey hi)
    {
        return Keys(lo, hi).Count();
    }
    
    /// <summary>
    /// 计算以 <paramref name="root"/> 为根结点的二叉树的大小。
    /// </summary>
    /// <param name="root">二叉树的根结点下标。</param>
    /// <returns>二叉树中的结点个数。</returns>
    private int Size(int root)
    {
        if (root == -1)
        {
            return 0;
        }
    
        return 1 + Size(_left[root]) + Size(_right[root]);
    }
    
    /// <summary>
    /// 获得符号表中所有键的集合。
    /// </summary>
    /// <returns>符号表中所有键的集合。</returns>
    public IEnumerable<TKey> Keys()
    {
        if (IsEmpty())
        {
            return new List<TKey>();
        }
        return Keys(Min(), Max());
    }
    
    /// <summary>
    /// 获得符号表中 [<paramref name="lo"/>, <paramref name="hi"/>] 之间的键。
    /// </summary>
    /// <param name="lo">范围起点。</param>
    /// <param name="hi">范围终点。</param>
    /// <returns>符号表中 [<paramref name="lo"/>, <paramref name="hi"/>] 之间的键。</returns>
    public IEnumerable<TKey> Keys(TKey lo, TKey hi)
    {
        if (lo == null)
            throw new ArgumentNullException("first argument to keys() is null");
        if (hi == null)
            throw new ArgumentNullException("second argument to keys() is null");
    
        var queue = new Queue<TKey>();
        Keys(_root, queue, lo, hi);
        return queue;
    }
    
    /// <summary>
    /// 获取二叉查找树中在 <paramref name="lo"/> 和 <paramref name="hi"/> 之间的所有键。
    /// </summary>
    /// <param name="x">二叉查找树的根结点。</param>
    /// <param name="queue">要填充的队列。</param>
    /// <param name="lo">键的下限。</param>
    /// <param name="hi">键的上限。</param>
    private void Keys(int x, Queue<TKey> queue, TKey lo, TKey hi)
    {
        if (x == -1)
        {
            return;
        }
    
        var cmplo = lo.CompareTo(_nodes[x].Key);
        var cmphi = hi.CompareTo(_nodes[x].Key);
        if (cmplo < 0)
        {
            Keys(_left[x], queue, lo, hi);
        }
    
        if (cmplo <= 0 && cmphi >= 0)
        {
            queue.Enqueue(_nodes[x].Key);
        }
    
        if (cmphi > 0)
        {
            Keys(_right[x], queue, lo, hi);
        }
    }
    
    /// <summary>
    /// 最小的键。
    /// </summary>
    /// <returns>最小的键。</returns>
    public TKey Min()
    {
        if (IsEmpty())
        {
            throw new InvalidOperationException("BST is Empty!");
        }
    
        return _nodes[Min(_root)].Key;
    }
    
    /// <summary>
    /// 在二叉查找树中查找包含最小键的结点。
    /// </summary>
    /// <param name="x">二叉查找树的根结点。</param>
    /// <returns>包含最小键的结点。</returns>
    private int Min(int x)
    {
        if (_left[x] == -1)
        {
            return x;
        }
    
        return Min(_left[x]);
    }
    
    /// <summary>
    /// 最大的键。
    /// </summary>
    /// <returns>最大的键。</returns>
    public TKey Max()
    {
        if (IsEmpty())
        {
            throw new InvalidOperationException("BST is Empty!");
        }
    
        return _nodes[Max(_root)].Key;
    }
    
    /// <summary>
    /// 在二叉查找树中查找包含最大键的结点。
    /// </summary>
    /// <param name="x">二叉查找树的根结点。</param>
    /// <returns>包含最大键的结点。</returns>
    private int Max(int x)
    {
        if (_right[x] == -1)
        {
            return x;
        }
    
        return Max(_right[x]);
    }
    
    /// <summary>
    /// 小于等于 <paramref name="key"/> 的最大值。
    /// </summary>
    /// <returns>小于等于 <paramref name="key"/> 的最大值。</returns>
    public TKey Floor(TKey key)
    {
        if (key == null)
        {
            throw new ArgumentNullException("argument to floor is null");
        }
    
        if (IsEmpty())
        {
            throw new InvalidOperationException("calls floor with empty symbol table");
        }
        var x = Floor(_root, key);
        if (x == -1)
        {
            return default;
        }
        else
        {
            return _nodes[x].Key;
        }
    }
    
    /// <summary>
    /// 获得符号表中小于等于 <paramref name="key"/> 的最大结点。
    /// </summary>
    /// <param name="x">二叉查找树的根结点。</param>
    /// <param name="key">键。</param>
    /// <returns>小于等于 <paramref name="key"/> 的最大结点。</returns>
    private int Floor(int x, TKey key)
    {
        if (x == -1)
        {
            return -1;
        }
        var cmp = key.CompareTo(_nodes[x].Key);
        if (cmp == 0)
        {
            return x;
        }
        else if (cmp < 0)
        {
            return Floor(_left[x], key);
        }
        var t = Floor(_right[x], key);
        if (t != -1)
        {
            return t;
        }
    
        return x;
    }
    
    /// <summary>
    /// 大于等于 <paramref name="key"/> 的最小值。
    /// </summary>
    /// <returns>大于等于 <paramref name="key"/> 的最小值。</returns>
    public TKey Ceiling(TKey key)
    {
        if (key == null)
        {
            throw new ArgumentNullException("argument to ceiling is null");
        }
    
        if (IsEmpty())
        {
            throw new InvalidOperationException("calls ceiling with empty symbol table");
        }
        var x = Ceiling(_root, key);
        if (x == -1)
        {
            return default;
        }
        return _nodes[x].Key;
    }
    
    /// <summary>
    /// 获取符号表中大于等于 <paramref name="key"/> 的最小结点。
    /// </summary>
    /// <param name="x">二叉查找树的根结点。</param>
    /// <param name="key">键。</param>
    /// <returns>符号表中大于等于 <paramref name="key"/> 的最小结点。</returns>
    private int Ceiling(int x, TKey key)
    {
        if (x == -1)
        {
            return -1;
        }
        var cmp = key.CompareTo(_nodes[x].Key);
        if (cmp == 0)
        {
            return x;
        }
        if (cmp < 0)
        {
            var t = Ceiling(_left[x], key);
            if (t != -1)
            {
                return t;
            }
            return x;
        }
        return Ceiling(_right[x], key);
    }
    
    /// <summary>
    /// 小于 <paramref name="key"/> 的键的数量。
    /// </summary>
    /// <returns>小于 <paramref name="key"/> 的键的数量。</returns>
    public int Rank(TKey key)
    {
        if (key == null)
        {
            throw new ArgumentNullException("argument to rank() is null");
        }
        return Rank(_root, key);
    }
    
    /// <summary>
    /// 返回 <paramref name="key"/> 在二叉查找树中的排名。
    /// </summary>
    /// <param name="x">二叉查找树的根结点。</param>
    /// <param name="key">要查找排名的键。</param>
    /// <returns><paramref name="key"/> 的排名。</returns>
    private int Rank(int x, TKey key)
    {
        if (x == -1)
        {
            return 0;
        }
        var cmp = key.CompareTo(_nodes[x].Key);
        if (cmp < 0)
        {
            return Rank(_left[x], key);
        }
        else if (cmp > 0)
        {
            return 1 + Size(_left[x]) + Rank(_right[x], key);
        }
        else
        {
            return Size(_left[x]);
        }
    }
    
    /// <summary>
    /// 获得排名为 k 的键。
    /// </summary>
    /// <param name="k">需要获得的键的排名。</param>
    /// <returns>排名为 k 的键。</returns>
    public TKey Select(int k)
    {
        if (k < 0 || k >= Size())
        {
            throw new ArgumentException("argument to select() is invaild: " + k);
        }
        var x = Select(_root, k);
        return _nodes[x].Key;
    }
    
    /// <summary>
    /// 挑拣出排名为 <paramref name="k"/> 的结点。
    /// </summary>
    /// <param name="x">树的根结点。</param>
    /// <param name="k">要挑拣的排名。</param>
    /// <returns>排名为 <paramref name="k"/> 的结点。</returns>
    private int Select(int x, int k)
    {
        if (x == -1)
        {
            return -1;
        }
        var t = Size(_left[x]);
        if (t > k)
        {
            return Select(_left[x], k);
        }
        else if (t < k)
        {
            return Select(_right[x], k - t - 1);
        }
        else
        {
            return x;
        }
    }
    
    /// <summary>
    /// 删除最小的键。
    /// </summary>
    public void DeleteMin()
    {
        if (IsEmpty())
        {
            throw new InvalidOperationException("Symbol table underflow");
        }
    
        var minIndex = Min(_root);
        _root = DeleteMin(_root);
        RemoveNode(minIndex);
    }
    
    /// <summary>
    /// 在以 <paramref name="x"/> 为根结点的二叉查找树中删除最小结点。
    /// </summary>
    /// <param name="x">二叉查找树的根结点。</param>
    /// <returns>删除后的二叉查找树。</returns>
    private int DeleteMin(int x)
    {
        if (_left[x] == -1)
        {
            return _right[x];
        }
    
        _left[x] = DeleteMin(_left[x]);
        return x;
    }
    
    /// <summary>
    /// 删除最大的键。
    /// </summary>
    public void DeleteMax()
    {
        if (IsEmpty())
        {
            throw new InvalidOperationException("Symbol table underflow");
        }
    
        var maxIndex = Max(_root);
        _root = DeleteMax(_root);
        RemoveNode(maxIndex);
    }
    
    /// <summary>
    /// 从指定二叉查找树中删除最大结点。
    /// </summary>
    /// <param name="x">二叉查找树的根结点。</param>
    /// <returns>删除后的二叉查找树。</returns>
    private int DeleteMax(int x)
    {
        if (_right[x] == -1)
            return _left[x];
        _right[x] = DeleteMax(_right[x]);
        
        return x;
    }
    
    /// <summary>
    /// 删除下标为 <paramref name="index"/> 的结点。
    /// </summary>
    /// <param name="index">要删除的结点下标。</param>
    private void RemoveNode(int index)
    {
        _size--;
        // Remove Node
        for (var i = index; i < _size; i++)
        {
            _nodes[i] = _nodes[i + 1];
            _left[i] = _left[i + 1];
            _right[i] = _right[i + 1];
        }
        // Adjust Index
        if (_root >= index)
        {
            _root--;
        }
        for (var i = 0; i < _size; i++)
        {
            if (_left[i] >= index)
            {
                _left[i]--;
            }
    
            if (_right[i] >= index)
            {
                _right[i]--;
            }
        }
    }
    
    另请参阅

    BinarySearchTree 库

    3.2.42

    解答

    按照题意实现即可,关键点有两个:

    一是选择前驱的实现方式,只要选择左子树中的最大结点即可。

    if (_random.NextDouble() < 0.5)
    {
        x = Min(t.Right);
        x.Right = DeleteMin(t.Right);
        x.Left = t.Left;
    }
    else
    {
        x = Max(t.Left);
        x.Left = DeleteMax(t.Left);
        x.Right = t.Right;
    }
    

    二是内部路径长度的计算方式,需要用层序遍历把所有结点的深度加起来。

    var internalPath = 0;
    var nowLayer = new Queue<Node>();
    var nextLayer = new Queue<Node>();
    nextLayer.Enqueue(root);
    
    var depth = 0;
    while (nextLayer.Count > 0)
    {
        var temp = nowLayer;
        nowLayer = nextLayer;
        nextLayer = temp;
    
        while (nowLayer.Count > 0)
        {
            var node = nowLayer.Dequeue();
            if (node.Left != null)
            {
                nextLayer.Enqueue(node.Left);
            }
    
            if (node.Right != null)
            {
                nextLayer.Enqueue(node.Right);
            }
    
            internalPath += depth;
        }
    
        depth++;
    }
    
    return internalPath;
    

    结果如下:

    另请参阅

    BinarySearchTree 库

    3.2.43

    解答

    依照题意实现即可,put/get 大约 10 倍差距。

    MostFrequentlyKey 的实现:

    public static TKey MostFrequentlyKey<TKey>(IST<TKey, int> st, TKey[] keys)
    {
        foreach (var s in keys)
        {
            if (st.Contains(s))
                st.Put(s, st.Get(s) + 1);
            else
                st.Put(s, 1);
        }
    
        var max = keys[0];
        foreach (var s in st.Keys())
            if (st.Get(s) > st.Get(max))
                max = s;
    
        return max;
    }
    
    另请参阅

    BinarySearchTree 库

    3.2.44

    解答

    使用类似于 3.1.38 的方法进行绘图,当 n=10000 时的结果如下:

    代码

    绘图部分:

    public void Draw(long[] data)
    {
        var panel = CreateGraphics();
        var unitX = (float)ClientRectangle.Width / data.Length;
        var unitY = (float)ClientRectangle.Height / data.Max();
    
        var accumulation = 0f;  // f = float
        for (var i = 0; i < data.Length; i++)
        {
            // Gray
            panel.FillEllipse(Brushes.Gray, (i + 1) * unitX, ClientRectangle.Bottom - data[i] * unitY, 2, 2);
            // Red
            panel.FillEllipse(Brushes.Red, (i + 1) * unitX, ClientRectangle.Bottom - accumulation / (i + 1) * unitY, 2, 2);
            accumulation += data[i];
        }
    
        panel.DrawString($"n:{data.Length}
    ave:{accumulation / data.Length}", SystemFonts.DefaultFont, Brushes.Red, 0, 0);
    }
    

    测试部分:

    private long[] Test(int n)
    {
        var testCases = new long[n];
        var testResult = new long[n];
        for (var i = 0; i < n; i++)
        {
            testCases[i] = i;
        }
        Shuffle(testCases);
    
        var bst = new BSTAnalysis<long, int>();
        for (var i = 0; i < n; i++)
        {
            bst.CompareTimes = 0;
            bst.Put(testCases[i], 1);
            testResult[i] = bst.CompareTimes;
        }
    
        return testResult;
    }
    
    static void Shuffle<T>(T[] a)
    {
        var random = new Random();
        for (var i = 0; i < a.Length; i++)
        {
            var r = i + random.Next(a.Length - i);
            var temp = a[i];
            a[i] = a[r];
            a[r] = temp;
        }
    }
    
    另请参阅

    BinarySearchTree 库

    3.2.45

    解答

    结果如下,可参考 3.1.39。

    SequentialSearchST

    BinarySearchST

    BST

    可以看到 BST 的曲线更为平滑,插入和查找部分耗时十分接近。

    另请参阅

    BinarySearchTree 库

    3.2.46

    解答

    翻译有些问题,其实指的是用 Ndouble 构造一个 BSTBinarySearchST 的速度对比。
    Get 速度 BST 是不会比 BinarySearchST 快的。((1.39lg N) > (lg N)

    二叉搜索树一次查找平均需要 (1.39lg N) 次比较,二分查找则是 (N/2),于是可以求得开销:
    二叉查找树:(1.39 sum_{i=1}^{N-1} lg i=1.39 lg (N-1)!=1.39(N-1)lg(N-1))
    二分查找实现的符号表:(1/2+2/2+ cdots+(N-1)/2=N(N-1)/4)
    令两式相等,可以求得快 10 倍,100 倍,1000 倍的 (N) 值。
    例如快 10 倍的方程:

    [13.9(N-1)lg(N-1)=N(N-1)/4 \ 13.9lg (N-1)=N/4 ]

    这是一个超越方程,可以简单用程序穷举出一个数值解。

    for (var i = 0d; i < int.MaxValue; i++)
    {
        if (13.9 * Math.Log2(i - 1) < i / 4)
        {
            Console.WriteLine(i);
            return;
        }
    }
    

    解得的三个 N 值分别为 499,7115,91651。

    除了基本的新元素赋值外,二叉树在插入时只需要进行比较即可。
    但二分查找实现的符号表还需要维持数组有序,需要额外的赋值操作。
    因此二分查找实现的符号表和二叉搜索树的开销如下:

    其中 Array 包含了比较和额外的赋值次数,Tree 只有比较次数。

    一般我们认为比较(Compare)和赋值(=)开销是一样的,但实际上比较会慢 6 倍左右(.net core 3.1),因此如果直接进行计时测试,可能得不出快 10 倍/100 倍/1000 倍的结果。

    另请参阅

    BinarySearchTree 库

    3.2.47

    解答

    如下图所示,内部路径平均长度是比较符合规律的:

    方差:

    代码

    一次测试:

    private int Test(int n)
    {
        var data = GetRandomInt(n);
        var bst = new BST<int, int>();
        foreach (var d in data)
        {
            bst.Put(d, d);
        }
    
        return bst.AverageInternalPathLength();
    }
    

    求解内部路径长度:

    public int AverageInternalPathLength() => InternalPath() / Size() + 1;
    
    private int InternalPath()
    {
        var internalPath = 0;
        var nowLayer = new Queue<Node>();
        var nextLayer = new Queue<Node>();
        nextLayer.Enqueue(root);
    
        var depth = 0;
        while (nextLayer.Count > 0)
        {
            var temp = nowLayer;
            nowLayer = nextLayer;
            nextLayer = temp;
    
            while (nowLayer.Count > 0)
            {
                var node = nowLayer.Dequeue();
                if (node.Left != null)
                {
                    nextLayer.Enqueue(node.Left);
                }
    
                if (node.Right != null)
                {
                    nextLayer.Enqueue(node.Right);
                }
    
                internalPath += depth;
            }
    
            depth++;
        }
    
        return internalPath;
    }
    
    另请参阅

    BinarySearchTree 库

  • 相关阅读:
    爬虫框架概述
    Django【进阶篇 】
    Django【基础篇】
    【tornado】系列项目(二)基于领域驱动模型的区域后台管理+前端easyui实现
    刷题记录:[安洵杯 2019]easy_serialize_php
    刷题记录:[SWPU2019]Web1
    2019-2020-1 20175210 20175211 20175219 实验五 通讯协议设计
    2019-2020-1 20175210 20175211 20175219 实验三 实时系统
    2019-2020-1 20175210 20175211 20175219 实验四 外设驱动程序设计
    2019-2020-1 20175210 20175211 20175219 实验二 固件程序设计
  • 原文地址:https://www.cnblogs.com/ikesnowy/p/13431775.html
Copyright © 2011-2022 走看看