自己动手实现java数据结构(八)优先级队列
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
⾃⼰动⼿实现java数据结构(⼋)优先级队列
1.优先级队列介绍
1.1 优先级队列
有时在调度任务时,我们会想要先处理优先级更⾼的任务。
例如,对于同⼀个柜台,在决定队列中下⼀个服务的⽤户时,总是倾向于优先服务VIP⽤户,⽽让普通⽤户等待,即使普通的⽤户是先加⼊队列的。
优先级队列和普通的先进先出FIFO的队列类似,最⼤的不同在于,优先级队列中优先级最⾼的元素总是最先出队的,⽽不是遵循先进先出的顺序。
1.2 堆
优先级队列的接⼝要求很简单。
从逻辑上来说,、或者等数据结构都可⽤于实现优先级队列。
但考虑到时间和空间的效率,就必须仔细斟酌和考量了。
⽽⼀种被称为堆的数据结构⾮常适合实现优先级队列。
’
堆和⼆叉搜索树类似,存储的元素在逻辑上是按照层次排放的,在全局任意地⽅其上层元素优先级⼤于下层元素,这⼀顺序性也被称为堆序性,⽽其中优先级最⼤的元素被放在最⾼的层级(⼤顶堆)。
和⼆叉搜索树的排序⽅式不同的是,堆中元素的顺序并不是完全的排序,⽽只是维护了⼀种偏序关系,被称为堆序性。
在这种偏序关系下,元素之间的顺序性⽐较疏散,维护堆序性的代价⽐较低,因⽽在实现优先级队列时,堆的效率要⾼于平衡⼆叉搜索树。
1.3 完全⼆叉堆
完全⼆叉堆是堆的⼀种,其元素在逻辑上是以完全⼆叉树的形式存放的,但实际却是存储在向量(数组)中的。
在这⾥,我们使⽤完全⼆叉堆来实现优先级队列。
2.优先级队列ADT接⼝
/**
* 优先级队列 ADT接⼝
*/
public interface PriorityQueue <E>{
/**
* 插⼊新数据
* @param newData 新数据
* */
void insert(E newData);
* @return当前优先级最⼤的数据
* */
E peekMax();
/**
* 获得并且删除当前优先级最⼤值
* @return被删除的当前优先级最⼤的数据
*/
E popMax();
/**
* 获得当前优先级队列元素个数
* @return当前优先级队列元素个数
* */
int size();
/**
* 是否为空
* @return true 队列为空
* false 队列不为空
* */
boolean isEmpty();
}
3.完全⼆叉堆实现细节
3.1 基础属性
完全⼆叉堆内部使⽤之前封装好的向量作为基础。
和⼆叉搜索树类似,⽤户同样可以通过传⼊Comparator⽐较器来指定堆中优先级⼤⼩⽐较的逻辑。
public class CompleteBinaryHeap<E> implements PriorityQueue<E>{
/**
* 内部向量
* */
private ArrayList<E> innerArrayList;
/**
* ⽐较逻辑
* */
private final Comparator<E> comparator;
/**
* 当前堆的逻辑⼤⼩
* */
private int size;
}
构造⽅法:
/**
* ⽆参构造函数
* */
public CompleteBinaryHeap() {
this.innerArrayList = new ArrayList<>();
parator = null;
}
/**
* 指定初始容量的构造函数
* @param defaultCapacity 指定的初始容量
* */
public CompleteBinaryHeap(int defaultCapacity){
this.innerArrayList = new ArrayList<>(defaultCapacity);
parator = null;
}
/**
* 指定初始容量的构造函数
* @param comparator 指定的⽐较器逻辑
* */
public CompleteBinaryHeap(Comparator<E> comparator){
this.innerArrayList = new ArrayList<>();
parator = comparator;
}
/**
* 指定初始容量和⽐较器的构造函数
* @param defaultCapacity 指定的初始容量
public CompleteBinaryHeap(int defaultCapacity, Comparator<E> comparator) {
this.innerArrayList = new ArrayList<>(defaultCapacity);
parator = comparator;
}
/**
* 将指定数组转换为⼀个完全⼆叉堆
* @param array 指定的数组
* */
public CompleteBinaryHeap(E[] array){
this.innerArrayList = new ArrayList<>(array);
parator = null;
this.size = array.length;
// 批量建堆
heapify();
}
/**
* 将指定数组转换为⼀个完全⼆叉堆
* @param array 指定的数组
* @param comparator 指定的⽐较器逻辑
* */
public CompleteBinaryHeap(E[] array, Comparator<E> comparator){
this.innerArrayList = new ArrayList<>(array);
parator = comparator;
this.size = array.length;
// 批量建堆
heapify();
}
3.2 辅助⽅法
由于完全⼆叉堆在逻辑上等价于⼀颗完全⼆叉树,但实际上却采⽤了⼀维的向量数据结构来存储元素。
因⽽我们需要实现诸如getParentIndex、getLeftChildIndex、getRightChildIndex等⽅法来进⾏完全⼆叉树和向量表⽰⽅法的转换。
这⾥,定义了⼀些私有⽅法来封装常⽤的逻辑,⽤以简化代码。
/**
* 获得逻辑上双亲节点下标
* @param currentIndex 当前下标
* */
private int getParentIndex(int currentIndex){
return (currentIndex - 1)/2;
}
/**
* 获得逻辑上左孩⼦节点下标
* @param currentIndex 当前下标
* */
private int getLeftChildIndex(int currentIndex){
return (currentIndex * 2) + 1;
}
/**
* 获得逻辑上右孩⼦节点下标
* @param currentIndex 当前下标
* */
private int getRightChildIndex(int currentIndex){
return (currentIndex + 1) * 2;
}
/**
* 获得末尾下标
* */
private int getLastIndex(){
return this.size - 1;
}
/**
* 获得最后⼀个⾮叶⼦节点下标
* */
private int getLastInternal(){
return (this.size()/2) - 1;
}
/**
* 交换向量中两个元素位置
* @param b 另⼀个元素的下标
* */
private void swap(int a, int b){
// 现暂存a、b下标元素的值
E aData = this.innerArrayList.get(a);
E bData = this.innerArrayList.get(b);
// 交换位置
this.innerArrayList.set(a,bData);
this.innerArrayList.set(b,aData);
}
/**
* 进⾏⽐较
* */
@SuppressWarnings("unchecked")
private int compare(E t1, E t2){
// 迭代器不存在
if(parator == null){
// 依赖对象本⾝的 Comparable,可能会转型失败
return ((Comparable) t1).compareTo(t2);
}else{
// 通过迭代器逻辑进⾏⽐较
return pare(t1,t2);
}
}
3.3 插⼊和上滤
当新元素插⼊完全⼆叉堆时,我们直接将其插⼊向量末尾(堆底最右侧),此时新元素的优先级可能会⼤于其双亲元素甚⾄祖先元素,破坏了堆序性,因此我们需要对插⼊的新元素进⾏⼀次上滤操作,使完全⼆叉堆恢复堆序性。
由于堆序性只和双亲和孩⼦节点相关,因此堆中新插⼊元素的⾮祖先元素的堆序性不会受到影响,上滤只是⼀个局部性的⾏为。
上滤操作
上滤的元素不断的和⾃⼰的双亲节点进⾏优先级的⽐较:
1. 如果上滤元素的优先级较⼤,则与双亲节点交换位置,继续向上⽐较。
2. 如果上滤元素的优先级较⼩(等于),堆序性恢复,终⽌⽐较,结束上滤操作。
3. 特别的,当上滤的元素被交换到树根节点时(向量下标第0位),此时由于上滤的元素是堆中的最⼤元素,终⽌上滤操作。
上滤操作的时间复杂度:
上滤操作时,上滤元素进⾏⽐较的次数正⽐于上滤元素的深度。
因此,上滤操作的时间复杂度为O(logN)。
@Override
public void insert(E newData) {
// 先插⼊新数据到向量末尾
this.innerArrayList.add(newData);
// 获得向量末尾元素下标
int lastIndex = getLastIndex();
// 对向量末尾元素进⾏上滤,以恢复堆序性
siftUp(lastIndex);
}
/**
* 上滤操作
* @param index 需要上滤的元素下标
* */
private void siftUp(int index){
while(index >= 0){
// 获得当前节点
int parentIndex = getParentIndex(index);
E currentData = this.innerArrayList.get(index);
E parentData = this.innerArrayList.get(parentIndex);
// 如果当前元素⼤于双亲元素
if(compare(currentData,parentData) > 0){
// 交换当前元素和双亲元素的位置
swap(index,parentIndex);
// 继续向上迭代
index = parentIndex;
}else{
// 当前元素没有违反堆序性,直接返回
return;
}
}
3.4 删除和下滤
当优先级队列中极值元素出队时,需要在满⾜堆序性的前提下,选出新的极值元素。
我们简单的将当前向量末尾的元素放在堆顶,堆序性很有可能被破坏了。
此时,我们需要对当前的堆顶元素进⾏⼀次下滤操作,使得整个完全⼆叉堆恢复堆序性。
下滤操作:
下滤的元素不断的和⾃⼰的左、右孩⼦节点进⾏优先级的⽐较:
1. 双亲节点最⼤,堆序性恢复,终⽌下滤。
2. 左孩⼦节点最⼤,当前下滤节点和⾃⼰的左孩⼦节点交换,继续下滤。
3. 右孩⼦节点最⼤,当前下滤节点和⾃⼰的右孩⼦节点交换,继续下滤。
4. 特别的,当下滤的元素抵达堆底时(成为叶⼦节点),堆序性已经恢复,终⽌下滤。
下滤操作时间复杂度:
下滤操作时,下滤元素进⾏⽐较的次数正⽐于下滤元素的⾼度。
因此,下滤操作的时间复杂度为O(logN)。
@Override
public E popMax() {
if(this.innerArrayList.isEmpty()){
throw new CollectionEmptyException("当前完全⼆叉堆为空");
}
// 将当前向量末尾的元素和堆顶元素交换位置
int lastIndex = getLastIndex();
swap(0,lastIndex);
// 暂存被删除的最⼤元素(之前的堆顶最⼤元素被放到了向量末尾)
E max = this.innerArrayList.get(lastIndex);
this.size--;
// 对当前堆顶元素进⾏下滤,以恢复堆序性
siftDown(0);
return max;
}
/**
* 下滤操作
* @param index 需要下滤的元素下标
* */
private void siftDown(int index){
int size = this.size();
// 叶⼦节点不需要下滤
int half = size >>> 1;
while(index < half){
int leftIndex = getLeftChildIndex(index);
int rightIndex = getRightChildIndex(index);
if(rightIndex < size){
// 右孩⼦存在 (下标没有越界)
E leftData = this.innerArrayList.get(leftIndex);
E rightData = this.innerArrayList.get(rightIndex);
E currentData = this.innerArrayList.get(index);
// ⽐较左右孩⼦⼤⼩
if(compare(leftData,rightData) >= 0){
// 左孩⼦更⼤,⽐较双亲和左孩⼦
if(compare(currentData,leftData) >= 0){
// 双亲最⼤,终⽌下滤
return;
}else{
// 三者中,左孩⼦更⼤,交换双亲和左孩⼦的位置
swap(index,leftIndex);
// 继续下滤操作
index = leftIndex;
}
}else{
// 双亲最⼤,终⽌下滤
return;
}else{
// 三者中,右孩⼦更⼤,交换双亲和右孩⼦的位置
swap(index,rightIndex);
// 继续下滤操作
index = rightIndex;
}
}
}else{
// 右孩⼦不存在 (下标越界)
E leftData = this.innerArrayList.get(leftIndex);
E currentData = this.innerArrayList.get(index);
// 当前节点⼤于左孩⼦
if(compare(currentData,leftData) >= 0){
// 终⽌下滤
return;
}else{
// 交换左孩⼦和双亲的位置
swap(index,leftIndex);
// 继续下滤操作
index = leftIndex;
}
}
}
}
3.5 批量元素建堆
有时,我们需要将⼀个⽆序的元素集合数组转换成⼀个完全⼆叉堆,这⼀操作被称为批量建堆。
⼀个朴素的想法是:将⽆序集合中的元素依次插⼊⼀个空的完全⼆叉堆,对每⼀个新插⼊的元素进⾏上滤操作。
使⽤上滤操作实现的对N个元素进⾏批量建堆的算法,其时间复杂度为O(n.logn),⽐较直观。
但还存在⼀种效率更加⾼效的批量建堆算法,是以下滤操作为基础实现的,被称为Floyd建堆算法。
下滤操作可以看做是将两个较⼩的堆合并为⼀个更⼤堆的过程(单个元素可以被视为⼀个最⼩的堆),通过从底到⾼不断的下滤操作,原本⽆序的元素集合将通过不断的合并建⽴较⼩的堆,最终完成整个集合的建堆过程。
Floyd建堆算法的时间复杂度的证明较为复杂,其时间复杂度⽐起以上滤为基础的朴素算法效率⾼⼀个数量级,为O(n)。
简单的⼀种解释是:在完全⼆叉树中,低层元素的数量要远远少于⾼层的数量。
⾼层元素的⾼度较⾼⽽深度较低;底层元素的⾼度较低⽽深度较⾼。
由于上滤操作的时间复杂度正⽐于⾼度,对于存在⼤量底层元素的完全⼆叉堆很不友好,使得基于上滤的批量建堆算法效率较低。
/**
* 批量建堆(将内部数组转换为完全⼆叉堆)
* */
// 获取下标最⼤的内部⾮叶⼦节点
int lastInternalIndex = getLastInternal();
// Floyd建堆算法时间复杂度"O(n)"
// 从lastInternalIndex开始向前遍历,对每⼀个元素进⾏下滤操作,从⼩到⼤依次合并
for(int i=lastInternalIndex; i>=0; i--){
siftDown(i);
}
}
4.堆排序
堆排序主要分为两步进⾏:
1. 堆排序⾸先将传⼊的数组转化为⼀个堆(floyd建堆算法,时间复杂度O(n))。
2. 和选择排序类似,堆排序每次都从未排序的区间中选择出⼀个极值元素置⼊已排序区域,在堆中极值元素就是堆顶元素,可以通过popMax⽅法(时间复杂度O(logN))获得。
从数组末尾向前遍历,循环往复直⾄排序完成,总的时间复杂度为O(N logN)。
综上所述,堆排序的渐进时间复杂度为O(N logN)。
同时由于堆排序能够在待排序数组中就地的进⾏排序,因此空间效率很⾼,空间复杂度为(O(1))。
public static <T> void heapSort(T[] array){
CompleteBinaryHeap<T> completeBinaryHeap = new CompleteBinaryHeap<>(array);
for(int i=array.length-1; i>=0; i--){
array[i] = completeBinaryHeap.popMax();
}
}
5.完整代码
优先级队列ADT接⼝:
1/**
2 * 优先级队列 ADT接⼝
3*/
4public interface PriorityQueue <E>{
5
6/**
7 * 插⼊新数据
8 * @param newData 新数据
9 * */
10void insert(E newData);
11
12/**
13 * 获得优先级最⼤值(窥视)
14 * @return当前优先级最⼤的数据
15 * */
16 E peekMax();
17
18/**
19 * 获得并且删除当前优先级最⼤值
20 * @return被删除的当前优先级最⼤的数据
21*/
22 E popMax();
23
24/**
25 * 获得当前优先级队列元素个数
26 * @return当前优先级队列元素个数
27 * */
28int size();
29
30/**
31 * 是否为空
32 * @return true 队列为空
33 * false 队列不为空
34 * */
35boolean isEmpty();
36 }
View Code
完全⼆叉堆实现:
/**
* 完全⼆叉堆实现优先级队列
*/
public class CompleteBinaryHeap<E> implements PriorityQueue<E>{
// =========================================成员属性=========================================== /**
* 内部向量
* */
private ArrayList<E> innerArrayList;
/**
* ⽐较逻辑
* */
private final Comparator<E> comparator;
/**
* 当前堆的逻辑⼤⼩
* */
private int size;
// ===========================================构造函数======================================== /**
* ⽆参构造函数
* */
public CompleteBinaryHeap() {
this.innerArrayList = new ArrayList<>();
parator = null;
}
/**
* 指定初始容量的构造函数
* @param defaultCapacity 指定的初始容量
* */
public CompleteBinaryHeap(int defaultCapacity){
this.innerArrayList = new ArrayList<>(defaultCapacity);
parator = null;
}
/**
* 指定初始容量的构造函数
* @param comparator 指定的⽐较器逻辑
* */
public CompleteBinaryHeap(Comparator<E> comparator){
this.innerArrayList = new ArrayList<>();
parator = comparator;
}
/**
* 指定初始容量和⽐较器的构造函数
* @param defaultCapacity 指定的初始容量
* @param comparator 指定的⽐较器逻辑
* */
public CompleteBinaryHeap(int defaultCapacity, Comparator<E> comparator) {
this.innerArrayList = new ArrayList<>(defaultCapacity);
parator = comparator;
}
/**
* 将指定数组转换为⼀个完全⼆叉堆
* @param array 指定的数组
* */
public CompleteBinaryHeap(E[] array){
this.innerArrayList = new ArrayList<>(array);
parator = null;
this.size = array.length;
// 批量建堆
heapify();
}
/**
* 将指定数组转换为⼀个完全⼆叉堆
* @param array 指定的数组
* @param comparator 指定的⽐较器逻辑
* */
public CompleteBinaryHeap(E[] array, Comparator<E> comparator){
this.innerArrayList = new ArrayList<>(array);
parator = comparator;
this.size = array.length;
heapify();
}
// ==========================================外部⽅法=========================================== @Override
public void insert(E newData) {
// 先插⼊新数据到向量末尾
this.innerArrayList.add(newData);
// 获得向量末尾元素下标
int lastIndex = getLastIndex();
// 对向量末尾元素进⾏上滤,以恢复堆序性
siftUp(lastIndex);
}
@Override
public E peekMax() {
// 内部数组第0位即为堆顶max
return this.innerArrayList.get(0);
}
@Override
public E popMax() {
if(this.innerArrayList.isEmpty()){
throw new CollectionEmptyException("当前完全⼆叉堆为空");
}
// 将当前向量末尾的元素和堆顶元素交换位置
int lastIndex = getLastIndex();
swap(0,lastIndex);
// 暂存被删除的最⼤元素(之前的堆顶最⼤元素被放到了向量末尾)
E max = this.innerArrayList.get(lastIndex);
this.size--;
// 对当前堆顶元素进⾏下滤,以恢复堆序性
siftDown(0);
return max;
}
@Override
public int size() {
return this.size;
}
@Override
public boolean isEmpty() {
return this.size() == 0;
}
@Override
public String toString() {
//:::空列表
if(this.isEmpty()){
return "[]";
}
//:::列表起始使⽤"["
StringBuilder s = new StringBuilder("[");
//:::从第⼀个到倒数第⼆个元素之间
for(int i=0; i<size-1; i++){
//:::使⽤", "进⾏分割
s.append(this.innerArrayList.get(i)).append(",").append(" ");
}
//:::最后⼀个元素使⽤"]"结尾
s.append(this.innerArrayList.get(size-1)).append("]");
return s.toString();
}
public static <T> void heapSort(T[] array){
CompleteBinaryHeap<T> completeBinaryHeap = new CompleteBinaryHeap<>(array);
for(int i=array.length-1; i>=0; i--){
array[i] = completeBinaryHeap.popMax();
}
}
// =========================================内部辅助函数=========================================== /**
* @param index 需要上滤的元素下标
* */
private void siftUp(int index){
while(index >= 0){
// 获得当前节点
int parentIndex = getParentIndex(index);
E currentData = this.innerArrayList.get(index);
E parentData = this.innerArrayList.get(parentIndex);
// 如果当前元素⼤于双亲元素
if(compare(currentData,parentData) > 0){
// 交换当前元素和双亲元素的位置
swap(index,parentIndex);
// 继续向上迭代
index = parentIndex;
}else{
// 当前元素没有违反堆序性,直接返回
return;
}
}
}
/**
* 下滤操作
* @param index 需要下滤的元素下标
* */
private void siftDown(int index){
int size = this.size();
// 叶⼦节点不需要下滤
int half = size >>> 1;
while(index < half){
int leftIndex = getLeftChildIndex(index);
int rightIndex = getRightChildIndex(index);
if(rightIndex < size){
// 右孩⼦存在 (下标没有越界)
E leftData = this.innerArrayList.get(leftIndex);
E rightData = this.innerArrayList.get(rightIndex);
E currentData = this.innerArrayList.get(index);
// ⽐较左右孩⼦⼤⼩
if(compare(leftData,rightData) >= 0){
// 左孩⼦更⼤,⽐较双亲和左孩⼦
if(compare(currentData,leftData) >= 0){
// 双亲最⼤,终⽌下滤
return;
}else{
// 三者中,左孩⼦更⼤,交换双亲和左孩⼦的位置 swap(index,leftIndex);
// 继续下滤操作
index = leftIndex;
}
}else{
// 右孩⼦更⼤,⽐较双亲和右孩⼦
if(compare(currentData,rightData) >= 0){
// 双亲最⼤,终⽌下滤
return;
}else{
// 三者中,右孩⼦更⼤,交换双亲和右孩⼦的位置 swap(index,rightIndex);
// 继续下滤操作
index = rightIndex;
}
}
}else{
// 右孩⼦不存在 (下标越界)
E leftData = this.innerArrayList.get(leftIndex);
E currentData = this.innerArrayList.get(index);
// 当前节点⼤于左孩⼦
if(compare(currentData,leftData) >= 0){
// 终⽌下滤
return;
}else{
// 交换左孩⼦和双亲的位置
swap(index,leftIndex);
// 继续下滤操作
index = leftIndex;
}
}
}
/**
* 批量建堆(将内部数组转换为完全⼆叉堆)
* */
private void heapify(){
// 获取下标最⼤的内部⾮叶⼦节点
int lastInternalIndex = getLastInternal();
// Floyd建堆算法时间复杂度"O(n)"
// 从lastInternalIndex开始向前遍历,对每⼀个元素进⾏下滤操作,从⼩到⼤依次合并for(int i=lastInternalIndex; i>=0; i--){
siftDown(i);
}
}
/**
* 获得逻辑上双亲节点下标
* @param currentIndex 当前下标
* */
private int getParentIndex(int currentIndex){
return (currentIndex - 1)/2;
}
/**
* 获得逻辑上左孩⼦节点下标
* @param currentIndex 当前下标
* */
private int getLeftChildIndex(int currentIndex){
return (currentIndex * 2) + 1;
}
/**
* 获得逻辑上右孩⼦节点下标
* @param currentIndex 当前下标
* */
private int getRightChildIndex(int currentIndex){
return (currentIndex + 1) * 2;
}
/**
* 获得当前向量末尾下标
* */
private int getLastIndex(){
return this.size - 1;
}
/**
* 获得最后⼀个⾮叶⼦节点下标
* */
private int getLastInternal(){
return (this.size()/2) - 1;
}
/**
* 交换向量中两个元素位置
* @param a 某⼀个元素的下标
* @param b 另⼀个元素的下标
* */
private void swap(int a, int b){
// 现暂存a、b下标元素的值
E aData = this.innerArrayList.get(a);
E bData = this.innerArrayList.get(b);
// 交换位置
this.innerArrayList.set(a,bData);
this.innerArrayList.set(b,aData);
}
/**
* 进⾏⽐较
* */
@SuppressWarnings("unchecked")
private int compare(E t1, E t2){
// 迭代器不存在
if(parator == null){
// 依赖对象本⾝的 Comparable,可能会转型失败
return ((Comparable) t1).compareTo(t2);
}else{
// 通过迭代器逻辑进⾏⽐较
return pare(t1,t2);
}
}
}
View Code
本系列博客的代码在我的 github上:,存在许多不⾜之处,请多多指教。