实现代码放开头,供大家查阅,堆只有两个操作:删除和插入 (以大堆举例)
目录
一、实现代码
import java.util.Arrays;
public class Heap {
public int[] elem;
public int usedSize;
public Heap() {
this.elem =new int[10];
}
//初始化,就是把值放进去,没调整
public void initHeap(int[] arr){
for(int i=0;i<arr.length;i++){
elem[i]=arr[i];
usedSize++;
}
}
//调整,按大堆排序
public void createHeap(){
for (int parent=(usedSize-1-1)/2;parent>=0;parent--){
siftDown(parent,usedSize);
}
}
//交换
public void swap(int i,int j){
int tmp=elem[i];
elem[i]=elem[j];
elem[j]=tmp;
}
//向下调整
public void siftDown(int parent,int end){
int child=2*parent+1;
while (child<end){
if(child+1<end&&elem[child]<elem[child+1]){
child++;
}
if(elem[parent]<elem[child]){
swap(parent,child);
parent=child;
child=2*parent+1;
}else{
break;
}
}
}
//判断是否为满
public boolean isFull(){
return elem.length==usedSize;
}
//向上调整
public void siftUp(int child){
int parent=(child-1-1)/2;
while(parent>=0){
if(elem[parent]<elem[child]){
swap(parent,child);
child=parent;
parent=(child-1)/2;
}else {
break;
}
}
}
//插入元素
public void offer(int num){
if(isFull()){
elem= Arrays.copyOf(elem,2*elem.length);
}
elem[usedSize]=num;
usedSize++;
siftUp(usedSize-1);
}
//弹出堆顶元素
public int poll(){
if(isEmpty()){
return -1;
}
int old=elem[0];
swap(0,usedSize-1);
usedSize--;
siftDown(0,usedSize);
return old;
}
//判断是否为空
public boolean isEmpty(){
return usedSize==0;
}
}
二、什么是堆
概念:
前面介绍过队列,队列是一种先进先出(FIFO)的数据结构,但有些情况下,操作的数据可能带有优先级,一般出队列时,可能需要优先级高的元素先出队列,比如:在手机上玩游戏的时候,如果有来电,那么系统应该优先处理打进来的电话;初中那会班主任排座位时可能会让成绩好的同学先挑座位。
在这种情况下,数据结构应该提供两个最基本的操作,一个是返回最高优先级对象,一个是添加新的对象。这种数据结构就是优先级队列(Priority Queue)。
堆分两类:大根堆、小根堆 :
如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储在一个一维数组中,并满足:Ki = K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为小堆(或大堆)。将根节点最大的堆叫做大根堆,根节点最小的堆叫做小根堆。
堆的性质:
- 堆中某个节点的值总是不大于或不小于其父节点的值;
- 堆总是一棵完全二叉树。
但是!注意的是,并不是全部都按顺序排列,看下图,25比56小,但是还是排前面,只要保持它的子节点不大于或者不小于父节点就行了。
小根堆:
大根堆 :
所以我们可以知道为什么堆也可以叫优先级队列?
答:逻辑结构来说像完全二叉树,一堆一堆的,但是存储结构又像队列,所以叫优先级队列(堆)。
将元素存储到数组中后,可以根据二叉树章节的性质5对树进行还原。假设i为节点在数组中的下标,则有:
- 如果i为0,则i表示的节点为根节点,否则i节点的双亲节点为 (i - 1)/2
- 如果2 * i + 1 小于节点个数,则节点i的左孩子下标为2 * i + 1,否则没有左孩子
- 如果2 * i + 2 小于节点个数,则节点i的右孩子下标为2 * i + 2,否则没有右孩子
三、堆的创建
堆的向下调整
对于集合{ 27,15,19,18,28,34,65,49,25,37 }中的数据,如果将其创建成堆呢?
- 先随便排成堆
- 将堆头向下调整
仔细观察上图后发现:根节点的左右子树已经完全满足堆的性质,因此只需将根节点向下调整好即可。
向下过程(以小堆为例):
- 让parent标记需要调整的节点,child标记parent的左孩子(注意:parent如果有孩子一定先是有左孩子)
- 如果parent的左孩子存在,即:child < size, 进行以下操作,直到parent的左孩子不存在
- parent右孩子是否存在,存在找到左右孩子中最小的孩子,让child进行标
- 将parent与较小的孩子child比较,如果:
- parent小于较小的孩子child,调整结束
- 否则:交换parent与较小的孩子child,交换完成之后,parent中大的元素向下移动,可能导致子 树不满足对的性质,因此需要继续向下调整,即parent = child;child = parent*2+1; 然后继续2。
四、堆的时间复杂度
向下调整:O(N)
向上调整O(N*logN)
因为向上调整的复杂度大于向下调整,所以在建堆时,我们习惯用向下调整。
五、堆的插入与删除
插入:
- 先将元素放入到底层空间中(注意:空间不够时需要扩容)
- 将最后新插入的节点向上调整,直到满足堆的性质
这时是向上调整,因为其他都有序了,如果还向下调整,那么全部就都调整了一遍,浪费时间
删除:
注意:
堆的删除一定删除的是堆顶元素。具体如下:
- 将堆顶元素对堆中最后一个元素交换
- 将堆中有效数据个数减少一个
- 对堆顶元素进行向下调整
六、自带的堆PriorityQueue
Java集合框架中提供了PriorityQueue和PriorityBlockingQueue两种类型的优先级队列,PriorityQueue是线程不安全的,PriorityBlockingQueue是线程安全的,本文主要介绍PriorityQueue。
关于PriorityQueue的使用要注意:
- .使用时必须导入PriorityQueue所在的包,
- PriorityQueue中放置的元素必须要能够比较大小,不能插入无法比较大小的对象,否则会抛出 ClassCastException异常
- 不能插入null对象,否则会抛出NullPointerException
- 没有容量限制,可以插入任意多个元素,其内部可以自动扩容
- 插入和删除元素的时间复杂度为
- PriorityQueue底层使用了堆数据结构
- PriorityQueue默认情况下是小堆---即每次获取到的元素都是最小的元素
优先级队列的构造
构造器 | 功能介绍 |
PriorityQueue() | 创建一个空的优先级队列,默认容量是11 |
PriorityQueue(int initialCapacity) | 创建一个初始容量为initialCapacity的优先级队列,注意: initialCapacity不能小于1,否则会抛IllegalArgumentException异 常 |
PriorityQueue(Collection c) | 用一个集合来创建优先级队列 |
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异常,时 间复杂度 ,注意:空间不够时候会进行扩容 |
E peek() | 获取优先级最高的元素,如果优先级队列为空,返回null |
E poll() | 移除优先级最高的元素并返回,如果优先级队列为空,返回null |
int size() | 获取有效元素的个数 |
void clear() | 清空 |
boolean isEmpty() | 检测优先级队列是否为空,空返回true |
static void TestPriorityQueue2(){
int[] arr = {4,1,9,2,8,0,7,3,6,5};
// 一般在创建优先级队列对象时,如果知道元素个数,建议就直接将底层容量给好
// 否则在插入时需要不多的扩容
// 扩容机制:开辟更大的空间,拷贝元素,这样效率会比较低
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("优先级队列不为空");
}
}
扩容方式:
注意:以下是JDK 1.8中,PriorityQueue的扩容方式:
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;
}
简单来讲:
- 如果容量小于64时,是按照oldCapacity的2倍方式扩容的
- 如果容量大于等于64,是按照oldCapacity的1.5倍方式扩容的
- 如果容量超过MAX_ARRAY_SIZE,按照MAX_ARRAY_SIZE来进行扩容
七、概念习题和OJ习题
概念习题
1.下列关键字序列为堆的是:()
A: 100,60,70,50,32,65 B: 60,70,65,50,32,100 C: 65,100,70,32,50,60
D: 70,65,100,32,50,60 E: 32,50,100,70,65,60 F: 50,100,70,65,60,32
答案选:A。100的子类为60和70(都比100小,ok的),60的子类为50和32(都比60小,ok的),70的子类为65(比70小,ok的),是大堆。再随便举一个C吧,65的子类为100和70(没有都大于或小于65,所以不是堆)
2.已知小根堆为8,15,10,21,34,16,12,删除关键字8之后需重建堆,在此过程中,关键字之间的比较次数是()
A: 1 B: 2 C: 3 D: 4
答案选:C。这是个小根堆,首先8和12调换位置,usedSize--删掉8,然后比大小向下调整12的位置,首先第一次比较15和10谁小,10小,这时让10和12比较,这是第二次比较,然后16和12比较,这是第三次。
3.最小堆[0,3,2,5,7,4,6,8],在删除堆顶元素0之后,其结果是()
A: [3,2,5,7,4,6,8] B: [2,3,5,7,4,6,8] C: [2,3,4,5,7,8,6] D: [2,3,4,5,6,7,8]
答案选C。首先让0和8交换位置,然后删掉0【8,3,2,5,7,4,6】,这个是小根堆,3和2比,2小和8交换【2,3,8,5,7,4,6】;4和6比,4小和8交换 ,所以答案是【2,3,4,5,7,8,6】
OJ问题
Top-k问题 OJ链接
最大或者最小的前k个数据。比如:世界前500强公司
我们只需要建立一个小根堆,把那些数据都装进去,这样堆顶元素就是最小的,每次都poll一下到新的数组里,返回新数组。
public int[] smallestK(int[] arr, int k) {
PriorityQueue<Integer> minHeap=new PriorityQueue<>();
for(int i=0;i<arr.length;i++){
minHeap.offer(arr[i]);
}
int[] tmp=new int[k];
for(int j=0;j<k;j++){
tmp[j]=minHeap.poll();
}
return tmp;
}
但是,这却不是我们topk的正规解法,为什么?
答:时间复杂度太大了,如果n的数字越大,产生的时间很多, 接下来请看下一种解法!
思路如下:
- 堆的大小为k
- 前k个建堆
- 建大根堆
代码实现:
class IntCmp implements Comparator<Integer> {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
}
public int[] smallestK(int[] arr, int k) {
int[] tmp = new int[k];
if(k == 0) {
return tmp;
}
PriorityQueue<Integer> maxHeap = new PriorityQueue<>(new IntCmp());
//1.把前K个元素放进堆中
for (int i = 0; i < k; i++) {
maxHeap.offer(arr[i]);
}
//2.遍历剩下的N-K个元素
for (int i = k; i < arr.length; i++) {
int val = maxHeap.peek();
if(val > arr[i]) {
maxHeap.poll();
maxHeap.offer(arr[i]);
}
}
for (int i = 0; i < k; i++) {
tmp[i] = maxHeap.poll();
}
return tmp;
}
同理:
对于求第k小,第k大都可以用这种思路,求k就建k个数的堆,求小建大堆,求大建小堆
八、堆排序
堆排序即利用堆的思想来进行排序,总共分为两个步骤:
- 1. 建堆
- 升序:建大堆
- 降序:建小堆
- 2. 利用堆删除思想来进行排序
1.一组记录排序码为(5 11 7 2 3 17),则利用堆排序方法建立的初始堆为()
A: (11 5 7 2 3 17) B: (11 5 7 2 17 3) C: (17 11 7 2 3 5) D: (17 11 7 5 3 2) E: (17 7 11 3 5 2) F: (17 7 11 3 2 5)
答案:C。首先,堆排序,思考你要排升序还是降序,升序:建大堆,降序:建小堆。5与17交换,从上往下交换看谁大,交换,堆删除一样的思想,但是发现已经排序完了,break。