堆及堆排序(JS)

堆及堆排序

代碼實現

堆有序:當一棵二叉樹的每一個結點都大於等於它的兩個子結點時,它被稱爲堆有序。數組

**二叉堆:**二叉堆是一組可以用堆有序的徹底二叉樹排序的元素,並在數組中按照層級儲存(不使用數組的第一個位置)。緩存

image-20190810102011914

咱們的下標從1開始,下標變量爲indbash

那對於給定位置ind的節點:數據結構

  • 左側節點位置是2*ind
  • 右側節點位置是2*ind+ 1
  • 父節點位置是parseInt(ind /2)

parseInt(3/2) === parseInt(2/2) === 1less

咱們使用堆這個數據結構主要有三個操做函數

  • pusk(val):向堆中插入一個新的值
  • pop(val):彈出最值
  • top():查看最值

向堆中插入值

image-20190810105334293

咱們先假設咱們堆裏面已是堆有序的,且含有的元素爲2,3,4測試

這個時候,若是咱們往裏面添加1ui

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] = valthis

這種寫法就達到了咱們數組首位爲空的目的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))
複製代碼

image-20190810112642855

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
}
複製代碼

這裏就會又遇到上面插入元素時遇到的問題,此時的堆多是無序的

image-20190810114328765

很明顯,咱們是不須要緩存本來的根節點的

this.swap(1,this.len--)
複製代碼

這表示,咱們在交換完後,就對堆的長度減一

image-20190810114548824

可是實際上咱們的數組裏仍是對該元素有引用的,由於這裏咱們只是讓咱們所謂的堆的長度刪減,爲了防止內存泄漏,咱們須要讓數組取消對該節點的引用

在真實項目中,咱們存儲都是一個對象裏的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*ind2*ind + 1,

因此當2*ind還在堆的長度範圍內,就說明還要和子節點進行大小比較

sink(ind) {
  while (2 * ind <= this.len) { 
    // ...
  }
}
複製代碼

固然咱們不能忽略了2*ind + 1的存在

咱們是最值堆,指望的固然是把子節點中更小的往上放

因此若是2*ind2*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要判斷的節點

如今咱們能夠開始進行判斷了

若是indj小的話,咱們就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.heapthis.len屬性,咱們顯然是不想暴露的,可是js中沒有私有屬性,咱們就用__來表示私有屬性

改成this_heapthis._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的子節點

若是indj大,就交換,因此就是把大的往下沉,最後這個堆就是一個最小堆了

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 ]
複製代碼

image-20190810195251898

在實際待排序的數組是從arr[0 ~ (len - 1)],而不是咱們圖上的arr[1 ~ len]

因此,咱們在後面有個小技巧能夠彌補,使得咱們仍是僞裝是在1~len間操做

那上面的代碼是怎麼實現使得數組轉換爲最大堆的呢

image-20190810205316757

上圖是原始數組

咱們從最後一個節點的父子節點開始,往上遍歷

每遍歷到該節點時,執行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)
}
複製代碼

如今頂點就是最大值了,如今咱們就把首元素和數組的最尾交換

image-20190810220337065

swap(arr, 1, len--)
複製代碼

這裏的len--,說明咱們首位和尾位交換後,就把堆的長度減減,可是實際上數組是沒有改變的,這也是前面使用sink(arr,ind,len)這裏要傳長度而不是在函數裏經過arr.length獲取的緣由.

此時的數組仍是

image-20190810220508070

交換後的位置,堆就不是有序的了,因此咱們須要把首位下沉

while (len > 1) {
  swap(arr, 1, len--)
  sink(arr, 1, len)
}
複製代碼

image-20190810221505273

而後咱們不斷上面的操做

最後咱們就會把最大的數都不斷累積在後面

image-20190810221808530

image-20190810222127140

image-20190810222322496

image-20190810222414162

image-20190810222651429

此時堆的長度爲1

雖然此時的堆的長度爲len === 1

可是這個數組已經就有序了

image-20190810222938948
相關文章
相關標籤/搜索