Java data structure: use of heap and PriorityQueue priority queue


1 What is a heap

 A heap is essentially a full binary tree with some tweaks. In Java's PriorityQueuepriority queue, the bottom layer is a structure such as a heap. Therefore, we try to better understand the priority queue by simulating the implementation of the heap.

Knowledge Supplement: What is a complete binary tree?
Answer: A binary tree with a depth of k and n nodes, the nodes in the tree are numbered from top to bottom and from left to right, if the number is i (1≤i≤n) If the node has the same position in the binary tree as the node number i in the full binary tree, then this binary tree is called a complete binary tree.

  For a set of key codes K = {k0, k1, k2, k3, …, kn-1}, if for all k, ki >= k2i+1 and ki >= k2i+2, then The heap is a big root heap. On the contrary, it is called a small root pile.
 The most intuitive feeling is that for a large root heap, its root node is the largest; for a small root heap, its root node is the smallest.

Let's take sequence: {3, 7, 4, 2, 1, 8, 9, 10, 5}as an example to construct a big root heap as shown in the figure below:
insert image description here
In the figure, the red serial number is the array subscript corresponding to the element, numbered sequentially from left to right, top to bottom, starting from 0. Observing the heap, the following characteristics can be summarized:

  • The value of a certain node of the heap is always not less than or not greater than the value of the parent node;
  • The heap is always a complete binary tree.

In this article, we will take the big root heap as an example to briefly describe the idea of ​​constructing the big root heap, and use the Java language to implement it.


2 heap implementation ideas

2.1 Introduction to the member variables of the big root heap

Specifically, you can see the code comments in the figure. The author's implementation of the big root heap is encapsulated in the BigHeap class.
insert image description here

2.2 Relevant Knowledge Review of Trees

 For the heap, it is a slight adjustment to the complete binary tree, and if we use the sequential storage structure to represent the heap, then the parent node and the child node have the following properties. In this article, we agree that all parent nodes Use parent to refer to, all child nodes, use child to refer to. And as we described in the first part, the numbering starts from 0 from top to bottom and from left to right.

  • For the parent node parent, the numbers of its left child and its child are 2 * parent + 1, 2 * parent + 2;
  • For the child node child, its parent node satisfies parent = (child - 1)/2;
  • For the above two properties, it is necessary to satisfy the boundary range numbered between 0 and useSize-1.

2.3 Adjust downward to create a large root pile

So for a sequence: {3, 7, 4, 2, 1, 8, 9, 10, 5}, how do we convert it into a large root heap?

The idea is as follows:

  1. The sequence is numbered from left to right, from top to bottom, and after constructing a complete binary tree, as shown in the figure, all blue circles are parent nodes;
    insert image description here

  2. The serial number of the last parent node satisfies (useSize-1-1)/2, we start from the last parent node and adjust forward in turn, and we adopt the downward adjustment strategy for each subtree;

  3. Starting from the subtree of the purple box corresponding to 2, judge the size relationship between the parent and the values ​​of the left and right children, compare the larger value in the child node with the parent node, and exchange if child > parent is satisfied, as shown in the figure As shown, at this time, the subtree with 2 as the root node has been adjusted to a large root heap;insert image description here

  4. Continue to look for the parent node forward, adjust the subtree with 4 as the root node, and convert it into a large root pile, as shown in the figure;
    insert image description here

  5. Adjust the subtree with 7 as the root node, and adjust it down to a large root heap. After exchanging 7 and 10, the new subtree with 7 as the root node also needs to be adjusted to a large root heap, but since it already satisfies the large root heap, so no exchange
    insert image description here

  6. Finally, adjust the tree with 3 as the root node, and adjust it into a large root heap, that is, convert the sequence into a large root heap.
    insert image description here

The relevant code is as follows:

    // 根据给定的数组向下调整构建大根堆 时间复杂度O(n)
    public void creadHeap(int[] data){
    
    
        // 依次向下调整初始数组的每一个元素 构建成堆
        // 从后往前找父节点
        this.elements = data;
        this.useSize = data.length;
        for (int parent = (useSize-1-1) / 2; parent >= 0; parent--) {
    
    
            siftDown(parent, useSize);
        }
    }

    // 向下调整
    // parent为要调整的父节点 len为每颗子树的调整范围
    private void siftDown(int parent, int len){
    
    
        int child = 2 * parent + 1; // 找到左孩子
        while (child < len){
    
    
            if (child + 1 < len && elements[child+1] > elements[child]){
    
    
                child = child + 1; // 保证child指向的位置一定是子节点中最大的 再与parent进行比较
            }
            if (elements[child] > elements[parent]){
    
    
                swap(elements, child, parent);
                parent = child; // 继续向下调整
                child = 2 * parent + 1;
            }else {
    
    
                break;
            }
        }
    }

2.4 Heap insertion

The idea is as follows:

  • Elements are placed at the end of the heap first, after the last child. From the perspective of the elements array, put the newly inserted val into the position of elements[useSize] (you need to pay attention to whether expansion is required);
  • Compare the newly inserted node with the parent node in turn, and adjust upward until it is compared with the parent whose position is 0, and the adjustment is completed. At this time, the new heap after inserting the new node also satisfies the large root heap.

Let's take inserting a new value val = 99 as an example. The schematic diagram is as follows:
insert image description here
It can be found that each time only the newly inserted node needs to be compared with the parent.

Why don't you need to compare with sibling nodes and select a larger value to exchange with the parent node?

Because, before each new val is inserted, the existing elements satisfy the nature of the large root heap, that is to say, for each subtree, its parent node is the maximum value, so there is no need to compare with sibling nodes compared. If the newly inserted node is larger than the parent, it must be larger than the sibling node!

The relevant code is as follows:

    // 判断堆是否为满
    public boolean isFull(){
    
    
        return useSize == elements.length;
    }

    // 入堆
    public void offer(int val){
    
    
        if (isFull()){
    
    
            elements = Arrays.copyOf(elements, elements.length + DEFAULT_CAPACITY); // 扩容
        }
        elements[useSize++] = val;
        // 向上调整
        siftUp(useSize-1);
    }

    // 向上调整 用于插入元素 每次插入元素放入数组最后的位置(注意容量) 然后向上调整重新构造
    private void siftUp(int child){
    
    
        // 依次与parent比较 若比parent大 则交换
        int parent = (child - 1) / 2;
        while (parent >= 0){
    
    
            if (elements[parent] < elements[child]){
    
    
                swap(elements, parent, child);
                child = parent;
                parent = (child - 1) / 2;
            }else {
    
    
                break;
            }
        }
    }

2.5 Heap deletion

As for the deletion of the heap, we agree that the deletion must be the top element of the heap !

The idea is as follows:

  • Exchange the top element with the last element;
  • The effective data of the heap is reduced by one, that is, useSize = useSize - 1;
  • The top element of the heap is adjusted downwards so that the result satisfies the large root heap.

Let's take deleting val = 99 as an example, the schematic diagram is as follows:

insert image description here
The relevant code is as follows:

    // 判断是否为空
    public boolean isEmpty(){
    
    
        return useSize == 0;
    }

    // 删除堆顶元素 让堆顶元素和最后一个值替换 useSize-- 并且重新向下调整以堆顶元素起始的堆
    public int pop(){
    
    
        if (isEmpty()){
    
    
            throw new RuntimeException("堆为空");
        }
        int ret = elements[0];
        swap(elements, 0, useSize-1);
        useSize--;
        // 向下调整
        siftDown(0, useSize);
        return ret;
    }

3 Dagenheap implementation code and test

In the specific implementation code, the author additionally implemented the TopK problem. The specific ideas can be found in the code comments, and the oj link is attached: the minimum k number , readers can practice by themselves.

import java.util.Arrays;
import java.util.Comparator;
import java.util.PriorityQueue;
import java.util.Queue;

/**
 * @author 兴趣使然黄小黄
 * @version 1.0
 * 大根堆
 */
@SuppressWarnings({
    
    "all"})
public class BigHeap {
    
    

    private int[] elements; // 存储堆的元素

    private int useSize; // 堆的大小

    private final int DEFAULT_CAPACITY = 10; // 默认容量

    public BigHeap(){
    
    
        this.elements = new int[DEFAULT_CAPACITY]; // 默认初始堆的大小为10
        this.useSize = 0;
    }

    // 根据给定的数组向下调整构建大根堆 时间复杂度O(n)
    public void creadHeap(int[] data){
    
    
        // 依次向下调整初始数组的每一个元素 构建成堆
        // 从后往前找父节点
        this.elements = data;
        this.useSize = data.length;
        for (int parent = (useSize-1-1) / 2; parent >= 0; parent--) {
    
    
            siftDown(parent, useSize);
        }
    }

    // 向下调整
    // parent为要调整的父节点 len为每颗子树的调整范围
    private void siftDown(int parent, int len){
    
    
        int child = 2 * parent + 1; // 找到左孩子
        while (child < len){
    
    
            if (child + 1 < len && elements[child+1] > elements[child]){
    
    
                child = child + 1; // 保证child指向的位置一定是子节点中最大的 再与parent进行比较
            }
            if (elements[child] > elements[parent]){
    
    
                swap(elements, child, parent);
                parent = child; // 继续向下调整
                child = 2 * parent + 1;
            }else {
    
    
                break;
            }
        }
    }

    // 判断堆是否为满
    public boolean isFull(){
    
    
        return useSize == elements.length;
    }

    // 入堆
    public void offer(int val){
    
    
        if (isFull()){
    
    
            elements = Arrays.copyOf(elements, elements.length + DEFAULT_CAPACITY); // 扩容
        }
        elements[useSize++] = val;
        // 向上调整
        siftUp(useSize-1);
    }

    // 向上调整 用于插入元素 每次插入元素放入数组最后的位置(注意容量) 然后向上调整重新构造
    private void siftUp(int child){
    
    
        // 依次与parent比较 若比parent大 则交换
        int parent = (child - 1) / 2;
        while (parent >= 0){
    
    
            if (elements[parent] < elements[child]){
    
    
                swap(elements, parent, child);
                child = parent;
                parent = (child - 1) / 2;
            }else {
    
    
                break;
            }
        }
    }

    // 判断是否为空
    public boolean isEmpty(){
    
    
        return useSize == 0;
    }

    // 删除堆顶元素 让堆顶元素和最后一个值替换 useSize-- 并且重新向下调整以堆顶元素起始的堆
    public int pop(){
    
    
        if (isEmpty()){
    
    
            throw new RuntimeException("堆为空");
        }
        int ret = elements[0];
        swap(elements, 0, useSize-1);
        useSize--;
        // 向下调整
        siftDown(0, useSize);
        return ret;
    }

    // 交换arr的i j位置值
    private void swap(int[] arr, int i, int j){
    
    
        int temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    // 求前k个最大的值 则构造有k个元素的最小堆 每次入堆的时候和堆顶进行比较 若更大 则替换 最终剩下的就是前k个最大的值
    public int[] maxK(int[] arr, int k) {
    
    
        int[] ret = new int[k];
        // 合法性检验
        if(arr == null || k == 0) {
    
    
            return ret;
        }
        if(arr.length >= k){
    
    
            ret = Arrays.copyOf(arr, k);
            return ret;
        }
        Queue<Integer> minHeap = new PriorityQueue<>(k);
        //1、遍历数组的前K个 放到堆当中
        for(int i = 0; i < k; i++){
    
    
            minHeap.offer(arr[i]);
        }
        //2、遍历剩下的K-1个,每次和堆顶元素进行比较
        for (int i = k; i < arr.length; i++) {
    
    
            if (arr[i] > minHeap.peek()) {
    
    
                minHeap.poll(); // 出堆顶后添加
                minHeap.offer(arr[i]);
            }
        }
        //3、存储结果
        for (int i = 0; i < k; i++) {
    
    
            ret[i] = minHeap.poll();
        }
        return ret;
    }

    // 求前k个最小的值 构造大顶堆 如果新值比堆顶还要小 则替换 重新构造堆
    public int[] smallestK(int[] arr, int k) {
    
    
        int[] ret = new int[k];
        // 合法性检验
        if (arr == null || k <= 0){
    
    
            return ret;
        }
        if (k >= arr.length){
    
    
            ret = Arrays.copyOf(arr, k);
        }
        // 构造大顶堆
        Queue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
    
    
            @Override
            public int compare(Integer o1, Integer o2) {
    
    
                return o2 - o1;
            }
        });
        // 先将前k个值入堆
        for (int i = 0; i < k; i++) {
    
    
            maxHeap.offer(arr[i]);
        }
        // 后判断剩余n-k个元素 如果比堆顶还要小 则替换
        for (int i = k; i < arr.length; i++) {
    
    
            if (arr[i] < maxHeap.peek()){
    
    
                maxHeap.poll();
                maxHeap.offer(arr[i]);
            }
        }
        // 存储并返回结果
        for (int i = 0; i < k; i++) {
    
    
            ret[i] = maxHeap.poll();
        }
        return ret;
    }

    // 以数组方式输出堆
    public void showHeap(){
    
    
        for (int i = 0; i < useSize; i++) {
    
    
            System.out.print(elements[i] + " ");
        }
        System.out.println();
    }
}

The test code and test results are shown in the figure:
insert image description here


4 Use of PriorityQueue

4.1 Feature Introduction

 The Java collection framework provides two types of priority queues, PriorityQueue and PriorityBlockingQueue. PriorityQueue is thread-unsafe, and PriorityBlockingQueue is thread-safe. Its inheritance system is as follows:
insert image description here
A few notes:

  1. The elements placed in the PriorityQueue must be able to compare in size, and objects that cannot be compared in size cannot be inserted, otherwise a ClassCastException will be thrown
  2. A null object cannot be inserted, otherwise a NullPointerException will be thrown
  3. The time complexity of inserting and deleting elements is O(logn)
  4. The bottom layer of PriorityQueue uses a heap data structure
  5. PriorityQueue is a small heap by default

4.2 Common methods

Commonly used construction methods are as follows:
insert image description here
Commonly used methods are as follows:
insert image description here
For other methods not listed, readers can refer to the help documentation by themselves.

4.3 Using PriorityQueue to implement a large root heap

 Just mentioned, by default, PriorityQueue implements a small root heap, so how to implement a large root heap?
 In fact, you only need to pass in the comparator and change the comparison rules. In the following code sample, the author implements a priority queue storing Integer objects in the form of a big root heap:

        // 构造大顶堆
        Queue<Integer> maxHeap = new PriorityQueue<>(new Comparator<Integer>() {
    
    
            @Override
            public int compare(Integer o1, Integer o2) {
    
    
                return o2 - o1;
            }
        });
        
        // 构造大顶堆 lambda
        Queue<Integer> maxHeap2 = new PriorityQueue<>(((o1, o2) -> {
    
    
            return o2.compareTo(o1);
        }));

write at the end

This article is included in the Java data structure点击订阅专栏 and is being continuously updated.
 Creation is not easy, if you have any questions, welcome to private message, thank you for your support!

insert image description here

Guess you like

Origin blog.csdn.net/m0_60353039/article/details/128661213