赞
踩
前面介绍过队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队 列时,可能需要优先级高的元素先出队列,该中场景下,使用队列显然不合适,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。 在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。
JDK1.8中的 PriorityQueue 底层使用了堆这种数据结构,而堆实际就是在完全二叉树的基础上进行了一些调整。 下面将详细介绍堆。
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一 个一维数组中,并满足:Ki <= K2i+1 且 Ki<= K2i+2 (Ki >= K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为小堆(或大 堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。
堆的性质: 1. 堆中某个节点的值总是不大于或不小于其父节点的值。 2. 堆总是一棵完全二叉树。
从堆的概念可知,堆是一棵完全二叉树,因此可以层序的规则采用顺序的方式来高效存储,而对于非完全二叉树,则不适合使用顺序方式进行存储,因为为了能够还原二叉树,空间中必须要存储空节 点,就会导致空间利用率比较低。 将元素存储到数组中后,假设 i 为节点在数组中的下标,则有: 如果 i 为0,则 i 表示的节点为根节点,否则 i 节点的双亲节点为 (i - 1)/2 如果 2 * i + 1 小于节点个数,则节点i的左孩子下标为 2 * i + 1 ,否则没有左孩子 如果 2 * i + 2 小于节点个数,则节点i的右孩子下标为 2 * i + 2 ,否则没有右孩子
堆是存储在一个一维数组中的,那么怎么用通过一个数组,让其变成一个堆呢?先简单介绍一下向下调整形成大根堆的思路:由于输入的数组是随机的,其对应的二叉树,也并不总是父亲节点的值大于孩子节点,所以我们要做的就是调整位置,让父亲节点的值与左右孩子节点的最大值做比较,小了,就与孩子节点交换位置,而大了,不做改变,从而让这个二叉树变成大根堆。
- public class TextHeap {
- public int[] elem;
- public int usedSized;
-
- public TextHeap() {
- this.elem = new int[10];
- }
-
- public void initElem(int[] arr){
- for (int i = 0; i < arr.length; i++) {
- elem[i] = arr[i];
- usedSized++;
- }
- }
-
- /*
- //创建大根堆
- */
- public void createHeap(){
- //找到这个完全二叉树中,最后一个父亲节点的下标:
- //最后一个节点的父亲节点,也是该二叉树的最后一个父亲节点,以此开始
- for (int parent = (usedSized - 1 - 1 )/ 2; parent >= 0 ; parent--) {
- //使用 usedSize 作为向下调整的最大边界
- shiftDown(parent,usedSized);
- }
- }
-
- private void shiftDown(int parent,int len){
- //最起码有左孩子
- int child = 2 * parent + 1;
-
- // 可能需要多次向下调整:
- while(child < len){
- //如果有右孩子时,得让 child 这个下标对应左右孩子最大值的下标
- if(child + 1 < len && elem[child] < elem[child + 1]){
- child++;
- }
-
- if(elem[child] > elem[parent]){
- int temp = elem[child];
- elem[child] = elem[parent];
- elem[parent] = temp;
- parent = child;
- child = 2 * parent + 1;
- }else{
- break;
- }
- }
- }
- }
按照最坏的情况来看,即创建一棵高度为 h 的满二叉树的话,这样的堆的时间复杂度该如何计算呢?
高度为 h ,那么最后一层就是第 h 层,该层的节点总数为 ,这一层都是叶子节点,并不需要向下调整。而倒数第二层,即 h - 1 层,该层的节点总数为 ,而这里的每一个节点都需要向下调整 1 次。以此类推到第一层。
①
②
由② - ① 得:
化简得:
又因为总节点数 n 满足:
所以有
所以时间复杂度为
①
②
② - ① 得
化简得:
最后:
因此时间复杂度为:
将要插入的元素放在堆的最后位置,向上调整,时间复杂度为二叉树的高度,只需一路向上,跟父亲节点比较交换即可。
- private void shiftUp(int child){
- int parent = (child - 1)/2;
- while(child > 0){
- if(elem[child] > elem[parent]){
- int temp = elem[child];
- elem[child] = elem[parent];
- elem[parent] = temp;
- child = parent;
- parent = (child - 1)/2;
- }else{
- break;
- }
- }
- }
-
- //堆的插入
- public void offer(int val){
- if(isFull()){
- //满了就扩容
- elem = Arrays.copyOf(elem,elem.length+5);
- }
- elem[usedSized++] = val;
- shiftUp(usedSized - 1);
- }
-
- private boolean isFull(){
- return usedSized == elem.length;
- }
堆的删除,只能删除根节点元素。删除的思路:把根节点元素与堆中最后一个元素交换,usedSize--,同时向下调整。
- //堆的删除
- public void pop(){
- if(isEmpty()){
- return;
- }
- elem[0] = elem[usedSized - 1];
- usedSized--;
- //只需从0下标的这个父亲节点去向下调整就可以了
- shiftDown(0,usedSized);
-
-
- }
- private boolean isEmpty(){
- return usedSized == 0;
- }
堆排序:将存储堆的数组里的元素,从小到大或从大到小排序
从小到大,建大堆
- public class Text{
- public static void main(String[] args) {
- int[] arr = {1,4,6,235,15,75,123,61,70,13};
- TextHeap heap = new TextHeap();
- heap.initElem(arr);
- heap.createHeap();
- heap.heapSort();
- System.out.println();
- }
- }
-
- public class TextHeap{
- ........
- //省略的部分可往前文查找
- private void swap(int[] arr,int i ,int j){
- int temp = arr[i];
- arr[i] = arr[j];
- arr[j] = temp;
- }
-
- //堆排序
- public void heapSort(){
- int end = usedSized - 1;
- while(end > 0 ){
- swap(elem,0,end);
- shiftDown(0,end);
- end--;
- }
- }
- }
时间复杂度O(nlogn),空间复杂度O(1)。
而从大到小,则建小堆。这里就不再赘述。
Java集合框架中提供了 PriorityQueue 和 PriorityBlockingQueue 两种类型的优先级队列,PriorityQueue是线 程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。
关于PriorityQueue的使用要注意: 1. 使用时必须导入PriorityQueue所在的包,即:
import java.util.PriorityQueue;
2. PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException异常
比如以下代码,priorityQueue中存放的是Student的对象,此时这些对象并不能比较大小:
- public static void main(String[] args) {
- Queue<Student> priorityQueue = new PriorityQueue<>();
- priorityQueue.offer(new Student("huahua",15));
- priorityQueue.offer(new Student("liu",34));
- System.out.println(priorityQueue.poll());
- }
因此报错:
3. 不能插入null对象,否则会抛出NullPointerException
4. 没有容量限制,可以插入任意多个元素,其内部可以自动扩容 5. 插入和删除元素的时间复杂度为,也就是二叉树的高度 6. PriorityQueue底层使用了堆数据结构 7. PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素
- //检验一下 PriorityQueue 真的默认为小根堆?
- public static void main(String[] args) {
- Queue<Integer> priorityQueue = new PriorityQueue<>();
- priorityQueue.offer(1);
- priorityQueue.offer(88);
- System.out.println(priorityQueue.poll());
- }
设计一个算法,找出数组中最小或最大的前k个数。以任意顺序返回这k个数均可。
将这个数组建成小根堆,拿这小根堆的前10个元素,但这种方法效率低下。
- //top-k问题
- //排序思路
- public int[] topK1(int[] arr,int k){
- int[] ret = new int[k];
- if(arr == null || k == 0){
- return ret;
- }
- Queue<Integer> priorityQueue = new PriorityQueue<>();
- for (int i = 0; i < arr.length; i++) {
- priorityQueue.offer(arr[i]);
- }
- for (int i = 0; i < k; i++) {
- ret[i] = priorityQueue.poll();
- }
- return ret;
- }
如果是输出数组中前k个最大的元素,其实只要创建容量为k的优先队列即可,而不是像3.2.1那样,创建一个容量为arr.length的优先队列。
此时容量为k的优先队列默认为小根堆,让数组的前k个元素放入优先级队列中,之后让数组从第k+1个元素往后遍历,每一个都与小根堆的堆顶元素做比较。由于是小根堆,堆顶元素就是k个元素中,最小的一个,与数组第k+1个元素及以后的元素作比较,挨个置换比根节点大的元素,那么等数组遍历完之后,小根堆里存储的元素都比数组中其余元素要大,不然的话,还是会被置换出来的。如此一来,小根堆存储的就是数组中前k个最大元素。
- public static int[] topK2(int[] arr,int k){
- int[] ret = new int[k];
- if(arr == null || k == 0){
- return ret;
- }
- Queue<Integer> priorityQueue = new PriorityQueue<>(k);
- for (int i = 0; i < k; i++) {
- priorityQueue.offer(arr[i]);
- }
- for (int i = k; i < arr.length; i++) {
- if(arr[i] > priorityQueue.peek()){
- priorityQueue.poll();
- priorityQueue.offer(arr[i]);
- }
- }
- for (int i = 0; i < k; i++) {
- ret[i] = priorityQueue.poll();
- }
- return ret;
- }
-
- public static void main(String[] args) {
- int[] arr = {1,6,43,2,7,88,67,17,29,257,35};
- int[] ret = topK2(arr,3);
- System.out.println(Arrays.toString(ret));
- }
输出:
[67, 88, 257]
问题换一换,找第k大的元素?
其实就是堆顶元素啦~~
那如果要求的是前k个最小元素呢?这时候就要用到大根堆,可优先级队列默认是小根堆,那该如何在优先级队列中创建大根堆呢?
此处只是列出了PriorityQueue中常见的几种构造方式。
构造器 | 功能介绍 |
PriorityQueue() | 创建一个空的优先级队列,默认容量是11 |
PriorityQueue(int initialCapacity) | 创建一个初始容量为initialCapacity的优先级队列,注意: initialCapacity不能小于1,否则会抛IllegalArgumentException异 常 |
PriorityQueue(int initialCapacity, Comparator<? super E> comparator) | 创建一个初始容量为initialCapacity的优先级队列,并且有比较器 |
PriorityQueue(Collection<? extends E> c) | 用一个集合来创建优先级队列 |
- public static void main(String[] args) {
- // 创建一个空的优先级队列,底层默认容量是11
- Queue<Integer> q1 = new PriorityQueue<>();
-
- // 创建一个空的优先级队列,底层的容量为 100
- Queue<Integer> q2 = new PriorityQueue<>(100);
-
- // 用ArrayList对象来构造一个优先级队列的对象
- // q3中已经包含了三个元素
- List<Integer> list = new ArrayList<>();
- list.add(1);
- list.add(45);
- list.add(23);
- Queue<Integer> q3 = new PriorityQueue<>(list);
- q3.offer(9);
- System.out.println(q3.size());
- }
输出:
4
优先级队列在插入元素时有个要求:插入的元素不能是null或者元素之间必须要能够 进行比较,为了简单起见,我们只是插入了Integer类型,那优先级队列中能否插入自定义类型对象呢?
优先级队列底层使用堆,而向堆中插入元素时,为了满足堆的性质,必须要进行元素的比较,而在优先级队列的性质那一小节中,优先级队列插入的是引用类型,是没有办 法直接进行比较的,因此抛出异常。因此在这里引出一个问题,对象的比较方式,以下介绍三种方法:
- class Student{
- public String name;
- public int age;
-
- public Student(String name, int age) {
- this.name = name;
- this.age = age;
- }
- }
- public static void main(String[] args) {
- Student student1 = new Student("shanshan",15);
- Student student2 = new Student("shanshan",15);
- System.out.println(student1==student2);
- System.out.println(student1.equals(student2));
- }
输出:
false
false
==比较的是地址,输出是false;而当我们一开始直接就用equals时,返回的依旧是false。这是因为Student类中并无equals方法,而所有类都是Object类的子类,所以调用的就是Object类的equals方法:
我们发现,此处的equals方法也等同于==,比较的是引用变量的地址。
但是此处,我们需要比较的是对象中的内容,那该如何处理呢?基于此,我们可以覆写基类的equals方法。(可右击自动生成)
- class Student{
- public String name;
- public int age;
-
- public Student(String name, int age) {
- this.name = name;
- this.age = age;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
- Student student = (Student) o;
- return age == student.age && Objects.equals(name, student.name);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(name, age);
- }
- }
输出:
false
true
覆写基类equal的方式虽然可以比较,但缺陷是:equal只能按照相等进行比较,不能按照大于、小于的方式进行比较。
因此,优先级队列并不采用equals方法。
Comparble是JDK提供的泛型的比较接口类,源码实现具体如下:
- public interface Comparable<T> {
-
- public int compareTo(T o);
- }
对用用户自定义类型,如果要想按照大于、小于的方式进行比较时:在定义类时,实现Comparble接口即可,然后在类 中重写compareTo方法,举例如下:
- class Student implements Comparable<Student>{
-
- ......
-
- @Override
- public int compareTo(Student o) {
- return this.age - o.age;
- }
- }
但这样compareTo就只能比较年龄了,那要是想比较学生的成绩或是其他,该怎么办呢?
Comparator部分源代码:
- public interface Comparator<T> {
- int compare(T o1, T o2);
-
- ......
- }
按照比较器方式进行比较,具体步骤如下: 用户自定义比较器类,实现Comparator接口并且重写Comparator中的compare方法:
- //姓名比较器
- class nameComparator implements Comparator<Student>{
- @Override
- public int compare(Student o1, Student o2) {
- return o1.name.compareTo(o2.name);
- }
- }
-
- //年龄比较器
- class AgeComparator implements Comparator<Student>{
- @Override
- public int compare(Student o1, Student o2) {
- return o1.age - o2.age;
- }
- }
上面的compareTo方法中,因为name是String类的对象,可以调用String中的compareTo方法,这里比较的是两个字符串的大小。
- NameComparator nameComparator = new NameComparator();
- System.out.println(nameComparator.compare(student1,student2));
集合框架中的PriorityQueue底层使用堆结构,因此其内部的元素必须要能够比大小,PriorityQueue采用了: Comparble和Comparator两种方式。 1. Comparble是默认的内部比较方式,如果用户插入自定义类型对象时,该类对象必须要实现Comparble接 口,并覆写compareTo方法 2. 用户也可以选择使用比较器对象,如果用户插入自定义类型对象时,必须要提供一个比较器类,让该类实现 Comparator接口并覆写compare方法。
继续用上面Student类的例子,来演示如何将自定义类型对象插入优先级队列,以及如何创建大根堆:
Comparble接口:
- class Student implements Comparable<Student>{
- public String name;
- public int age;
-
- public Student(String name, int age) {
- this.name = name;
- this.age = age;
- }
-
-
- //为创建大根堆
- @Override
- public int compareTo(Student o) {
- return o.age - this.age;
- }
- }
-
- public static void main(String[] args) {
- Student student1 = new Student("shansan",10);
- Student student2 = new Student("shanan",15);
- PriorityQueue<Student> priorityQueue = new PriorityQueue<>();
- priorityQueue.offer(student1);
- priorityQueue.offer(student2);
-
- System.out.println("sssssssss");
- }
Comparator接口:
- class InCmp implements Comparator<Integer>{
- @Override
- public int compare(Integer o1, Integer o2) {
- //调用了Integer这个类中的compareTo方法
- return o2.compareTo(o1);
- //return o1 - o2;
- }
- }
-
- public static void main(String[] args) {
- InCmp inCmp = new InCmp();
- PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(inCmp);
- priorityQueue.offer(5);
- priorityQueue.offer(1);
- priorityQueue.offer(98);
- priorityQueue.offer(23);
- priorityQueue.offer(51);
- System.out.println("2222222222222222222");
- }
还可以使用匿名内部类的方法写:
- //一个实现了Comparator接口的匿名内部类
- PriorityQueue<Integer> priorityQueue1 = new PriorityQueue<>(new Comparator<Integer>() {
- @Override
- public int compare(Integer o1, Integer o2) {
- //小根堆
- return o1.compareTo(o2);
- }
- });
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。