【数据结构】PriorityQueue和堆

 当手机有来电时,手机总会优先处理来电,这种时候就要用到带有优先级的队列。在数据结构中,能添加新的对象并返回最高优先级对象的结构叫作优先级队列。堆和PriorityQueue就是一种优先级队列。

什么是堆

 堆是一颗完全二叉树,且其每个节点的值总是不大于或不小于其父节点的值,每个节点都大于其父节点的叫大根堆,每个节点都小于其父节点的叫小根堆。

堆的存储方式

因为堆是一棵完全二叉树,因此可以按照层序遍历的次序采用顺序的方式来存储在数组中(若不是完全二叉树,空间中必须要存储空节点,会导致空间利用率比较低)。

36678cb84ecc4f34b945cd6438c4cbfe.png

完全二叉按顺序存储在数组中,节点 i 的左孩子节点的下标为 2*i+1,右孩子节点的下标为 2*i+2,父节点的下标为 (i-1)/2 。

下面是模拟实现堆的存储结构:

public class Heap {
    //存储堆的数组
    private int[] elem;
    //堆中有效元素的个数
    private int usedSize;
    //数组的默认大小
    private static final int DEFAULT_SIZE = 10;
    //构造方法初始化数组
    public Heap() {
        elem = new int[DEFAULT_SIZE];
    }
}

堆的创建

堆是一颗特殊的完全二叉树,我们可以通过向下调整一颗普通的二叉树来创建堆,这里以创建小根堆为例。

下图为一次向下调整的示意图:

0bbf072a0530427fa2dff1a229a5415e.png

要想将一颗普通的二叉树转换成堆需要进行多次的向下调整,具体步骤如下:

需要parent和child两个标签,让parent标记需要调整的节点,child标记parent的左孩子(parent如果有孩子一定先是有左孩子),如果parent有左孩子, 进行以下操作:

1.判断parent右孩子是否存在,存在则让child标记左右孩子节点中最小的节点

2.将parent与child比较,如果parent小于child,调整结束 ;如果parent大于child,交换parent与child。

3.交换完成之后,parent中大的元素向下移动,可能导致子树不满足堆的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后继续1,2,3步骤。

    //建堆
    public void createHeap() {
        //找到为倒数第一个非叶子节点,从该节点位置开始往前一直到根节点,遇到一个节点,应用向下调整
        for (int parent = (usedSize-1-1)/2; parent >= 0; parent--) {
            //向下调整
            shiftDown(parent, usedSize);
        }
    }
    //向下调整
    public void shiftDown(int  parent, int len) {
        //child先标记parent的左孩子,因为parent可能右左没有右
        int child = parent*2 + 1;
        while (child < len) {
            //如果右孩子存在,找到左右孩子中较小的孩子,用child进行标记
            if (child+1 < len && elem[child] > elem[child+1]) {
                child++;
            }
            if (elem[parent] > elem[child]) {
                //父节点比其最小的孩子节点大
                //将父节点与较小的孩子节点交换
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                //parent中大的元素往下移动,可能会造成子树不满足堆的性质,因此需要继续向下调整
                parent = child;
                child = 2*parent + 1;
            } else {
                //父节点比其最小的孩子节点还小,符合小根堆的特性
                break;
            }
        }
    }

建堆的时间复杂度

因为堆是完全二叉树,为了简化计算我们用满二叉树来推导堆的时间复杂度:

假设堆有n个节点,高度为 h,则最坏情况下:

第一层有2^0个节点,每个节点需要向下调整h-1层;

第二层有2^1个节点,每个节点需要向下调整h-2层;

第三层有2^2个节点,每个节点需要向下调整h-3层;

.

.

.

第h-1层有2^(h-1)个节点,每个节点需要向下调整1层。

将上述内容写成函数的形式:

T(n) = 2^0*(h-1) + 2^1*(h-2) + 2^2*(h-3) + ... + 2^(h-2)*1                                  ①   

由①*2得 2*T(n) = 2^1*(h-1) + 2^2*(h-2) + ... + 2^(h-2)*2 + 2^(h-1)*1                ②

由②-①得:

T(n) = 1-h + 2^1 + 2^2 + ... + 2^(h-2) + 2^(h-1)

T(n) = 2^h - 1

又∵ n = 2^h-1,h = log(n+1)

∴ T(n) = n-log(n+1)

故根据大O的渐进表示法,建堆的时间复杂度为O(n)

堆的插入

建好小根堆后,我们就可以进行插入操作了。插入操作我们可以采用向上调整(插入的节点比其父节点小的交换)的方式:

5dfee122fcf14789b8aa1a1aff61bbed.png

具体步骤是先将元素放入到底层空间中(空间不够时需要扩容) ,再将最后新插入的节点向上调整,直到满足堆的性质:

    public void offer(int val) {
        //堆满了,需要扩容
        if (usedSize == elem.length) {
            elem = Arrays.copyOf(elem, elem.length * 2);
        }
        //插入数组
        elem[usedSize] = val;
        //有效数字加一
        usedSize++;
        //向上调整
        shiftUp(usedSize-1);
    }
    //向上调整
    private void shiftUp(int child) {
        //找到child的父节点
        int parent = (child-1) / 2;
        while (child > 0) {
            if (elem[parent] > elem[child]) {
                // 将双亲与孩子节点进行交换
                int tmp = elem[child];
                elem[child] = elem[parent];
                elem[parent] = tmp;
                //小的元素向上移动,可能会造成子树不满足堆的性质,因此需要继续向上调增
                child = parent;
                parent = (parent-1) / 2;
            } else {
                // 如果双亲比孩子大,parent满足堆的性质,调整结束
                break;
            }
        }
    }

堆的删除

堆的删除删除的是堆顶元素,我们可以让最后一个叶子节点填充到堆顶,这样我们其他子树就还是大/小根堆,我们只需调整最大的那颗树即可。

具体步骤是 ①.将堆顶元素对堆中最后一个元素交换 ②.将堆中有效数据个数减少一个 ③.对堆顶元素进行向下调整:

    //删除元素
    public int pop() {
        //堆为空,返回-1
        if (usedSize == 0) {
            return -1;
        }
        //记录要删除的元素的值
        int tmp = elem[0];
        //最后面的叶子节点替换掉根节点
        elem[0] = elem[usedSize-1];
        //有效数字减一
        usedSize--;
        //向下调整
        shiftDown(0, usedSize);
        //返回被删除元素的值
        return tmp;
    }

 

PriorityQueue

什么是PriorityQueue

PriorityQueue是Java集合框架中的优先级队列,它的底层是堆,且其默认情况下是小根堆。

b6b29d4db8aa496393c45b18ae708056.png

 注意:

1.使用时必须导入PriorityQueue所在的包

2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出ClassCastException异常

3. 不能插入null对象,否则会抛出NullPointerException

4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容

5. 插入和删除元素的时间复杂度为O(logN)

6. PriorityQueue底层使用了堆数据结构,且默认情况下是小堆(每次获取到的元素都是最小的元素) 

priorityQueue的构造方法

构造方法描述
PriorityQueue()创建一个空的优先级队列,默认容量是11

PriorityQueue(int

initialCapacity)

创建一个初始容量为initialCapacity的优先级队列,注意:

initialCapacity不能小于1,否则会抛IllegalArgumentException异

PriorityQueue(Collection<?

extends E> c)

用一个集合来创建优先级队列
import java.util.ArrayList;
import java.util.PriorityQueue;

public class Test {
    public static void main(){
        // 创建一个空的优先级队列,底层默认容量为11
        PriorityQueue<Integer> q1 = new PriorityQueue<>();
        // 创建一个空的优先级队列,底层的容量为100
        PriorityQueue<Integer> q2 = new PriorityQueue<>(100);
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        // 用ArrayList对象来构造一个优先级队列的对象
        PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
    }
}

上面例举的只是常见的几种构造方法,PriorityQueue还有其他一些构造方法:

f2e1905e6bb9466eaedd5d7058a6795c.png

PriorityQueue的常见方法 

方法描述

boolean offer(E e)

插入元素e,插入成功返回true,如果e对象为空,抛出NullPointerException异常,空间不够时候会进行扩容,时间复杂度为O(logN)
E peek()获取优先级最高的元素,如果优先级队列为空,返回null
E poll()移除优先级最高的元素并返回,如果优先级队列为空,返回null
int size()

获取有效元素的个数

void clear()

清空优先级队列

boolean isEmpty()

检测优先级队列是否为空
import java.util.PriorityQueue;

public class Test {
    public static void main() {
        int[] arr = {1, 2, 3, 4, 5, 6};
        // 一般在创建优先级队列对象时,如果知道元素个数,建议就直接将底层容量给好
        PriorityQueue<Integer> q = new PriorityQueue<>(arr.length);
        for (int e : arr) {
            q.offer(e);
        }
        System.out.println(q.size()); // 打印优先级队列中有效元素个数
        System.out.println(q.peek()); // 获取优先级最高的元素
        // 从优先级队列中删除两个元素之和,再次获取优先级最高的元素
        q.poll();
        q.poll();
        System.out.println(q.size()); // 打印优先级队列中有效元素个数
        System.out.println(q.peek()); // 获取优先级最高的元素
        q.offer(0);
        System.out.println(q.peek()); // 获取优先级最高的元素
        // 将优先级队列中的有效元素删除掉,检测其是否为空
        q.clear();
        if (q.isEmpty()) {
            System.out.println("优先级队列已经为空!!!");
        } else {
            System.out.println("优先级队列不为空");
        }
    }
}

PriorityQueue如何创建大根堆

在PriorityQueue中也是通过 shiftUp(向上调整)或者shiftDown(向下调整)来使堆成为小根堆的:

4f44e847a6ea4c1b87d24af5a92beb26.png

a5a03c75b77640b58c8487cc692ab091.png从上图源码可以发现,shiftup和shiftdown在用户有传比较器(comparator!=null)的时候,是优先使用比较器比较的,而用户没有传比较器(comparator==null)的时候,堆的节点的类型必须是实现了comparable接的。故要想创建大根堆,自定义类型可以通过传入比较器和重写compareTo实现,包装类类型可以通过传入比较器实现:

通过重写compareTo方法:

class Student implements Comparable<Student>{
    public int age;
    public Student(int age) {
        this.age = age;
    }

    @Override
    public int compareTo(Student o) {
        //return this.age - o.age;
        return o.age - this.age;
    }
}

public class Test {
    public static void main(String[] args) {
        PriorityQueue<Student> priorityQueue = new PriorityQueue<>();
        priorityQueue.offer(new Student(1));
        priorityQueue.offer(new Student(2));
        priorityQueue.offer(new Student(3));
        System.out.println(priorityQueue.peek().age);
    }
}

运行结果:

9ca60dced92046d5a596ba65bf2db0ac.png

通过传入比较器:

class IntCmp implements Comparator<Integer> {

    @Override
    public int compare(Integer o1, Integer o2) {
        //return o2-o1;
        return o2.compareTo(o1);
    }
}
public class Test {
    public static void main(String[] args) {
        //传入IntCmp比较器
        PriorityQueue<Integer> priorityQueue1 = new PriorityQueue<>(new IntCmp());
        priorityQueue1.offer(1);
        priorityQueue1.offer(2);
        priorityQueue1.offer(3);
        System.out.println(priorityQueue1.peek());
        //匿名内部类实现比较器
        PriorityQueue<Integer> priorityQueue2 = new PriorityQueue<>(new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                return o2.compareTo(o1);
            }
        });
        priorityQueue2.offer(1);
        priorityQueue2.offer(2);
        priorityQueue2.offer(3);
        System.out.println(priorityQueue1.peek());
        //lambda表达式实现比较器
        PriorityQueue<Integer> priorityQueue3 = new PriorityQueue<>((x,y)->{return x.compareTo(y);});
        PriorityQueue<Integer> priorityQueue4 = new PriorityQueue<>((x,y)-> x.compareTo(y));
        priorityQueue3.offer(1);
        priorityQueue3.offer(2);
        priorityQueue3.offer(3);
        System.out.println(priorityQueue1.peek());
        priorityQueue4.offer(1);
        priorityQueue4.offer(2);
        priorityQueue4.offer(3);
        System.out.println(priorityQueue1.peek());
    }
}

运行结果:

75ee284f302644f9996489f2d2dfa76b.png

PriorityQueue的扩容方式

PriorityQueue源码中grow()就是用来扩容的函数:

67eaec364b83420a96ccf29a30cd9a63.png

通过观察源码可以发现:当容量小于64时,是按照oldCapacity的2倍+2的方式扩容的,当容量大于等于64,是按照oldCapacity的1.5倍方式扩容的 ,如果容量超过了MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

记得开心一点啊

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值