文章目录

  • 1. 什么是堆?
  • 2. 堆的基本操作
  • 2.1 建堆
  • 建堆时间复杂度分析:O(n)
  • 2.2 堆的插入--O(logn)
  • 堆插入建堆
  • 堆插入建堆时间复杂度:O(nlogn)
  • 2.3 堆的删除--O(logn)
  • 2.4 堆排序--O(nlogn)
  • 3. 优先级队列PriorityQueue
  • 3.1 PriorityQueue与建堆
  • 3.2 调整PriorityQueue的比较规则
  • 3.3 PriorityQueue的应用


1. 什么是堆?

  • 堆是由树延续过来的结构,相较于树链式存储,堆是一种顺序存储。从树的角度来看,堆中的元素是以树的层次遍历规则将结点依次放到顺序表中,且这个树一定是完全二叉树,这样可以保证高效存储
  • 堆中的元素是要经过调整才能成为堆,根据调整规则,分为大根堆和小根堆。
  • 以0下标开始的堆,任意结点的左孩子的下标为 2i+1,右孩子的下标为2i+2,父节点的下标为(i-1)/2。

2. 堆的基本操作

2.1 建堆

  • 这里的建堆是指将一组已经在堆结构中的数据调整成堆!!!
  • 建堆的核心是向下调整堆。调整规则是从最后一个父节点开始向下调整,然后退到根节点调整完成后结束。
public int[] elem;
    public int usedSize;

    public Heap() {
        this.elem = new int[11]; 
        this.usedSize = 0;
    }
	
	// (以大根堆为例)
	public void createHeap(int[] array) {
        for (int j : array) {
            if (isFull()) {
                elem = Arrays.copyOf(elem, elem.length + (elem.length >>> 1));
            }
            elem[usedSize++] = j;
        }
//        int root = (usedSize - 1 - 1) >>> 1;// err当只有一个元素的时候直接整型最大值!!!
        int root = (usedSize - 1 - 1) >> 1;

        while (root >= 0) {
            shiftDown(root--, usedSize);
        }
    }

	public boolean isFull() {
        return usedSize == elem.length;
    }

	// (以大根堆为例)时间复杂度O(n)
    private void shiftDown(int root, int len) {
        int child = root * 2 + 1;
        while (child < len) {
            if (child + 1 < len && elem[child] < elem[child + 1]) {
                child = child + 1;
            }
            if (elem[root] > elem[child]) {
                break;
            }
            swap(root, child);
            root = child;
            child = root * 2 + 1;
        }
    }

	public void swap(int x, int y) {
        int temp = elem[x];
        elem[x] = elem[y];
        elem[y] = temp;
    }
建堆时间复杂度分析:O(n)

java堆的结构 java堆和数据结构的堆_java堆的结构

2.2 堆的插入–O(logn)

  • 堆的插入的核心操作是向上调整
  • 整体的步骤是先把插入元素直接放到顺序表有效容量下标位置(usedSize),接着从该位置向上调整即可。
public void push(int val) {
        if (isFull()) {
            elem = Arrays.copyOf(elem, elem.length + (elem.length >>> 1));
        }
        elem[usedSize++] = val;
        shiftUp(usedSize - 1);
    }

 	// (以大根堆为例)时间复杂度O(n)
    private void shiftUp(int child) {
        while (child > 0) {
            int parent = (child - 1) >>> 1;
            if (elem[parent] < elem[child]) {
                swap(parent, child);
                child = parent;
            }else{
                break;
            }
        }
    }

    public boolean isFull() {
        return usedSize == elem.length;
    }
堆插入建堆
  • 堆插入建堆指将数据挨个插入堆中,插入操作会直接向上调整成堆
堆插入建堆时间复杂度:O(nlogn)

java堆的结构 java堆和数据结构的堆_数据结构_02

2.3 堆的删除–O(logn)

  • 堆的删除操作的核心是向下调整
  • 删除操作删除的是堆顶元素,具体操作是先将堆顶元素与最后一个元素交换,然后有效容量减一,再从堆顶开始向下调整。
public void pollHeap() {
        if (isEmpty()) {
            throw new RuntimeException("Heap is Empty!");
        }
        swap(0, usedSize - 1);
        usedSize--;
        shiftDown(0, usedSize);
    }

    public boolean isEmpty() {
        return usedSize == 0;
    }

2.4 堆排序–O(nlogn)

堆排序分为两步:

  1. 建堆,如果升序则建大根堆,反之相反。此时的堆也叫初始堆。
  2. 排序,利用堆删除思想,每次都要将堆顶元素换下去,所以升序要建大根堆。
// nlogn
    public void heapSort(int[] arr){
        // 建堆 O(n)
        createHeap(arr);
        // 排序 O(nlogn)
        int end = usedSize -1;
        while (end>0){
            swap(0,end);
            shiftDown(0,end);
            end--;
        }
    }

3. 优先级队列PriorityQueue

  • PriorityQueue是集合框架中的一个实现类,其底层数据结构就是堆,存储结构为动态数组
  • PriorityQueue中的元素必须能够比较,因为它相当于把空间给你划分好了,每次插入或者删除都会按照大根堆/小根堆的规则去调整。
  • PriorityQueue默认是以小根堆的规则调整。

3.1 PriorityQueue与建堆

java堆的结构 java堆和数据结构的堆_java堆的结构_03

  • 首先PriorityQueue的构造方法有很多。
  • 一个是你直接创建好一个优先级队列,不管你用哪个构造,然后将带处理的数据挨个插入(push)建堆。
  • 另一个是直接使用带集合参数的构造,直接将待处理数据直接通过构造方法建堆。

3.2 调整PriorityQueue的比较规则

默认是小根堆的调整规则,如果改变调整规则,优先级队列要求提供比较器(Comparator)

// 以Integer为例,改造使用匿名内部类
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2 - o1;
            }
        });

3.3 PriorityQueue的应用

  • 求最大或者最小的K个数
  • 求第K大或者第K小的数

以最小K个数为例。

具体算法思路:需要大堆,建立容量为K的默认优先级队列,将前K个树进堆,然后从第K+1个数开始遍历,依次和堆顶元素比较,比堆顶元素小则出(poll)一个堆元素然后进(offer)该元素,在继续遍历,最后的堆中即为最小的K个数。可以将堆顶元素视为整个数据中最大的值,然后在剩余数据中比他小的。

class Solution {
    public int[] smallestK(int[] arr, int k) {
        int[] ret = new int[k];
        if(arr.length == 0||k == 0){
            return ret;
        }
        
        // 建大堆
        PriorityQueue<Integer> queue = new PriorityQueue<>(k,new Comparator<Integer>(){
            @Override
            public int compare(Integer o1,Integer o2){
                return o2-o1;
            }
        });
        
        int i = 0;
        //O(klogk)
        for(;i<k;i++){
            queue.offer(arr[i]);
        }
        
        // O((n-k)logk) -- 维护的是已建好的堆,k肯能为n
        for(;i<arr.length;i++){
            if(queue.peek()>arr[i]){
                queue.poll();
                queue.offer(arr[i]);
            }
        }
        
        for(i= 0;i<k;i++){
            ret[i] = queue.poll();
        }
        return ret;
    }
}