完全二叉树的应用 - 图解堆排序算法及代码实现

1. 二叉堆

堆是一个数组A, 它可以被看成一个近似的完全二叉树
这里写图片描述

以(a)二叉树和(b)数组形式展现的是一个最大堆. 结点上方的数字是它在数组中相应的下标.

若一个结点下标为 i , 可以得到它的父结点, 左孩子和右孩子的下标:

  • PARENT( i )
    • return i / 2
  • LEFT( i ):
    • return 2 i
  • RIGHT( i ):
    • return 2 i + 1

更加快速地计算?

  • 在大多数计算机中,LEFT过程可以在一条指令内(左移1位)计算出 2 i , RIGHT过程可以通过将 i 左移1位并低位加1, 快速计算 2 i + 1 . RIGHT过程可以通过把 i 的值右移1位计算得到 i / 2 . 在堆排序的好的实现中, 这三个函数通常是以宏(define)内联函数(inline)实现的.

注意: 根节点的下标不是0而是1

  • 所以, 代码中的所有Index都需要考虑减去1, 满足数组的初始下标为0的规律

2. 重要步骤

  • MaxHeapify: 负责维护最大堆, 时间复杂度为 O ( lg n )
  • BuildMaxHeap: 负责从一个无序的输入数据数组中构造最大堆, 时间复杂度为 O ( n )
  • HeapSort: 负责对一个数组进行原址排序, 时间复杂度为 O ( n lg n )

3. 维护堆的性质

MaxHeapify(A, i)是用于维护最大堆性质的重要过程. 在调用该函数时, 我们假定根结点为LEFT( i )和RIGHT( i )的二叉树都是最大堆. 如果A[ i ]小于其孩子, 则违背了最大堆的性质. MaxHeapify(A, i)通过让A[ i ]的值在最大堆中”逐级下降”, 从而使下标为 i 的跟结点的子树重新遵循最大堆的性质.

下图展示了MaxHeapify的过程
这里写图片描述

代码实现为:

        /// <summary>
        /// 负责维护最大堆
        /// </summary>
        private void MaxHeapify(int[] originInts, int rootIndex, int heapSize) {            
            if ((rootIndex << 1) + 1 > heapSize) return;
            //如果当前结点不是叶子结点
            var maxClildIndex = FindMaxClildIndex(originInts, rootIndex, heapSize);
            //如果根结点满足大顶堆条件, 结束维护大顶堆过程
            if (maxClildIndex == rootIndex) return;
            //如果根结点小于子结点
            Swap(ref originInts[maxClildIndex], ref originInts[rootIndex]);
            //发生替换, 需要检查新的子结点是否满足大顶堆条件
            MaxHeapify(originInts, maxClildIndex, heapSize);
        }

思考

递归调用可能使某些编译器产生低效的代码, 可以用循环控制结构取代递归, 用伪代码重写为:

MAX-HEAPIFY(A, i)
    while true
        left = LEFT(i)
        right = RIGHT(i)
        if left > A.heap-size
            return
        largest = FIND-MAX-CHILD-INDEX(A, i)
        if largest == i
            return
        exchange A[i] with A[largest]
        i = largest

4. 建堆

我们可以用自底向上的方法利用BuildMaxHeap把一个大小n = A.Length 的数组A[1..n]转换为最大堆. 每一个叶结点都可以看成只包含一个元素的堆(此时无需建立子堆). 过程BuildMaxHeap对树中的其它结点(非叶子结点)都调用一次MaxHeapify.

扫描二维码关注公众号,回复: 955186 查看本文章

我们利用heapSize来表示每一次构成大顶堆时的总结点数(即堆的大小), 堆初始化时, heapSize = A.Length

下图展示了BuildMaxHeap的过程

这里写图片描述

代码实现为:

        /// <summary>
        /// 负责从一个无序的输入数据数组中构造最大堆
        /// </summary>
        private void BuildMaxHeap(int[] originInts, int heapSize) {
            //获得最后一个结点的父结点 -> 需要维护的起始结点
            var currentIndex = (heapSize - 1) >> 1;
            //自底向上建立大顶堆
            while (currentIndex >= 0) {
                MaxHeapify(originInts, currentIndex--, heapSize);
            }
        }

5. 堆排序算法

初始时, 堆排序算法利用BuildMaxHeap将输入的数组A[1..n]建成最大堆. 其中n = A.Length. 因为数组中的最大元素总是在A[1]中, 通过把它与A[n]进行替换, 可以把该元素放到正确的位置. 此时, 需要从堆中去掉该结点(可以通过设置heapSize -= 1来实现), 再将剩下的结点构造成大顶堆(通过调用MaxHeapify(A, 1))

下图展示了堆排序算法的过程

这里写图片描述

这里写图片描述

代码实现为:

        /// <summary>
        /// 负责对一个数组进行原址排序
        /// </summary>
        public int[] HeapSort(int[] originInts) {
            if (originInts == null || originInts.Length == 0) {
                return originInts;
            }
            //需要排序的堆结点数
            var heapSize = originInts.Length;
            PrintArray("初始数组", originInts);
            //构造大顶堆 
            BuildMaxHeap(originInts, heapSize);
            var count = 1;
            while (true) {
                PrintArray("第" + count++ + "个大顶堆", originInts);                
                //取出最大的结点放到数组尾部>>Swap(A[1], A[heapSize])
                Swap(ref originInts[0], ref originInts[heapSize-- -1]);
                //如果此时只剩下一个结点, 排序结束
                if (heapSize == 1) {
                    break;
                }
                //维护A[1]大顶堆, 直至heapSize == 1
                MaxHeapify(originInts,0, heapSize);               
            }
            PrintArray("排序后数组", originInts);            
            return originInts;
        }

        /// <summary>
        /// 查看是否满足大顶堆条件
        /// </summary>
        /// <returns>返回一个最大的孩子索引或者父结点</returns>
        private int FindMaxClildIndex(int[] originInts, int rootIndex, int heapSize) {
            var leftChildIndex = (rootIndex << 1) + 1;
            //排除已经排序好的子结点
            if (leftChildIndex > heapSize - 1) {
                return rootIndex;
            }
            var rightChildIndex = (rootIndex << 1) + 2;
            var maxIndex = leftChildIndex;
            //如果有右孩子, 比较左孩子和右孩子
            if (rightChildIndex <= heapSize - 1) {
                if (originInts[rightChildIndex] > originInts[leftChildIndex]) {
                    maxIndex = rightChildIndex;
                }
            }
            //比较根结点和最大的孩子
            if (originInts[rootIndex] > originInts[maxIndex]) {
                maxIndex = rootIndex;
            }
            return maxIndex;
        }

        /// <summary>
        /// 负责打印数组
        /// </summary>
        private void PrintArray(string info, int[] originInts) {
            Console.Write(info + ": ");
            foreach (var temp in originInts) {
                Console.Write(temp + " ");
            }
            Console.WriteLine();
        }

        /// <summary>
        /// 负责交换两个数字
        /// </summary>
        private void Swap(ref int a, ref int b) {
            var temp = a;
            a = b;
            b = temp;
        }

输入: { 3, 27, 4, 5, 6, 8 , 13, 46}

运行结果为:

初始数组: 3 27 4 5 6 8 13 46
第1个大顶堆: 46 27 13 5 6 8 4 3
第2个大顶堆: 27 6 13 5 3 8 4 46
第3个大顶堆: 13 6 8 5 3 4 27 46
第4个大顶堆: 8 6 4 5 3 13 27 46
第5个大顶堆: 6 5 4 3 8 13 27 46
第6个大顶堆: 5 3 4 6 8 13 27 46
第7个大顶堆: 4 3 5 6 8 13 27 46
排序后数组: 3 4 5 6 8 13 27 46

6. 堆排序的应用

7. 参考

猜你喜欢

转载自blog.csdn.net/jingangxin666/article/details/79944835