2.4.42

2.4.42 #

解答 #

前序序列与完全二叉树 #

二叉树前序遍历的顺序是:自身,左子树,右子树。

因此对于一个前序遍历序列,第一个元素是根结点,第二个元素是左子结点。

再把左子结点找到,就可以把数组分成三部分:根结点,左子树,右子树,进而递归的构造出整个二叉树。

现在问题是,右子结点在哪,或者说,左子树有多大?

这里就要用到完全二叉树的性质了,我们先从比较简单的满二叉树入手。

就满二叉树而言,根结点的左子树和右子树是一样大的,即左右子树大小均为 $(n-1)/2$ 。

在这种情形下,右子结点的下标显然是 $(n+1)/2$ ,根结点下标为 0。

完全二叉树可以视为在满二叉树的基础上加了一层叶子结点,现在我们已知结点总数 $n$。

于是可以求得二叉树的高度 $k=\lfloor \log_2(n) \rfloor$ ,注意只有一个结点的树高度为 0。

那么最后一层的叶子结点数目为 $l=n-2^{k}+1$ 个,如下图所示:

如果把最后一层(第 $k$ 层)去掉,剩余部分即为高度为 $k-1$ 的满二叉树,结点总数为 $2^k - 1$ 。

按照之前的说明可以知道左右子树大小都等于 $(2^{k}-2)/2=2^{k-1}-1$。

现在要将第 $k$ 层的 $l$ 个结点分到左右子树里面去。

第 $k$ 层最多能有 $2^k$ 个结点,取半就是 $2^k / 2 = 2^{k-1}$ 个。

于是当 $l<=2^{k-1}$ 时,左右子树大小分别为 $2^{k-1}-1+l$ 和 $2^{k-1}-1$ 。

当 $l > 2^{k-1}$ 时,左右子树大小分别为 $2^{k} - 1$ 和 $2^{k-1} -1 +l -2^{k-1}=l-1$ 。

实际上,我们只要先取根结点,然后再取 $2^{k-1} -1$ 个结点给左子树,再做判断:

如果 $n-2^{k-1} < 2^{k}-1$ ,那么对应第一种情况,末尾的 $2^{k-1}-1$ 个结点即为右子树。

否则就是第二种情况,前面的 $ 2^k$ 个结点就是根结点和左子树,剩下的为右子树。

堆排序的实现 #

现在我们能够根据结点总数 $n$ 来确定相应的完全二叉树,接下来则是如何进行堆排序。

堆排序的第一步就是建堆,建堆时最主要的就是 sink 操作了。

但前序序列中除了第一个结点(根结点)之外,其他结点的左右子结点下标是不能直接通过计算得到的。

因此在前序实现中,sink 操作只能对根结点进行,引出了下面的递归建堆方法。

如果根结点的左右两棵子树都已经是堆了,那么只要对根结点进行 sink 操作即可使整个二叉树变成堆。

第一步先检查子树的大小,如果小于等于 1 则说明是叶结点,直接返回。

否则计算出左右子结点的位置,递归地建堆。

最后对根结点进行 sink 操作。

那么这个 sink 操作是怎么做的呢?

计算得到了左右子结点的下标后,比较得出较大的那个,如果大于根结点则交换,否则返回。

交换后根结点变成了某一侧子树的根结点,递归地进行 sink 即可。

接下来是排序,最主要的操作是 DelMax

前序序列的根结点好找,但是最后一个结点就比较麻烦了,它既可能在左子树也可能在右子树。

但我们可以根据之前的大小关系来寻找,

如果左子树小于等于 $2^k - 1$ ,那么最后一个结点一定在左子树,否则就在右子树。

递归进行上述过程,直到到达叶子结点,该叶子结点就是最后一个结点。

之后我们将最后一个结点暂存,整个数组从后向前补缺(这一步将导致算法变成 $O(n^2)$ ),

再把第一个结点放到最后的空位上,最后把存起来的结点放到第一位,对该结点进行 sink 操作。

依次往复即可完成排序。

测试结果:

这个算法在设计上与一般实现的比较次数大体相等,

只是移动数组耗时较长,这里只给到 $10^7$。

代码 #

public static class HeapPreorderAnalysis
{
    private static long _compareTimes;

    /// <summary>
    /// 利用堆排序对数组进行排序。
    /// </summary>
    /// <param name="pq">需要排序的数组。</param>
    public static long Sort<T>(T[] pq) where T : IComparable<T>
    {
        _compareTimes = 0;
        var n = pq.Length;
        BuildTree(pq, 0, pq.Length);
        // 排序
        while (n > 1)
        {
            var tail = GetTail<T>(0, n);
            var temp = pq[tail];
            for (var i = tail + 1; i < n; i++)
                pq[i - 1] = pq[i];
            n--;
            Exch(pq, 0, n);
            pq[0] = temp;
            Sink(pq, 0, n);
        }
        return _compareTimes;
    }

    /// <summary>
    /// 递归获得堆的最后一个元素。
    /// </summary>
    /// <typeparam name="T">堆中元素类型。</typeparam>
    /// <param name="p">当前位置。</param>
    /// <param name="n">堆的元素数目。</param>
    /// <returns>最后一个元素的下标。</returns>
    private static int GetTail<T>(int p, int n)
    {
        if (n <= 1)
            return p;
        var k = (int)(Math.Log10(n) / Math.Log10(2));   // 高度

        var left = (int)Math.Pow(2, k - 1) - 1;
        var right = left;
        if (n - left <= (int)Math.Pow(2, k))
        {
            // 叶子结点全在左侧
            left = n - right - 1;
            return GetTail<T>(p + 1, left);
        }

        left = (int)Math.Pow(2, k) - 1;
        right = n - left - 1;
        return GetTail<T>(p + 1 + left, right);
    }

    /// <summary>
    /// 递归建堆。
    /// </summary>
    /// <typeparam name="T">堆中元素。</typeparam>
    /// <param name="pq">堆所在的数组。</param>
    /// <param name="p">堆的起始下标。</param>
    /// <param name="n">堆的元素数目。</param>
    private static void BuildTree<T>(T[] pq, int p, int n) where T : IComparable<T>
    {
        if (n <= 1)
            return;
        var k = (int)(Math.Log10(n) / Math.Log10(2));   // 高度

        var left = (int)Math.Pow(2, k - 1) - 1;
        var right = left;
        if (n - left <= (int)Math.Pow(2, k))
        {
            // 叶子结点全在左侧
            left = n - right - 1;
        }
        else
        {
            left = (int)Math.Pow(2, k) - 1;
            right = n - left - 1;
        }

        BuildTree(pq, p + 1, left);
        BuildTree(pq, p + 1 + left, right);
        Sink(pq, p, n);
    }

    /// <summary>
    /// 令堆中的元素下沉。
    /// </summary>
    /// <param name="pq">需要执行操作的堆。</param>
    /// <param name="p">需要执行下沉的结点下标。</param>
    /// <param name="n">堆中元素的数目。</param>
    private static void Sink<T>(T[] pq, int p, int n) where T : IComparable<T>
    {
        if (n <= 1)
            return;
        var k = (int)(Math.Log10(n) / Math.Log10(2));   // 高度

        var left = (int)Math.Pow(2, k - 1) - 1;
        var right = left;
        if (n - left <= (int)Math.Pow(2, k))
        {
            // 叶子结点全在左侧
            left = n - right - 1;
        }
        else
        {
            left = (int)Math.Pow(2, k) - 1;
            right = n - left - 1;
        }

        // 找出较大的子结点
        int j = p + 1, size = left;
        if (right != 0) // 有右结点
        {
            if (Less(pq, j, p + left + 1))
            {
                j = p + left + 1;
                size = right;
            }
        }

        // 与根结点比较
        if (!Less(pq, p, j))
            return;

        // 交换,继续下沉
        Exch(pq, p, j);
        Sink(pq, j, size);
    }

    /// <summary>
    /// 比较堆中下标为 <paramref name="a"/> 的元素是否小于下标为 <paramref name="b"/> 的元素。
    /// </summary>
    /// <param name="pq">元素所在的数组。</param>
    /// <param name="a">需要比较是否较小的结点序号。</param>
    /// <param name="b">需要比较是否较大的结点序号。</param>
    /// <returns></returns>
    private static bool Less<T>(T[] pq, int a, int b) where T : IComparable<T>
    {
        _compareTimes++;
        return pq[a].CompareTo(pq[b]) < 0;
    }


    /// <summary>
    /// 交换堆中的两个元素。
    /// </summary>
    /// <param name="pq">要交换的元素所在堆。</param>
    /// <param name="a">要交换的结点序号。</param>
    /// <param name="b">要交换的结点序号。</param>
    private static void Exch<T>(T[] pq, int a, int b)
    {
        var temp = pq[a];
        pq[a] = pq[b];
        pq[b] = temp;
    }
}

另请参阅 #

PriorityQueue 库