发生在树中的简单情况证明

一些有关树的证明

树这一数据结构的变化比较多,常常涉及到一些结论及相关证明。本文介绍简单的证明。对于了解这些结论及证明可以帮助我们在编写程序时有一个顶层设计视角,从而更好设计,即使这些结论并不能直接提高我们的代码能力。


1、证明在具有\(N\)个节点的二叉树中,存在\(N+1\)个空链(nullptr)。注:\(N+1\)个空链(nullptr)指没有子的链接。

证明:根据二叉树的定义,在二叉树中每个节点都有两个链,所以\(N\)个节点的二叉树一共有\(2N\)个链。我们令根有两个子,其余节点只有一个子或没有子(叶没有子),所以此时有子的链的数量是\(N-1\)。那么空链的数量是链的总数减去有子链数:

\[2N-(N-1)=N+1 \]

故空链数是\(N+1\)


2、证明高度是h的二叉树,它的最大节点数是\(2^{h+1}-1。\)

证明:本证明使用数学归纳法。当\(h=0\)时,等式成立。假设对\(h=1,2,3,\dots,k\)时,等式也成立,所以得到如下等式:

\[N_{max|h=k}=2^{k+1}-1 \]

现在需证明当\(h=k+1\)时,如下等式成立:

\[N_{max|h=k+1}=2^{k+2}-1 \]

当二叉树的高度是\(k+1\)时,它至多有两个子树的高度是\(k\)。所以它的最大节点数:

\[N_{max|h=k+1}=2N_{max|h=k}+1=2(2^{k+1}-1)+1=2^{k+2}-1 \]

因此,得出结论高度是\(h\)的二叉树,它的最大节点数是\(2^{h+1}-1\)


3、二叉树中满节点是指有两个子的节点。证明在非空二叉树中满节点数量加\(1\)等于叶数量。

证明:令\(N\)表示所有节点数、\(F\)表示满节点数、\(L\)表示叶数、\(H\)表示半节点数(在二叉树中,半节点指有一个子的节点)。所以:

\[N = F + L + H \]

根据(1)中的推理,一个节点数是\(N\)的二叉树,它的总链接数是\(2N\)。满节点有两个非空链接,半节点有一个非空链接与一个空链接,叶有两个空连接,它们构成了一棵二叉树的全部链接\(2N\)。由(1)知总链接数是非空链接加上空连接,而且空链接是\(N+1\)。因为非空链接只能是满节点或者半节点,所以:

\[2F + H + N + 1 = 2N \]

计算整理后得到:

\[2F + H = N - 1 \]

\(N\)代换,得到:

\[F + 1 = L \]

\(F\)是满节点,\(L\)是叶,所以得出结论:在非空二叉树中,满节点数量加\(1\)等于叶数量。


4、假设二叉树的叶是\(I_1,I_2,\dots,I_M\),而它们对应的深度分别是\(d_1,d_2,\dots,d_M\)。可以证明不等式\(\sum\limits_{i=1}^{M} 2^{-d_i} \le 1\),并给出何时取得等式。

证明:本证明使用数学归纳法。令\(N\)表示树的节点树。对于\(N=0\),这意味没有节点,任何深度为\(0\);对于\(N=1\),叶的深度为\(0\)(说明:依据深度定义,叶的深度是叶到根的路径长,所以\(1-1=0\))。假设\(N=1,2,3,\dots,k\)满足不等式,现在证明\(N=k+1\)时仍然满足不等式。

对于\(N=k+1\)时的树,令左子树的节点数是\(n\),右子树的节点数是\(k-n\)。依据归纳假设,左子树与右子树都满足不等式,不妨简化为如下不等式:

\[S_{left} \le 1 \\ S_{right} \le 1 \]

上式中\(S_{left}\)\(S_{right}\)是不等式\(\sum\limits_{i=1}^{M} 2^{-d_i}\)的简写,前者\(M=n\),后者\(M=k-n\)

在树中,子树的叶的深度比树的叶的深度小\(1\)。即对于子树的叶\(I_i\)对应的深度\(d_i\),在树中它的深度是\(d_i+1\)。所以,子树满足的不等式做出如下修改则是树的不等式:

\[S_{left}^{'} = \sum\limits_{i = 1}^{n} 2^{-(d_i + 1)} = \frac{1}{2} \sum\limits_{i = 1}^{n} 2^{-d_i} = \frac{1}{2} S_{left} \\ S_{right}^{'} = \sum\limits_{i = 1}^{k - n} 2^{-(d_i + 1)} = \frac{1}{2} \sum\limits_{i = 1}^{k - n} 2^{-d_i} = \frac{1}{2} S_{right} \]

因此,对于\(N=k+1\)时,得到如下不等式:

\[S = S_{left}^{'} + S_{right}^{'} = \frac{1}{2} S_{left} + \frac{1}{2} S_{right} \le \frac{1}{2} + \frac{1}{2} = 1 \]

在完全平衡的二叉树中,所有叶都具有相同的深度,即\(d_1 = d_2 = \dots = d_M = d\)。叶数量是\(2^{d}\),这是因为二叉树每一层级的节点数量是上一层级的\(2\)倍,这个过程中,叶的层级恰好是深度。所以此时取到等式。


5、令\(f(N)\)表示在\(N\)个节点的二叉树中满节点的平均数,规定\(f(0)=f(1)=0\),证明

\[f(N)=\frac{N-2}{N} + \frac{1}{N} \sum\limits_{i=0}^{N-1} (f(i)+f(N-i-1)), \quad N>1 \]

证明:这个等式是递推公式,可以使用递推的方法证明。根据满节点的定义,一个节点是满节点当且仅当它没有空链。如果一棵树有\(N\)个节点,根的左子树与右子树的节点数量取值范围是\([0,N-1]\)。令左子树的节点数量是\(i\),则右子树的节点数量是\(N-i-1\)。如果根是满节点,那么左子树的节点数量取值范围满足:

\[i \in [1,N-2] \]

因为此时右子树的节点数量取值范围也满足:

\[N - i - 1 \in [1,N - 2] \]

这样根的左子树与右子树都至少有一个节点,在二叉树中,根是满节点。左子树与右子树这一对分割二叉树有\(N\)种情况,其中满足根是满节点的情况有\(N-2\)种,也就是根是满节点的数量总和:

\[R(N) = N-2 \]

满节点总数是根节点是满节点+左子树满节点+右子树满节点。根据上述讨论,对于节点数是\(N\)的二叉树,根是满节点的情况总是\(N-2\)。对于左子树与右子树的每一次分割,只需递归使用\(f(N)\)即可。这样就容易得到子树的满节点数量\(T(N)\)

\[T(N) = \sum\limits_{i=0}^{N-1} (f(i)+f(N-i-1)) \]

所以得到二叉树的满节点总数:

\[F(N) = Q(N) + T(N) = N-2 + \sum\limits_{i=0}^{N-1} (f(i)+f(N-i-1)) \]

但是\(F(N)\)不是期望值,即不能正确表达平均情况。左子树与右子树的分割一共是\(N\)种情况,所以对每种情况平均加权得到:

\[\begin{aligned} f(N) &= \frac{1}{N} (N-2 + \sum\limits_{i=0}^{N-1} (f(i)+f(N-i-1))) \\ &= \frac{N-2}{N} + \frac{1}{N} \sum\limits_{i=0}^{N-1} (f(i)+f(N-i-1)) \end{aligned} \]

最后所得方程式即为所需证明的等式,证毕。


6、证明\(f(N)=(N-2)/3\)是(5)中等式的一个解,并规定\(f(0)=f(1)=0\)

证明:本证明使用数学归纳法。根据(5)中等式规定,当\(N=0\)\(N=1\)时,\(f(N)=0\),这在两个等式中均成立。验证当\(N=2,3,4,5\)时,该解与(5)中等式吻合。因此假设如下等式对\(N=2,3,\dots,k\)成立:

\[f(N) = \frac{N-2}{3} = \frac{N-2}{N} + \frac{1}{N} \sum\limits_{i=0}^{N-1} (f(i)+f(N-i-1)),\quad N \in [2,k] \]

现在需要证明\(N=k+1\)时,上述等式仍然成立,即证明:

\[f(k+1) = \frac{k-1}{3} = \frac{k-1}{k+1} + \frac{1}{k+1} \sum\limits_{i=0}^{k} (f(i)+f(k-i)) \]

观察\(\sum\limits_{i=0}^{k}(f(i)+f(k-i))\),发现\(f(i)\)\(f(k-i)\)在求和式中具有对称性,所以:

\[\sum\limits_{i=0}^{k} (f(i)+f(k-i)) = 2 \sum\limits_{i=0}^{k} f(i) \]

已经假设\(N=k\)时等式成立,即\(f(N)=\frac{N-2}{3}\)成立,并且规定了\(f(0)=f(1)=0\),所以:

\[\sum\limits_{i=0}^{k} f(i) = \sum\limits_{i=2}^{k} \frac{i-2}{3} \]

不妨令\(j=i-2\),当\(i=2\)时,\(j=0\);当\(i=k\)时,\(j=k-2\),所以:

\[\sum\limits_{i=2}^{k} \frac{i-2}{3} = \frac{1}{3} \sum\limits_{j=0}^{k-2} j = \frac{1}{3} \times \frac{(k-2)(k-1)}{2} = \frac{(k-2)(k-1)}{6} \]

将整理后的计算式代入原式,得到:

\[f(k+1) = \frac{k-1}{k+1} + \frac{(k-2)(k-1)}{3(k+1)} \]

简单的通分母,并附上计算过程:

\[f(k+1) = \frac{3(k-1)+(k-2)(k-1)}{3(k+1)} = \frac{3k-3+k^2-3k+2}{3(k+1)} = \frac{k^2-1}{3(k+1)} = \frac{k-1}{3} \]

最终结果与所需证明等式一致,证毕。(小结:难点在于方程式整理,需要一定计算能力。)


7、证明一个随机二叉查找树的平均深度是\(O(\log N)\)。注意:树的深度是最深节点的深度。

解:规定一个树的内部路径长是所有节点的深度和,对于具有\(N\)个节点的树,不妨其内部路径长用\(C_n\)表示。那么平均深度是\(C_n / N\)

对于具有\(N\)个节点的树,每个节点用键\({1, 2, 3, \dots, N}\)标注。假设树的根是键\(i\),那么左子树的节点数是\(i-1\),右子树的节点数是\(N-i\)。因此,树的内部路径长的定义式:

\[\begin{align*} C_n &= n-1 + [ (C_0 + C_{n-1}) + (C_1 + C_{n-2}) + (C_2 + C_{n-3}) + \dots + (C_{n-1} + C_0) ] / n \\ &= n-1 + 2 \frac{\sum_{i=0}^{n-1} C_i}{n} \tag{1} \end{align*} \]

补充说明,当树是链式的,也就是链表那样,它的深度是\(n-1\)。其余情况,根据根在所有节点中的相对位置,分左子树与右子树两种情况进行递归(也就是数学归纳法)定义,分成\(n\)对,因为是对称的,所以才会除以\(n\)。若对上述等式两边同时乘以\(n\),可以得到:

\[nC_n = n(n-1) + 2 \sum\limits_{i=0}^{n-1} C_i \tag{2} \]

\(n-1\)代替上述等式中的\(n\),得到:

\[(n-1)C_n = 2 \sum\limits_{i=0}^{n-2} C_i + (n-1)(n-2) \tag{3} \]

\(\text{(2)} - \text{(3)}\)

\[\begin{aligned} &nC_n - (n-1)C_{n-1} = 2C_{n-1} + 2(n-1) \\ \Rightarrow &nC_n - (n+1)C_{n-1} = 2(n-1) \end{aligned} \]

上述等式两边同时除以\(n(n+1)\),得到:

\[\frac{C_n}{n+1} - \frac{C_{n-1}}{n} = \frac{2(n-1)}{n(n+1)} \tag{4} \]

\(n\)足够大时,我们知道:

\[\lim_{n \to \infin} \frac{n-1}{n} = 1 \]

所以\(\text{(4)}\)式可修改为:

\[\frac{C_n}{n+1} - \frac{C_{n-1}}{n} \approx \frac{2}{n+1} \tag{5} \]

\((5)\)式两边求和并且将\(n\)改为\(i\),得到:

\[\sum\limits_{i=1}^{n}( \frac{C_i}{i+1} - \frac{C_{i-1}}{i}) \approx \sum\limits_{i=1}^{n} \frac{2}{i+1} \tag{6} \]

\((6)\)式的左边实际上是一个伸缩级数,所以:

\[\sum\limits_{i=1}^{n} (\frac{C_i}{i+1} - \frac{C_{i-1}}{i}) = \frac{C_i}{i+1} - C_0 \]

这样\((6)\)式就可以修改为(由定义式可知\(C_0 = 0\)):

\[C_n \approx 2(n+1)\sum\limits_{i=1}^{n} \frac{1}{i+1} \approx 2(n+1) \sum\limits_{i=1}^{n-1} \frac{1}{i} \]

后面一项实际上是调和数,其值近似为对数\(\ln n\)。所以对于大\(n\),其积分可以近似为:

\[C_n \approx 2n \ln n \]

所以得到平均深度:

\[\frac{C_n}{n} = O(\log n) \]

证毕。

(小结:难点在于递归定义式构造、伸缩级数的应用、以及调和数在大值下\(H_n = \sum_{i=1}^{n} \frac{1}{i} \approx \ln n\)


8、证明键值\(1,2,3,\dots,2^k-1\)插入到一个空的AVL树,其结果是完美平衡的。

解:当\(1 \le k \le 3\)时,树是完美平衡的。不妨假设\(k = 1, 2, 3, \dots, h\)时,树是完美平衡的,现在只需证明对于\(k = h+1\)时,树是完美平衡的即可。

在我们的假设中,树是完美平衡的,对于小值\(k\)树是完美平衡时其根是键值\(2^{k-1}\)。因此在我们的假设中根的键值也是成立的。下图是当\(k = 1, 2 , 3\)时的完美平衡的AVL树:

此时右子树的节点数是\(2^h-1 - 2^{h-1} = 2^{h-1} - 1\),它们的键值是从\(2^{h-1}+1\)\(2^h-1\)。因为我们需要证明\(k = h+1\)时假设仍然成立,所以我们需要考虑键值从\(2^h\)\(2^{h+1}-1\)的节点。它们的每次插入都是当前树的最大值,所以它们插入时都在右子树的最右边。

完美平衡树是左右子树对称,所以它们的高度肯定也是一样的。按照我们肯定的假设,根是\(2^{h-1}\),树的高度是\(h-1\),完美平衡时,子树的高度是\(h-2\)。继续插入新的键值时,树仍然平衡但非完美平衡,且此时的根不变,仍然是\(2^{h-1}\),按照AVL树平衡规则,左子树不变。下图是完美平衡树的完美平衡被打破以及建立下一次平衡的临界情况:

按照AVL树的平衡规则,左右子树高度差不能大于1,如果子树的键值数量与前一个完美平衡时树的键值数量相同,此时按照AVL树的平衡规则要建立新的平衡。令\(x\)表示建立新平衡前所能允许的最大键值,而原完美平衡状态的最大键值是\(2^h-1\)(这由我们的归纳假设所得),所以\(x\)满足以下关系:

\[x - (2^h-1) + \frac{2^h-1-1}{2} = 2^h-1 \]

解得上述方程式:

\[x = 2^h + 2^{h-1} - 1 \]

所以此时右子树的键值范围是\([2^{h-1}+1, 2^h+2^{h-1}-1]\),这样的右子树对于它本身是完美平衡的(因为如果是不完美平衡则意味着对于AVL树而言有溢出,此时原树已经是不能平衡的临界状态,所以不能溢出)。按照AVL树的递归定义,右子树的左右子树是对称的。而右子树的左子树是原来树的右子树(因为AVL平衡的旋转所得),它的键值范围是\([2^{h-1}+1, 2^h-1]\),所以右子树的根是\(2^h\)

因为每次新的键值是当前树的最大值,它一定在最右边。当插入\(2^h+2^{h-1}\)时,平衡被打破,进行单旋转。因此新平衡的树根键值是\(2^h\)。树的高度是\(h\)

新平衡的右子树键值是\([2^h+1, 2^h+2^{h-1}]\),我们现在要证明这个新平衡到其完美平衡所能允许的最大键值是\(2^{h+1}-1\)。左子树的键值数量只在新旧平衡交替时发生变化,所以它是一个定值。令\(y\)表示完美平衡所能允许的最大键值,它满足以下关系:

\[y - 2^h = 2^h-1 \]

解得上述方程式:

\[y = 2^{h-1}-1 \]

该解证明了我们的假设:当\(k = h+1\)时,树是完美平衡的。证毕。(小结:要理解AVL树的变化规则,将这个变化规则用数学表达出来。对于不好证明的内容要尝试数学归纳法。)


8、证明下述AVL树的删除操作是正确的。

// findMin( ) 是找到当前树的最小节点
// balance( ) 是平衡当前树
// 当前树包括子树

void remove( const Object & x, AvlNode * & t )
{
    if( t == nullptr )
        return;
    else if( x < t->element )
        remove( x, t->left );
    else if( x > t->element )
        remove( x, t->right );
    else if( t->left != nullptr && t->right != nullptr )
    {
        t->element = findMin( t->right )->element;
        remove( t->element, t->right );
    }
    else
    {
        AvlNode * oldNode = t;
        t = ( t->left != nullptr ) ? t->left : t->right;
        delete t;
    }
    
    balance( t );
}

解:实际删除操作在else代码块中。删除某个指定的元素,然后用它的右子树的最小元素来代替,最后再删除这个最小元素的节点。所以在这样的递归过程中,删除是一定有效的。之后要考虑平衡,因为传入的参数是指针引用,所以这个参数信息是树中的信息而非另外指针所指向的信息。递归过程本身存储了路径,因此删除结束以后,在返回过程中每次都平衡一次,最终删除路径中的所有节点都被平衡,这保证了AVL树的平衡。


9、(a)\(N\)节点AVL树每个节点存储的高度信息需要多少比特?用大\(O\)标记表示。
(b)使8比特高度计数器溢出的AVL树的最小高度是多少?

解:(a)AVL树是一个尽可能平衡的树,它的高度\(H\)与节点数\(N\)之间的关系式如下:

\[H = 1.44 \log (N + 2) - 1.328 \]

因此高度与节点数之间用大\(O\)表示是:

\[H = O(\log N) \]

比特是计算机二进制表示的,用\(B\)表示比特的位数,则比特与高度之间的关系是:

\[B = \lceil \log _2 H \rceil \]

所以,比特位数与高度之间的关系是:

\[B = O( \log \log N ) \]

(b)在计算机中8比特最小值是有符号表示,其值是\(2^7-1=127\),如果8比特作为高度计数器,那么AVL树的最小高度是127。

(注:本问题主要是考虑在实际工程中空间存储问题,一般对于大数据,我们用比特或者说位来标记数据以减小空间开销。)


10、(a)证明按序访问伸展树中的所有节点,其结果是左子链树。注:左子链树是树的每个节点都只有左子。
(b)证明按序访问伸展树中的所有节点,不管初始树如何,其访问的总时间是\(O(N)\)

解:(a)在证明之前,我们需要知道以下事实:

  1. 被访问的节点会被提升到根的位置。
  2. 被访问的节点是当前树结构其右子树的最小节点。(按序访问)
  3. 被访问的节点在伸展过程中没有左子树,直到它成为根。
  4. 两个节点间的单旋转不会影响到根的左子树。
  5. 三个节点间的之字形旋转也不会影响到根的左子树。

根据第一点与第二点,在第一次访问后结果是根只有右子树。

在第二次访问结束后,后面的任何访问都不会是一字形旋转。这是因为你所访问的节点是当前树的右子树的最小节点,它一定是父本的左子,而它的父本一定是祖父本的右子。当然,在伸展过程可能会出现所访问的节点是父本的左子,父本是祖父本的左子,但是在最后一次提升为根的旋转中不会出现此种情况。因此,后面的旋转只会是单旋转与之字形旋转。因为是单旋转与之字形旋转,不同于一字形旋转会增加左子树,单旋转与之字形旋转仅仅是位置调整,所以不会影响到左子树。(事实上前两次访问也不会时一字形访问,因为一字形访问要求3个节点间的关系,按序访问使得前两次访问不会组合成3个节点。)

在单旋转与之字形旋转过程中,原来的根的右子树是被访问节点的左子树(根是祖父本),但是正如第三点所述,被访问节点是不可能有左子树的,因为它是当前树的右子树的最小节点。这样旋转的结果是旧根被降低为左子树时它是没有右子树的。

上述推理以及结合第四点与第五点,在伸展逐步递进时,左子树层层压入增多,但是其中每个节点都不会有右子,故而左子树是一个局部的左子链树。当最大的节点被提升到根,由于最大节点肯定没有右子,结果是整个树都是左子链树。

证毕。

(b)证明这个结论需要用到伸展树更一般的属性——动态手指属性。

动态手指属性:在访问元素\(x\)后,在伸展树中访问\(y\)的摊还开销是\(O(\log (1+|x-y|))\)

证明这个属性是冗长且复杂的,很遗憾的是,笔者也不会。在这里仅仅是运用该属性做一个简单陈述:

动态手指属性是说当我们搜索某个元素\(x\)时,我们通过移动手指来记住在哪个节点中发现它的。当我们搜索下一个元素\(y\)时就从该节点开始搜索。其时间是在\(x\)为基准平衡时与\(x\)\(y\)的距离成正比。因此总时间是\(O(N)\)

(注:第二问可以作为扩展,不明白可以忽略。第一问要十分清楚伸展树是怎么运动的。)


11、(a)证明对于任意二叉查找树\(T_1\)经过AVL单旋转后可以转换为另一个给定的二叉查找树\(T_2\)\(T_1\)\(T_2\)具有相同的元素。注:这里不考虑树是否平衡,仅考虑行为

(b)证明上述过程在平均情况下的时间复杂度是\(O(NlogN)\)

证明:(a)AVL单旋转作用于两个变动的节点,它们交换在树中的高度,并将其一个作为另一个的子。不妨将\(T_1\)对所有节点实行单旋转而且方向相同,最终会得到一个单子链树(每个节点只有一个子而且方向相同);同理,对\(T_2\)也做相同的操作。最终二者是相同的单子链树。这说明二者通过合适的AVL旋转就能互相转换。

(b)在转换过程中,我们从树结构的根开始开始转换。即假设\(T_2\)的根是\(a\),那么我们需要在\(T_1\)寻找到\(a\)的节点,随后将这个节点进行AVL单旋转到根部。随后观察\(T_2\)的左右子树,在\(T_1\)中递归这个过程。

假设存在某个节点的数值是\(x\)且深度是\(d_N\),那么旋转到合适的位置的时间满足如下递归式:

\[T(N) = T(i) + T(N-i-1) + d_N \]

其中\(N\)\(x\)为根的树或子树的大小;\(i\)是其左子树的大小。在最坏情形下,\(d_N\)总是\(O(N)\),而\(i\)总是0,因为此时是单子链树。通过递归计算我们可以得到最坏情形下的时间复杂度:

\[\begin{align} T(N) &= O(N) + O(N-1) + O(N-2) + \dots + O(2) + O(1) \\ &= O(\frac{N(N+1)}{2}) \\ &= O(N^2) \end{align} \]

而在平均情形下,\(i\)的数值是等可能出现的,所以即使\(d_N\)\(O(N)\)

\[\begin{align} T(N) &= \frac{1}{N} \sum_{i=0}^{N-1} (T(i)+T(N-i-1)) + d_N \\ &= \frac{2}{N} \sum_{i=0}^{N-1} + d_N \\ &= O(NlogN) \end{align} \]

证毕。(\(d_N\)在平均情形下无意义,因为平均情形是考虑统计学上的分布)


posted @ 2026-01-26 04:09  永恒圣剑  阅读(2)  评论(0)    收藏  举报