文章目录
前置知识
- 完全二叉树:深度为h,除第 h 层外,其它各层 (1~h-1) 的结点数都达到最大个数,第h 层所有的结点都连续集中在最左边(即从最左边开始排列),从上到下,从左到右,节点序号和节点个数一一对应
-
满二叉树:除了叶结点外每一个结点都有左右子叶且叶子结点都处在最底层的二叉树
- 故满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树
- 故满二叉树一定是完全二叉树,但完全二叉树不一定是满二叉树
完全二叉树的特性
- 完全二叉树可以用数组进行存储
- 如果我们把根节点存放在位置 i=1 的位置,则它的左子节点位置为 2i = 2 ,右子节点位置为 2i+1 = 3 。
- 如果我们选取 B 节点 i=2 ,则它父节点为 i/2 = 1 ,左子节点 2i=4 ,右子节点 2i+1=5 。
- 所有的节点都满足这三种关系
- 位置为 i 的节点,它的父节点位置为 i/2
- 它的左子节点 2i
- 它的右子节点 2i+1
- 下面的完全二叉树用数组表示为,[1,9,2,8,3,7,4,6,5]
堆
-
堆是一个完全二叉树,分为大顶堆和小顶堆
-
大顶堆:堆上的任意节点值都必须大于等于其左右子节点值
- 故大顶堆的堆顶一定是最大的数
- 故大顶堆的堆顶一定是最大的数
-
小顶堆:堆上的任意节点值都必须小于等于其左右子节点值
- 故小顶堆的堆顶一定是最小的数
- 故小顶堆的堆顶一定是最小的数
建堆
- 如上所知,堆是个完全二叉树,那么堆其实可以用一个数组表示,给定一个节点的下标 i (i从1开始) ,那么它的父节点一定为 A[i/2] ,左子节点为 A[2i] ,右子节点为 A[2i+1]
插入式建堆:这里以大顶堆为例
- 将节点插入到队尾
- 自下往上堆化: 将插入节点与其父节点比较,如果插入节点大于父节点(大顶堆)或插入节点小于父节点(小顶堆),则插入节点与父节点调整位置
- 一直重复上一步,直到不需要交换或交换到根节点,此时插入完成。
- 时间复杂度为O(nlogn)
class MaxHeap {
constructor() {
// 初始化时将位置 0 留空,方便计算子节点和父节点的索引关系
this.heap = [null];
}
insert(value) {
this.heap.push(value);
let currentIndex = this.heap.length - 1;
while (currentIndex > 1 && this.heap[Math.floor(currentIndex / 2)] < this.heap[currentIndex]) {
[this.heap[currentIndex], this.heap[Math.floor(currentIndex / 2)]] = [this.heap[Math.floor(currentIndex / 2)], this.heap[currentIndex]];
currentIndex = Math.floor(currentIndex / 2);
}
}
}
方式二:
let arr = [];
function heapInsert(num) {
arr.push(num);
let insertIndex = arr.length - 1;
while (num > arr[Math.floor((insertIndex - 1) / 2)]) {
//和(i-1)/2父节点交换位置
[arr[insertIndex], arr[Math.floor((insertIndex - 1) / 2)]] = [
arr[Math.floor((insertIndex - 1) / 2)],
arr[insertIndex],
];
insertIndex = Math.floor((insertIndex - 1) / 2);
}
}
heapInsert(6);
heapInsert(5);
heapInsert(4);
heapInsert(1);
heapInsert(3);
heapInsert(2);
heapInsert(8);
console.log(arr) //8 5 6 1 3 2 4
原地建堆(堆化):这里以小顶堆为例
- 自下而上式堆化 :将节点与其父节点比较,如果节点大于父节点(大顶堆)或节点小于父节点(小顶堆),则节点与父节点调整位置
- 从第一个元素开始,每遍历一个元素,就和它的父元素进行比较交换,类似插入式建堆的做法
- 从第一个元素开始,每遍历一个元素,就和它的父元素进行比较交换,类似插入式建堆的做法
//原地建堆
function buildHeap(items, heapSize) {
while(heapSize < items.length-1) {
heapSize ++
heapify(items, heapSize)
}
}
function heapify(items, i) {
// 自下而上式堆化
当前元素索引取 Math.ceil(i/2)-1是因为,1和2的父元素索引都是0;
while (i/2 > 0 && items[i] <items[Math.ceil(i/2)-1]) {
swap(items, i, Math.ceil(i/2)-1); // 交换
i = Math.ceil(i/2)-1;
}
}
function swap(items, i, j) {
let temp = items[i]
items[i] = items[j]
items[j] = temp
}
// 测试
var items2 = [5,2,3,4,1]
buildHeap(items2, 0)
console.log(items2) // 1 2 3 5 4
- 自上往下式堆化 :从最后一个非叶子结点开始与其左右子节点比较,如果存在左右子节点大于该节点(大顶堆)或小于该节点(小顶堆),则将子节点的最大值(大顶堆)或最小值(小顶堆)与之交换
- 每交换一次,还要继续比较交换后节点与其子左右节点的大小,因为不能保证上一部交换的节点是以当前节点为根的树中最大或最小的
- 需要从最后一个非叶子节点开始,而非最后一个节点,因为完全二叉树的原因,最后一个非叶子结点就包含了最后两个或一个节点,从而能保证最后一个非叶子结点交换后是有序的(即是以当前节点为根的树,根为最大或最小),然后从最后一个非叶子结点的序号-1的节点开始遍历交换直到序号为0,因为完全二叉树的结构,从最后一个非叶子结点开始,令其序号不断-1,就能从整个树的左右子树的左右两边最底层开始不断往上提升,最终比较整个树的根节点和其两个子节点
- 最后一个非叶子节点公式:(n-1)/2
- 时间复杂度为O(n),比插入式建堆复杂度要低
// 原地建堆
// items: 原始序列
function buildHeap(items) {
let heapSize = items.length
// 从最后一个非叶子节点开始,然后遍历到序号为0的节点为止,使得完全二叉树能从左右两个子树的最底层开始往上整合,直到根节点
// 因为最后一排的节点不需要堆化
for (let i = Math.floor(heapSize/2); i >= 1; --i) {
heapify(items, heapSize, i);
}
}
//可以根据最后面的heapify,根据索引进行节点比较优化
function heapify(items, heapSize, i) {
// 当前节点比较交换后,还要继续比较交换后节点和其子左右节点大小,以保证当前节点的子树都满足大顶堆或小顶堆的结构
while (true) {
var maxIndex = i;
if(2*i <= heapSize && items[i] > items[i*2] ) {
maxIndex = i*2;
}
if(2*i+1 <= heapSize && items[maxIndex] > items[i*2+1] ) {
maxIndex = i*2+1;
}
if (maxIndex === i) break;
swap(items, i, maxIndex); // 交换
i = maxIndex;
}
}
function swap(items, i, j) {
let temp = items[i]
items[i] = items[j]
items[j] = temp
}
// 测试
var items = [,5, 2, 3, 4, 1]
buildHeap(items, items.length - 1)
console.log(items)
// [empty, 1, 2, 3, 4, 5]
二叉堆新增节点(小顶堆为例)
- 将新元素插入到数组末尾
- 新增节点和上面的插入建堆原理一致,不断向上与父节点比较
- 时间复杂度为O(logN),和树的高度有关
二叉堆提取或删除节点(小顶堆为例)
- 提取后重新放置的节点,和最小的子节点相比是因为最小子节点若交换到父节点,较小的节点一定比较大的节点小,不用再考虑较小节点是否还需要交换位置,否则若交换较大的节点,较大的节点还需要和较小的节点交换位置再进行排序
- 时间复杂度为O(logN),和树的高度有关
大顶堆删除节点
class MaxHeap {
constructor() {
// 初始化时将位置 0 留空,方便计算子节点和父节点的索引关系
this.heap = [null];
}
insert(value) {
this.heap.push(value);
let currentIndex = this.heap.length - 1;
while (currentIndex > 1 && this.heap[Math.floor(currentIndex / 2)] < this.heap[currentIndex]) {
[this.heap[currentIndex], this.heap[Math.floor(currentIndex / 2)]] = [this.heap[Math.floor(currentIndex / 2)], this.heap[currentIndex]];
currentIndex = Math.floor(currentIndex / 2);
}
}
remove() {
if (this.heap.length === 1) return null;
let maxValue = this.heap[1];
this.heap[1] = this.heap.pop();
let currentIndex = 1;
while (true) {
let leftChildIndex = currentIndex * 2;
let rightChildIndex = currentIndex * 2 + 1;
let swapIndex = null;
if (leftChildIndex < this.heap.length && this.heap[leftChildIndex] > this.heap[currentIndex]) {
swapIndex = leftChildIndex;
}
if (
rightChildIndex < this.heap.length &&
this.heap[rightChildIndex] > this.heap[currentIndex] &&
this.heap[rightChildIndex] > this.heap[leftChildIndex]
) {
swapIndex = rightChildIndex;
}
if (!swapIndex) break;
[this.heap[currentIndex], this.heap[swapIndex]] = [this.heap[swapIndex], this.heap[currentIndex]];
currentIndex = swapIndex;
}
return maxValue;
}
}
//大顶堆删除提取第一个节点
//这里实现的是能在任何一个位置开始堆化(前提是除了待改动的节点,已经堆化了)
function heapify(arr, index) {
let heapSize = arr.length;
//左子节点
let left = index * 2 + 1;
//当还存在子节点时,left可以理解为子节点的第一个
while (left < heapSize) {
//获取两个子节点最大的那个索引,left + 1 < heapSize表示如果越界了,表面left是最后一个子节点,取left
let largest =
left + 1 < heapSize && arr[left] < arr[left + 1] ? left + 1 : left;
//待比较节点和子节点最大值比较
largest = arr[index] >= arr[largest] ? index : largest;
//子节点为最大值,退出
if (largest == index) {
break;
}
//交换最大值,最大值往上
[arr[index], arr[largest]] = [arr[largest], arr[index]];
index = largest;
left = index * 2 + 1;
}
}
let arr = [8, 5, 6, 1, 3, 2, 4];
let max = arr[0];
arr[0] = arr.pop();
heapify(arr, 0);
console.log(arr); //[ 6, 5, 4, 1, 3, 2 ]
堆排序
- 如上通过大顶堆和小顶堆知道,当前堆的根节点一定是整个树的最大或最小的值
- 将根结点位置和最后一个节点位置交换,即对应数组中,将根结点和数组尾元素交换位置,然后每次将数组长度-1,让数组剩余元素再进行堆化,反复执行该操作,最终达成整个数组有序
- 可知,整个建堆过程和堆排序,都可以看做一个选择排序,整体时间复杂度是 O(nlogn)
class MaxHeap {
constructor() {
// 初始化时将位置 0 留空,方便计算子节点和父节点的索引关系
this.heap = [null];
}
insert(value) {
this.heap.push(value);
let currentIndex = this.heap.length - 1;
while (currentIndex > 1 && this.heap[Math.floor(currentIndex / 2)] < this.heap[currentIndex]) {
[this.heap[currentIndex], this.heap[Math.floor(currentIndex / 2)]] = [this.heap[Math.floor(currentIndex / 2)], this.heap[currentIndex]];
currentIndex = Math.floor(currentIndex / 2);
}
}
remove() {
if (this.heap.length === 1) return null;
let maxValue = this.heap[1];
this.heap[1] = this.heap.pop();
let currentIndex = 1;
while (true) {
let leftChildIndex = currentIndex * 2;
let rightChildIndex = currentIndex * 2 + 1;
let swapIndex = null;
if (leftChildIndex < this.heap.length && this.heap[leftChildIndex] > this.heap[currentIndex]) {
swapIndex = leftChildIndex;
}
if (
rightChildIndex < this.heap.length &&
this.heap[rightChildIndex] > this.heap[currentIndex] &&
this.heap[rightChildIndex] > this.heap[leftChildIndex]
) {
swapIndex = rightChildIndex;
}
if (!swapIndex) break;
[this.heap[currentIndex], this.heap[swapIndex]] = [this.heap[swapIndex], this.heap[currentIndex]];
currentIndex = swapIndex;
}
return maxValue;
}
}
// 堆排序
function heapSort(arr) {
const maxHeap = new MaxHeap();
for (const value of arr) {
maxHeap.insert(value);
}
for (let i = arr.length - 1; i >= 0; i--) {
arr[i] = maxHeap.remove();
}
}
const arr = [4, 2, 6, 3, 7, 9, 0];
heapSort(arr);
console.log(arr); // 输出:[0, 2, 3, 4, 6, 7, 9]
let arr = [8, 5, 6, 1, 3, 2, 4];
//开辟新数组
function heapSort(arr) {
let order = [];
while (arr.length) {
order.push(arr[0]);
if (arr.length == 1) {
break;
}
arr[0] = arr.pop();
heapify(arr, 0);
}
return order;
}
console.log(heapSort(arr)); //[8, 6, 5, 4, 3, 2, 1];
//原数组,空间复杂度为O(1)
function heapSort(arr,heapSize) {
[arr[0],arr[heapSize-1]]=[arr[heapSize-1],arr[0]];
heapSize--;
while(heapSzie>0){
//这里的heapify应该根据heapSize的长度建堆,上面的heapSize未实现
heapify(arr, 0, heapSize);
[arr[0],arr[heapSize-1]]=[arr[heapSize-1],arr[0]];
heapSize--;
}
}
heapSort(arr,arr.length);
console.log(heapSort(arr)); //[8, 6, 5, 4, 3, 2, 1];
改变堆中某个节点的值后,依旧成堆
- 大顶堆
- 如果改变后的节点比原来的节点大,则向上和父节点交换比较(heapInsert过程)
- 如果改变后的节点比原来的节点小,则向下和子节点比较(heapify过程)
- 时间复杂度为O(logN)