目录
1、优先队列的定义
优先队列:按照优先级与重要性来组织对象的一种ADT。
一种特殊的“队列”,取出元素的顺序是依照元素的优先权(关键字)大小,而不是元素进入队列的先后顺序。
生活中的例子:在多用户的环境中,操作系统调度程序必须决定在若干进程中运行哪个进程;发话到打印机中的若个作业可能在某些时候并不想按照先来先打印的方式运行。
优先队列需要的操作:
1、插入: 增加一个带有重要级别的元素,插入到队列中的位置并不在意
2、删除: 队列中的重要级别最高的那个元素
3、获得头元素: 队列中的重要级别最高的那个元素
对比一般队列:
1、插入:增加一个元素,这个元素被插入到队列中队尾
2、删除:剩除一个队列中队头的那个元素
3、获得头元素:获得队列中队头的那个元素
2、优先队列的实现方式:
1、数组 :
插入 — 元素总是插入尾部:Θ( 1 )
删除 — 查找最大(或最小)关键字 :Θ ( n )
从数组中删去找到的结点,同时需要移动元素:O( n )
2、链表:
插入 — 元素总是插入链表的头部:Θ( 1 )
删除 — 查找最大(或最小)关键字:Θ ( n )
删去找到的结点:Θ(1)
3、有序数组:
插入 — 找到合适的位置:O( n ) 或 O(log2 n )
移动元素并插入:O( n )
删除 — 删去最后一个元素:Θ( 1 )
4、有序链表:
插入 — 找到合适的位置:O( n )
插入元素:Θ( 1 )
删除 — 删除首元素或最后元素:Θ( 1 )
5、考虑用树来实现:
如果用二叉检索树(不采用AVL平衡),时间复杂度为树的高度,但对于每次都要删除/插入最大的树的操作,使得树平衡性被破坏,效率降低。
在对优先队列操作时,需要更关注的是删除最大值(插入可以次位考虑)。因此考虑将最大值放在树根处,根据二叉树的递归定义,规定任何结点都是以其为根结点的树中的最大值,删除时只需要去掉树根结点即可→堆。
3、堆的两个特性:
1、结构性:完全二叉树,因此可以用数组代替链表来实现;
2、有序性:任一结点的关键字是其子树所有结点的最大值(或最小值),由于二叉树的递归定义,满足任意结点的关键值大于其左右子结点即可。对根元素的访问是最快的获取速度,因此堆能够快速的找出重要级别最高的元素。
● 最大堆(MaxHeap),也称“大顶堆”:任意一个结点的键值都大于或等于其任意一个子结点存储的值(是以该结点为根结点的二叉树的最大值)
● 最小堆(MinHeap),也称“小顶堆”:任意一个结点的键值都小于或等于其任意一个子结点存储的值(是以该结点为根结点的二叉树的最小值)
注意:从根结点到任意结点的路径,始终满足有序性(一路从大到小/从小到大)
4、堆的ADT
下文以最大堆为例说明
void insert(E item); //将元素item插入最大堆MaxHeap
E findMax(); //获取MaxHeap中最大元素
E deleteMax(); //删除MaxHeap中最大元素(优先级最高)并返回
boolean isFull(); //判断最大堆MaxHeap是否已满
boolean isEmpty(); //判断最大堆MaxHeap是否为空
5、堆的实现
相关参数:
private int currentSize; //当前堆的大小,也是当前存放的末尾元素位置
private E[] array; //堆数组
(1)插入操作:
步骤:
Step1 将要插入的元素插入到堆中的最后一个位置
Step2 若当前结点为根结点(基准情形1)则返回
Step3 比较当前元素与其父结点的重要性:
1、若满足堆的有序性(基准情形2)则返回
2、若不满足则将当前元素和其父元素交换,重复Step2-3此操作的时间复杂度为:T (N) = O ( log N )
代码实现:
public void insert(E item) {
if (isFull()) {
System.out.println("heap is full!");
return;
}
array[++currentSize] = item; //先自增再存放。将item放在数组最后
int temp = currentSize;
while ((temp != 1) && (array[temp].compareTo(array[getParent(temp)]) > 0)) {
//如果非根节点且比父结点的键值大则交换
swap(array, temp, getParent(temp));
temp = getParent(temp);//始终定位在插入的元素位置
}
}
代码优化:过滤结点,比交换数据要快。
public void insert(E item) {
if (isFull()) {
System.out.println("heap is full!");
return;
}
int temp = ++currentSize;//temp指向当前要插入的位置
while ((temp != 1) && (item.compareTo(array[getParent(temp)]) > 0)) {
array[temp] = array[getParent(temp)]; //如果非根节点且插入元素比父结点的键值大,则向下过滤父结点
temp = getParent(temp);
}
array[temp] = item; //将item插入
}
(2)删除操作:队列中的重要级别最高的那个元素
步骤:
Step1 用临时变量保存根结点(优先级最高的)的元素并删除
Step2 将堆最末尾的元素填补到根结点位置
Step3 若填补的结点为叶结点(基准情形1)则返回
Step4 将填补的结点与它的孩子结点进行重要性的比较
1、若满足堆的有序性(基准情形2)则返回
2、若不满足则将当前元素和其重要级别高的孩子结点交换,重复Step3-4
此操作的时间复杂度为:T (N) = O ( log N )
public E deleteMax() {
if (isEmpty()) {
System.out.println("heap is empty!");
return null;
}
E maxItem = array[1];
E itemFill = array[currentSize--];//用最大堆中最后一个元素从根结点开始向上过滤下层结点
//删除了堆中唯一的元素的情况:此时currentSize==0,下述代码也可以满足该情况,无需单独判断
int tempFill = 1;
while (!isLeaf(tempFill)) { //不是叶子结点,则必定有左子结点
int child = tempFill * 2; //child定位到填补位置的左子结点处
if ((child != currentSize) && (array[child].compareTo(array[child + 1]) < 0))
child++; //若有右结点,child指向左右子结点的较大者
if (itemFill.compareTo(array[child]) < 0) {
array[tempFill] = array[child]; //若不满足有序性(itemFill比左右子结点的较大者小),将左右子结点中的较大者向上过滤
tempFill = child; //tempFill始终指向要填补的位置,下移一层
} else
break;
}
array[tempFill] = itemFill;
return maxItem;
}
(3)堆的建立
建立最大堆:将已经存在的N个元素按最大堆的要求存放在一个一维数组中
方法1:
通过插入操作,将N个元素一个个相继插入到一个初始为空的堆中去,其时间代价最大为O(N log N)。
方法2:在线性时间复杂度下建立最大堆
先将N个元素按输入顺序存入,满足完全二叉树的结构特性
再调整各结点位置,以满足最大堆的有序特性:
[思想]
删除操作中调整的思想核心:已知该结点左右子树均为堆,如何调整该结点的位置使以其为根结点的二叉树为堆?→和左右子结点比较,将三者中最大的向上过滤。
因此,将该思想用于调整无序的序列中:从满足有序性的堆开始(基准情形),向上对其根结点进行调整。
叶结点即有序的堆,而对于完全二叉树,叶结点近乎占了一半,所以对于初始化的数组来说,其中有一半以上的元素满足堆序。
[步骤]
Step1 将N个元素按输入顺序存入数组中,修改相应的参数值。Step2 从从非叶子结点开始从右到左,从下到上调整直至根结点(具体实现与删除结点是类似)
此操作的线性时间复杂度T(n)=O(n)
代码实现:
public MaxHeap(E[] array) {
this.array = (E[]) new Object[array.length + 1];
for (int i = 0; i < array.length; i++) {
this.array[i + 1] = array[i];
}
currentSize = array.length;
buildHeap();
}
public void buildHeap() {
for (int i = currentSize / 2; i > 0; i--) {
//从非叶子结点开始从右到左,从下到上调整直至根结点
int temp = i; //记录当前调整元素应放置的位置
E item = array[temp]; //临时存储下当前位置的元素值
while (!isLeaf(temp)) { //不是叶子结点,则必定有左子结点
int child = temp * 2; //child定位到填补位置的左子结点处
if ((child != currentSize) && (array[child].compareTo(array[child + 1]) < 0))
child++; //若有右结点,child指向左右子结点的较大者
if (item.compareTo(array[child]) < 0) {
array[temp] = array[child]; //若不满足有序性(itemFill比左右子结点的较大者小),将左右子结点中的较大者向上过滤
temp = child; //tempFill始终指向要填补的位置,下移一层
} else
break;
}
array[temp] = item;
}
}
6、堆的应用
(1)堆排序
依次输出根结点元素并删除(其中需要调整操作)
时间复杂度:O ( N )+O ( (N-1) logN ) = O ( N log N )
public void heapSort() {
while (currentSize != 0) {
System.out.print(deleteMax()+ " ");
}
}
(2)Huffman树的建立
适用频繁增加删除的情况,具体见【数据结构】4.3Huffman树。
7、完整代码
public class MaxHeap<E extends Comparable<E>> {
private static final int DEFAULT_CAPACITY = 10;//默认大小
private int currentSize;//当前堆的大小,也是当前存放的末尾元素位置
private E[] array;//堆数组
//constructor部分,(考虑到表达的精简性,0不存储,从1开始存储元素)。
public MaxHeap() {
this.array = (E[]) new Object[DEFAULT_CAPACITY + 1];
currentSize = 0;
}
public MaxHeap(int maxSize) {
this.array = (E[]) new Object[maxSize + 1];
currentSize = 0;
}
public MaxHeap(E[] array) {
this.array = (E[]) new Object[array.length + 1];
for (int i = 0; i < array.length; i++) {
this.array[i + 1] = array[i];
}//从1开始算
currentSize = array.length;
buildHeap();
}
public void insert(E item) {
if (isFull()) {
System.out.println("heap is full!");
return;
}
/*方法1
array[++currentSize] = item; //先自增再存放。将item放在数组最后
int temp = currentSize;
while ((temp != 1) && (array[temp].compareTo(array[getParent(temp)]) > 0)) {
//如果非根节点且比父结点的键值大则交换
swap(array, temp, getParent(temp));
temp = getParent(temp);//始终定位在插入的元素位置
}
*/
//方法2,过滤结点,比交换数据要快:T(N)=O(log N)
int temp = ++currentSize;//temp指向当前要插入的位置
while ((temp != 1) && (item.compareTo(array[getParent(temp)]) > 0)) {
array[temp] = array[getParent(temp)]; //如果非根节点且插入元素比父结点的键值大,则向下过滤父结点
temp = getParent(temp);
}
array[temp] = item; //将item插入
}
public E deleteMax() {
if (isEmpty()) {
System.out.println("heap is empty!");
return null;
}
E maxItem = array[1];
E itemFill = array[currentSize--];//用最大堆中最后一个元素从根结点开始向上过滤下层结点
//删除了堆中唯一的元素的情况:此时currentSize==0,下述代码也可以满足该情况,无需单独判断
int tempFill = 1;
while (!isLeaf(tempFill)) { //不是叶子结点,则必定有左子结点
int child = tempFill * 2; //child定位到填补位置的左子结点处
if ((child != currentSize) && (array[child].compareTo(array[child + 1]) < 0))
child++; //若有右结点,child指向左右子结点的较大者
if (itemFill.compareTo(array[child]) < 0) {
array[tempFill] = array[child]; //若不满足有序性(itemFill比左右子结点的较大者小),将左右子结点中的较大者向上过滤
tempFill = child; //tempFill始终指向要填补的位置,下移一层
} else
break;
}
array[tempFill] = itemFill;
return maxItem;
}
public E findMax() {
return array[1];
}
public void buildHeap() {
for (int i = currentSize / 2; i > 0; i--) { //从非叶子结点开始从右到左,从下到上调整直至根结点
int temp = i; //记录当前调整元素应放置的位置
E item = array[temp]; //临时存储下当前位置的元素值
while (!isLeaf(temp)) { //不是叶子结点,则必定有左子结点
int child = temp * 2; //child定位到填补位置的左子结点处
if ((child != currentSize) && (array[child].compareTo(array[child + 1]) < 0))
child++; //若有右结点,child指向左右子结点的较大者
if (item.compareTo(array[child]) < 0) {
array[temp] = array[child]; //若不满足有序性(itemFill比左右子结点的较大者小),将左右子结点中的较大者向上过滤
temp = child; //tempFill始终指向要填补的位置,下移一层
} else
break;
}
array[temp] = item;
}
}
public void print() {
for (int i = 1; i <= currentSize; i++) {
System.out.print(array[i] + " ");
}
}
public void heapSort() {
while (currentSize != 0) {
System.out.print(deleteMax()+ " ");
}
}
public boolean isFull() {
return currentSize == array.length - 1;
}
public boolean isEmpty() {
return currentSize == 0;
}
private int getParent(int i) {
return i / 2;
}
private boolean isLeaf(int i) {
return i * 2 > currentSize;
}
private int getLeftSon(int i) {
return i * 2;
}
private void swap(E[] array, int x, int y) {
E temp = array[y];
array[y] = array[x];
array[x] = temp;
}
public static void main(String[] args) {
Integer[] test = {1, 2, 6, 4, 5, 7, 3};
MaxHeap<Integer> maxHeap = new MaxHeap<Integer>(test);
maxHeap.print();
maxHeap.deleteMax();
System.out.println();
System.out.println("delete 7:");
maxHeap.print();
maxHeap.insert(7);
System.out.println();
System.out.println("insert 7:");
maxHeap.print();
System.out.println();
System.out.println("heapsort: ");
maxHeap.heapSort();
}
}