ps:再来实现一个非常重要的数据结构 ——二叉堆
知道导航:
- 什么是二叉堆
- 二叉堆的具体实现(以最小堆为主)
那什么是二叉堆呢?
首先二叉堆它是一种特殊的二叉树,它有两个特性
- 它是一棵完全二叉树,表示树的每一层都有左侧和右侧子节点(除了最后一层的叶节点),
并且最后一层的叶节点尽可能都是左侧子节点,这叫作结构特性。 - 二叉堆不是最小堆就是最大堆。最小堆允许你快速导出树的最小值,最大堆允许你快速
导出树的最大值。所有的节点都大于等于(最大堆)或小于等于(最小堆)每个它的子
节点。这叫作堆特性
额。。说白了,二叉堆是一棵完全二叉树,且它不是最小堆就是最大堆。
那么什么是最小堆呢?
即从树顶到树底,一层比一层的数值小
最大堆与它相反
来看二叉堆的实现吧
我们接下来我实现以下功能:
- insert(val)像堆中插入新数据
- extract()剔除最小值(最小堆),最大堆就剔除最大值
- findMin() 返回最小者,最大堆中就findMax返回最大值呗
前提:
我们要知道储存一棵二叉树一般有两种方式,一个是“指针”的形式即保存数据之间的联系。
第二种形式即使用数组,我们可以事先给树编写序号,利用数组的索引位置一一对应。如下图
这时我们就得需要知道一些东西
在数组中给我们一个数据的位置,我们如何拿到它的父、子节点?
通过一些规律观察我们可以有以下结论。
数组中如果给定一已存储了节点元素个位置index,那么
- 它的左子节点位置是 2 * index + 1(如果位置可用);
- 它的右子节点位置是 2 * index + 2(如果位置可用);
- 它的父节点位置是index / 2(如果位置可用)。
二叉堆的数据存储结构就是使用的数组。故即接下我们先要实现上面的三个辅助函数
getLeftIndex(index) {
return 2 * index + 1;
}
getRightIndex(index) {
return 2 * index + 2;
}
getParentIndex(index) {
if (index === 0) {
return undefined;
}
return Math.floor((index - 1) / 2);
}
开始构建最小堆类 MinHeap
class MinHeap {
constructor() {
this.heap = [];
}
}
为了书写简单我将辅助函数也写在了类的里面
class MinHeap {
constructor() {
this.heap = [];
}
//下面三个是工具函数
getLeftIndex(index) {
return 2 * index + 1;
}
getRightIndex(index) {
return 2 * index + 2;
}
getParentIndex(index) {
if (index === 0) {
return undefined;
}
return Math.floor((index - 1) / 2);
}
}
来实现insert方法吧
实现insert方法的核心就是它里面的上移操作。
什么上移及怎么上移呢?
看图
新来一个节点2,要加入堆中,开始加入堆底。即放到节点3的右孩子位置上
这是因为二叉堆的特性,最小堆父节点数据是要小于它每个子节点的。现在不符合这一特性了,即节点2要进行上移。怎么上移呢,它先和自己的父节点进行数据的比较。若比其父节点数值小。则进行位置交换
交换完成
然后再再此位置上与它现在的父节点进行数据的比较。还是若小于其父节点交换位置。不小於则固定其现有位置
代码实现:
insert(value) {
if (value != null) {
this.heap.push(value);
this.upHead(this.heap.length - 1); //上移操作
return true;
}
return false;
}
//上移
upHead(index) {
//拿到父节点
let parent = this.getParentIndex(index);
while (index > 0 && (this.heap[index] < this.heap[parent])) {
//进行父子节点交换
this.swap(this.heap, parent, index);
index = parent;
parent = this.getParentIndex(index);
}
}
//交换
swap(arr, a, b) {
[arr[b], arr[a]] = [arr[a], arr[b]];
}
再来实现extract方法吧
extract方法的作用是剔除堆顶。
extract方法的核心是堆化,先来看看堆化的思路吧。
如上图我们要剔除堆顶,不能直接剔除就完事了吧。。。我们还要保持它的堆结构呢,怎么保持呢?
首先让这个堆顶节点1与它的子节点比较,小于它的子节点(废话嘛)。交换两者位置
交换完成之后,再与它的子节点比较。这时就出现了一点问题。如果是和节点7交换,那么我们直接push掉即可,和6交换我们要怎么办呢?去heap里面去找到吗?这显然不是我们不需要的。
来再换一种方法吧
首先我们就先把堆顶和堆底节点互换,然后把换完的新堆底扔出去。
这时新堆顶是7,它要下移。则有子节点比较。交换位置
接着比较,交换位置
大功告成
代码实现:
extract() {
// 空堆
if (this.heap.length === 0) {
return false;
}
// 堆中就一值
if (this.heap.length === 1) {
return this.heap.shift();
}
//堆中多值
let removeValue = this.heap[0];
// 交换堆顶堆底
[this.heap[0], this.heap[this.heap.length - 1]] = [this.heap[this.heap.length - 1], this.heap[0]];
this.heap.pop();
//进行堆化
this.sinkDown(0);
return removeValue;
}
//堆化
sinkDown(index) {
let element = index;
const left = this.getLeftIndex(index);
const right = this.getRightIndex(index);
const size = this.size();
if (left < size && (this.heap[element] > this.heap[left])) {
element = left;
}
if (right < size && this.heap[index] > this.heap[right]) {
element = right;
}
if (index !== element) {
this.swap(this.heap, index, element);
this.sinkDown(element);
}
}
findMin的实现就简单多了,要找的就在堆顶也即数组头部
findMin() {
return this.heap.length===0() ? undefined : this.heap[0];
}
最大堆原理一样,就是改一下小于大于号了这里不做实现了,堆排序我接下来会把它放到几个常见的排序总结中去
完整代码
<script>
class MinHeap {
constructor() {
this.heap = [];
}
//下面三个是工具函数
getLeftIndex(index) {
return 2 * index + 1;
}
getRightIndex(index) {
return 2 * index + 2;
}
getParentIndex(index) {
if (index === 0) {
return undefined;
}
return Math.floor((index - 1) / 2);
}
insert(value) {
if (value != null) {
this.heap.push(value);
this.upHead(this.heap.length - 1); //上移操作
return true;
}
return false;
}
//上移
upHead(index) {
//拿到父节点
let parent = this.getParentIndex(index);
while (index > 0 && (this.heap[index] < this.heap[parent])) {
//进行父子节点交换
this.swap(this.heap, parent, index);
index = parent;
parent = this.getParentIndex(index);
}
}
//交换
swap(arr, a, b) {
[arr[b], arr[a]] = [arr[a], arr[b]];
}
extract() {
// 空堆
if (this.heap.length === 0) {
return false;
}
// 堆中就一值
if (this.heap.length === 1) {
return this.heap.shift();
}
//堆中多值
let removeValue = this.heap[0];
// 交换堆顶堆底
[this.heap[0], this.heap[this.heap.length - 1]] = [this.heap[this.heap.length - 1], this.heap[0]];
this.heap.pop();
//进行堆化
this.sinkDown(0);
return removeValue;
}
//堆化
sinkDown(index) {
let element = index;
const left = this.getLeftIndex(index);
const right = this.getRightIndex(index);
const size = this.size();
if (left < size && (this.heap[element] > this.heap[left])) {
element = left;
}
if (right < size && this.heap[index] > this.heap[right]) {
element = right;
}
if (index !== element) {
this.swap(this.heap, index, element);
this.sinkDown(element);
}
}
size() {
return this.heap.length;
}
isEmpty() {
return this.size === 0;
}
findMin() {
return this.heap.length===0() ? undefined : this.heap[0];
}
}
let heap = new MinHeap();
heap.insert(4);
heap.insert(5);
heap.insert(1);
heap.insert(2);
heap.insert(3);
heap.extract();
console.log(heap);