java实现堆排序/优先队列


前言

这段时间重新复习了一遍数据结构,学习完排序算法之后,想实现一个堆排序,但只完成堆排序的算法并不是很合我的心意。因此打算用java实现一个优先队列,也就是堆。以此文章记录。


一、什么是堆/优先队列?

1. 数组角度

数据结构课本的解释是:
存在n个元素的序列{k1,k2,k3,…,kn} ,当且仅当满足以下关系时,
{ k i > = k 2 i k i > = k 2 i + 1 \begin{cases} k_i >= k_{2i} \\ k_i >= k_{2i+1} \end{cases} {ki>=k2iki>=k2i+1
就构成了一个大顶堆,而小顶堆则相反。(本文所讲均为大顶堆)


2.完全二叉树角度

从完全二叉树的角度来看,大顶堆要满足每一个非叶子节点都比它的左右孩子(若存在)大。
即有:
{ r o o t . v a l > = r o o t . l e f t . v a l r o o t . l e f t ! = n u l l r o o t . v a l > = r o o t . r i g h t . v a l r o o t . r i g h t ! = n u l l \begin{cases} root.val >= root.left.val & root.left != null \\ root.val >= root.right.val & root.right != null \end{cases} {root.val>=root.left.valroot.val>=root.right.valroot.left!=nullroot.right!=null
如下图所示:
在这里插入图片描述


3.两种解释的联系

如果对于树这种数据结构比较熟悉的话,特别是完全二叉树的性质,即
编号为i的结点,如果它的左孩子存在的话,编号为2i; 如果它的右孩子存在的话,编号为2i+1
在这里插入图片描述
上述的两个不等式就可以表示为
在这里插入图片描述

4.堆的性质

  1. 堆的根节点(也就是数组的第一个元素)始终是所有元素中最大的。
    这个很容易根据定义得到——根节点要大于左右子树的结点,而左右子树又分别大于其左右子树,递归下去。由于一棵树除了根节点之外都有父节点,每一个结点都小于其父节点,所有可以知道根节点一定是所有元素中最大的。
  2. 所有的非叶子结点大于其左右孩子的结点。
    这个是堆的定义。

二、堆排序实现

1. 初始化一个大顶堆

借用别的大佬的图示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2. 不断弹出堆顶元素并调整堆

  1. 弹出堆顶(将堆顶元素和最后一个元素交换位置)
  2. 重新调整堆顶元素使其变成一个最大堆
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

3. 细节

  1. 当进行交换之后,不是交换完就形成一个堆了,而是需要继续判断下坠的结点是否满足堆的性质。

三、代码实现

1.仅实现堆排序

调整根节点为i的子树,使其变为一个大顶堆

    private void adjustHeap(int i) {
        int temp = array[i];
        //2i+1为左子树
        //这里的length是一个全局变量,表示的是堆的逻辑长度(弹出元素长度减一)
        for (int k = 2 * i + 1; k < length; k = 2 * k + 1) {
            //选出左右子树最大的值
            if (k + 1 < length && array[k + 1] > array[k]) {
                k = k + 1;
            }
            //注意是 array[k] 与 temp 比较
            if (array[k] > temp) {
                array[i] = array[k];
                i = k;
            } else {
                break;
            }
        }
        array[i] = temp;
    }

初始化大顶堆

    private void sort() {
        for (int i = length / 2 - 1; i >= 0; i--) {
            adjustHeap(i);
        }
    }

进行堆排序,这里是直接将元素打印出来(也可以new一个新的数组存储)
(课本的做法是将弹出元素赋给最后一个元素)

    public void heapSort(){
        //如果堆不空
        while(!isEmpty()){
            //将堆顶赋值给res
            int res = array[0];
            //将最后一个数值赋给堆顶,并使得该堆的逻辑长度减一
            array[0] = array[--length];
            //重新调整结点0所在的树
            adjustHeap(0);
            System.out.print(res+" ");
        }
    }

测试
在这里插入图片描述

2.进行封装为数据结构

直接看代码即可

import java.util.Arrays;

public class HeapSort {
	//声明array数组用于存储堆
    private int[] array;
    //数组的初始容量
    private static final int SIZE = 10;
    //数组每次扩大的增量
    private static final int INCREMENTAL = 5;
    //数组的实际有效长度(存储了多少个数据)
    private int length;
	
	/*
	无参构造函数
	初始化数组
	有效长度赋值为0
	*/
    HeapSort() {
        array = new int[SIZE];
        length = 0;
    }

	/*
	有参构造函数
	可以传入一个数组或者一串int型的变量
	会将这些数据赋值给array
	并调用私有的初始化方法init将数据调整为最大堆
	*/
    HeapSort(int... array) {
    	this.array = new int[array.length];
    	//这里不要使用引用赋值,不然会修改到原数组
        for(int i = 0; i < array.length; i++){
        	this.array[i] = array[i];
        }
        length = array.length;
        init();
    }
	
	/*
	初始化堆的函数
	调整length/2 到 0 这些非叶子节点
	*/
    private void init() {
        for (int i = length / 2 - 1; i >= 0; i--) {
            adjustHeap(i);
        }
    }

	//判空
    private boolean isEmpty() {
        return length <= 0;
    }

	//弹出堆顶元素并重新调整堆
    public int poll() {
        if (!isEmpty()) {
            int res = array[0];
            array[0] = array[--length];
            adjustHeap(0);
            return res;
        } else {
            throw new ArrayIndexOutOfBoundsException();
        }
    }

	//插入新的数据
    public void add(int n) {
   		//如果当前的堆已经满了,重新创建一个新的堆
        if (length == array.length) {
            array = Arrays.copyOf(array, length + INCREMENTAL);
        }
        //插入新元素
        array[length++] = n;
        //这里是重点: 由于是在数组最后面插入,需要调整插入位置到根节点的整棵树
        // length/2-1是插入结点的父节点
        // 要寻找插入结点的父节点的父节点,使用 i = (i-1) / 2
        for (int i = length / 2 - 1; i >= 0; i = (i - 1) / 2) {
            adjustHeap(i);
            if (i == 0) {
                break;
            }
        }
    }
	
	//返回堆顶元素不弹出
    public int peek() {
        if (!isEmpty()) {
            return array[0];
        } else {
            throw new ArrayIndexOutOfBoundsException();
        }
    }
	
	//返回堆的有效长度
    public int size() {
        return length;
    }
	
	//调整算法——核心
    private void adjustHeap(int i) {
        int temp = array[i];
        //2i+1为左子树 
        //之前说左子树为2i是基于索引从1开始,这里用2i+1是因为数组下标从0开始
        for (int k = 2 * i + 1; k < length; k = 2 * k + 1) {
            //选出左右子树最大的值
            if (k + 1 < length && array[k + 1] > array[k]) {
                k = k + 1;
            }
            //注意是 array[k] 与 temp 比较
            if (array[k] > temp) {
                array[i] = array[k];
                i = k;
            } else {
                break;
            }
        }
        array[i] = temp;
    }
    
//    //测试使用
//    public void display(){
//        System.out.print("当前堆内情况:");
//        for(int i = 0; i < length; i++){
//            System.out.print(array[i] + " ");
//        }
//        System.out.println();
//    }

}

测试代码

@Test
    public void heapSortTest() {
        HeapSort hs = new HeapSort();
        //从小到大插入15个数
        for (int i = 0; i <= 14; i++) {
            hs.add(i * i);
        }
        hs.display();

        System.out.print("堆的长度为:");
        System.out.println(hs.size());

        System.out.print("弹出的堆顶元素为:");
        System.out.println(hs.poll());
        hs.display();

        System.out.print("再弹出一个元素:");
        System.out.println(hs.poll());
        hs.display();

        System.out.print("当前堆的大小:");
        System.out.println(hs.size());

        System.out.print("堆顶元素为(不弹出):");
        System.out.println(hs.peek());

        for (int i = 1; i <= 14; i++) {
            System.out.print(hs.poll() + " ");
        }
    }

测试结果
在这里插入图片描述
测试代码解释:
首先初始化一个空堆
循环调用add方法(从小到大)插入15(>10)个元素,这里测试了(add方法的自动增长)即使数据再多,堆也能自适应增长
插入结束!
第一行输出:调用display查看堆内情况(堆顶为最大(196),非叶子结点大于左右孩子)
第二行输出:调用了size方法
第三行输出:调用poll方法,不同于peek。poll会弹出,peek只是查看。输出为196
第四行输出:查看弹出之后堆的情况(每次弹出都会调整)
第五、六行输出:同样是poll测试
第七行输出:调用peek方法,只查不出
第八行是一个for循环,循环弹出14个元素(这里的size为13)所以所有元素出堆之后,再次调用poll方法会抛出异常并捕获。

时间复杂度分析

直接分析核心函数adjustHeap,每次循环k=2*k+1,最坏情况就是堆顶为最小,需要调整log2(n)个数,调整只涉及简单赋值,故调整的时间复杂度为O(1)。
调用一次adjustHeap函数的时间复杂度为O(log2(n))
init方法会调用n/2次adjustHeap。建堆的时间复杂度为O(nlog2(n))
排序会调用n次的adjustHeap。复杂度为O(nlog2(n))

应用

  1. 可以在分支限界的剪枝使用(涉及到算法就不赘述)
  2. 优先队列的使用,例如在构建最小生成树,使用克鲁斯卡尔算法,每次会找剩余边的最小值,这不就刚好是堆吗,每次弹出一个最小值判断是否联系了两个不连通的分支
  3. 从大量元素(假设1010)中选出前100个最大/小的,其他排序算法基本都是需要把所有数据排完再挑选出前100个。或者使用快排,枢纽选得合适的话也很快。堆排不需要所有元素有序,只需要执行1次init和100次的poll方法,复杂度为(100+1010 / 2)* log2(1010).
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值