Heap and heap sorting

Reference resources:

Introduction to Heap

I. Concepts and Introduction

         Heap is a general term for a special kind of data structure in computer science.

         A heap is usually an array object that can be viewed as a complete binary tree.

2. Nature:

  • The value of a node in the heap is always no greater or less than the value of its parent node.
  • A heap is always a complete binary tree.

3. Maximum heap and minimum heap

        Maximum heap: A binary heap is a complete binary tree, and The value of a node in the heap is always not greater than the value of its parent node. The depth of the complete binary tree is K. Except for layer k, the number of nodes in all other layers (1-k-1) reaches the maximum number, and all nodes in layer K are continuously concentrated on the leftmost side.
        Minimum heap: A binary heap is a complete binary tree, and The value of a node in the heap is always not less than the value of its parent node. The depth of the complete binary tree is K. Except for layer k, the number of nodes in all other layers (1-k-1) reaches the maximum number, and all nodes in layer K are continuously concentrated on the leftmost side.

4. Pre-specification

  • Except for special instructions, the following code takes the maximum heap as an example
  • The following implementations of heaps are stored through a normal array.
  • Assume the heap volume is n, that is, the array length is n
  • If the index of the current node is i, where I <[0,n], then if there are left and right subnodes for the current node, the index of the left subnode is 2*i+1, and the index of the right subnode is 2*i+1
  • i is the int type of Java code
  • If the last level of a binary heap (a complete binary tree) is a leaf node, then the index of the last non-leaf node i n the second-last layer (a heap) is I = [(n-1) -1] / 2    <==  2 * i + 1 = n - 1   && It does not matter whether the last node is a right node or not, which is verified.

5. Shift up of heap

        When elements are added to a (max/min) heap, to maintain the (max/min) heap's characteristics, they are called shift up.

        Purpose: To add elements to the heap.

        Algorithmic steps:
  1.   Add the newly added element to the end of the last layer (from left to right);
  2.   Float Up by Comparing with Parent Node
/**
* @author wkn
* @create 2021-11-21 17:02
*/
public class HeapStructure {
    public static void main(String[] args) {
        Heap heap =  new Heap(11);
        heap.insert(16);
        heap.insert(15);
        heap.insert(17);
        heap.insert(19);
        heap.insert(13);
        heap.insert(22);
        heap.insert(28);
        heap.insert(30);
        heap.insert(41);
        heap.insert(62);
        heap.insert(52);
        heap.show();
    }
}


/**
* Build maximum heap
*/
class Heap{
    private int capacity;//Volume of heap
    private int count;//Number of elements in the current heap = count + 1
    private int[] data;

    public Heap(int capacity){  //Used to add an element to build a heap
        this.capacity = capacity;
        data = new int[capacity];
        count = -1;
    }

    public Heap(int[] arr){ //Used to build all elements directly into a heap at once
        data = arr;
    }

    // Returns the number of elements in the heap
    public int size(){
        return count + 1;
    }

    // Returns a Boolean value indicating whether the heap is empty
    public boolean isEmpty(){
        return count == -1;
    }

    public void show(){
        for (int val : data) {
            System.out.println(val);
        }
    }

    /**
     * Two elements indexed i and j in the swap heap
     * @param i
     * @param j
     */
    public void swap(int i, int j){
        int temp = data[i];
        data[i] = data[j];
        data[j] = temp;
    }

    /**
     * For an insertion element
     * @param chiledIndex Last inserted element number with parent node number [(k-1)/2]
     */
    public void shiftUp(int chiledIndex){


        //When a chiledIntex node has a parent node, it is determined whether the maximum heap requirement is met and the correct chiledIndex insertion location is found
        int temp = data[chiledIndex];
        while(chiledIndex > 0 && data[(chiledIndex - 1) / 2] < temp){
            data[chiledIndex] = data[(chiledIndex - 1) / 2];
            chiledIndex = (chiledIndex - 1) / 2;
        }


        data[chiledIndex] = temp;
    }

    public void insert(int element){
        if(count < capacity){   //Determine if the heap is full
            count++;
            data[count] = element;
            shiftUp(count);
        }else{
            throw new RuntimeException("Maximum heap is full!");
        }
    }
}

6. Shift down of heap

         After removing the root node from the heap and moving the last leaf node to the root node, the operation on the last leaf node to move to the root node in order for the full binary tree to satisfy the (maximum/minimum) heap characteristics is called shift down.
        Objective: To remove the root node.
/**
* @author wkn
* @create 2021-11-21 17:02
*/
public class HeapStructure {
    public static void main(String[] args) {
        Heap heap =  new Heap(11);
        heap.insert(16);
        heap.insert(15);
        heap.insert(17);
        heap.insert(19);
        heap.insert(13);
        heap.insert(22);
        heap.insert(28);
        heap.insert(30);
        heap.insert(41);
        heap.insert(62);
        heap.insert(52);
        heap.show();
        System.out.println("***************");
        int root = heap.extractRoot();
        System.out.println("The root node is:"+ root);
        heap.show();
    }
}


/**
* Build maximum heap
*/
class Heap{
    private int capacity;//Volume of heap
    private int count;//Number of elements in the current heap = count + 1
    private int[] data;

    public Heap(int capacity){  //Used to add an element to build a heap
        this.capacity = capacity;
        data = new int[capacity];
        count = -1;
    }

    public Heap(int[] arr){ //Used to build all elements directly into a heap at once
        data = arr;
    }

    // Returns the number of elements in the heap
    public int size(){
        return count + 1;
    }

    // Returns a Boolean value indicating whether the heap is empty
    public boolean isEmpty(){
        return count == -1;
    }

    public void show(){
        for (int i = 0; i <= count; i++) {
            System.out.println(data[i]);
        }
    }

    /**
     * Two elements indexed i and j in the swap heap
     * @param i
     * @param j
     */
    public void swap(int i, int j){
        int temp = data[i];
        data[i] = data[j];
        data[j] = temp;
    }

    /**
     * For an insertion element
     * @param chiledIndex Last inserted element number with parent node number [(k-1)/2]
     */
    public void shiftUp(int chiledIndex){
        //When a chiledIntex node has a parent node, it is determined whether the maximum heap requirement is met and the correct chiledIndex insertion location is found
        int temp = data[chiledIndex];
        while(chiledIndex > 0 && data[(chiledIndex - 1) / 2] < temp){
            data[chiledIndex] = data[(chiledIndex - 1) / 2];
            chiledIndex = (chiledIndex - 1) / 2;
        }


        data[chiledIndex] = temp;
    }

    public void insert(int element){
        if(count < capacity){   //Determine if the heap is full
            count++;
            data[count] = element;
            shiftUp(count);
        }else{
            throw new RuntimeException("Maximum heap is full!");
        }
    }

    public int extractRoot(){//Extract the root node of the maximum heap, which is the maximum
        if(count >= 0){
            int root = data[0];
            swap(0, count);
            count--;
            shiftDown(0);
            return root;
        }else{
            throw new RuntimeException("The current maximum heap is empty!");
        }
    }

    /**
     * Starting at the root node, the total sinkable (2*rootIndex + 1) <= count layer
     * @param rootIndex
     */
    public void shiftDown(int rootIndex){
        int temp = data[rootIndex];
        /*System.out.println(temp);
        System.out.println(data[count]);
        System.out.println(data[count+1]);*/
        while((2*rootIndex + 1) <= count){
            //Find the index location of the largest child node of data[rootIndex]
            int childIndex = 2*rootIndex + 1;//Left node index <==initialized as data[rootIndex] cannot be inserted beyond count
            if(2 * rootIndex + 2 <= count){
                childIndex = data[2 * rootIndex + 1] > data[2 * rootIndex + 2] ? 2 * rootIndex + 1 : 2 * rootIndex + 2;
            }
            if(temp >= data[childIndex]){
                break;//Location of insertion found
            }
            data[rootIndex] = data[childIndex];
            rootIndex = childIndex;
        }
        data[rootIndex] = temp;
    }
}

7. heapify of ordinary arrays

        Previously, the heap was constructed by invoking insert methods on a data-by-data basis and inserting shift UPS into the heap one by one, with a time complexity of O(nlogn).
         Objective: To construct a binary heap directly from an ordinary array.

         A complete binary tree has an important property:

        Suppose there are n nodes i n total and the current node number is I (i < 0, 1, 2...., n - 1)

  • If the current node has left and right children, the number of the left node is (2 * i + 1) and the number of the right node is (2 * i + 2).
  • The number of the last non-leaf node is: [(n-1) - 1] / 2
        Algorithms: From the last non-leaf node to the root node, sink in turn
/**
* @author wukangning
* @create 2021-11-21 17:02
*/
public class HeapStructure {
    public static void main(String[] args) {
        Heap heap =  new Heap(11);
        heap.insert(16);
        heap.insert(15);
        heap.insert(17);
        heap.insert(19);
        heap.insert(13);
        heap.insert(22);
        heap.insert(28);
        heap.insert(30);
        heap.insert(41);
        heap.insert(62);
        heap.insert(52);
        heap.show();
        System.out.println("***************");
        int root = heap.extractRoot();
        System.out.println("The root node is:"+ root);
        heap.show();
        System.out.println("***************");
        Heap heap2 = new Heap(new int[]{1, 2, 3, 4, 5, 6, 7, 8});
        heap2.show();
    }
}

/**
* Build maximum heap
*/
class Heap{
    private int capacity;//Volume of heap
    private int count;//Number of elements in the current heap = count + 1
    private int[] data;

    public Heap(int capacity){  //Used to add an element to build a heap
        this.capacity = capacity;
        data = new int[capacity];
        count = -1;
    }

    public Heap(int[] arr){ //Used to build all elements directly into a heap at once
        data = arr;
        this.count = arr.length - 1;
        this.capacity = arr.length;
        heapify();
    }

    // Returns the number of elements in the heap
    public int size(){
        return count + 1;
    }

    // Returns a Boolean value indicating whether the heap is empty
    public boolean isEmpty(){
        return count == -1;
    }

    public void show(){
        for (int i = 0; i <= count; i++) {
            System.out.println(data[i]);
        }
    }

    /**
     * Two elements indexed i and j in the swap heap
     * @param i
     * @param j
     */
    public void swap(int i, int j){
        int temp = data[i];
        data[i] = data[j];
        data[j] = temp;
    }

    /**
     * For an insertion element
     * @param chiledIndex Last inserted element number with parent node number [(k-1)/2]
     */
    public void shiftUp(int chiledIndex){


        //When a chiledIntex node has a parent node, it is determined whether the maximum heap requirement is met and the correct chiledIndex insertion location is found
        int temp = data[chiledIndex];
        while(chiledIndex > 0 && data[(chiledIndex - 1) / 2] < temp){
            data[chiledIndex] = data[(chiledIndex - 1) / 2];
            chiledIndex = (chiledIndex - 1) / 2;
        }

        data[chiledIndex] = temp;
    }

    public void insert(int element){
        if(count < capacity){   //Determine if the heap is full
            count++;
            data[count] = element;
            shiftUp(count);
        }else{
            throw new RuntimeException("Maximum heap is full!");
        }
    }

    public int extractRoot(){//Extract the root node of the maximum heap, which is the maximum
        if(count >= 0){
            int root = data[0];
            swap(0, count);
            count--;
            shiftDown(0);
            return root;
        }else{
            throw new RuntimeException("The current maximum heap is empty!");
        }
    }

    /**
     * Starting at the root node, the total sinkable (2*rootIndex + 1) <= count layer
     * @param rootIndex
     */
    public void shiftDown(int rootIndex){
        int temp = data[rootIndex];
        /*System.out.println(temp);
        System.out.println(data[count]);
        System.out.println(data[count+1]);*/
        while((2 * rootIndex + 1) <= count){
            //Find the index location of the largest child node of data[rootIndex]
            int childIndex = 2*rootIndex + 1;//Left node index <==initialized as data[rootIndex] cannot be inserted beyond count
            if(2 * rootIndex + 2 <= count){
                childIndex = data[2 * rootIndex + 1] > data[2 * rootIndex + 2] ? 2 * rootIndex + 1 : 2 * rootIndex + 2;
            }

            if(temp >= data[childIndex]){
                break;
            }

            data[rootIndex] = data[childIndex];
            rootIndex = childIndex;
        }
        data[rootIndex] = temp;
    }

    public void heapify(){
        for(int i = (count - 1) / 2; i >= 0; i--){
            shiftDown(i);
        }
    }    
}

With headify, normal data can be built into a maximum heap, resulting in the maximum value of the array.

Extend again:

         Assuming the length of the array is n, the root node of the largest heap can be sorted by extracting it n times (normal heap sorting): adding extra space to store the root node of each extract can make the sorting descending. (Instead of writing code, it's easy to define an additional array to store the maximum value for each extraction).

8. In-place heap sorting

         Optimize normal heap sorting, use in-place heap sorting, and achieve heap sorting without opening up additional space.
        The idea is that for a maximum heap, first the start position data is exchanged with the end value of the array, then the end of the array is the maximum element, then the root node is shiftdowned to regenerate the maximum heap, and then the newly generated maximum number is exchanged with the second-last position of the entire array, where the second-last data is everywhere. This process is analogous.
        My understanding:
  • For the largest heap, in-place heap sorting implements ascending order.
  • For the smallest heap, in-place sorting implements descending order.
/**
* In-place heap sorting (optimization of base heap sorting without taking up additional storage space)
* @author wukangning
* @create 2021-11-21 21:31
*/
public class HeapSort {
    public static void main(String[] args){
        int[] data = new int[]{5,4,3,2,1,0};
        heapAscSort(data);//Sort ascending
        show(data);
        System.out.println("*******************");
        heapDescSort(data);//Sort in descending order
        show(data);
    }

    public static void show(int[] data){
        for (int val : data) {
            System.out.print(val + " ");
        }
    }

    /**
     * Implement an ascending sort of arr arrays
     * @param data
     */
    public static void heapAscSort(int[] data){
        int length = data.length;

        maxHeadify(data, 0, length - 1);
        //show(data);

        for(int i = length - 1; i > 0; i--){
            swap(data, 0, i);

            minShiftDown(data, 0, 0, i - 1);
        }
    }

    /**
     * Implement descending sort of arr array
     * @param data
     */
    public static void heapDescSort(int[] data){
        int length = data.length;

        minHeadify(data, 0, length - 1);
        //show(data);

        for(int i = length - 1; i > 0; i--){
            swap(data, 0, i);
            maxShiftDown(data, 0, 0, i - 1);
        }
    }

    /**
     * Build maximum heap
     * @param data
     */
    public static void maxHeadify(int[] data, int start, int end){
        int length = end - start + 1;
        int lastNoLeafNodeIndex = (length / 2) -1;
        for(int i = lastNoLeafNodeIndex; i >= 0; i--){
            minShiftDown(data, i, start, end);
        }
    }

    /**
     * Build minimum heap
     * @param data
     */
    public static void minHeadify(int[] data, int start, int end){
        int length = end - start + 1;
        int lastNoLeafNodeIndex = (length / 2) -1;
        for(int i = lastNoLeafNodeIndex; i >= 0; i--){
            maxShiftDown(data, i, start, end);
        }
    }

    /**
     * To restore the correct maximum heap after extracting the root node: smaller values sink
     * @param rootIndex
     */
    public static void minShiftDown(int[] data, int rootIndex, int start, int end){
        int temp = data[rootIndex];
        int length = end - start + 1;
        while(2 * rootIndex + 1 <= length - 1){
            int childIndex = 2 * rootIndex + 1;
            if(2 * rootIndex + 2 <= length - 1){
                childIndex = data[childIndex] >= data[2 * rootIndex + 2] ? childIndex : 2 * rootIndex + 2;
            }
            if(temp >= data[childIndex]){
                break;
            }
            data[rootIndex] = data[childIndex];
            rootIndex = childIndex;
        }
        data[rootIndex] = temp;
    }

    /**
     * To restore the correct minimum heap after extracting the root node: the larger the value, the sink
     * @param rootIndex
     */
    public static void maxShiftDown(int[] data, int rootIndex, int start, int end){

        int temp = data[rootIndex];
        int length = end - start + 1;
        while(2 * rootIndex + 1 <= length - 1){
            int childIndex = 2 * rootIndex + 1;
            if(2 * rootIndex + 2 <= length - 1){//If there is a right node
                childIndex = data[childIndex] <= data[2 * rootIndex + 2] ? childIndex : 2 * rootIndex + 2;
            }
            if(temp <= data[childIndex]){
                break;
            }
            data[rootIndex] = data[childIndex];
            rootIndex = childIndex;
        }
        data[rootIndex] = temp;
    }

    public static void swap(int[] data, int i , int j){
        int temp = data[i];
        data[i] = data[j];
        data[j] = temp;
    }
}

Note:

  • Implementing the maximum heap and minimizing the heap only causes who sinks in the shift down section, the maximum heap sinks the small temp, and the minimum heap sinks the large temp.
  • As you can see from the code implementation, no additional storage space is taken up.

9. Exercises

Source: Force buckle 215. The K largest element in the array
My implementation:
class Solution {
    public int findKthLargest(int[] nums, int k) {
        heapDescSort(nums);
        return nums[k-1];
    }

    /**
     * Implement descending sort of arr array
     * @param data
     */
    public static void heapDescSort(int[] data){
        int length = data.length;

        minHeadify(data, 0, length - 1);
        //show(data);

        for(int i = length - 1; i > 0; i--){
            swap(data, 0, i);

            maxShiftDown(data, 0, 0, i - 1);
        }
    }

    /**
     * Build minimum heap
     * @param data
     */
    public static void minHeadify(int[] data, int start, int end){
        int length = end - start + 1;
        int lastNoLeafNodeIndex = (length / 2) -1;
        for(int i = lastNoLeafNodeIndex; i >= 0; i--){
            maxShiftDown(data, i, start, end);
        }
    }

    /**
     * To restore the correct minimum heap after extracting the root node: the larger the value, the sink
     * @param rootIndex
     */
    public static void maxShiftDown(int[] data, int rootIndex, int start, int end){

        int temp = data[rootIndex];
        int length = end - start + 1;
        while(2 * rootIndex + 1 <= length - 1){
            int childIndex = 2 * rootIndex + 1;
            if(2 * rootIndex + 2 <= length - 1){//If there is a right node
                childIndex = data[childIndex] <= data[2 * rootIndex + 2] ? childIndex : 2 * rootIndex + 2;
            }
            if(temp <= data[childIndex]){
                break;
            }
            data[rootIndex] = data[childIndex];
            rootIndex = childIndex;
        }
        data[rootIndex] = temp;
    }

    public static void swap(int[] data, int i , int j){
        int temp = data[i];
        data[i] = data[j];
        data[j] = temp;
    }

}

Execution time: 3 ms at all   Java   58.59% of users were defeated in submission

Memory consumption: 38.6 MB at all   Java   81.29% of users were defeated in submission

Pass test case: 32  /  32

10. Priority Queue

Reference resources: Comics: What is the Priority Queue? Heap top

Features of normal queues: FIFO

Priority Queue:

  • Maximum priority queue, regardless of the queue entry order, the largest element currently queued first.

  • Minimum priority queue, the current smallest element takes precedence regardless of the queue entry order.

For example, if there is a maximum priority queue with a maximum element of 8, then even though element 8 is not the first element in the queue, element 8 still leaves the queue first when it leaves the queue:

To meet these needs, linear data structures are not impossible, but time complexity is high and worst-case time complexity O(n) lookup + move is not the best way.

Using binary heap:

  • The top of the largest heap is the largest element in the entire heap

  • The top of the smallest heap is the smallest element in the entire heap

Therefore, we can use the maximum heap to achieve the maximum priority queue, each inbound operation is the heap insert operation (shift up operation), and each outbound operation is the deletion of the top node of the heap (shift down operation). The time complexity of upstream and downward binary heap nodes is logn, so the time complexity of queuing and queuing for priority queues is also logn.

Arrays are used in the code to store elements of the binary heap, so resize is needed to increase the length of the array when the element exceeds the range of the array.

Tags: Java data structure

Posted on Sun, 21 Nov 2021 13:33:48 -0500 by kazer