堆有序:當一棵二叉樹的每一個結點都大於等於它的兩個子結點時,它被稱爲堆有序。數組
**二叉堆:**二叉堆是一組可以用堆有序的徹底二叉樹排序的元素,並在數組中按照層級儲存(不使用數組的第一個位置)。緩存
咱們的下標從1
開始,下標變量爲ind
bash
那對於給定位置ind
的節點:數據結構
2*ind
2*ind+ 1
parseInt(ind /2)
parseInt(3/2) === parseInt(2/2) === 1
less
咱們使用堆這個數據結構主要有三個操做函數
pusk(val)
:向堆中插入一個新的值pop(val)
:彈出最值top()
:查看最值咱們先假設咱們堆裏面已是堆有序的,且含有的元素爲2,3,4
測試
這個時候,若是咱們往裏面添加1
ui
class MinHeap {
constructor() {
this.heap = []
this.len = 0
}
push(val) {
this.heap[++this.len] = val
}
}
複製代碼
this.heap[++this.len]
先進行this.len
加加後再賦值,若是此時this.len
爲0的話,那麼實際是this.heap[1] = val
this這種寫法就達到了咱們數組首位爲空的目的spa
爲了達到堆有序,咱們應該對添加的元素進行調整,由於每次咱們都是在末尾添加元素的,那咱們把這個調整的過程稱爲上浮swim
push(val) {
this.heap[++this.len] = val
this.swim(this.len)
}
複製代碼
那咱們如今來思考swim
的實現,咱們先卻次明確堆有序的概念
堆有序:當一棵二叉樹的每一個結點都大於等於它的兩個子結點時,它被稱爲堆有序。
若是咱們是要實現一個最小堆,那它的父節點必定是比子節點大的,爲了方便咱們使用一個函數來表示比較
more(i, j) {
return this.heap[i] > this.heap[j]
}
複製代碼
若是此時節點爲ind
那麼父節點的下標就是parseInt(ind/2)
咱們是想創建最小堆,因此小的值應該在更上頭
若是父節點比該節點還大
那就應該交換二者的位置
而後咱們不斷重複該過程
直到父節點小於子節點
即達到了堆有序
那須要的條件就是
while ( this.more(parseInt(ind / 2), ind))
複製代碼
爲 了避免當parseInt(ind / 2) === 0
的時候,會對不存在的this.heap[0]
進行操做
咱們須要確保ind > 1
因此循環的添加應該是
while (ind > 1 && this.more(parseInt(ind / 2), ind))
複製代碼
swim(ind) {
while (ind > 1 && this.more(parseInt(ind / 2), ind)) {
this.swap(parseInt(ind / 2), ind)
ind = parseInt(ind / 2)
}
}
複製代碼
交換元素swap
的函數實現
swap(i, j) {
let temp = this.heap[i]
this.heap[i] = this.heap[j]
this.heap[j] = temp
}
複製代碼
每次彈出的都是最值,即根節點,若是是最小堆就是最小值,最大堆就是最大值
根據咱們上面的講述及圖,咱們很容易知道最值就是
pop() {
const top = this.heap[1]
return top
}
複製代碼
可是若是把根元素直接刪除的話,整個堆就毀了
因此咱們思考思考着使用內部的某一個元素先頂替根節點的位置
這個元素顯而易見的是最後一個元素
由於最後一個元素的移動不會使得樹的結構改變
pop() {
const top = this.heap[1]
this.swap(1,this.len)
return top
}
複製代碼
這裏就會又遇到上面插入元素時遇到的問題,此時的堆多是無序的
很明顯,咱們是不須要緩存本來的根節點的
this.swap(1,this.len--)
複製代碼
這表示,咱們在交換完後,就對堆的長度減一
可是實際上咱們的數組裏仍是對該元素有引用的,由於這裏咱們只是讓咱們所謂的堆的長度刪減,爲了防止內存泄漏,咱們須要讓數組取消對該節點的引用
在真實項目中,咱們存儲都是一個對象裏的
key
,因此咱們須要解除對對象的引用,使其內存回收
this.heap[len + 1] = undefined
複製代碼
如今代碼就有
pop() {
const ret = this.heap[1]
this.swap(1, this.len--)
this.heap[this.len + 1] = undefined
return ret
}
複製代碼
雖然如今終於去掉了這個不要的節點了,可是咱們堆的有序性仍是沒有解決
本來這個末尾節點就是在下層的,因此此時應該也是慢慢的回到下層,咱們就把這個下沉操做稱爲sink
一樣這個操做應該也是不斷循環的直至ind
所指的節點下面再無元素
若是該節點子節點,下標可能就是2*ind
和2*ind + 1
,
因此當2*ind
還在堆的長度範圍內,就說明還要和子節點進行大小比較
sink(ind) {
while (2 * ind <= this.len) {
// ...
}
}
複製代碼
固然咱們不能忽略了2*ind + 1
的存在
咱們是最值堆,指望的固然是把子節點中更小的往上放
因此若是2*ind
比2*ind + 1
還大的話,咱們應該讓j++
,而後就會指向2*ind + 1
,即更小的值
let j = 2 *ind
if (this.more(j, j + 1)) j++
複製代碼
這裏須要考慮j
此時可能就等於 this.len
,那麼根本就不存在j+1
的元素了
因此咱們須要讓j < this.len
,那麼這樣就說明必定有j+1
存在
sink(ind) {
while (2 * ind <= this.len) {
let j = 2 * ind
if (j < this.len && this.more(j, j + 1)) j++
// 此時j表示的就是子節點最小的那個了
}
}
複製代碼
上面這麼多隻是確認與ind
要判斷的節點
如今咱們能夠開始進行判斷了
若是ind
比j
小的話,咱們就break
,中止向下循環了,由於此時ind
的位置就是正確的
不然,咱們就交換二者的位置
而後再把ind
改成交換後的位置,即j
,再進行下次循環
sink(ind) {
while (2 * ind <= this.len) {
let j = 2 * ind
if (j < this.len && this.more(j, j + 1)) j++
if (!this.more(ind, j)) break
this.swap(ind, j)
ind = j
}
}
複製代碼
當咱們把sink
方法實現完後, 咱們就能夠完成彈出的所有操做了
pop() {
const top = this.heap[1]
this.swap(1, this.len--)
this.heap[this.len + 1] = undefined
this.sink(1)
return top
}
複製代碼
top
查看最值size
查看堆長度isEmpty
查看是否爲空關於堆,咱們還要須要提供一個API,top
讓使用者知道當前的最值是多少
top(){
return this.heap[1]
}
複製代碼
size() {
return this.len
}
複製代碼
isEmpty() {
return this.len === 0
}
複製代碼
class MinHeap {
constructor() {
this.heap = []
this.len = 0
}
push(val) {
this.heap[++this.len] = val
this.swim(this.len)
}
pop() {
const top = this.heap[1]
this.swap(1, this.len--)
this.heap[this.len + 1] = undefined
this.sink(1)
return top
}
top() {
return this.heap[1]
}
size() {
return this.len
}
isEmpty() {
return this.len === 0
}
swim(ind) {
while (ind > 1 && this.more(parseInt(ind / 2), ind)) {
this.swap(parseInt(ind / 2), ind)
ind = parseInt(ind / 2)
}
}
sink(ind) {
while (2 * ind <= this.len) {
let j = 2 * ind
if (j < this.len && this.more(j, j + 1)) j++
if (!this.more(ind, j)) break
this.swap(ind, j)
ind = j
}
}
more(i, j) {
return this.heap[i] > this.heap[j]
}
swap(i, j) {
let temp = this.heap[i]
this.heap[i] = this.heap[j]
this.heap[j] = temp
}
}
複製代碼
對於this.heap
和this.len
屬性,咱們顯然是不想暴露的,可是js中沒有私有屬性,咱們就用__
來表示私有屬性
改成this_heap
和this._len
咱們拿LeetCode 215題測試
var findKthLargest = function (nums, k) {
let minHeap = new MinHeap()
for (let i = 0; i < nums.length; i++) {
if (minHeap.size() < k) {
minHeap.push(nums[i])
} else if (minHeap.top() < nums[i]) {
minHeap.pop()
minHeap.push(nums[i])
}
}
return minHeap.top()
};
複製代碼
經過了👌
最大堆與最小堆的區別就是咱們在下沉或者上浮時,是讓小的仍是讓大的上浮或者下沉
在代碼中咱們都是經過
sink(ind) {
while (2 * ind <= this.len) {
let j = 2 * ind
if (j < this.len && this.more(j, j + 1)) j++
if (!this.more(ind, j)) break
this.swap(ind, j)
ind = j
}
}
複製代碼
咱們注意看第5行代碼if (!this.more(ind, j)) break
說明若是this.more(ind, j)
爲真, 就會執行後面的交換函數
ind
指的當下元素j
指的是ind*2
或者ind*2 + 1
,即ind
的子節點若是ind
比j
大,就交換,因此就是把大的往下沉,最後這個堆就是一個最小堆了
more(i, j) {
return this.heap[i] > this.heap[j]
}
複製代碼
若是把>
改爲<
就是反面,即此時的最小堆變成了最大堆
那咱們思考着能不能在建立的時候,經過傳入參數來肯定是最小堆仍是最大堆呢
class Heap {
constructor(maxOfMin = 0) {
this.heap = []
this.len = 0
this.maxOfMin = parseInt(maxOfMin)
}
more(i, j) {
let ret = this.heap[i] > this.heap[j]
return this.maxOfMin === 0 ? ret : !ret
}
}
複製代碼
如今默認是0
,就是最小堆,若是是別的就是最大堆了
const maxHeap = new Heap(1)
const minHeap = new Heap()
let arr = [11, 2, 33, 4, 55, 6]
for (let i = 0; i < arr.length; i++) {
maxHeap.push(arr[i])
minHeap.push(arr[i])
}
console.log('最大堆');
console.log(maxHeap.pop());
console.log(maxHeap.pop());
console.log(maxHeap.pop());
console.log(maxHeap.pop());
console.log(maxHeap.pop());
console.log(maxHeap.pop());
console.log('最小堆');
console.log(minHeap.pop());
console.log(minHeap.pop());
console.log(minHeap.pop());
console.log(minHeap.pop());
console.log(minHeap.pop());
console.log(minHeap.pop());
複製代碼
最大堆
55
33
11
6
4
2
最小堆
2
4
6
11
33
55
複製代碼
忽然發現一個特別有趣的點,上面的輸出都是有序的了,咱們能夠利用這一特性來對數組進行排序
function heapSort(arr) {
let len = arr.length
for (let i = parseInt(len / 2); i >= 1; i--) {
sink(arr, i, len)
}
while (len > 1) {
swap(arr, 1, len--)
sink(arr, 1, len)
}
return arr
}
function sink(arr, ind, len) {
while (2 * ind <= len) {
let j = 2 * ind
if (j < len && more(arr, j, j + 1)) j++
if (!more(arr, ind, j)) break
swap(arr, ind, j)
ind = j
}
}
function swap(arr, i, j) {
i--; j--;
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
function more(arr, i, j) {
i--; j--;
return arr[i] < arr[j]
}
let arr = [11, 2, 33, 4, 55]
let ret = heapSort(arr)
console.log(ret);
複製代碼
咱們來分析上面的代碼
function heapSort(arr) {
let len = arr.length
// 構造最大堆
for (let i = parseInt(len / 2); i >= 1; i--) {
sink(arr, i, len)
}
console.log('<<<===arr==>>>');
console.log(arr);
console.log('==>>>><<<==');
//...
}
複製代碼
打印出來的結果是
[ 55, 11, 33, 4, 2 ]
複製代碼
在實際待排序的數組是從
arr[0 ~ (len - 1)]
,而不是咱們圖上的arr[1 ~ len]
因此,咱們在後面有個小技巧能夠彌補,使得咱們仍是僞裝是在
1~len
間操做
那上面的代碼是怎麼實現使得數組轉換爲最大堆的呢
上圖是原始數組
咱們從最後一個節點的父子節點開始,往上遍歷
每遍歷到該節點時,執行sink
操做,使其下沉到屬於他的位置
這樣咱們就能夠確保每次遍歷某一節點時,他的子孫節點最大的就是子節點
最後一個節點的下標就是len
,那他的父節點的下標就是parseInt(len / 2)
而後咱們就不斷i++
直到把把根節點也執行後就結束
根節點的位置就是i === 1
,那當i < 1
時,就無節點能夠遍歷了,故循環爲
for (let i = parseInt(len / 2); i >= 1; i--) {
sink(arr, i, len)
}
複製代碼
這裏的sink
函數和咱們上面的代碼實現是一致的,只不過,函數是獨立的,咱們須要把咱們的數組,下標,及長度傳入
這裏的長度須要傳入,是由於後續操做中咱們會對
len
進行修改,因此不能在函數裏直接經過獲取arr.length
實現
function sink(arr, ind, len) {
while (2 * ind <= len) {
let j = 2 * ind
if (j < len && less(arr, j, j + 1)) j++
if (!less(arr, ind, j)) break
swap(arr, ind, j)
ind = j
}
}
複製代碼
這裏的比較函數是less
,表示arr[ind]
小於arr[j]
時返回true
,因此上面的邏輯是使得更小的元素arr[ind]
下沉,即咱們實現的是最大堆
除了less
還有一個swap
輔助函數,這兩個函數的實現要具體講下,由於和上面的堆結構稍微有點不同
function swap(arr, i, j) {
i--; j--;
let temp = arr[i]
arr[i] = arr[j]
arr[j] = temp
}
function less(arr, i, j) {
i--; j--;
return arr[i] < arr[j]
}
複製代碼
不同在,在執行操做時,咱們對傳入的參數都進行了減減操做
何故呢?
咱們在上面的操做時,創建的基礎都是把數組的下標從1
開始的,因此咱們在涉及到真正的數組操做時,下標是從0
開始的
就像一個邏輯地址和物理地址的區別,只不過這個轉換特別簡單的,就是把地址減1
while (len > 1) {
swap(arr, 1, len--)
sink(arr, 1, len)
}
複製代碼
如今頂點就是最大值了,如今咱們就把首元素和數組的最尾交換
swap(arr, 1, len--)
複製代碼
這裏的len--
,說明咱們首位和尾位交換後,就把堆的長度減減,可是實際上數組是沒有改變的,這也是前面使用sink(arr,ind,len)
這裏要傳長度而不是在函數裏經過arr.length
獲取的緣由.
此時的數組仍是
交換後的位置,堆就不是有序的了,因此咱們須要把首位下沉
while (len > 1) {
swap(arr, 1, len--)
sink(arr, 1, len)
}
複製代碼
而後咱們不斷上面的操做
最後咱們就會把最大的數都不斷累積在後面
此時堆的長度爲1
雖然此時的堆的長度爲len === 1
可是這個數組已經就有序了