1、概念
优先队列是用来做什么事情的?应用程序需要处理优先级的事件,为每个事件分配一个优先级,优先处理优先级高的事件,这样就需要优先队列。
什么是优先队列?支持删除最大元素和插入元素。
2、怎样实现?
1、无序数组实现,在插入的时候往数组末尾插入,获取的时候再获取最大的,这样插入的时间复杂度是O(1),获取的时间复杂度是O(n)。
2、有序数组实现,在插入的时候排序,获取的时候获取元素第一个,这样插入时间复杂度是O(n),获取的时间复杂度是O(1)。
3、二叉堆实现。
3、堆的定义
数据结构二叉堆能很好的实现优先队列的基本操作,在二叉堆数组中,每个元素都要保证大于等于另外两个特定位置的元素,相应的,这些位置的元素又至少要大于等于数组中另外两个元素,以此类推。
定义:当一颗二叉树的每个节点都大于等于它的两个子节点的时候,它被称为堆有序。
二叉堆表示法:如果用指针来表示,每个元素都需要三个指针来找到它的上下结点(父节点和两个子节点各需要一个)。但是如果我们使用完全二叉树来表示,表达将会变得十分方便,完全二叉树只需要用数组而不需要用指针就可以表示。
定义:二叉堆是一组能够用完全二叉树排序的元素,并且在数组中按照层级存储(不使用数组的第一个位置)。
在一个堆中:位置为k的父节点位置为(k/2 需要向下取值),而它的两个子节点的位置分别为2K和2K+1,这样在不使用指针的情况下,我们也可以通过计算数组的索引在树中上下移动。用数组(堆)实现的完全二叉树是很严格的,但是它的灵活性已经足以让我们高效的实现优先队列。用它们我们将能实现对数级别的插入元素和删除最大的元素的操作。利用数组中无需指针即可沿着树上下移动的便利以及以下性质,算法保证了对数复杂度的性能。
4、堆的算法
1、由下至上的堆有序化(上浮)如果堆的有序状态因为某个结点变得比它的父结点更大而被打破。那么我们通过交换它和它的父节点来修复堆。
2、由上至下的堆有序化(下沉) 如果堆的有序状态因为某个结点变得比它的两个子节点或者是其中之一更小而被打破了,那么我们可以通过将它和它的两个子结点中的较大者交换来恢复堆。
3、插入元素:我们将新添加的加到数组末尾,增加堆的大小并且让这个新元素上浮到合适的位置。
4、删除最大元素:我们从数组顶端删去最大元素并将数组的最后一个元素放到顶端,减小堆的大小,并且让这个元素下沉到合适的位置。
5、总结
优先队列由一个基于堆的完全二叉树表示,存储于数组pq[1··N]中,p[0]没有使用,在insert中,我们将N加一并把新元素添加在数组最后,然后用swim()恢复堆的秩序。在delMax()中我们从pq1]中返回需要返回的元素,然后将pq[N]移动到pq[1],将N减一并用sink恢复堆的秩序,同时我们还将不再使用的pq[N+1]设置为null,以便回收所占用的空间。
命题Q。对于一个含有N个元素的基于堆的优先队列,插入元素操作只需要不过(lgN+1)次比较(上浮操作),删除最大元素的操作需要不超过2lgN次比较。(下沉操作)。
证明。由命题P可知,两种操作都需要在根节点和堆底之间移动元素,而路径长度不超过lgN,对于路径上的每个结点,删除最大元素需要两次比较(除了堆底元素),一次用来找出最大的子结点,一次用来确认子结点是否需要上浮
对于需要大量混杂插入和删除最大元素的操作的典型应用来说,命题Q意味着一个重要的性能突破。
使用有序或者无序数组的优先队列的初级实现总是需要线性时间来完成其中一种操作,但基于堆的实现能够保证在对数时间内完成他们,这种差别使得我们解决以前无法解决的问题。
6、扩展
1、堆排序
堆排序的基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了
堆排序实现原理?
堆排序分为两个阶段:第一阶段实现一个堆,第二阶段然后再下沉阶段从堆中按照递减顺序取出所有元素并得到排序结果。
N个元素构造一个堆的时间复杂度是多少?NlogN
具体实现:我们将数组直接作为一个堆,然后对这个堆进行下沉操作。这样做的好处是不需要额外空间了
1、堆的构造:用swim上浮方法,需要从左到右扫描全部,用sink下沉方法,只需要扫描一半 如何利用下沉进行排序?
1、从最后一个非叶子结点开始,从左至右,从下至上进行调整。这样就可以构成一个大碓顶
2、将大碓顶元素和最后一个互换,然后再将剩下的进行下沉操作,等到只剩一个元素的时候下沉操作完成,排序也就完成了,因为每次下沉操作的复杂度是logn所以总体复杂度是Nlogn
7、代码实现
二叉堆实现
class MaxPQ{
constructor(){
//声明一个数组保存二叉堆
//0开头启用
this.tp=[null];
}
size(){
return this.tp.length;
}
isEmpty(){
return this.tp.length===0;
}
//删除二叉堆最大的元素(删除头部元素,然后最末尾的补上来,再下沉)
delMax(){
//获取头部元素,并且头部元素置为空
var heade=this.tp[1];
this.tp[1]=this.tp.pop();
this.sink(1)
console.log(this.tp);
return heade;
//出队尾部
//然后赋值给头部,然后下沉
}
//插入(将新元素添加到数组末尾,增加堆的大小让这个新元素上浮到合适的位置)
insert(item){
this.tp.push(item);
this.swim(this.size()-1);
console.log(this.tp);
}
//上浮
swim(k){
// k>1代表不是根节点并且根节点比子节点小
while(k>1&&this.less(Math.floor(k/2),k)==-1){
this.exch(Math.floor(k/2),k);
k=Math.floor(k/2);
console.log(k);
}
}
//下沉
sink(k){
//如果是最后一个结点就不需要下沉了
var j;
while(2*k<=this.size()){
//判断
j=2*k;
//先比较子节点哪个大,然后如果父节点比大的小,就交换
//j小于N是为了给另外结点留位置
if(j<this.size()&&this.less(j,j+1)==-1) j++;
//比对,如果k比j小就交换然后停止
if(this.less(j,k)==1){
this.exch(k,j);
}else{
break;
}
k=j;
//没有的话就把j赋值为k
}
}
//私有方法
//i小于j 返回-1 i大于j返回1 i==j返回0
less(i,j){
if(this.tp[i]<this.tp[j]){
return -1;
}
if(this.tp[i]>this.tp[j]){
return 1;
}
if(this.tp[i]==this.tp[j]){
return 0;
}
}
//交换ij
exch(i,j){
var temp=this.tp[i];
this.tp[i]=this.tp[j];
this.tp[j]=temp;
}
}
复制代码
堆排序实现
function sort(arrs) {
//第一步获取大顶堆(到这个的时候已经构建成为了一个堆)
//如何获取大顶堆
//此时我们从最后一个非叶子结点开始,从左至右,从下至上进行调整。
//这样从最后一棵树进行下沉操作每往上一个结点,就排好了一个堆,这样就初步得到了一个最大堆顶
for(let k=Math.floor(arr.length/2);k>=1;k--){
sink(arrs,k,arrs.length)
}
// console.log(arrs);
var n=arr.length;
while(n>1){
exch(arrs,1,n--);
sink(arrs,1,n);
}
console.log(arrs);
}
function sink(arrs, k, n) {
//下沉步骤
var j;
//保证自己没有下降到最后
while (2 * k <= n-1) {
//判断
j = 2 * k;
//先比较子节点哪个大,然后如果父节点比大的小,就交换
//j小于N是为了给另外结点留位置
if (j < n-1 && less(j, j + 1) == -1) j++;
//比对,如果k比j小就交换然后停止
if (less(j, k) == 1) {
exch(arrs,k, j);
} else {
break;
}
k = j;
//没有的话就把j赋值为k
}
function less(i, j){
if(arrs[i]<arrs[j]){
return -1;
}
if(arrs[i]>arrs[j]){
return 1;
}
if(arrs[i]==arrs[j]){
return 0;
}
}
}
function exch(arrs, i, j) {
var temp = arrs[i];
arrs[i] = arrs[j];
arrs[j] = temp;
}
let arr = [null,1, 6, 5, 8, 4, 2];
sort(arr)
复制代码