• Java 数据结构与算法 堆


    堆也叫做优先级队列。队列是一种先进先出的数据结构,有的时候,要操作的数据有优先级,需要优先级高的先出队列,堆就是这样的数据结构。堆的数据结构提供了两个最基本的操作:返回最高级优先级对象和添加新的对象。堆实际上是一种在完全二叉树的基础上进行元素的调整。

    1、堆基本概念

    如果有一个集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一 个一维数组中,并满足:Ki = K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大 堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

    堆的性质:1,堆总是一颗完全二叉树。2,堆中的某个节点的值总是不大于或不小于其父亲节点的值。这里就是指大根堆或小根堆。

    堆是一颗使用数组来存储的完全二叉树,整个的遍历过程使用了层序遍历。和二叉树相比,堆是线性顺序存储的,但是在画图理解的时候,我们画成完全二叉树的形式。

    2、堆的创建

    关于堆的创建,需要用到堆的向下调整。因为堆是把元素存在数组中的,如果我们假设i为节点在堆也叫做优先级队列。队列是一种先进先出的数据结构,有的时候,要操作的数据有优先级,需要优先级高的先出队列,堆就是这样的数据结构。堆的数据结构提供了两个最基本的操作:返回最高级优先级对象和添加新的对象。堆实际上是一种在完全二叉树的基础上进行元素的调整。数组中的下标,那么根据完全二叉树的性质,i是被称为父亲节点的话:

    (1)如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2;

    (2)如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子;

    (3)如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子。

    这个在后面堆的创建中需要用到,下面我们以建立大堆为例。堆(优先级队列)的存储数据的方式是使用数组来存储的,先定义好数组和有效数组的大小。

    class TestHeap {
        public int[] elem;
        public int usedSize;
    
        public TestHeap(int[] elem) {
            this.elem = new int[10];
        }
    }

    然后把要建立堆的数组进行拷贝。也可以不拷贝,直接使用。拷贝好之后,从什么地方开调整?这就需要画图来看一看。把使用数组存储的数据画成完全二叉树的形式后,发现:

    直接从0下标开始的话,调整起来不方便。所以从尾节点对应的父节点开始,一步一步调整,一直到0下标。以9下标为例,它的父节点是4下标,是整个数组有效长度的一半。所以调整要从整个数组有效长度的一半开始调整。使用createHeap方法。

    class TestHeap {
        public int[] elem;
        public int usedSize;
    
        public TestHeap(int[] elem) {
            this.elem = new int[10];
        }
    
        //建堆
        public void createHeap(int[] array) {
            //拷贝过程
            for (int i = 0; i < array.length; i++) {
                this.elem[i] = array[i];
                ++this.usedSize;
            }
            //开始调整过程
            for (int p = (usedSize - 1 - 1) >>> 1; p >= 0; p--) {
                shiftDown(p, usedSize);
            }
        }
    }

    调整的过程使用一个shiftDown方法,传进去这个下标和数组的有效长度。

    shiftDown方法

    这个方法就是具体的调整方法。为了方便,我们定义parent和child变量。其中,parent就是父亲节点,child就是父亲节点对应的孩子节点,有左右孩子之分。他们的关系需要用到前面的性质。其实从图上就能看出来,以3号下标为例,它的左右孩子下标就是7和8。对于左孩子来说,和父节点的下标关系就是child = parent * 2 + 1;右孩子下标就是左孩子下标在加1。要注意的是整个的组织是按照完全二叉树的方式来组织的,调整需要知道父亲和孩子节点的下标关系。而孩子节点的下标在整个数组有效长度里面就说明存在的,比如父节点为5下标的节点。

    private void shiftDown(int root, int len) {
            int parent = root;
            int child = 1 + root << 1;
    }

    然后就开始循环调整。整个堆成为大根堆,不能只调整一部分,要调整到从根节点到叶子节点的全部。大根堆是父亲节点的值大于孩子节点。所以需要先找到孩子节点的最大值,在和父亲节点对比交换。

    进入循环的条件是只要有左孩子就可以调整了。父亲节点和孩子节点调整的前提是至少有一个孩子。进入循环后,寻找左右孩子的最大值和父亲节点交换。寻找的前提是要存在左右孩子。

    找到孩子的最大值后,就和父节点对比,交换。如果没有交换,说明已经是大根堆了,就直接结束循环。否则,就移动父节点到孩子节点位置继续调整。

    建立堆的时间复杂度是O(N)。

    private void shiftDown(int root, int len) {
            int parent = root;
            int child = 1 + root << 1;
            //进入这个循环,至少有一个孩子
            while (child < len) {
                //如果有右孩子,找到左右孩子的最大值
                if (child + 1 < len && this.elem[child] < this.elem[child + 1]) {
                    ++child;
                }
                //child一定是最大值的下标
                //进行交换
                if (this.elem[child] > this.elem[parent]) {
                    swap(elem, parent, child);
                    parent = child;
                    child = 1 + parent << 1;
                } else {
                    break;
                }
            }
        }
    
        private void swap(int[] elem, int parent, int child) {
            int tmp = this.elem[child];
            this.elem[child] = this.elem[parent];
            this.elem[parent] = tmp;
        }

    3、堆的插入

    堆的插入是把元素插入到堆的尾部,也就是数组的尾部,然后进行调整。插入的时候要判断数组元素的空间够不够,不够的话就进行扩容。因为要调整的是一条路径,需要向上调整。使用push方法。调整的方法使用shiftUp方法。

    public void push(int val) {
            //判断是不是满了
            if (isFull()) {
                elem = Arrays.copyOf(elem,2 * elem.length);
                elem[usedSize] = val;
                //开始调整
                shiftUp(usedSize);
                ++usedSize;
            }
        }
    private boolean isFull() {
            return usedSize == elem.length;
        }

    shiftUp方法

    因为整个数据按照完全二叉树的形式组织的,所以,新插入的元素要找到它的父亲节点。对于孩子节点,设下标为i,它的父亲节点下标为 ( i - 1 )/ 2。就然后进行比较交换。这里的交换可以直接使用前面的swap方法。一次比较交换完成后,还要继续比较,一直到整个路径上的元素调整好。如果没有,就结束。

    private void shiftUp(int child) {
            int parent = (child - 1) >>> 1;
            while (child > 0) {
                if (elem[child] > elem[parent]) {
                    int tmp = elem[child];
                    elem[child] = elem[parent];
                    elem[parent] = tmp;
                    child = parent;
                    parent = (child - 1) >>> 1;
                } else {
                    break;
                }
            }
        }

    4、堆的删除

    堆删除需要删除堆顶的元素,但是不能直接在堆顶上删,而是要把堆顶元素和堆尾元素交换后来删除。减少有效元素个数,同时,对堆顶元素进行向下调整。整个的过程需要保持大根堆。删除之前先要判断这个堆是不是空的堆。

    public void poll() {
            if (isEmpty()) {
                return;
            }
            int tmp = elem[0];
            elem[0] = elem[usedSize - 1];
            elem[usedSize - 1] = tmp;
            --usedSize;
            //调整
            shiftDown(0, usedSize);
        }
    
        //判断堆是不是空
        private boolean isEmpty() {
            return usedSize == 0;
        }

    从堆的插入和删除来看,堆的操作中的核心就是对堆的向上调整和向下调整方法。

    5、获取堆顶元素

    直接返回第一个元素下标就可以。如果是空的堆,就返回-1。

    public int peek() {
            if (isEmpty()) {
                return -1;
            }
            return elem[0];
        }

    6、堆排序

    要从小到大排序,需要建立大根堆,而从大到小排序,需要建立小根堆。从堆最后一个元素开始调整。每一次调整将该元素与堆顶元素交换,在向下调整。

    public void HeapSort() {
            int end = this.usedSize - 1;
            while (end > 0) {
                swap(this.elem, 0, end);
                shiftDown(0, end);
                --end;
            }
        }

    堆中最重要的几个操作都调用了向上调整方法或向下调整方法。搞明白这两个方法是关键。

    7、Java官方的实现

    Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,这里介绍PriorityQueue。

    使用这个方法的时候,要注意:

    (1)PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException异常;

    (2)不能插入null对象,否则会抛出NullPointerException;

    (3)PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素。

    PriorityQueue (Java Platform SE 7 ) (oracle.com) 正在上传…重新上传取消 https://docs.oracle.com/javase/7/docs/api/java/util/PriorityQueue.html

    PriorityQueue<Integer> queue = new PriorityQueue<Integer>();
            queue.add(3);
            queue.add(2);
            queue.add(1);
            System.out.println(queue);
    
            System.out.println("=============");
            queue.poll();
            System.out.println(queue);
    
            System.out.println("=============");
            System.out.println(queue.peek());

    要改为大堆创建的形式,需要实现接口。

    class IntCmp implements Comparator<Integer> {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    }
    
    public class Main {
    
        public static void main(String[] args) {
            PriorityQueue<Integer> queue = new PriorityQueue<Integer>(new IntCmp());
            queue.offer(3);
            queue.offer(2);
            queue.offer(1);
            System.out.println(queue);
    
            System.out.println("=============");
            queue.poll();
            System.out.println(queue);
    
            System.out.println("=============");
            System.out.println(queue.peek());
        }
  • 相关阅读:
    VUE3 element-plus源码解析之- 001 dom aria.ts 文件解析
    【英语:基础高阶_经典外刊阅读】L3.长句子扒皮—如何快速寻找主干
    操作系统面试题汇总(不定期更新)
    Mycat配置文件详解
    【Python】 Python 操作PDF文档
    JVM运行时数据区
    C++11--lambda表达式
    力扣LeatCode算法题第三题-无重复字符的最长子串
    Vue3最佳实践 第七章 TypeScript 创建Trello 任务管理器
    一遍关于vue基础语法下篇
  • 原文地址:https://blog.csdn.net/Trouvailless/article/details/125524684