优先队列和堆
一、优先队列
普通队列:先进先出,后进后出
优先队列:出队顺序和入队顺序无关;和优先级相关;(例如医院排队和操作系统动态选择优先级最高的任务执行)
图解:关键词“动态”;队列中的元素是在不断变化的,不断有新元素入队,不仅仅是按照优先级排序;【优先级可以具体定义】
与普通队列的区别:出队元素是优先级最高的元素;队首元素也是优先级最高的元素,而不是最早进入队列的元素;
二、堆的基础结构
1.二叉堆(Binary Heap)
二叉堆要满足的条件:二叉堆是一颗完全二叉树;完全二叉树:把元素顺序排列成树的形状;【最下面一层都是叶子节点,但在最后一层上面可能还有叶子节点,但这些叶子节点必须全在树的右侧】
二叉堆的性质:【节点大小和所处层次之间并没有必然的联系】
1.最大堆:堆中某个节点的值总是小于其父节点的值;【父亲节点的值要大于左右孩子的值】
2.最小堆:堆中某个节点的值总是大于其父节点的值;【父亲节点的值要小于左右孩子的值】
可以用数组来实现完全二叉树:
代码实现:最大堆 MaxHeap.java
public class MaxHeap<E extends Comparable<E>> { //要具有可比较性Comparable
private Array<E> data;
public MaxHeap(int capacity){
data = new Array<>(capacity);
}
public MaxHeap(){
data = new Array<>();
}
// 返回堆中的元素个数
public int size(){
return data.getSize();
}
// 返回一个布尔值, 表示堆中是否为空
public boolean isEmpty(){
return data.isEmpty();
}
// 返回完全二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
private int parent(int index){
if(index == 0) //根节点,没有父亲节点
throw new IllegalArgumentException("index-0 doesn't have parent.");
return (index - 1) / 2;
}
// 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
private int leftChild(int index){
return index * 2 + 1;
}
// 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
private int rightChild(int index){
return index * 2 + 2;
}
}
三、 向堆中添加元素
堆中元素上浮(Sift up)
1.在堆中添加元素 52
2. 添加 52 后,52 > 16 ,不满足条件 父亲节点 > 孩子节点;要进行调整:从 52 开始一路与其父亲节点做比较并交换顺序
将 52 与 16 交换顺序;但 52 > 41 ,继续交换位置
这时,62 > 52 ,符合堆的性质
堆中元素上浮(Sift up):52 从底部逐渐上浮,直到其合适的位置依然可以维持堆的性质
代码实现:
MaxHeap.java
public class MaxHeap<E extends Comparable<E>> {
private Array<E> data;
public MaxHeap(int capacity){
data = new Array<>(capacity);
}
public MaxHeap(){
data = new Array<>();
}
// 返回堆中的元素个数
public int size(){
return data.getSize();
}
// 返回一个布尔值, 表示堆中是否为空
public boolean isEmpty(){
return data.isEmpty();
}
// 返回完全二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
private int parent(int index){
if(index == 0)
throw new IllegalArgumentException("index-0 doesn't have parent.");
return (index - 1) / 2;
}
// 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
private int leftChild(int index){
return index * 2 + 1;
}
// 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
private int rightChild(int index){
return index * 2 + 2;
}
// 向堆中添加元素(新增代码)
public void add(E e){
data.addLast(e);
siftUp(data.getSize() - 1);
}
private void siftUp(int k){
while(k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0 ){
//K所在的父亲元素与K做比较,如果父亲元素更小的话,二者交换位置
data.swap(k, parent(k)); //使用 swap 方法进行k 与k父亲元素交换
k = parent(k);
}
}
}
Array.java
public class Array<E> {
private E[] data;
private int size;
// 构造函数,传入数组的容量capacity构造Array
public Array(int capacity){
data = (E[])new Object[capacity];
size = 0;
}
// 无参数的构造函数,默认数组的容量capacity=10
public Array(){
this(10);
}
// 获取数组的容量
public int getCapacity(){
return data.length;
}
// 获取数组中的元素个数
public int getSize(){
return size;
}
// 返回数组是否为空
public boolean isEmpty(){
return size == 0;
}
// 在index索引的位置插入一个新元素e
public void add(int index, E e){
if(index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
if(size == data.length)
resize(2 * data.length);
for(int i = size - 1; i >= index ; i --)
data[i + 1] = data[i];
data[index] = e;
size ++;
}
// 向所有元素后添加一个新元素
public void addLast(E e){
add(size, e);
}
// 在所有元素前添加一个新元素
public void addFirst(E e){
add(0, e);
}
// 获取index索引位置的元素
public E get(int index){
if(index < 0 || index >= size)
throw new IllegalArgumentException("Get failed. Index is illegal.");
return data[index];
}
// 修改index索引位置的元素为e
public void set(int index, E e){
if(index < 0 || index >= size)
throw new IllegalArgumentException("Set failed. Index is illegal.");
data[index] = e;
}
// 查找数组中是否有元素e
public boolean contains(E e){
for(int i = 0 ; i < size ; i ++){
if(data[i].equals(e))
return true;
}
return false;
}
// 查找数组中元素e所在的索引,如果不存在元素e,则返回-1
public int find(E e){
for(int i = 0 ; i < size ; i ++){
if(data[i].equals(e))
return i;
}
return -1;
}
// 从数组中删除index位置的元素, 返回删除的元素
public E remove(int index){
if(index < 0 || index >= size)
throw new IllegalArgumentException("Remove failed. Index is illegal.");
E ret = data[index];
for(int i = index + 1 ; i < size ; i ++)
data[i - 1] = data[i];
size --;
data[size] = null; // loitering objects != memory leak
if(size == data.length / 4 && data.length / 2 != 0)
resize(data.length / 2);
return ret;
}
// 从数组中删除第一个元素, 返回删除的元素
public E removeFirst(){
return remove(0);
}
// 从数组中删除最后一个元素, 返回删除的元素
public E removeLast(){
return remove(size - 1);
}
// 从数组中删除元素e
public void removeElement(E e){
int index = find(e);
if(index != -1)
remove(index);
}
public void swap(int i, int j){
if(i < 0 || i >= size || j < 0 || j >= size)
throw new IllegalArgumentException("Index is illegal.");
E t = data[i];
data[i] = data[j];
data[j] = t;
}
@Override
public String toString(){
StringBuilder res = new StringBuilder();
res.append(String.format("Array: size = %d , capacity = %d\n", size, data.length));
res.append('[');
for(int i = 0 ; i < size ; i ++){
res.append(data[i]);
if(i != size - 1)
res.append(", ");
}
res.append(']');
return res.toString();
}
// 将数组空间的容量变成newCapacity大小
private void resize(int newCapacity){
E[] newData = (E[])new Object[newCapacity];
for(int i = 0 ; i < size ; i ++)
newData[i] = data[i];
data = newData;
}
}
四、从堆中取出元素
取出元素,只取出堆顶元素(二叉树根节点),这个元素是二叉树中存储的最大的元素【取出操作只能取出该元素,不能取出其他元素】;
过程:1.访问根节点,数组中索引为 0 的元素;将其拿走之后,对整个堆来说,可以看成是有两颗子树;
2.将堆中的最后一个元素(即 16)顶到堆顶去;数组0变为16并删掉数组中10对应的16
3.但16在堆顶的话,打破了堆的性质,16 < 52 也 < 30 ;不符合堆中父亲节点大于左右孩子的性质;
Sift Down:堆中元素下沉;
下沉过程1.父亲节点和左右孩子进行比较(16与52和30进行比较);
2.选择孩子中较大的元素,交换位置;
3.父亲节点继续与左右孩子对比,重复上面步骤
4.16 > 15,满足堆的性质,最终形态为下图
代码实现:MaxHeap.java
public class MaxHeap<E extends Comparable<E>> {
private Array<E> data;
public MaxHeap(int capacity){
data = new Array<>(capacity);
}
public MaxHeap(){
data = new Array<>();
}
// 返回堆中的元素个数
public int size(){
return data.getSize();
}
// 返回一个布尔值, 表示堆中是否为空
public boolean isEmpty(){
return data.isEmpty();
}
// 返回完全二叉树的数组表示中,一个索引所表示的元素的父亲节点的索引
private int parent(int index){
if(index == 0)
throw new IllegalArgumentException("index-0 doesn't have parent.");
return (index - 1) / 2;
}
// 返回完全二叉树的数组表示中,一个索引所表示的元素的左孩子节点的索引
private int leftChild(int index){
return index * 2 + 1;
}
// 返回完全二叉树的数组表示中,一个索引所表示的元素的右孩子节点的索引
private int rightChild(int index){
return index * 2 + 2;
}
// 向堆中添加元素
public void add(E e){
data.addLast(e);
siftUp(data.getSize() - 1);
}
private void siftUp(int k){
while(k > 0 && data.get(parent(k)).compareTo(data.get(k)) < 0 ){
data.swap(k, parent(k));
k = parent(k);
}
}
// 看堆中的最大元素
public E findMax(){
if(data.getSize() == 0)
throw new IllegalArgumentException("Can not findMax when heap is empty.");
return data.get(0);
}
// 取出堆中最大元素(新增代码)
public E extractMax(){
E ret = findMax();
data.swap(0, data.getSize() - 1);
data.removeLast(); //删掉最大元素
siftDown(0); //进行下沉操作,对应索引为0
return ret;
}
private void siftDown(int k){
while(leftChild(k) < data.getSize()){//左孩子的索引比数组元素总数还要小,k节点没有孩子了
int j = leftChild(k); // 在此轮循环中,data[k]和data[j]交换位置
if( j + 1 < data.getSize() &&
data.get(j + 1).compareTo(data.get(j)) > 0 )
j ++; //j 存储的实际上是右孩子的索引
// data[j] 是 leftChild 和 rightChild 中的最大值
if(data.get(k).compareTo(data.get(j)) >= 0 ) //k 与孩子中最大元素j进行对比
break;
data.swap(k, j); //若 j>k,则交换二者位置
k = j;
}
}
}
Main.java(测试)
import java.util.Random;
public class Main {
public static void main(String[] args) {
int n = 1000000; //随机数数量
MaxHeap<Integer> maxHeap = new MaxHeap<>();
Random random = new Random();
for(int i = 0 ; i < n ; i ++)
maxHeap.add(random.nextInt(Integer.MAX_VALUE));//对1000000个随机数进行排序(从大到小)
int[] arr = new int[n];
for(int i = 0 ; i < n ; i ++)
arr[i] = maxHeap.extractMax();
for(int i = 1 ; i < n ; i ++)
if(arr[i-1] < arr[i]) //比较相邻数(右边>左边)
throw new IllegalArgumentException("Error");
System.out.println("Test MaxHeap completed.");
}
}
输出:
时间复杂度分析:添加(add) 和 取出操作(extractMax) 的时间复杂度都是:O(log n)【还是二叉树高度这个级别,但对于堆来说,由于其是完全二叉树,故永远不会变为链表形态,不存在最差时为 n 的情况】