什么是二叉堆?
二叉堆本质上是一种完全二叉树,它分为两个类型:最大堆 和 最小堆
- 最大堆: 任何一个父节点的值,都大于或等于它左、右孩子节点的值。根节点是整个二叉堆中最大的数。
- 最小堆: 和最大堆相反,任何一个父节点的值,都小于或等于它左、右孩子节点的值。根节点是整个二叉堆中最小的数。
二叉堆的根节点也叫作堆顶。
二叉堆的自我调整
所谓的自我调整,就是把一个不符合二叉堆的完成二叉树,转换成一个二叉堆的过程。
这里以最小堆举例:
插入节点
插入节点的思路:在完全二叉树的最后一个位置插入一个节点,然后让这个节点向上比较(上浮过程),如果比父节点小,则与父节点交换位置,依次向上比较,直至根节点;或者出现比父节点大的情况就停止删除节点
删除节点的思路:只要不是删除最后一个叶子节点,就将最后一个叶子节点替换到被删除节点的位置,然后让这个节点向下比较,如果比它的左右孩子节点都大,就把左右节点中较小值节点和这个节点的位置进行交换(下沉过程),直到没有孩子节点为止。构建二叉堆
构建二叉堆,就是把一个无序的二叉树调整为二叉堆,本质就是让所有非叶子节点依次下沉。
它从最后一个非叶子节点开始,进行下沉操作,也就是如果它比它的左右子节点都大,则它与最小子节点交换位置。然后找到倒数第二个非叶子节点,重复此操作,直至根节点下沉完,结束。
二叉堆的存储方式
虽然二叉堆本质上(形态上也是)是一个完全二叉树,但它的存储方式不是链表,而是顺序存储,也就是数组形式。
在数组方式存储中,树的节点间关系是这样的:
- 父节点的位置下标parent, 左孩子节点位置就是:2parent+1, 右孩子节点位置就是:2parent+2,即如果父节点下标为0,则其左节点为1,右节点为2
- 如果根据子节点来计算父节点的位置,如果子节点下标是奇数child(也就是左孩子节点),那么父节点为 (child - 1) / 2; 如果子节点是偶数(也就是右孩子节点),那么父节点为 (child - 2) / 2
二叉堆的实现方式
/*
二叉堆的代码实现
*/
// 根据子节点下标的奇偶性,来获取对应父节点的下标
function getParentIndex(childIndex) {
let parentIndex = 0;
if(childIndex % 2 === 0) {
parentIndex = (childIndex - 2) / 2 // 如果是右节点,父节点的下标
}else{
parentIndex = (childIndex - 1) / 2 // 如果是左节点,父节点的下标
}
return parentIndex;
}
// 最小堆上浮(一般对应插入操作)
function upAdjust(arr) {
let childIndex = arr.length -1; // 初始子节点的下标
parentIndex = getParentIndex(childIndex)
let temp = arr[childIndex] // 子节点值
while((childIndex > 0) && (temp < arr[parentIndex])) {
arr[childIndex] = arr[parentIndex]
childIndex = parentIndex
// 根据子节点下标的奇偶性,来获取对应父节点的下标
parentIndex = getParentIndex(childIndex)
}
arr[childIndex] = temp
return arr;
}
const testArr1 = upAdjust([4,2,9,36,7,8,1])
console.log(testArr1); // [ 1, 2, 4, 36, 7, 8, 9 ]
/**
* @description 二叉堆下沉(最小堆)
* @param arr: 待调整的二叉堆
* @param parentIndex: 要下沉的父节点位置
* @return arr: 调整后的二叉堆数组
*/
function downAdjust(arr, parentIndex) {
const len = arr.length;
// 保存父节点的值,用于最后赋值
let temp = arr[parentIndex]
// 左节点下标值
let childIndex = parentIndex * 2 + 1;
// 如果存在左孩子节点
while(childIndex < len) {
// 检测是否存在右节点,且右节点比左节点小点,则取右节点下标值;否则还是使用左节点下标
if(childIndex + 1 < len && arr[childIndex + 1] < arr[childIndex]) {
childIndex++;
}
// 如果父节点值比左右节点中最小的节点值都要小,那么结束
if(temp < arr[childIndex]) break;
// 否则,将爷节点与最小子节点值交换
arr[parentIndex] = arr[childIndex]
parentIndex = childIndex
childIndex = parentIndex * 2 + 1
}
arr[parentIndex] = temp
return arr;
}
const testArr2 = downAdjust([4,2,9,36,7,8,1,3], 0)
console.log(testArr2); // [ 2, 4, 9, 36, 7, 8, 1, 3 ]
/**
* @description 构造二叉堆,将无序的完全二叉树转换成一个二叉堆(最小堆)
* @param arr: 待调整的完全二叉树数组
* @return arr: 调整后的二叉堆
*/
function buildHeap(arr) {
let childIndex = arr.length - 1
let parentInddex = getParentIndex(childIndex)
for(let i = parentInddex; i >= 0; i--) {
downAdjust(arr, i)
}
return arr;
}
const testArr3 = buildHeap([4,2,9,36,7,8,1,3], 0)
console.log(testArr3); // [ 1, 2, 4, 3, 7, 8, 9, 36 ]
二叉堆的应用:最大优先队列和最小优先队列
我们先看看队列的特点:先进先出
比如:[7,1,2,3,4,5]
要入队8:[7,1,2,3,4,5,8]
出队时时,如果要出队8,就必须先出队7, 1,2,3, 4,5,最后才能出队8
那么,如果我不想出队其它的,就想直接出队8呢?要怎么做到呢?或者我想每次出队最小的,怎么做到呢?
暴力遍历
这就是我们日常思考的问题,怎么从一些集合中拿到最大值和最小值,相信大家脑瓜里第一想到的是暴力解法,遍历!
for(let i =0; i< arr.length; i++) {
// 比较拿出最大最小值..blablabla
}
这当然可以解决问题,但必须遍历n次(集合长度),也就是时间复杂度为O(n)
最大优先队列和最小优先队列
什么是最大优先队列和最小优先队列呢?就是无论入队顺序如何,都是当前最大或最小的元素优先出队
再瞅瞅刚刚实现的最大最小堆,是不是可以做到直接拿出堆顶,就是我们想要的答案了?
然后我们再来看看,二叉堆的时间复杂度,上浮和下沉操作每一次操作平均都是n/2,所以是O(logn),构建堆的操作是O(n),所以如果使用堆来实现队列的入队和出队,是不是就可以将O(n)优化成了O(logn)
因为上面实现的是最小堆,所以这里我实现的就是最小优先队列,最大优先队列同理是用最大堆做的,上代码:
/**
* 使用二叉堆实现最小优先队列
*/
class ProorityQueue {
constructor() {
// 设置队列初始长度为32
this.queue = new Array(32)
this.size = 0; // 记录已有个数的最后下标
}
// 入队
enQueue(key) {
if(this.size >= this.queue.length) {
this.resize()
}else {
this.queue[this.size] = key
this.size++;
this.upAdjust()
}
}
// 出队
deQueue() {
if(!this.size) {
throw new Error("this queue is empty!!")
}
const head = this.queue[0]
this.queue[0] = this.queue[--this.size]
this.downAdjust()
return head
}
// 根据子节点下标的奇偶性,来获取对应父节点的下标
getParentIndex(childIndex) {
let parentIndex = 0;
if(childIndex % 2 === 0) {
parentIndex = (childIndex - 2) / 2 // 如果是右节点,父节点的下标
}else{
parentIndex = (childIndex - 1) / 2 // 如果是左节点,父节点的下标
}
return parentIndex;
}
upAdjust() {
let childIndex = this.size -1; // 初始子节点的下标
let parentIndex = this.getParentIndex(childIndex)
let temp = this.queue[childIndex] // 子节点值
while((childIndex > 0) && (temp < this.queue[parentIndex])) {
this.queue[childIndex] = this.queue[parentIndex]
childIndex = parentIndex
// 根据子节点下标的奇偶性,来获取对应父节点的下标
parentIndex = this.getParentIndex(childIndex)
}
this.queue[childIndex] = temp
}
downAdjust() {
const len = this.size;
// 保存父节点的值,用于最后赋值
let temp = this.queue[0]
let parentIndex = 0;
// 左节点下标值
let childIndex = 1;
// 如果存在左孩子节点
while(childIndex < len) {
// 检测是否存在右节点,且右节点比左节点小点,则取右节点下标值;否则还是使用左节点下标
if(childIndex + 1 < len && this.queue[childIndex + 1] < this.queue[childIndex]) {
childIndex++;
}
// 如果父节点值比左右节点中最小的节点值都要小,那么结束
if(temp < this.queue[childIndex]) break;
// 否则,将爷节点与最小子节点值交换
this.queue[parentIndex] = this.queue[childIndex]
parentIndex = childIndex
childIndex = parentIndex * 2 + 1
}
this.queue[parentIndex] = temp
}
// 扩容
resize() {
this.queue = [...this.queue, ...new Array(this.size)]
}
}
let queue = new ProorityQueue()
queue.enQueue(10)
queue.enQueue(8)
queue.enQueue(3)
queue.enQueue(1)
queue.enQueue(5)
queue.enQueue(9)
console.log(queue); // ProorityQueue { queue: [ 1, 3, 8, 10, 5, 9, <26 empty items> ], size: 6 }
const min = queue.deQueue()
console.log(min); // 1