1、概念
队列是一种先进先出的结构(排队买饭)。然而有的时候我们需要让优先级更高的元素出去,此时就出现了优先级队列的概念。
此时数据结构应该满足最基本的两个要求:1.可以输出最高优先级的对象。2.可以输入新对象。
堆本质上,就是用数组或者链表来存储数据,通过模拟完全二叉树的方式,开始先优先级的区分。
2、堆的存储
对于非完全二叉树而言,如果采用数组的方式进行存储,会导致数组空间浪费,此时应采用链表形式进行存储。
将数组转化为一颗二叉树的时候,牢记二叉树的性质进行还原。
假设i为结点在数组中的下标,则:
a.如果i为0,则i表示根结点。否则,可以根据i计算出双亲地址,为(i-1) / 2
b.如果2*i+1小于总结点个数(其实就是根据a进行反推的,由父亲结点推孩子结点),则结点i的左孩子下标为2*i+1。否则,该父亲结点没有左孩子。
c.如果2*i+2小于总结点个数,则结点i的右孩子下标为2*i+2。否则,该父亲结点没有右孩子。
3、堆的创建
对于一个数组{27,15,19,18,28,34,65,49,25,37},如何将其创建成一个堆呢(其实就是对数组中的元素进行排序,排完序以后,这个数组就称为我要的堆)。原始的二叉树的图如下所示。
3.1向下调整法
向下调整的过程:
1.让parent表示需要调整的结点,child表示parent的左孩子。
2.如果parent的左孩子存在,即child<size,则进行一下操作,直到parent的左孩子不存在为止。
a.判断parent的右孩子是否存在,如果存在,则找到左右孩子中最小的孩子,让child表示这个孩子。
b.将parent表示的孩子与child表示的孩子进行比较
如果parent小于child,则调整结束
否则交换parent与child,交换完成以后,其他的树可能发生变化,导致不是小根堆,因此需要继续向下调整,将每棵树都调整一次。即parent = child(调整child的子树),child =
parent * 2 + 1(找到新parent的左子树,继续上述调整);
public void shiftDown(int[] array , int parent){
//child
int child = 2 * parent + 1;
int size = array.length;
while (child < size){
//如果右孩子存在,并且右孩子比左孩子更小
if (child + 1 < array.length && array[child+1] < array[child]){
//那么就让child指向右孩子,因为它更小
child += 1;
}
if (array[parent] < array[child]){
//此时已经是小根堆了
break;
}else {
//交换parent和child,让最小的上去
int tmp = array[child];
array[child] = array[parent];
array[parent] = tmp;
//换完以后可能导致其他的树不是小根堆了,要继续调整
parent = child;
child = 2 * parent + 1;
}
}
}
/**
* 一颗普通的二叉树,即每棵树都有可能不是小根堆,需要自己调整
*/
public void createHeap(int[] array){
//找到倒数第一个非叶子结点(即最后一个父亲结点),从该节点位置一直向前直到根结点,每遇到一个结点就调用向下调整法。
//array.length-1表示最后一个叶子结点下标
//(叶子结点-1) / 2表示最后一个根结点下标
int root = ((array.length-1)-1) /2 ;
for (;root>0;root--) {
shiftDown(array,root);
}
}
4、堆的插入与删除
堆的插入总共有两个步骤:
1.先将元素放入到底层空间中(注意扩容)
2.将最后的新加入的结点向上调整,直至满足堆性质。
public void shiftUp(int child) {
//找到child的父亲
int parent = (child-1)/2;
while (child > 0){
//如果父亲比孩子大,满足性质
if (array[parent] < array[child]){
break;
}else {
//父亲儿子结点交换
int tmp = array[child];
array[child] = array[parent];
array[parent] = tmp;
//牵一发而动全身,需要将其他的子树也都调整
//向上的过程也就体现在这个地方。儿子不断替代父亲的位置
child = parent;
parent = (child-1)/2;
}
}
}
堆的删除:堆的删除,一定是删除堆顶的元素,具体如下:
1.将堆顶元素与最后一个元素进行交换。
2.将堆中有效数据个数减少一个(表示删除操作)。
3.对堆顶元素进行向下调整
public void delete(){
//1.交换
int top = array[0];
int last = array[array.length-1];
swap(array,top,last);
shiftDown(top);
usedSize--;
}
5、用堆模拟实现优先级队列
public class MyPriorityQueue {
// 演示作用,不再考虑扩容部分的代码
private int[] array = new int[100];
private int size = 0;
public void offer(int e) {
array[size++] = e;
shiftUp(size - 1);
}
public int poll() {
int oldValue = array[0];
array[0] = array[--size];
shiftDown(0);
return oldValue;
}
public int peek() {
return array[0];
}
}
6、常用接口
上面都是自己模拟实现优先级队列的特性,下面是直接调用java中已有的代码。
static void TestPriorityQueue(){
// 创建一个空的优先级队列,底层默认容量是11
PriorityQueue<Integer> q1 = new PriorityQueue<>();
// 创建一个空的优先级队列,底层的容量为initialCapacity
PriorityQueue<Integer> q2 = new PriorityQueue<>(100);
ArrayList<Integer> list = new ArrayList<>();
list.add(4);
list.add(3);
list.add(2);
list.add(1);
// 用ArrayList对象来构造一个优先级队列的对象
// q3中已经包含了三个元素
PriorityQueue<Integer> q3 = new PriorityQueue<>(list);
System.out.println(q3.size());
System.out.println(q3.peek());
}
// 注意:默认情况下,PriorityQueue队列是小堆,如果需要大堆需要用户提供比较器
// 用户自己定义的比较器:直接实现Comparator接口,然后重写该接口中的compare方法即可
class IntCmp implements Comparator<Integer>{
@Override
public int compare(Integer o1, Integer o2) {
return o2-o1;
}
}
public class TestPriorityQueue {
public static void main(String[] args) {
PriorityQueue<Integer> p = new PriorityQueue<>(new IntCmp());
p.offer(4);
p.offer(3);
p.offer(2);
p.offer(1);
p.offer(5);
System.out.println(p.peek());
}
}
boolean offer(E e) |
插入元素
e
,插入成功返回
true
,如果
e
对象为空,抛出
NullPointerException
异常,时间复杂度O(logN),注意:空间不够时候会进行扩容
|
E peel() |
获取优先级最高的元素,如果优先级队列为空,返回
null
|
E poll() |
移除优先级最高的元素并返回,如果优先级队列为空,返回
null
|
int size() | 获取有效元素个数 |
void clear() | 清空 |
boolean isEmpty() | 判断是否为空 |
扩容:
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
int oldCapacity = queue.length;
// Double size if small; else grow by 50%
int newCapacity = oldCapacity + ((oldCapacity < 64) ?
(oldCapacity + 2) :
(oldCapacity >> 1));
// overflow-conscious code
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
queue = Arrays.copyOf(queue, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
优先级队列扩容说明:
1.如果容量小于64,则2倍扩容。
2.如果容量大于等于64,则1.5倍扩容。
3.如果容量大于MAX_ARRAY_SIZE,则按照MAX_ARRAY_SIZE扩容
7、关于比较,如equals、comparble、compareTo
7.1 equals比较
基本数据类型可以直接比较
自定义的对象,需要重写比较方法。
自定义类型都继承Object类,而Object类中提供了equals方法,因此需要在创建的类对象中重写该方法。
equals特点:只能比较是否相等,不能比较谁大谁小。
public class Card {
public int rank; // 数值
public String suit; // 花色
public Card(int rank, String suit) {
this.rank = rank;
this.suit = suit;
}
@Override
public boolean equals(Object o) {
// 自己和自己比较
if (this == o) {
return true;
}
// o如果是null对象,或者o不是Card的子类
if (o == null || !(o instanceof Card)) {
return false;
}
// 注意基本类型可以直接比较,但引用类型最好调用其equal方法
Card c = (Card)o;
return rank == c.rank && suit.equals(c.suit);
}
}
7.2 comparable比较
public interface Comparable<E> {
// 返回值:
// < 0: 表示 this 指向的对象小于 o 指向的对象
// == 0: 表示 this 指向的对象等于 o 指向的对象
// > 0: 表示 this 指向的对象大于 o 指向的对象
int compareTo(E o);
}
对用用户自定义类型,如果要想按照大小与方式进行比较时:在定义类时,实现Comparble接口即可,然后在类中重写compareTo方法。
public class Card implements Comparable<Card> {
@Override
public int compareTo(Card o) {
if (o == null) {
return 1;
}
return rank - o.rank;
}
}
7.3 基于比较器比较
public interface Comparator<T> {
// 返回值:
// < 0: 表示 o1 指向的对象小于 o2 指向的对象
// == 0: 表示 o1 指向的对象等于 o2 指向的对象
// > 0: 表示 o1 指向的对象等于 o2 指向的对象
int compare(T o1, T o2);
}
Object.equals |
因为所有类都是继承自
Object
的,所以直接覆写即可,不过只能比较相等与否
|
Comparable.compareTo |
需要手动实现接口,侵入性比较强,但一旦实现,每次用该类都有顺序,属于内部顺序
|
Comparator.compare |
需要实现一个比较器对象,对待比较类的侵入性弱,但对算法代码实现侵入性强
|