二叉堆
特点
- 二叉堆是一个完全二叉树
- 二叉堆上的任意节点值都必须大于等于(大顶堆)或小于等于(小顶堆)其左右节点值
完全二叉树
在完全二叉树中,除了最底层的节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层节点都集中在该层最左边的若干位置。若最底层为第h层,则该层包含1~2^(h-1)个节点
堆属性计算
二叉堆是完全二叉树,树的高度是指从树的根节点到最低叶节点所需的步数
树高度的定义:节点之间边的最大值
- 一个高度为h的堆有h+1层
- 如果一个堆有n个节点,那么它的高度为
Math.floor(log2(n))
// 如果一个堆有15个结点,求这个堆的高度
h = Math.floor(log2(15)) = Math.floor(3.91) = 3;
- 如果最下面一层已经填满,那这一层的节点为
2^h
个
// 高度为3,即最下面一层有8个节点
2^3 = 8;
- 最下面已经填满的那一层以上的所有节点数目为 2^h -1个
// 高度为3,即其他层的所有节点有7个
2^3 -1 = 7;
- 整个堆中的节点数为: 2^(h+1)-1
// 高度为3,即堆中的节点数为15
2^4 -1 = 16 - 1 = 15;
最大堆
如果堆上的任意父节点都大于等于子节点值,则称为最大堆。如图:
最小堆
如果堆上的任意父节点都小于等于子节点值,则称为最小堆。如图:
最大堆和最小堆的特点决定了:最大堆的堆顶是整个堆中的最大元素,最小堆的堆顶是整个堆中的最小元素
堆存储
堆可以用一维数组来表示,堆中的节点在数组的位置与它的父节点以及子节点的索引有一个映射关系,
用公式来描述当前节点的父节点和子节点在数组中的位置(i为当前节点的索引):
二叉堆的自我调整
堆的自我调整,就是把一个不符合堆性质的完全二叉树,调整成一个堆。主要操作有:
- 插入节点
二叉堆插入节点时,插入位置是完全二叉树的最后一个位置。时间复杂度为O(logn)
- 删除节点
二叉堆删除节点,删除的是处于堆顶的节点。为了维持完全二叉树的结构,会把堆的最后一个节点临时补到原本堆顶的位置。时间复杂度为O(logn)
- 构建二叉堆
构建二叉堆,是把一个无序的完全二叉树调整为二叉堆。时间复杂度为O(n)
二叉堆的代码实现
/**
* 上浮调整(以最小堆为例)
* @param {*} arr
*/
function bubbleUp(arr) {
let childIndex = arr.length - 1;
// 获取父节点索引
let parentIndex = (childIndex - 1) >> 1;
// tmp保存插入的叶子节点值,用于最后的赋值
let tmp = arr[childIndex];
// 如果叶子节点的值小于父节点的值
while (childIndex > 0 && tmp < arr[parentIndex]) {
arr[childIndex] = arr[parentIndex];
childIndex = parentIndex;
parentIndex = (parentIndex - 1) >> 1;
}
arr[childIndex] = tmp;
return arr;
}
/**
* 下沉调整(以最小堆为例)
* @param {*} arr 待调整的堆
* @param {*} i 要下沉的父节点
* @param {*} length 堆的有效大小
*/
function bubbleDown(arr, i, length) {
// tmp保存父节点值,用于最后的赋值
let tmp = arr[i];
let childIndex = 2 * i + 1;
while (childIndex < length) {
// 如果有右孩子,且右孩子的值小于左孩子的值,则将childIndex指向右孩子
if (childIndex + 1 <= length && arr[childIndex + 1] < arr[childIndex]) {
childIndex++;
}
// 如果父节点大于任何一个孩子的值,则交换,直至循环结束
if (tmp > arr[childIndex]) {
arr[i] = arr[childIndex];
i = childIndex;
childIndex = 2 * i + 1;
}else{
break;
}
}
arr[i] = tmp;
}
/**
* 构建堆(以最大堆为例)
* @param {*} arr 待调整的堆
*/
function buildHeap(arr) {
const len = arr.length;
for (let i = Math.floor((len - 2) / 2); i >= 0; i--) {
bubbleDown(arr, i, len);
}
return arr;
}
堆排序
堆排序的步骤可以概括为:
- 把无序数组构建成二叉堆,需要从小到大排序,则构建成最大堆;需要从大到小排序,则构建成最小堆
- 循环删除堆顶元素,替换到二叉树的末尾,调整堆产生新的堆顶
/**
* 堆排序(升序)
* @param {*} arr
*/
function heapSort(arr) {
// 将无序数组构建成最大堆
buildHeap(arr);
// 循环删除堆顶元素,移到集合尾部,调整堆产生新的堆
for (let i = arr.length - 1; i > 0; i--) {
let tmp = arr[i];
arr[i] = arr[0];
arr[0] = tmp;
// 下沉调整最大堆
bubbleDown(arr, 0, i);
}
}
时间复杂度:
- 把无序数组构建成二叉堆,这一步的时间复杂度为O(n);
- 第二步要进行n-1次循环,每次循环调用一次
bubbleDown
方法,所以第二步的计算规模是(n-1)*logn,时间复杂度为O(nlogn); - 两个步骤并列关系,整体时间复杂度为O(nlogn)
总结
最小堆模版
class MinHeap {
constructor(data = []) {
this.data = data;
this.comparator = (a, b) => a[1] - b[1];
this.heapify();
}
heapify() {
if (this.size() < 2) return;
// 将每个元素插入,往上冒到合适位置
for (let i = 0; i < this.size(); i++) {
this.bubbleUp(i);
}
}
// 获得堆顶元素
peek() {
if (this.size() === 0) return null;
return this.data[0];
}
// 插入元素
offer(value) {
this.data.push(value);
// 在最后的位置插入且向上冒泡
this.bubbleUp(this.size() - 1);
}
// 移除顶堆元素
poll() {
if (this.size() === 0) {
return null;
}
const result = this.data[0];
const last = this.data.pop();
if (this.size() !== 0) {
// 最末尾元素放到堆顶
this.data[0] = last;
// 向下调整直至放到合适位置
this.bubbleDown(0);
}
return result;
}
bubbleUp(index) {
while (index > 0) {
// 获得父节点索引
const parentIndex = (index - 1) >> 1;
if (this.comparator(this.data[index], this.data[parentIndex]) < 0) {
// 交换位置往上冒
this.swap(index, parentIndex);
index = parentIndex;
} else {
break;
}
}
}
bubbleDown(index) {
const last = this.size() - 1;
while (true) {
// 获得要调整的节点的左子节点和右字节点的索引
const left = 2 * index + 1;
const right = 2 * index + 2;
let min = index;
if (left <= last && this.comparator(this.data[left], this.data[min]) < 0) {
min = left;
}
if (right <= last && this.comparator(this.data[right], this.data[min]) < 0) {
min = right;
}
// 交换
if (index != min) {
this.swap(index, min);
index = min;
} else {
break;
}
}
}
// 交换元素
swap(i, j) {
[this.data[i], this.data[j]] = [this.data[j], this.data[i]];
}
// 获取堆大小
size() {
return this.data.length;
}
}