从二叉堆进阶到堆排序与优先队列,前端大佬都这样学...

开发 前端
当遇到二叉堆的时候,我们应该学会如何构建一个大根堆(小根堆),这个时候就涉及到加堆和减堆的过程,下面让从加堆和减堆开始,从而使我们能够丝滑般的了解整个二叉堆的构建过程。

一、二叉堆分类

二叉堆根据排序不同可分为大根堆和小根堆。

大根堆

在是完全二叉树的前提下,其节点值大于其左右子节点值,称为大根堆。在大根堆中根节点是所有堆节点中的最大值。

小根堆

在是完全二叉树的前提下,其节点值小于其左右子节点值,称为小根堆。在小根堆中根节点是所有堆节点中的最小值。

二、二叉堆的存储

上述阐述了二叉堆其实就是一棵完全二叉树,然后数据满足某些特性,那么在编程中应该如何存储这些数据呢?这些数据间满足那些关系呢?

1.以数组结构存储二叉堆

在编程中我们会以数组存储二叉堆,以上述的最大堆为例,存储到数组中为如下结构:

2.二叉堆中父子节点间满足何种关系

既然数据将存储到数组中,那么就必须知道父子节点之间的关系,即:

(1)在知道父节点的索引的时候必须找到其对应的子节点;

(2)在知道子节点的索引的时候找到其对应的父节点;

如下的二叉堆,设置其根节点索引为0,然后依次递增:

已知某节点的索引为i,则其左右子节点索引为:

  • 左节点索引 = 2 * i + 1
  • 右节点索引 = 左节点索引 + 1

已知某节点的索引为i,则其父节点索引为:

  • 父节点 = parseInt((i - 1) / 2)

三、堆的基本操作

当遇到二叉堆的时候,我们应该学会如何构建一个大根堆(小根堆),这个时候就涉及到加堆和减堆的过程,下面让从加堆和减堆开始,从而使我们能够丝滑般的了解整个二叉堆的构建过程。(注:下面的例子以大根堆为例)

// 注:此处是封装的交换数组中元素的方法
// 数组中交换数据
function swap(arr, i, j) {
let temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}

1.加堆

加堆的过程实际上就是再往堆里面加入一个新的元素,然后维护该堆结构的过程,实际上就是在数组尾部加入一个元素,然后通过不断上浮,使其又调整为二叉堆结构的过程。


// 大根堆加堆过程中
function heapInsert(arr, index) {
// 比较当前位置和其父位置,若大于其父位置,则进行交换,并将索引移动到其父位置进行循环,否则跳过
// 结束条件是比其父位置小或者到达根节点处
while (index > 0 && arr[index] > arr[parseInt((index - 1) / 2)]) {
swap(arr, index, parseInt((index - 1) / 2));
index = parseInt((index - 1) / 2);
}

return arr;
}

有了加堆的过程,很容易我们就可以实现创建二叉堆,如下所示:

// 创建二叉堆
function createBigTopHeap(arr) {
for (let i = 0; i < arr.length; i++) {
arr = heapInsert(arr, i);
}

return arr;
}


const arr = [1, 5, 9, 10, 8, 12];

console.log(createBigTopHeap(arr)); // [ 12, 9, 10, 1, 8, 5 ]

2.减堆

减堆的过程就是将堆顶元素A和最后元素B交换,然后删除A元素并将B元素调整到合适的位置的过程,实际上就是将堆顶数据交换后,下沉数据的过程

// 减堆过程
// size指的是这个数组前多少个数构成一个堆
// 如果想把堆顶弹出,则把堆顶和最后一个数交换,把size减1,然后从0位置经历一次heapify,调整一下,剩余部分变成大顶堆
function heapify(arr, index, size) {
// 获取其左节点索引
let left = 2 * index + 1;
while (left < size) {
// 判断一下左右子节点哪个值最大,获取其最大值
let largest = (left + 1 < size && arr[left] < arr[left + 1]) ? left + 1 : left;
// 判断左右子节点最大值与当前要比较的节点哪个大
largest = arr[index] > arr[largest] ? index : largest;

// 如果最大值索引和传进来的索引一样,则该值到达指定位置,直接结束循环
if (index === largest) {
break;
}

// 进行交换,改变索引及其左子节点
swap(arr, index, largest);
index = largest;
left = 2 * index + 1;
}

return arr;
}

const arr = [1, 5, 9, 10, 8, 12];

console.log(createBigTopHeap(arr));

// 交换堆顶数据与最后位置数据
swap(arr, 0, arr.length - 1);

// 通过下沉的方式获取新的堆结果
console.log(heapify(arr, 0, arr.length - 1));

四、叉堆的用途

因为二叉堆能够高效的找出最大值和最小值,所以可用于堆排序和构建优先队列。

1.堆排序

堆排序是常见的一种排序算法,整个堆排序过程如下所示:

  • 让数组变成大根堆
  • 把最后一个位置和堆顶交换
  • 则最大值在最后,则剩余部分做heapify,则重新调整为大根堆,则堆顶位置和该部分最后位置做交换
  • 重复进行,直到减完,则这样最后就调整完毕,整个数组排完序(为一个升序)
function heapSort(arr) {
if (arr === null || arr.length === 0) {
return [];
}

// 首先创建大根堆
for (let i = 0; i < arr.length; i++) {
arr = heapInsert(arr, i);
}

let size = arr.length; // 这个值用来指定多少个数组构成堆

// 由于当前已经是大根堆,所以直接交换
swap(arr, 0, --size);

while (size > 0) {
// 重新变成大顶堆
heapify(arr, 0, size);
// 进行交换
swap(arr, 0, --size);
}

return arr;
}

const arr = [1, 5, 9, 10, 8, 12];

console.log(heapSort(arr)); // [ 1, 5, 8, 9, 10, 12

2.优先队列

普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。通常采用堆数据结构来实现。

优先队列的插入

优先队列中元素的插入就是上述heapInsert过程,通过将元素添加到数组末尾,然后通过上浮的方式将内容插入到合适位置。

优先队列中元素的删除

优先队列中元素的删除就是将根节点的元素与最后元素交换,然后将交换后的根节点下沉,调整到合适位置,重新构成二叉堆。

责任编辑:武晓燕 来源: 前端点线面
相关推荐

2011-12-02 10:58:06

数据结构Java

2020-08-31 07:43:58

二叉堆大顶堆存储

2023-04-06 07:39:48

2021-05-06 05:29:32

二叉堆数据结构算法

2021-03-02 10:57:39

二叉树二叉堆节点

2022-09-05 08:06:49

数据结构Java

2020-11-23 08:53:34

堆Heap

2020-03-06 16:08:46

堆结构堆排序应用

2021-01-13 09:23:23

优先队列React二叉堆

2020-11-24 09:04:55

PriorityBlo

2020-10-18 17:09:14

前端开发技术

2020-09-23 18:25:40

算法二叉树多叉树

2022-12-26 00:51:33

双向链表二叉搜索树

2021-03-29 10:13:47

Java编程数据结构算法

2023-09-19 23:07:53

Python算法

2020-12-22 08:56:51

JavaScript数据结构前端

2020-04-27 07:05:58

二叉树左子树右子树

2014-10-30 15:59:10

2012-03-31 10:49:18

ibmdw

2020-12-11 09:49:29

二叉树搜索树数据
点赞
收藏

51CTO技术栈公众号