堆与堆操作、堆排序—— 图表示+ Java 实现 简单直观

目录

1.堆概念

2.堆实现与堆操作实现

 (1)上浮:

(2)下沉

(3) 插入元素

(4)删除最大的元素

3 堆排序

(1)构建堆

(2)不停交换栈顶与最后的元素


1.堆

堆是用数组实现的完全二叉树。

完全二叉树:除了最高层以外,其余层节点个数都达到最大值,而最高层节点都优先集中在最左边,如图所示。

图1:完全二叉树

同时,数组第一位不存储元素,索引从 1 开始储存元素。图一中红色数字为索引。

堆又分为:

  • 大根堆:Max-heap:父节点的值大于或等于子节点
  • 小根堆:Min-heap:父节点的值小于或等于子节点
图2

用公式表示大根堆:ki  >=  k2i + 1 且  ki >= k2i + 2  i = 1 , … , [n-1/ 2]

                  小根堆:   ki  <= k2i + 1 且  ki <= k2i + 2     i = 1 , … , [n -1/ 2]        n为节点个数

2.堆实现与堆操作实现

大根堆为例,用 Java 实现,其中用到了泛型。

构造函数新建数组时,建立的是长度比节点总数 MaxN 大1的数组,因为索引 0 不存储数据。

less() 函数是判断节点  i  所在的值是否小于 节点  j,

swap() 函数是交换两个节点的值,这两个函数是为了方便后续操作,简化代码。

public class Heap<T extends Comparable<T>> {
    private T[] heap;
    private int N = 0;   //当前的节点数 也是最后一个节点的索引

    public Heap(int maxN){
        this.heap = (T[]) new Comparable[maxN+1]; //索引0不存储数据,所以要新建一个长度比节点数大1的数组
    }
    public boolean isEmpty(){
        return N == 0;
    }
    public int size(){
        return N;
    }
    public boolean less(int i, int j){
        return heap[i].compareTo(heap[j]) < 0;
    }
    public void swap(int i, int j){
        T t = heap[i];
        heap[i] = heap[j];
        heap[j] = t;
    }

   
}

堆操作:

 (1)上浮:

我们实现的是大根堆,需要满足: 子节点 < 父节点。

在堆中,当一个节点比父节点大,那么需要交换这个两个节点。

交换后还可能比它新的父节点大,因此需要不断地向上进行比较和交换操作,把这种操作称为上浮

例:对节点 7 进行上浮操作。

上浮操作示意图

如图(a)是原始数据, 如图(b),节点 7 与 父节点 3 比较,大于父节点3,所以交换。

交换后,节点 3 值 与父节点 1 比较,大于父节点 1 ,所以交换,此时已经到堆顶,上浮完成。 

代码实现: 上图操作即为 swim( 7 ) ;

    private void swim(int k){
        while (k > 1 && less(k / 2, k)){
            swap(k / 2, k);
            k = k / 2;
        }
    }

(2)下沉

 当一个节点比子节点来得小,也需要不断地向下与子节点进行比较和交换操作,把这种操作称为下沉。

一个节点如果有两个子节点,应当与两个子节点中最大那个节点进行交换。

例:对节点 1 进行下沉操作。

下沉操作

 

如图(a)是原始数据, 如图(b),节点 1 与 子节点 2 比较,小于子节点2,所以交换。

交换后,节点 2 值 与子节点4 比较,小于子节点 4 ,所以向下交换,此时节点4 的值大于两个子节点,所以操作结束。

代码实现: 上图操作即为 sink( 1 ) ;

    private void sink(int k){
        while ( 2 * k < N){              //要考虑 是否存在子节点,即子节点是否会超出数组
            int j = 2 * k;
            if (j < N && less(j, j + 1)){  //选出两个子节点较大的那一个
                j++;            // 如果奇数节点更大就与奇数节点对换
            }
            if (!less(k, j))
                break;
            swap(k, j);
            k = j;
        }
    }

(3) 插入元素

思路:将新元素放到数组末尾,然后让其上浮到合适位置。

如图所示,插入值为10的节点10,然后对节点10进行上浮操作,使起上浮到合适位置。上浮操作前面已做过图示,这里不展开。

代码实现: 

    private void insert(T n){
        heap[++N] = n;
        sink(N);
    }

(4)删除最大的元素

思路:从数组顶端删除最大的元素,并将数组的最后一个元素放到顶端,并让这个元素下沉到合适的位置。

我们可以先交换栈顶和最后一个元素,然后让最后一个元素为空,再让栈顶下沉到合适位置。

如图所示,删除最大元素,先将栈顶与节点9交换,删除节点9,对节点1进行下沉操作,下沉操作前面已做过图示,这里不展开。

代码实现: 

    private T delete(){
        T max = heap[1];
        swap(1, N);
        heap[N] = null;
        N--;   //此时最后一个元素为空
        sink(1);
        return max;
    }

3 堆排序

用大根堆来实现排序,大根堆的栈顶是堆中的最大元素,我们将栈顶(最大元素)和当前堆中数组的最后一个元素交换位置,并且不删除它,然后再将栈顶(元素)与倒数第二个元素交换.... 重复下去...

这样从尾到头就是一个递减序列,从正向来看就是一个递增序列。

所以堆排序主要有两个步骤

(1)构建堆

无序数组建立堆最直接的方法是从左到右遍历数组进行上浮操作。一个更高效的方法是从右至左进行下沉操作,如果一个节点的两个节点都已经是堆有序,那么进行下沉操作可以使得这个节点为根节点的堆有序。叶子节点(没有子节点的元素)不需要进行下沉操作,可以忽略叶子节点的元素,因此只需要遍历一半的元素即可。

如图(a)所示,已经是完全二叉树,我们从右往左对每个除了叶子节点的元素进行下沉操作。

首先是9、8、7、6、5都是叶子节点,我们跳过到节点 4,对节点4进行下沉操作,子节点没有比它大的,不需要动,

转至图(b)对节点3进行下沉,同样不动

然后对节点2进行下沉,与节点5交换

然后对节点1进行下沉操作,没有比它大的子节点,此时大根堆建立完毕。

大根堆 构建过程
标题

(2)不停交换栈顶与最后的元素

如图所示,从最后一个节点开始,与栈顶交换,然后对栈顶进行下沉sink(1, 6),

然后继续交换栈顶与倒数第二个节点,  sink(i, n) 意为在前n个节点里进行下沉。虚线框中的元素是已经完成排序的元素,不进行处理了。

 

代码实现:

public class HeapSort<T extends Comparable<T>> {
    /**
     * 数组第 0 个位置不能有元素
     */
    public void sort(T[] nums) {
        //1.0 构建堆
        int N = nums.length - 1;
        for (int k = N / 2; k >= 1; k--)
            sink(nums, k, N);
        //2.0 不停交换栈顶与最后的元素
        while (N > 1) {
            swap(nums, 1, N--);
            sink(nums, 1, N);
        }
    }

    /**下沉
     */
    private void sink(T[] nums, int k, int N) {
        while (2 * k <= N) {
            int j = 2 * k;
            if (j < N && less(nums, j, j + 1))
                j++;
            if (!less(nums, k, j))
                break;
            swap(nums, k, j);
            k = j;
        }
    }

    /**判断是否小于
     */
    private boolean less(T[] nums, int i, int j) {
        return nums[i].compareTo(nums[j]) < 0;
    }

    /**交换两个节点
     */
    public void swap(T[] nums, int i, int j){
        T t = nums[i];
        nums[i] =nums[j];
        nums[j] = t;
    }

}

堆排序复杂度分析:

一个堆的高度为 logN,因此在堆中插入元素和删除最大元素的复杂度都为 logN。

对于堆排序,由于要对 N 个节点进行下沉操作,因此复杂度为 NlogN。

堆排序是一种原地排序,没有利用额外的空间。

现代操作系统很少使用堆排序,因为它无法利用局部性原理进行缓存,也就是数组元素很少和相邻的元素进行比较和交换。

参考链接:

1. CyC :https://cyc2018.github.io/CS-Notes/#/notes/%E7%AE%97%E6%B3%95%20-%20%E6%8E%92%E5%BA%8F?id=%e5%a0%86%e6%8e%92%e5%ba%8f 代码来源

2. https://www.cnblogs.com/Java3y/p/8639937.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值