JAVA-堆 和 堆排序

一、概念

之前在队列的时候讲过,队列是先进先出的,是根据你的插入顺序来确定优先级的。

而有时候我们希望可以通过元素内容来确定优先级。

比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。
在这种情况下, 数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象 。这种数据结构就是优先级队列 (Priority Queue)

堆的性质:

  • 堆总是一棵完全二叉树。
  • 堆中某个节点的值总是不大于或不小于其父节点的值。如果是某个节点值大于左右结点就是大根堆如果是某个节点值小于左右结点,就是小根堆

二、堆的底层结构

堆实际上是二叉树的顺序存储,就是用数组的方式,来表示二叉树。

小根堆:

而且我们可以发现,数组中的存储顺序是层序遍历的结果

大根堆:

从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储

如果不是完全二叉树:

中间就会浪费许多空间,所以并不是一个好的选择

如果是一个完全二叉树:

不仅不会浪费空间,我们还可以利用完全二叉树的一些公式:

  • 如果i为0,则i表示的节点为根节点,否则****i节点的父亲节点为 (i - 1)/2
  • 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
  • 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子

三、堆的实现和创建

我们一边写代码一边进行画图介绍

首先创建一个堆类,指定堆初始大小为10,可以传入数组:

java 复制代码
public class MyHeap {

    //存放数组元素
    private int[] elem;
    //记录有效数据个数
    private int usedSize;

    //初始化为10
    public MyHeap() {
        elem = new int[10];
    }

    //初始化
    public void init(int[] array) {
        for (int i = 0; i < elem.length && i < array.length; i++) {
            elem[i] = array[i];
            usedSize++;
        }
    }
}

1.将数据转化为堆的存储

那么我们如何把数组转化为,表示堆的逻辑呢?

我们要将现在的这串数据转化为堆,相信大家想到的是通过逆序排序就直接Ok了。

那我问你

所以单纯的逆序排序是不太ok的
正确的排序方式:


我们现在就可以写一部分代码了:

java 复制代码
    public void createHeap() {
        for (int parent = (usedSize-1-1) / 2; parent >= 0; parent--) {
            //用来调整
            siftDown();
        }
    }

    private void siftDown() {

    }


继续之前的操作:

最终代码实现:

java 复制代码
    public void createHeap() {
        for (int parent = (usedSize-1-1) / 2; parent >= 0; parent--) {
            //用来调整
            siftDown(parent,usedSize);
        }
    }

    private void siftDown(int parent, int end) {
        //假设左孩子是最大值
        //通过 i * 2 + 1 = 左子树
        int child = (parent * 2) + 1;

        //孩子节点越界就说明结束了
        while (child < end) {

            //右孩子没有越界的情况     左孩子       <    右孩子 吗?
            if (child + 1 < end && elem[child] < elem[child+1]) {
                //右孩子比左孩子大
                child++;
            }

            //孩子节点大于父亲节点
            if (elem[child] > elem[parent]) {
                swap(parent,child);
                parent = child;
                child = (child * 2) + 1;
            }else {
                //说明不需要调整了
                break;
            }
        }
    }

    private void swap(int i, int j) {
        int tmp = elem[i];
        elem[i] = elem[j];
        elem[j] = tmp;
    }

那么来测试一下:

java 复制代码
    public static void main(String[] args) {
        int [] array = {27,15,19,18,28,34,65,49,25,37};
        MyHeap myHeap = new MyHeap();
        myHeap.init(array);
        myHeap.createHeap();
    }

结果:

2.在堆中添加数据

由于我们本质是通过数组存储的,所以还是会存在数组满的情况,那就需要判断一下是不是满了,满了就扩容。

那我们就当是已经解决了长度不够的问题:

代码:

java 复制代码
    public void offer(int val) {
        if (isFull()) {
            elem = Arrays.copyOf(elem,elem.length * 2);
        }

        elem[usedSize] = val;
        usedSize++;

        siftUp(usedSize-1);
    }

    private void siftUp(int child) {
        //计算父亲节点位置
        int parent = (child - 1) / 2;

        while (parent >= 0) {
            if (elem[parent] < elem[child]) {
                swap(parent,child);
                child = parent;
                parent = (child - 1) / 2;
            }else {
                break;
            }
        }
    }

测试:

java 复制代码
    public static void main(String[] args) {
        int [] array = {27,15,19,18,28,34,65,49,25,37};
        MyHeap myHeap = new MyHeap();
        myHeap.init(array);
        myHeap.createHeap();
        myHeap.offer(70);
    }

结果:


如果我们直接通过offer方法来创建堆呢?是不是每一次都是堆,测试一下:

java 复制代码
    public static void main(String[] args) {
        MyHeap myHeap = new MyHeap();
        myHeap.offer(27);
        myHeap.offer(15);
        myHeap.offer(19);
        myHeap.offer(18);
        myHeap.offer(28);
        myHeap.offer(34);
        myHeap.offer(65);
        myHeap.offer(49);
        myHeap.offer(25);
        myHeap.offer(37);

    }

根据结果看虽然顺序会有不同,但是仍然是满足大根堆条件的。

3.堆中删除数据

代码实现:

java 复制代码
    public int poll() {
        //判断是否为null
        if (isEmpty()) {
            return -1;
        }
        int old = elem[0];
        //交换后逻辑删除
        swap(0,usedSize-1);
        usedSize--;

        //向下调整
        siftDown(0,usedSize);

        return old;
    }

    private boolean isEmpty() {
        return usedSize == 0;
    }

四、PriorityQueue

**1.**特性

Java 集合框架中提供了 PriorityQueuePriorityBlockingQueue 两种类型的优先级队列, PriorityQueue 是线 程不安全的, PriorityBlockingQueue 是线程安全的 ,本文主要介绍 PriorityQueue 。

关于PriorityQueue的使用要注意:

  1. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常
  2. 不能插入null对象,否则会抛出****NullPointerException
  3. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容(理论上的,实际肯定是不可以无限插入的)
  4. 插入和删除元素的时间复杂度为O(log2N)
  5. PriorityQueue****底层使用了堆数据结构
  6. PriorityQueue****默认情况下是小堆---即每次获取到的元素都是最小的元素
  7. 使用时必须导入PriorityQueue所在的包,即:
java 复制代码
import java.util.PriorityQueue;

2.常用方法函数名

|--------------------|---------------------------------------------------------------------------------|
| 函数名 | 功能介绍 |
| boolean offer(E e) | 插入元素 e ,插入成功返回 true ,如果 e 对象为空,抛出 NullPointerException 异常,时 间复杂度,注意:空间不够时候会进行扩容 |
| E peek() | 获取优先级最高的元素,如果优先级队列为空,返回 null |
| E poll() | 移除优先级最高的元素并返回,如果优先级队列为空,返回 null |
| int size() | 获取有效元素的个数 |
| void clear() | 清空 |
| boolean isEmpty() | 检测优先级队列是否为空,空返回 true |

java 复制代码
    public static void main(String[] args) {
        PriorityQueue<Integer> queue = new PriorityQueue<>();

        //判断是否为null
        System.out.println(queue.isEmpty());

        //入堆
        queue.offer(21);
        queue.offer(56);
        queue.offer(44);
        queue.offer(23);

        //看一下堆顶元素
        System.out.println(queue.peek());
        //返回堆顶元素并退出
        System.out.println(queue.poll());
        System.out.println(queue.poll());

        //返回堆中元素个数
        System.out.println(queue.size());

        //清控堆中元素
        queue.clear();

        //再次判断是否为null
        System.out.println(queue.isEmpty());
        
    }

结果:

3.PriorityQueue的构造方法

|------------------------------------------------|----------------------------------------------------------------------------------------------|
| 构造函数 | 功能介绍 |
| PriortiyQueue() | 创建一个空的优先级队列,默认容量是11 |
| PriortiyQueue(int initialCapacity) | 创建一个初始容量为 initialCapacity 的优先级队列,注意:initialCapacity不能小于 1 ,否则会抛 IllegalArgumentException 异 常 |
| PriortiyQueue(Collection<? extends E> c) | 用一个集合来创建优先级队列 |

它们多数都调用了,有两个参数的构造方法。这两个参数的构造方法,给了一个比较器,你可以通过这个比较器来控制大根堆或小根堆。(两个参数的构造方法后面再做介绍他的作用)

3.1 参数为 Collection<? extends E> c 的构造方法

意思是只要实现了Collection的数据结构都可以转换为堆:

java 复制代码
    public static void main(String[] args) {
        LinkedList<Integer> list = new LinkedList<>();
        list.add(23);
        list.add(34);
        list.add(45);
        list.add(56);
        list.add(67);
        list.add(78);


        //将链表
        PriorityQueue<Integer> queue = new PriorityQueue<>(list);

        System.out.println(queue.toString());
    }

结构:

3.2 通过Comparator实现大根堆小根堆的控制

有这样一个构造方法,传入构造器就可以对控制元素的比较:

我们需要创建一个比较器:

java 复制代码
class IntCmp implements Comparator<Integer> {


    //大根堆写法
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2.compareTo(o1);
    }

    //小根堆写法
//    @Override
//    public int compare(Integer o1, Integer o2) {
//        return o1.compareTo(o2);
//    }
}

测试一下:

java 复制代码
    public static void main(String[] args) {
        PriorityQueue<Integer> queue = new PriorityQueue<>(new IntCmp());
        queue.offer(23);
        queue.offer(34);
        queue.offer(45);
        queue.offer(56);
        queue.offer(67);
        queue.offer(78);

        System.out.println(queue.toString());
    }

那代码是如何实现的呢?我们来看一下

大家可能compare和compareTo看懵了,我们来查看一下Integer里面实现的:

如果是我们自己的类如何比较呢?

我们有这样一个学生类:

java 复制代码
class Student {
    public int age;

    public Student(int val) {
        age = val;
    }
}
java 复制代码
    public static void main(String[] args) {
        PriorityQueue<Student> queue = new PriorityQueue<>();
        queue.offer(new Student(9));
        queue.offer(new Student(12));
        queue.offer(new Student(45));
        queue.offer(new Student(43));

    }

我们并没有指定Student的比较方式,这是不被允许的

那我们就就处理一下:

java 复制代码
class Student implements Comparable<Student>{
    public int age;

    public Student(int val) {
        age = val;
    }

    @Override
    public int compareTo(Student o) {
        return this.age - o.age;
    }

    @Override
    public String toString() {
        return age + " ";
    }
}
class IntCmp implements Comparator<Student> {


    //大根堆写法
    @Override
    public int compare(Student o1, Student o2) {
        //这里去调用Student的compareTo
        return o2.compareTo(o1);
    }

    //小根堆写法
//    @Override
//    public int compare(Student o1, Student o2) {
//        return o1.compareTo(o2);
//    }
}

测试:

java 复制代码
        public static void main(String[] args) {
            PriorityQueue<Student> queue = new PriorityQueue<>(new IntCmp());
            queue.offer(new Student(9));
            queue.offer(new Student(12));
            queue.offer(new Student(45));
            queue.offer(new Student(43));


            System.out.println(queue.toString());
        }

五、堆排序

经过上面这么多图的讲解,我们现在就知道了,如果是大根堆,堆顶元素一定是最大值,反之就是最小值 。那我们就可以像删除元素那样,把最后第一个元素和最后一个元素交换再进行操作保证仍然是堆,那么现在堆底元素就是最大值。下一次堆顶元素就是第二大的值了,以此类推。

图解:

代码:

java 复制代码
    /**
     * 堆排序
     * 时间复杂度:O(log2n * n)
     * 空间复杂度:O(N)
     * 稳定性:不稳定
     */
    public void heapSort(int[] array) {
        //先调整为大根堆
        createHeap(array);
        //end控制未排序数据的末尾
        int end = array.length - 1;
        while (end >= 0) {
            //交换堆首和堆尾元素
            swap(array,0,end);
            //向下调整
            siftDown(array,0,end);
            //有效数据已经放到末尾
            end--;

        }

    }

    private void createHeap(int[] array) {
        for (int parent = (array.length-1-1) / 2; parent >= 0; parent--) {
            //用来调整
            siftDown(array,parent,array.length);
        }
    }


    private void siftDown(int[] array, int parent, int end) {
        //假设左孩子是最大值
        //通过 i * 2 + 1 = 左子树
        int child = (parent * 2) + 1;

        //孩子节点越界就说明结束了
        while (child < end) {

            //右孩子没有越界的情况     左孩子       <    右孩子 吗?
            if (child + 1 < end && array[child] < array[child+1]) {
                //右孩子比左孩子大
                child++;
            }

            //孩子节点大于父亲节点
            if (array[child] > array[parent]) {
                swap(array,parent,child);
                parent = child;
                child = (child * 2) + 1;
            }else {
                //说明不需要调整了
                break;
            }
        }
    }
    private void swap(int[] array, int i, int j) {
        int tmp = array[i];
        array[i] = array[j];
        array[j] = tmp;
    }

测试:

java 复制代码
    public static void main(String[] args) {
        MyHeap heap = new MyHeap();
        int[] array = {27,15,19,18,28,34,65,49,25,37};
        heap.heapSort(array);

    }
相关推荐
QTX187307 分钟前
JavaScript 中的原型链与继承
开发语言·javascript·原型模式
xyliiiiiL9 分钟前
一文总结常见项目排查
java·服务器·数据库
shaoing11 分钟前
MySQL 错误 报错:Table ‘performance_schema.session_variables’ Doesn’t Exist
java·开发语言·数据库
The Future is mine1 小时前
Python计算经纬度两点之间距离
开发语言·python
Enti7c1 小时前
HTML5和CSS3的一些特性
开发语言·css3
腥臭腐朽的日子熠熠生辉1 小时前
解决maven失效问题(现象:maven中只有jdk的工具包,没有springboot的包)
java·spring boot·maven
爱吃巧克力的程序媛1 小时前
在 Qt 创建项目时,Qt Quick Application (Compat) 和 Qt Quick Application
开发语言·qt
ejinxian1 小时前
Spring AI Alibaba 快速开发生成式 Java AI 应用
java·人工智能·spring
杉之1 小时前
SpringBlade 数据库字段的自动填充
java·笔记·学习·spring·tomcat
圈圈编码1 小时前
Spring Task 定时任务
java·前端·spring