二叉堆本质上是一种完全二叉树,它分为两个类型
1. 最大堆
2. 最小堆
最大堆:最大堆的任何一个父节点的值,都大于或等于它的左右孩子节点的值
最小堆:最小堆的任何一个父节点的值,都小于或等于它左右孩子节点的值
二叉堆的根节点叫做堆顶
最大堆和最小堆的特点决定了:最大堆的堆顶是整个堆中的最大元素;最小堆的堆顶是整个堆中的最小元素
二叉堆的自我调整
如何构建二叉堆,就需要依靠二叉堆的自我调整
对于一个二叉堆,有如下几个操作:
1. 插入节点
2. 删除节点
3. 构建二叉堆
这几种操作都基于二叉堆的自我调整,所谓二叉堆的自我调整,就是把一个不符合堆性质的完全二叉树,调整称为一个堆,以下为最小堆为例,看一下二叉堆如何进行自我调整
插入节点
1. 当二叉树插入节点时,插入的位置是完全二叉树的最后一个位置,例如插入一个新的节点,值为 0
2. 这时,新插入节点的父节点 值为 5 ,比 0 大,显然不符合最小堆的性质,于是让新节点上浮,和父节点交换位置
3. 继续使用 0 和父节点 3 比较, 0 < 3 所以继续上浮,
4. 继续比较,最终上浮到堆顶的位置
删除节点
二叉堆删除节点的过程和插入节点的过程正好相反,所删除的是处于堆顶的节点,例如删除最小堆的堆顶节点 1
1. 这时为了维持完全二叉树的结构,我们把堆的最后一个节点 10 ,临时补到原本堆顶的位置
2. 接下来,让暂处堆顶位置的节点 10 和它的左、右孩子进行比较,如果左、右孩子中最小的一个(显然是节点2)比 节点10 小,那么让节点 10 "下沉"
3. 继续让节点 10 和它的左、右孩子做比较,左、右孩子中最小的是节点 7,由于 10 大于 7,让节点 10 继续下沉
这样一来,二叉堆重新得到了调整
构建二叉堆
构建二叉堆,也就是把一个无序的完全二叉树调整为二叉堆,本质上就是让所有非叶子节点一次下沉
下边是一个无序的完全二叉树的例子,如下所示
1. 首先从最后一个非叶子节点,也就是从节点 10 开始,如果节点 10 大于它左、右孩子中最小的那一个,则节点 10 下沉
2. 接下来轮到节点 3,如果节点 3 大于它左、右孩子中最小的那一个,则节点 3 下沉
3. 然后轮到 节点 1,如果节点 1 大于 它的左、右孩子中最小的那一个,则节点 1 下沉,事实上节点 1 小于它的 左、右孩子,所以不用改变
4. 接下来轮到节点 7,如果节点 7 大于它的 左、右孩子节点中最小的一个,则节点 7 "下沉"
5. 节点 7 继续比较,继续下沉
经过几轮的比较和下沉操作,最终每一个节点都小于它的左、右孩子节点,一个无序的完全二叉树就被构建成了一个最小堆
时间复杂度
堆的插入和删除操作,都是单一节点的"下沉",所以时间复杂度是O(logn)
构建堆的时间复杂度为O(n),这里的操作为从下向上,从上向下构建堆的时间复杂度为O(nlogn)
推算过程
插入和删除操作都是 单一节点进行下沉操作 所以 要计算 logn+1 次,即树的高度 所以时间复杂度为 O(logn)
构建堆:
1. 设 树的高度为 h = Math.floor(logn)+1 = logn(由于计算元素个数 时 需要再做 -1 操作 所以这里直接设为 logn)
2. 最下层非叶子节点的元素,只需要做一次比较即可,而这一层具有 2^(h -1 )个元素,则这一层所需要的时间为 2^(h-1 ) * 1
3. 同理可得 从下往上 第二层 非叶子节点 所需要时间为 2^(h -2)*2
4. 得,构建二叉堆的精确时间复杂度为
S = 2^(h - 1)*1 + 2^(h - 2)*2 +...+ 1*(h -1 )
则左右两边 * 2 得:
2S = 2^h*1 + 2^(h - 1)*2 +...+ 2*(h - 1)
相减得:
S = 2^h*1 - 1*(h - 1)
代入h得:
S = n - logn + 1
所以 从下往上构建的时间复杂度 为 O(n)
代码实现
二叉堆虽然是一个完全二叉树,但是他的储存方式并不是链式储存的,而是顺序储存,即二叉堆所以的节点都储存在数组中
在数组中,父节点的下标是 parent,那么它的左孩子的下标就是 2 * parent + 1; 右孩子下标就是 2 * parent + 2
/**
* @param {*} list 待调整的堆
* @description 上浮 调整
*/
function upAdJust(list:Array<number>) {
let childrenIndex = list.length-1
// 这里选择 - 1 /2 向下舍入 是因为右孩子为 -2 /2 如果 -1/2 余 1 则为右孩子
let parentIndex = Math.floor((childrenIndex-1)/2)
// temp 保存插入叶子节点的值 最后用作赋值
let temp = list[childrenIndex];
while(childrenIndex > 0 && temp < list[parentIndex]) {
list[childrenIndex] = list[parentIndex];
childrenIndex = parentIndex;
parentIndex = Math.floor((childrenIndex-1)/2)
}
list[childrenIndex] = temp
}
/**
* @param {*} list 待调整的堆
* @param {*} parent 要下沉的父节点
* @param {*} length 堆的有效长度
* @description 下沉调整
*/
function downAdjust(list:Array<number>, parentIndex:number, length:number) {
let temp = list[parentIndex]
// 左孩子下标
let childrenIndex = parentIndex * 2 + 1
while(childrenIndex < length) {
// 如果存在右孩子 且 右孩子比左孩子小 则定位到右孩子
if(childrenIndex + 1 < length && list[childrenIndex + 1] < list[childrenIndex]){
childrenIndex++
}
// 如果父节点 小于等于 最小子节点 则直接跳出
if(temp <= list[childrenIndex]) break
// 无序真正交换 只需要单向赋值 即可
list[parentIndex] = list[childrenIndex];
parentIndex = childrenIndex;
childrenIndex = childrenIndex * 2 + 1
}
list[parentIndex] = temp
}
/**
* @param {Array<number>} list 待调整的堆
* @description 构建堆
*/
function buildHeap(list:Array<number>){
let length = list.length;
// length-2 是因为 最后一个 元素为 length-1
for (let index = Math.floor((length-2)/2); index >= 0; index--) {
downAdjust(list, index, length - 1)
}
}
function main(){
// 模拟插入 一个节点 0
let insertList = [1,3,2,6,5,7,8,9,10,0];
upAdJust(insertList)
console.log(insertList)
// 模拟无序完全二叉树
let unorderedList = [7,1,3,10,5,2,8,9,6];
buildHeap(unorderedList)
console.log(unorderedList)
}
main()
优先队列
队列的特点是 先进先出(FIFO)
入队时,将新元素置于队尾,出队是,队头元素最先被移出
而优先队列不再遵循先进先出的原则,而是分为两种情况:
1. 最大优先队列,无论入队顺序如何,都是当前最大的元素优先出队
2. 最小优先队列,无论入队顺序如何,都是当前最小的元素优先出队
代码实现
class PriorityQueue{
data:Array<number>
constructor(){
this.data = []
}
/**
* @param {number} value
* @memberof PriorityQueue
* @description 入队
*/
enQueue(value:number){
this.data.push(value);
this.upAdjust()
}
/**
* @memberof PriorityQueue
* @description 上浮调整
*/
upAdjust(){
let childrenIndex = this.data.length - 1;
let parentIndex = Math.floor((childrenIndex-1)/2);
let temp = this.data[childrenIndex]
while( childrenIndex > 0 && temp < this.data[parentIndex]){
this.data[childrenIndex] = this.data[parentIndex];
childrenIndex = parentIndex;
parentIndex = Math.floor((childrenIndex-1)/2)
}
this.data[childrenIndex] = temp
}
/**
* @returns
* @memberof PriorityQueue
* @description 出队
*/
deQueue(){
if(!this.data.length) {
throw new Error('the queue is empty');
}
let head = this.data[0]
this.data[0] = this.data[this.data.length-1]
this.data.pop()
if(this.data.length > 1){ // 出队之后 数组长度大于1 调整才有意义
this.downAdjust()
}
return head
}
/**
* @memberof PriorityQueue
* @description 下沉调整
*/
downAdjust(){
let parentIndex = 0;
let childrenIndex = 1;
let size = this.data.length -1;
let temp = this.data[parentIndex]
while(childrenIndex < size){
if(childrenIndex + 1 < size && this.data[childrenIndex+1] < this.data[childrenIndex]){
childrenIndex ++
}
if(temp <= this.data[childrenIndex]) break;
this.data[parentIndex] = this.data[childrenIndex]
parentIndex = childrenIndex;
childrenIndex = childrenIndex * 2 + 1;
}
this.data[parentIndex] = temp
}
}
function main() {
let priorityQueue = new PriorityQueue();
console.log('入队')
priorityQueue.enQueue(10);
priorityQueue.enQueue(9);
priorityQueue.enQueue(8);
priorityQueue.enQueue(7);
priorityQueue.enQueue(11);
priorityQueue.enQueue(12);
priorityQueue.enQueue(5);
console.log(priorityQueue.data)
console.log('出队元素' + priorityQueue.deQueue())
console.log(priorityQueue.data)
console.log('出队元素' + priorityQueue.deQueue())
console.log(priorityQueue.data)
}
main()
小结
1. 什么是树
树是 n 个节点的有限集,有且仅有一个特定的称为根的节点,当 n > 1时,其余节点可以分为 m 个互不相交的有限集合,每一个集合本身又是一个树,并称为根的子树
2. 什么是二叉树
二叉树是树的一种特殊形式,每一个节点最多有两个孩子节点,二叉树包含完全二叉树和满二叉树两种特殊形式
3. 二叉树的遍历方式有几种
根据节点之间的关系,可以分为前序遍历、中序遍历、后序遍历,层序遍历这4种遍历方式;从更宏观的角度划分,可以划分为深度优先遍历和广度优先遍历两大类
4. 什么是二叉堆
二叉堆是一种特殊的完全二叉树,分为最大堆和最小堆
在最大堆中,任何一个父节点的值,都大于或等于它的左、右孩子节点的值
在最大堆中,任何一个父节点的值,都小于或等于它的左、右孩子节点的值
5. 什么是优先队列
优先队列分为最大优先队列和最小优先队列
在最大优先队列中,无论入队顺序如何,当前最大元素都会优先出队,这是基于最大堆实现的
在最小优先队列中,无论入队顺序如何,当前最小元素都会优先出队,这是基于最小堆实现的
摘要总结自: 漫画算法 小灰的算法之旅