• 数据结构基础(七)优先队列 PriorityQueue, 堆Heap


    来源:https://turingplanet.org/2020/03/07/%e4%bc%98%e5%85%88%e9%98%9f%e5%88%97-priorityqueue/

    在之前提到的数据结构中,如果我们想要寻找所存元素中最大值或者最小值,需要挨个查找,而本章所学的优先队列和堆会按照优先级给元素排列,帮助我们以最快的速度O(1)获取优先级最高的元素。

    优先队列 PriorityQueue

    我们先来学习优先队列,优先队列把优先级最高的元素设为链表的头节点,这样我们获取或删除优先级最高的元素只需要O(1)的时间复杂度,这么设计的代价就是牺牲插入的效率,每次插入一个新的元素时,我们都需要迭代链表,并找到合适的地方插入,这个时间复杂度往往是O(n)。

    以下是优先队列支持的操作:

    • push:插入一个新的元素
    • pop:将优先级最高的元素弹出(删除)
    • peek:查看优先级最高的值

    Java实现

    优先队列的定义和链表很相似,首先我们需要定义节点和PriorityQueue:

    public class PriorityQueue {
    
        static class Node {
            int value;
            int priority;
            Node next;
    
            public Node(int value, int priority) {
                this.value = value;
                this.priority = priority;
            }
        }
    
        Node head = null;
    }

    其中Node就是队列中的节点,包含value数组和优先级priority,如果希望数值大的节点优先级高,那么可以将pririty的数值设为和value一样。反之,我们可以将priority设为数值的相反数,那么在此队列中,一个数值越小,优先级就越高。在PriorityQueue中,我们只需要记录一个头节点head即可。以下是核心方法 push 的定义:

    public void push(int value, int priority) {
        if (head == null) {
            head = new Node(value, priority);
            return;
        }
        Node cur = head;
        Node newNode = new Node(value, priority);
        if(head.priority < priority) {
            newNode.next = head;
            this.head = newNode;
        } else {
            while(cur.next != null && cur.next.priority > priority) {
                cur = cur.next;
            }
            newNode.next = cur.next;
            cur.next = newNode;
        }
    }

    在Push中,我们需要检查是否头节点为空,如果是,那么就将新的节点设置为头节点。不然我们迭代循环头节点,将新的节点插入一个特定位置,插入后之前节点的优先级都比新节点高,之后节点的优先级都比新节点小。接下来实现的两个方法peek和pop很简单:

    public Node peek() {
        return head;
    }
    
    public Node pop() {
        if (head == null) {
            return null;
        }
        Node temp = head;
        head = head.next;
        return temp;
    }

    peek只需要返回头节点即可。pop只需要弹出head的值,并让head指向自己的下一个节点即可。最后的方法isEmpty只需要查看head是否为空:

    public boolean isEmpty() {
        return head == null;
    }

    以上就是优先队列的定义,以下是优先队列操作的复杂度分析:

    • push: O(n)
    • pop: O(1)
    • peek: O(1)

    接下来我们来学习另一个与优先级相关的数据结构:栈(heap)。

    堆 Heap

    堆(Heap)是一种特殊的平衡二叉树,堆中的节点满足以下的条件:一个节点的父节点优先级比自己高,而自己的子节点优先级比自己底。优先级可以根据数值的大小来决定。最常见的堆有以下两种类型:

    • 最大堆(Max Heap):最大堆中根节点数值最大,所有父节点的数值比各自的子节点数值大。
    • 最小堆(Min Heap):根节点数值最小, 父节点数值比其子节点数值小。
    Max Heap
    Min Heap

    最大堆(Max Heap)

    以下是最大堆支持的操作

    • add: 将新元素插入堆
    • poll: 将根节点(数值最大的元素)删除
    • peek: 获取根节点的数值

    在任何的时间点,最大堆都应该保持其特性:父节点的数值比所有子节点大。在插入新元素的时候,我们要遵循以下的步骤:

    1. 在堆的最后新建一个节点。
    2. 将数值赋予新节点。
    3. 将其节点和父节点比较。
    4. 如果新节点的数值比父节点大,调换父子节点的位置。
    5. 重复步骤3和4直到最大堆的特性被满足。

    以下是删除根节点的步骤:

    1. 移除根节点
    2. 将最后的元素移到根节点处
    3. 将子节点和父节点比较
    4. 如果父节点的数值比子节点小,替换父子节点
    5. 重复步骤3和4直到最大堆的特性被满足。

    以下是删除根节点44的动图演示:

    实现方式:Array

    虽然堆看起来非常像树,但是我们不用链表式的节点来实现。因为堆的特殊属性,我们可以直接使用数组来实现。以下是最大堆heap的基本定义:

    public class MaxHeap {
    
        private int capacity;
        private int size = 0;
        private int[] array;
    
        public MaxHeap(int capacity) {
            this.capacity = capacity;
            this.array = new int[capacity];
        }
    }

    其中capacity记录了可承载的节点数量,array就是用来储存节点的数组。而以下是一些有用的helper methods:

    private int getLeftChildIndex(int parentIndex) {
        return 2 * parentIndex + 1;
    }
    
    private int getRightChildIndex(int parentIndex) {
        return 2 * parentIndex + 2;
    }
    
    private int getParentIndex(int childIndex) {
        return (childIndex - 1) / 2;
    }
    
    private boolean hasLeftChild(int index) {
        return getLeftChildIndex(index) < size;
    }
    
    private boolean hasRightChild(int index) {
        return getRightChildIndex(index) < size;
    }
    
    private boolean hasParent(int index) {
        return getParentIndex(index) >= 0;
    }
    
    private int leftChild(int parentIndex) {
        return array[getLeftChildIndex(parentIndex)];
    }
    
    private int rightChild(int parentIndex) {
        return array[getRightChildIndex(parentIndex)];
    }
    
    private int parent(int childIndex) {
        return array[getParentIndex(childIndex)];
    }

    以上的方法看方法名应该就懂了,getLeftChildIndex就是拿到一个父节点左孩子节点的数组位置,hasLeftChild查看一个节点是否拥有一个不为空的左孩子,leftChild就是拿到左孩子的数值。其他方法以此类推,应该很好理解,我就不过多解释了。接下来是核心函数 add 的实现:

    public void add(int item) { // Time Complexity: O(logN)
        if(size == capacity) {
            array = Arrays.copyOf(array, capacity * 2);
            capacity = capacity * 2;
        }
        array[size] = item;
        size++;
        heapifyUp();
    }
    
    private void heapifyUp() {
        int index = size - 1;
        while (hasParent(index) && parent(index) < array[index]) {
            swap(getParentIndex(index), index);
            index = getParentIndex(index);
        }
    }

    在 add 中,我们首先查看数组是否为空,如果为空,则扩展数组的容量。不然就将新元素插入最后一个位置,然后heapifyUp新元素,heapifyUp的作用就是将元素不断与父节点对比,直到将新元素排到一个位置,它的父节点比他大,子节点都比他小。再来看一个核心函数poll:

    public void poll() { // Time Complexity: O(logN)
       if (size == 0) {
           throw new NoSuchElementException();
       }
       int element = array[0];
       array[0] = array[size - 1];
       size--;
       heapifyDown();
    }
    
    private void heapifyDown() {
        int index = 0;
        while (hasLeftChild(index)) {
            int largerChildIndex = getLeftChildIndex(index);
            if (hasRightChild(index) && rightChild(index) > leftChild(index)) {
                largerChildIndex = getRightChildIndex(index);
            }
            if (array[index] < array[largerChildIndex]) {
                swap(index, largerChildIndex);
            } else {
                break;
            }
            index = largerChildIndex;
        }
    }

    在poll中,我们将最后一个元素放到第一位,然后对第一位元素进行heapifyDown,其作用就是将此元素与子节点比较,然后插入到一个位置,其父节点都比此节点大,子节点都比其节点小。最后是peek的实现:

    public int peek() {
        if (size == 0) {
            throw new NoSuchElementException();
        }
        return array[0];
    }

    peek的实现很简单,只需要拿取第一个元素即可。 以下是各个操作的复杂度分析:

    • add: O(logN)
    • poll: O(logN)
    • peek: O(1)

    这就是最大堆的实现啦,最小堆只需要将实现中有关大小比对的逻辑取反即可。

    LeetCode题目和源代码

    Git源代码链接

  • 相关阅读:
    随笔1
    模块
    文件输入流
    一次javaweb登陆实验
    s7八进4
    jdbc连接数据库
    自己找茬
    用栈来求解汉诺塔问题
    用一个栈实现另一个栈的排序
    猫狗队列
  • 原文地址:https://www.cnblogs.com/qiu-hua/p/14880255.html
Copyright © 2020-2023  润新知