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

本文详细介绍了堆排序算法,包括二叉堆的概念、重要步骤、维护堆的性质、建堆过程、堆排序的代码实现及应用。堆排序的时间复杂度为O(nlgn),适用于原址排序。此外,文章还讨论了如何使用最大堆实现最大优先队列。

1. 二叉堆

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

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

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

  • PARENT(i)

    • return i/2 i / 2
    • LEFT( i i ):
      • return 2i
      • RIGHT( i i ):
        • return 2i+1
        • 更加快速地计算?

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

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

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

          2. 重要步骤

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

          3. 维护堆的性质

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

          我们利用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. 参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值