• 数据结构基础(七)优先队列 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源代码链接

  • 相关阅读:
    Notes of Daily Scrum Meeting(12.18)
    Notes of Daily Scrum Meeting(12.17)
    Notes of Daily Scrum Meeting(12.16)
    Notes of Daily Scrum Meeting(12.8)
    Notes of Daily Scrum Meeting(12.5)
    Notes of Daily Scrum Meeting(12.3)
    Notes of Daily Scrum Meeting(11.12)
    Linux中profile、bashrc、bash_profile之间的区别和联系
    Linux GCC编译
    mysql 5.7.16 远程连接
  • 原文地址:https://www.cnblogs.com/qiu-hua/p/14880255.html
Copyright © 2020-2023  润新知