排序算法之堆排序分析

堆排序

堆排序就是利用堆的特点所设计的一种排序算法。(完全二叉树,每个节点的值都大于其左右子节点(或者小于))

 

堆排序的过程大致可以分解成两个步骤:建堆 和 排序。

 

建堆

我们可以将数组原地建成一个堆。原地就是不借助与别的数组,在原数组上操作建堆

有两种方法:若原数组有n个数据,可以假设数组起初只包含一个数据,就是下标为1的数据。

然后将下标从 2 到 n 的数据依次插入到堆中。这样就将包含 n 个数据的数组,组织成了堆。

这种建堆的处理过程是从前往后处理数据,这是一个从下往上堆化的建堆过程

 

第二种方法和第一种相反,是一个从后往前处理数据的过程。就是说每个数据插入堆中的时候都是从上往下堆化。

因为叶子节点无法从上往下堆化,所以从第一个非叶子节点开始,依次堆化。

 

 

 

排序

建堆结束之后,数组中的第一个元素就是堆顶,也就是最大的元素。

将它跟最后一个元素交换,那最大元素就放到了下标为 n 的位置。

排序类似于“删除堆顶元素”,下标为 n 的元素放到堆顶,然后再通过堆化的方法,将剩下的 n−1 个元素重新构建成堆。

堆化完成之后,我们再取堆顶的元素,放到下标是 n−1 的位置。

一直重复这个过程,直到最后堆中只剩下标为 1 的一个元素,排序工作就完成了。

 

 

 

 

堆排序过程

堆排序的执行效率

建堆

叶子节点不需要堆化,所以需要堆化的节点从倒数第二层开始。

每个节点堆化的过程中,需要比较和交换的节点个数,跟这个节点的高度 k 成正比。

 

将每个非叶子节点的高度求和,就是下面这个公式:

错位相减法

把公式左右都乘以 2,就得到另一个公式 S2。我们将 S2 错位对齐,并且用 S2 减去 S1,可以得到 S。

 

而S的有部分是等比数列,等比数列求和得到

因为 h=log2​n,代入公式 S,就能得到 S=O(n),所以,建堆的时间复杂度就是 O(n)。 

 

排序

排序过程类似于“删除堆顶元素”,所以排序过程的时间复杂度是 O(nlogn)

 

排序包括建堆和排序两个操作,建堆过程的时间复杂度是 O(n),排序过程的时间复杂度是 O(nlogn)。

所以,堆排序整体的时间复杂度是 O(nlogn)。

 

堆排序的空间消耗

堆排序的不论建堆还是排序,都是在原数组上进行的,都只需要极个别临时存储空间,所以堆排序是原地排序算法。

 

堆排序的稳定性

堆排序不是稳定的排序算法。

因为在排序的过程,存在将堆的最后一个节点跟堆顶节点互换的操作,所以就有可能改变值相同数据的原始相对顺序。

 

堆排序的代码实现


public class Heap {
  private int[] a; // 数组,从下标1开始存储数据
  private int n;  // 堆可以存储的最大数据个数
  private int count; // 堆中已经存储的数据个数
 
  public Heap(int capacity) {
    a = new int[capacity + 1];
    n = capacity;
    count = 0;
  }
  //对于完全二叉树来说,下标从 2n​+1 到 n 的节点都是叶子节点。叶子结点不需要堆化
  private static void buildHeap(int[] a, int n) {
    for (int i = n/2; i >= 1; --i) {
      heapify(a, n, i);
    }
  }

  private static void heapify(int[] a, int n, int i) {
    while (true) {
      int maxPos = i;
      if (i*2 <= n && a[i] < a[i*2]) maxPos = i*2;
      if (i*2+1 <= n && a[maxPos] < a[i*2+1]) maxPos = i*2+1;
      if (maxPos == i) break;
      swap(a, i, maxPos);
      i = maxPos;
    }
  }  
  // n表示数据的个数,数组a中的数据从下标1到n的位置。
  public static void sort(int[] a, int n) {
    buildHeap(a, n);
    int k = n;
    while (k > 1) {
      swap(a, 1, k);
      --k;
      heapify(a, k, 1);
    }
  }
}

 

以上代码都是假设堆中的数据是从数组下标为 1 的位置开始存储。

那如果从 0 开始存储,实际上处理思路是没有任何变化的。

唯一变化的就是代码实现的时候,计算子节点和父节点的下标的公式改变了。

如果节点的下标是 i,那左子节点的下标就是 2∗i+1,右子节点的下标就是 2∗i+2,父节点的下标就是 (i−1)​/2。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值