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);