优先队列
- 堆有序定义:当一颗二叉树的每个节点都大于等于(或小于等于)它的两个子节点时,它被称为堆有序。
- 堆有序原理:根节点是堆有序的二叉树的最大节点(最小节点)。
- 二叉堆表示法:堆有序的二叉树使用完全二叉树来表示,由于使用了完全二叉树,使得我们可以用数组来存储二叉堆,而数组的索引代表节点的位置,即按照层级顺序放入数组,根节点在索引1(第一个位置不使用),两个子节点在索引2,3…
- 二叉堆定义:二叉堆是一组能够用堆有序的完全二叉树排序的元素,并在数组中按照层级存储(不使用数组中的第一个元素)。
- 优先队列:基于数据结构——二叉堆(简称为堆)实现。
- 定理:一颗具有N个节点的完全二叉树的高度为
$\lfloor lgN \rfloor$
(规定:只有根节点的树高度为0)。 - 复杂度:对于一个含有N个元素的基于堆的优先队列,插入元素操作只需不超过(lgN+1)次比较,删除最大(最小)元素的操作不超过2lgN次比较。
Show me the code
:
/**
* 可以动态扩容的优先队列
* @param <T>
*/
public class PriorityQueue<T extends Comparable<T>> {
private T[] pQueue; // 基于堆的完全二叉树,后面实现了数组的动态扩容,使数组的内存空间既不浪费又不至于紧张
private int N = 0; // N为当前索引值-1,存储于pQueue[1,N]之中,pQueue[0]没有使用,也代表数组中现存的元素个数
private int maxN; // maxN为给定数组的有效长度(数组实际长度-1)
public PriorityQueue(int n) {
this.maxN = n;
pQueue = (T[]) new Comparable[maxN+1]; // 不允许创建泛型数组,必须要转型
}
public boolean isEmpty() {
return N == 0;
}
public int size() {
return N;
}
// 插入
public void insert(T value) {
int usedLength = N+1;
if (usedLength >= maxN*3/4) // 大于数组长度的3/4,扩容成两倍
resize(maxN*2);
pQueue[++N] = value; // 执行插入操作;同时更新N,注意这里不能用N++
swim(N); // 从堆底加入
}
// 删除最大值
public T delMax() {
if (pQueue[1] == null) // 已为空,不执行任何操作
return null;
T max = pQueue[1];
exch(1,N--); // 把最后一个元素放在顶端,然后N--(堆的大小-1)
sink(1); // 让“最后”一个元素下沉
pQueue[N+1] = null; // 将垃圾(删除的最大值)清空
if (N <= maxN/4) // 小于数组长度的1/4,缩容成原来的一半
resize(maxN/2);
return max;
}
// 扩容 或者 缩容
private void resize(int newLength) {
this.maxN = newLength;
T[] newPQueue = (T[]) new Comparable[maxN+1];
for (int i = 1; i <= N; i++) // 注意这里是 1-N
newPQueue[i] = pQueue[i];
pQueue = newPQueue;
}
// 用于堆实现的比较方法:返回pQueue[i]是否小于pQueue[j]
private boolean less(int i, int j) {
return pQueue[i].compareTo(pQueue[j]) < 0;
}
// 用于堆实现的交换方法:交换pQueue[i] 和 pQueue[j]
private void exch(int i, int j) {
T temp = pQueue[i];
pQueue[i] = pQueue[j];
pQueue[j] = temp;
}
// 上浮:当某个节点变大(或在堆底加入了一个新的元素),主要用于插入
private void swim(int k) {
while(k > 1 && less(k/2,k)) {
exch(k/2,k); // k/2默认向下取整
k = k/2;
}
}
// 下沉:当某个节点变小(例如,将根节点替换为一个较小的元素),主要用于删除最大值
private void sink(int k) {
while (2*k <= N) {
int j = 2*k;
if (j < N && less(j,j+1)) // 找到较大的子节点,并将j指向它
j++;
if (!less(k,j)) // 此时j一定指向较大的子节点,如果pQueue[k] >= pQueue[j],则下沉结束
break;
exch(k,j); // 如果没有break则说明pQueue[k] < pQueue[j],交换pQueue[k]、pQueue[j]
k = j; // 交换k、j,让k始终指向下沉的元素
}
}
@Override
public String toString() {
return "PriorityQueue " +
Arrays.toString(pQueue) +
", UsedSize=" + N +
", ArraySize=" + maxN;
}
}
class TestPriorityQueue {
public static void main(String[] args) {
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(10);
// test 扩容
for (int i = 0; i < 10; i++)
priorityQueue.insert(i);
System.out.println(priorityQueue);
// test 缩容
priorityQueue.delMax();
priorityQueue.delMax();
priorityQueue.delMax();
priorityQueue.delMax();
priorityQueue.delMax();
priorityQueue.delMax();
System.out.println(priorityQueue);
}
}
/* Output:
PriorityQueue [null, 9, 8, 5, 6, 7, 1, 4, 0, 3, 2, null, null, null, null, null, null, null, null, null, null], UsedSize=10, ArraySize=20
PriorityQueue [null, 3, 2, 1, 0, null, null, null, null, null, null], UsedSize=4, ArraySize=10
*/
索引优先队列
讲解参考:https://www.cnblogs.com/nullzx/p/6624731.html
Show me the code
:
import java.util.Arrays;
/**
* 索引优先队列:优先队列只能操作队头和队尾的元素,而索引优先队列可以更新队列中任意位置的值,实现原理就是通过两个索引,一个正向索引,一个反向索引。
*/
public class IndexPriorityQueue<T extends Comparable<T>> {
/*示例:按照字母排序
Index 0 1 2 3 4 5 6 7 8 9 10 11
indexPq null 10 3 6 1 4 8 null null null null null
reIndexQp null 4 null 2 5 null 3 null 6 null 1 null
elements null k null f n null c null h null b null
*/
// 存储有优先级之分的元素(对象引用),不一定连续存放
private T[] elements;
// 索引二叉堆,从索引1开始按优先级(大小顺序)存储elements元素的下标,连续存放,即indexPq里存储的下标所对应的elements数组的元素才是真正有序的
private int[] indexPq;
// 反向索引:reIndexQp[indexPq[i]] = indexPq[reIndexQp[i]] = i,
// 作用是存储元素在elements数组中的索引值在index数组中的下标,这个数组也不是连续存放的,和elements数组对齐
private int[] reIndexQp;
private int N = 0; // elements数组中现存的元素个数
public IndexPriorityQueue(int maxN) {
elements = (T[]) new Comparable[maxN + 1]; // 不允许创建泛型数组,必须要转型
indexPq = new int[maxN + 1];
reIndexQp = new int[maxN +1];
for (int i = 0; i <= maxN; i++)
reIndexQp[i] = -1;
}
public boolean isEmpty() {
return N == 0;
}
public int size() {
return N;
}
public boolean contains(int k) {
return reIndexQp[k] != -1;
}
// 插入:在k位置插入元素,位置k并不代表任何含义,只是存储在elements数组的索引位置
public void insert(int k, T value) {
N++;
elements[k] = value; // 放入索引k
indexPq[N] = k; // 记录此元素所在索引位置(k)
reIndexQp[k] = N; // 记录indexPq数组中哪个位置(N)存储着此元素的索引
swim(N); // 从堆底加入并上浮,维护indexPq 和 reIndexQp
}
public T max() {
return elements[indexPq[1]];
}
public int maxIndex() {
return indexPq[1];
}
// 删除最大值,并返回其索引
public int delMax() {
int indexOfMax = indexPq[1];
if (elements[indexOfMax] == null) // 已为空,返回-1
return -1;
exch(1,N--); // 把最后一个元素(最小元素)放在顶端,然后N--(堆的大小-1)
sink(1); // 让“最后”一个元素下沉
elements[indexPq[N+1]] = null; // 将垃圾(删除的最大值)清空
reIndexQp[indexPq[N+1]] = -1; // 更新对应reIndexQp为-1
indexPq[N+1] = 0; // 更新最后一位删除的indexPq为0
return indexOfMax;
}
// 删除索引k位置的元素,与删除最大值类似
public void delete(int k) {
int indexOfPq = reIndexQp[k];
exch(indexOfPq,N--);
swim(indexOfPq);
sink(indexOfPq);
elements[k] = null;
reIndexQp[k] = -1;
indexPq[N+1] = 0;
}
// 更新值
public void change(int k, T newValue) {
elements[k] = newValue;
// 更新值后,可能出现三种情况:
// 1. 比父节点大:需要上浮
// 2. 比子节点小:需要下沉
// 3. 大小在父节点和子节点之间:不执行任何操作
// 所以此处采取的策略是先上浮在下沉(或先下沉再上浮)
swim(reIndexQp[k]); // 上浮
sink(reIndexQp[k]); // 下沉
}
// 用于堆实现的比较方法:这里怎么设计关乎着是大堆顶(<0)还是小堆顶(>0)
private boolean less(int i, int j) {
return elements[indexPq[i]].compareTo(elements[indexPq[j]]) < 0;
}
// 用于堆实现的交换方法:交换indexPq[i]、indexPq[j] 和 reIndexPq[i]、reIndexPq[j]
private void exch(int i, int j) {
int tempPq = indexPq[i];
indexPq[i] = indexPq[j];
indexPq[j] = tempPq;
reIndexQp[indexPq[i]] = i;
reIndexQp[indexPq[j]] = j;
}
// 上浮:当某个节点变大(或在堆底加入了一个新的元素),主要用于插入
private void swim(int k) {
while(k > 1 && less(k/2,k)) {
exch(k/2,k); // k/2默认向下取整
k = k/2;
}
}
// 下沉:当某个节点变小(例如,将根节点替换为一个较小的元素),主要用于删除最大值
private void sink(int k) {
while (2*k <= N) {
int j = 2*k;
if (j < N && less(j,j+1)) // 找到较大的子节点,并将j指向它
j++;
if (!less(k,j)) // 此时j一定指向较大的子节点,如果elements[indexPq[k]] >= elements[indexPq[j]],则下沉结束
break;
exch(k,j); // 如果没有break则说明elements[indexPq[k]] < elements[indexPq[j]],交换indexPq 和 reIndexQp
k = j; // 交换k、j,让k始终指向下沉的元素
}
}
@Override
public String toString() {
return " indexPq " +
Arrays.toString(indexPq) + "\n" +
" reIndexQp " +
Arrays.toString(reIndexQp) + "\n" +
"PriorityQueue " +
Arrays.toString(elements);
}
}
class TestPriorityQueue {
public static void main(String[] args) {
IndexPriorityQueue<Integer> indexPq = new IndexPriorityQueue<>(10);
indexPq.insert(1,1);
indexPq.insert(3,3);
indexPq.insert(4,4);
indexPq.insert(6,6);
indexPq.insert(8,8);
indexPq.insert(10,10);
System.out.println(indexPq);
System.out.println();
System.out.println(indexPq.max() + " " + indexPq.maxIndex());
System.out.println();
indexPq.delMax();
System.out.println(indexPq);
System.out.println();
indexPq.change(3,11);
System.out.println(indexPq);
System.out.println();
indexPq.delete(3);
System.out.println(indexPq);
}
}
/*
indexPq [0, 10, 6, 8, 1, 4, 3, 0, 0, 0, 0]
reIndexQp [-1, 4, -1, 6, 5, -1, 2, -1, 3, -1, 1]
PriorityQueue [null, 1, null, 3, 4, null, 6, null, 8, null, 10]
10 10
indexPq [0, 8, 6, 3, 1, 4, 0, 0, 0, 0, 0]
reIndexQp [-1, 4, -1, 3, 5, -1, 2, -1, 1, -1, -1]
PriorityQueue [null, 1, null, 3, 4, null, 6, null, 8, null, null]
indexPq [0, 3, 6, 8, 1, 4, 0, 0, 0, 0, 0]
reIndexQp [-1, 4, -1, 1, 5, -1, 2, -1, 3, -1, -1]
PriorityQueue [null, 1, null, 11, 4, null, 6, null, 8, null, null]
indexPq [0, 8, 6, 4, 1, 0, 0, 0, 0, 0, 0]
reIndexQp [-1, 4, -1, -1, 3, -1, 2, -1, 1, -1, -1]
PriorityQueue [null, 1, null, null, 4, null, 6, null, 8, null, null]
*/
堆排序
有了前面的基础,堆排序就非常简单了,思路就是先构造一个大顶堆,每次交换堆顶(数组头)和数组“最后”一个元素(最后一个元素的索引不断前移),然后让交换到堆顶的一个元素下沉,保持堆的有序性,最终输出从小到大排列有序的数组。
构造阶段 – sink 比 swim 更高效
高效的堆构造:因为数组的每个位置都已经是一个子堆的根节点了,所以以数组前半部分为根节点的子树一定包含了所有的元素,我们可以从N/2处自右至左扫描,即只扫描数组前半部分,然后sink()
,只需少于2N次比较以及少于N次交换就可以构造出有序的堆。
排序阶段
数组从左到右不断将最大元素放到最后,然后让"最后"一个元素下沉,保持堆的有序性,最终输出从小到大排列有序的数组
复杂度
将N个元素排序,堆排序只需少于(2NlgN+2N)次比较以及一半次数的交换。其中2NlgN来自下沉阶段,2N来自构造阶段。
Show me the code
:
import java.util.Arrays;
/**
* 堆排序
*/
public class HeapSort {
private Comparable[] toBeSortedArray; // 待排序的数组
public HeapSort(Comparable[] toBeSortedArray) {
this.toBeSortedArray = toBeSortedArray;
}
public void sort() {
int N = toBeSortedArray.length;
// 堆构造阶段:使用sink操作,从N/2处自右至左扫描,只需少于N次比较以及少于N次交换
for (int k = N/2; k >= 1; k--)
sink(k, N);
// 堆排序阶段:数组从左到右不断将最大元素放到最后,然后让最后一个元素下沉,保持堆的有序性,最终输出从小到大排列有序的数组
int n = N;
while (n > 1) {
exch(1, n--);
sink(1, n);
}
}
private void sink(int k, int N) {
while (2*k <= N) {
int j = 2*k;
if (j < N && less(j, j+1))
j++; // 找到两个子节点中较大的那个
if (!less(k, j)) // 这时j一定指向较大的子节点
break;
exch(k, j); // 如果没有break则说明pq[k-1] < pq[j-1],交换pq[k-1]、pq[j-1]
k = j;
}
}
// 注意:因为数组是从索引0开始存储的,而堆排序的索引为了计算方便从1开始,所以要 -1
private boolean less(int i, int j) {
return toBeSortedArray[i-1].compareTo(toBeSortedArray[j-1]) < 0;
}
private void exch(int i, int j) {
Comparable swap = toBeSortedArray[i-1];
toBeSortedArray[i-1] = toBeSortedArray[j-1];
toBeSortedArray[j-1] = swap;
}
// 判断数组是否有序
public boolean isSorted() {
for (int i = 1; i < toBeSortedArray.length; i++)
if (!less(i,i+1))
return false;
return true;
}
}
class TestHeapSort {
public static void main(String[] args) {
Integer[] testArray = {5,8,4,1,2,3,7,9,6,0}; // 注意数组是从索引0开始存储的,而堆排序的索引为了计算方便从1开始
HeapSort heapSort = new HeapSort(testArray);
heapSort.sort();
System.out.println(heapSort.isSorted());
System.out.println(Arrays.toString(testArray));
}
/* Output:
true
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
*/
}