JS數據結構與算法之《堆》

概念

堆的底層其實是一棵徹底二叉樹,能夠用數組實現。javascript

二叉樹的一種,知足如下條件:java

  1. 任意節點大於或小於它的全部子節點(大根堆、小根堆)
  2. 老是一徹底樹,即除了最底層,其它層的節點都被元素填滿

將根節點最大的堆叫作最大堆大根堆,根節點最小的堆叫作最小堆小根堆算法

image

將數組第一個元素置空,爲了方便計算。這樣咱們就能夠從下標1開始,下標變量爲i,那麼:api

  • 左子節點位置是 2*i
  • 右子節點位置是 2*i+1
  • 父節點位置是 Math.floor(i/2)

堆(以大頂堆爲例)相關的操做主要有:數組

  1. 大頂堆調整(Max-Heapify),將堆的末端子節點作調整,使得子節點永遠小於父節點;
  2. 建立大頂堆(Build-Max-Heap),將堆中全部數據調整位置,使其成爲大頂堆;
  3. 堆排序(Heap-Sort),移除在堆頂的根節點,並作大頂堆調整的迭代運算。

大頂堆調整

/** * 從index開始檢查並保持大頂堆 * @param arr 待檢查數組 * @param i 檢查的起始下標 * @param size 堆大小 */
function maxHeapify(arr, i, size) {
  let left = 2 * i
  let right = left + 1

  //左右孩子中較大的一個
  let maxlr = -1

  //無左右孩子節點
  if (left > size && right > size) {
    return
  }
  //只有左孩子節點
  if (left <= size && right > size) {
    maxlr = left
  }
  //只有右孩子節點
  if (right <= size && left > size) {
    maxlr = right
  }
  //同時有左右孩子節點
  if (left <= size && right <= size) {
    maxlr = arr[left] < arr[right] ? right : left
  }

  if (arr[i] < arr[maxlr]) {
    swap(arr, i, maxlr)
    maxHeapify(arr, maxlr, size)
  }
}

function swap(arr, i, j) {
  let temp = arr[i]
  arr[i] = arr[j]
  arr[j] = temp
}
複製代碼

非遞歸寫法:測試

/** * 從i開始檢查並保持大頂堆 * @param arr 待排數組 * @param i 檢查的起始下標 * @param size 堆大小 */
function maxHeapify2(arr, i, size) {
  let left, right, maxlr = -1

  while (i < size) {
    left = 2 * i
    right = left + 1

    //無左右孩子節點
    if (left > size && right > size) {
      break
    }
    //只有左孩子節點
    if (left <= size && right > size) {
      maxlr = left
    }
    //只有右孩子節點
    if (right <= size && left > size) {
      maxlr = right
    }
    //同時有左右孩子節點
    if (left <= size && right <= size) {
      maxlr = arr[left] < arr[right] ? right : left
    }

    if (arr[i] < arr[maxlr]) {
      swap(arr, maxlr, i)
      i = maxlr
    }
  }
}

function swap(arr, i, j) {
  let temp = arr[i]
  arr[i] = arr[j]
  arr[j] = temp
}
複製代碼

建立大頂堆

建立大頂堆(Build-Max-Heap)的做用是,將一個數組改形成一個大頂堆,會自下而上地調用 Max-Heapify 來改造數組。ui

function buildMaxHeap(arr) {
  if (!Array.isArray(arr)) return []

  //將null插到數組第一個位置上
  arr.unshift(null)
  let lastParentIndex = Math.floor((arr.length - 1) / 2)
  for (let i = lastParentIndex; i > 0; i--) {
    maxHeapify(arr, i, arr.length - 1)
  }
  arr.shift()
}
複製代碼

堆排序

堆排序(Heap-Sort)是堆排序的接口算法,其先要調用建立大頂堆(Build-Max-Heap)將數組改造爲大頂堆; 而後進入迭代,迭代中先將堆頂與堆底元素交換,並將堆長度縮短,繼而從新調用大頂堆調整(Max-Heapify)保持大頂堆性質。this

由於堆頂元素必然是堆中最大的元素,因此每一次操做以後,堆中存在的最大元素會被分離出堆,重複 n-1 次,數組排序完成。spa

function heapSort(arr) {
  arr[0] !== null && arr.unshift(null)
  for (let j = arr.length - 1; j > 0; j--) {
    //將堆頂與堆底元素交換,分離出最大的元素
    swap(arr, 1, j) 
    //從新調整大頂堆
    maxHeapify(arr, 1, j - 1)
  }
  arr.shift()
}
複製代碼

咱們來測試一下大頂堆的構建與排序:prototype

var arr = [5, 2, 8, 3, 1, 6, 9]
buildMaxHeap(arr)
console.log(arr)
heapSort(arr)
console.log(arr)

//[9,3,8,2,1,6,5]
//[1,2,3,5,6,8,9]
複製代碼

複雜度

堆排序是一種選擇排序,總體主要由構建初始堆+交換堆頂元素和末尾元素並重建堆兩部分組成。

其中構建初始堆經推導複雜度爲O(n),在交換並重建堆的過程當中,需交換n-1次,而重建堆的過程當中,根據徹底二叉樹的性質,[log2(n-1),log2(n-2)...1]逐步遞減,近似爲nlogn。

因此堆排序時間複雜度通常認爲就是O(nlogn)級。

添加元素

將元素添加到數組末尾,而後進行大頂堆調整

function maxHeapPush(arr = [], elem) {
  arr.push(elem)
  arr[0] !== null && arr.unshift(null)
  let lastParentIndex = Math.floor((arr.length - 1) / 2)
  for (let i = lastParentIndex; i > 0; i--) {
    maxHeapify(arr, i, arr.length - 1)
  }
  arr.shift()
}
複製代碼

彈出元素

每次只能彈出最值,即根節點,若是把根元素直接刪除的話, 整個堆就毀了, 因此咱們思考着使用內部的某一個元素先頂替根節點的位置,這個元素顯而易見的是最後一個元素,由於最後一個元素的移動不會使得樹的結構改變。 交換後,進行大頂堆調整。

function maxHeapPop(arr = []) {
  arr[0] !== null && arr.unshift(null)
  swap(arr, 1, arr.length - 1)
  const top = arr.pop()

  let lastParentIndex = Math.floor((arr.length - 1) / 2)
  for (let i = lastParentIndex; i > 0; i--) {
    maxHeapify(arr, i, arr.length - 1)
  }
  arr.shift()
  return top
}
複製代碼

測試一下:

var arr = [5, 2, 8, 3, 1, 6, 9]
buildMaxHeap(arr)
console.log(arr)
maxHeapPush(arr, 10)
console.log(arr)
maxHeapPop(arr)
console.log(arr)

//[10, 9, 8, 3, 1, 6, 5, 2]
//[9, 3, 8, 2, 1, 6, 5]
複製代碼

封裝一下,完整的代碼以下:

function Heap(type = 'max') {
  this.type = type
  this.arr = []
}

Heap.prototype.build = function() {
  this.arr.unshift(null)
  let lastParentIndex = Math.floor((this.arr.length - 1) / 2)
  for (let i = lastParentIndex; i > 0; i--) {
    this.heapify(i, this.arr.length - 1)
  }
  this.arr.shift()
}

Heap.prototype.heapify = function(i, size) {
  let left = 2 * i
  let right = left + 1

  //左右孩子中較大或較小的一個
  let lr = -1

  //無左右孩子節點
  if (left > size && right > size) {
    return
  }
  //只有左孩子節點
  if (left <= size && right > size) {
    lr = left
  }
  //只有右孩子節點
  if (right <= size && left > size) {
    lr = right
  }
  //同時有左右孩子節點
  if (left <= size && right <= size) {
    lr = this.type === 'max' ? (this.arr[left] < this.arr[right] ? right : left) : (this.arr[left] > this.arr[right] ? right : left)
  }

  if ((this.type === 'max' && this.arr[i] < this.arr[lr]) || (this.type === 'min' && this.arr[i] > this.arr[lr])) {
    this.swap(i, lr)
    this.heapify(lr, size)
  }
}

Heap.prototype.sort = function() {
  this.arr[0] !== null && this.arr.unshift(null)
  for (let j = this.arr.length - 1; j > 0; j--) {
    this.swap(1, j)
    this.heapify(1, j - 1)
  }
  this.arr.shift()
}

Heap.prototype.add = function(elem) {
  this.arr.push(elem)
  this.arr[0] !== null && this.arr.unshift(null)
  let lastParentIndex = Math.floor((this.arr.length - 1) / 2)
  for (let i = lastParentIndex; i > 0; i--) {
    this.heapify(i, this.arr.length - 1)
  }
  this.arr.shift()
}

Heap.prototype.pop = function() {
  this.arr[0] !== null && this.arr.unshift(null)
  this.swap(1, this.arr.length - 1)
  const top = this.arr.pop()

  let lastParentIndex = Math.floor((this.arr.length - 1) / 2)
  for (let i = lastParentIndex; i > 0; i--) {
    this.heapify(i, this.arr.length - 1)
  }
  this.arr.shift()
  return top
}

Heap.prototype.swap = function(i, j) {
  let temp = this.arr[i]
  this.arr[i] = this.arr[j]
  this.arr[j] = temp
}

var heap = new Heap()
heap.add(5)
heap.add(2)
heap.add(8)
heap.add(3)
heap.add(1)
heap.add(6)
heap.add(9)
console.log(heap.arr)

//heap.build()
//console.log(heap.arr)

heap.add(10)
console.log(heap.arr)

heap.pop()
console.log(heap.arr)

heap.sort()
console.log(heap.arr)
複製代碼

數據流中的中位數

如何獲得一個數據流中的中位數?若是從數據流中讀出奇數個數值,那麼中位數就是全部數值排序以後位於中間的數值。

若是從數據流中讀出偶數個數值,那麼中位數就是全部數值排序以後中間兩個數的平均值。咱們使用Insert()方法讀取數據流,使用GetMedian()方法獲取當前讀取數據的中位數。

步驟:

  1. 維護一個大頂堆,一個小頂堆,數據總數:
  • 小頂堆裏的值全大於大頂堆裏的;
  • 2個堆個數的差值小於等於1
  1. 當插入數字後數據總數爲奇數時:使小頂堆個數比大頂堆多1;當插入數字後數據總數爲偶數時,使大頂堆個數跟小頂堆個數同樣。
  2. 當總數字個數爲奇數時,中位數就是小頂堆堆頭;當總數字個數爲偶數時,中位數數就是2個堆堆頂平均數。
const maxHeap = new Heap('max');
const minHeap = new Heap('min');
let count = 0;
function Insert(num) {
  count++;
  if (count % 2 === 1) {
    maxHeap.add(num);
    minHeap.add(maxHeap.pop());
  } else {
    minHeap.add(num);
    maxHeap.add(minHeap.pop());
  }
}
function GetMedian() {
  if (count % 2 === 1) {
    return minHeap.value[0];
  } else {
    return (minHeap.value[0] + maxHeap.value[0]) / 2
  }
}
複製代碼

最小的k個數

輸入n個整數,找出其中最小的K個數。例如輸入4,5,1,6,2,7,3,8這8個數字,則最小的4個數字是1,2,3,4。

步驟:

  1. 把前k個數構建一個大頂堆
  2. 從第k個數開始,和大頂堆的最大值進行比較,若比最大值小,交換兩個數的位置,從新構建大頂堆
  3. 一次遍歷以後大頂堆裏的數就是整個數據裏最小的k個數
function getLeastNumbers(arr, k) {
  if (k > arr.length) {
    return []
  }
  arr.unshift(null)
  buildMaxHeap(arr, k + 1)

  for (let i = k + 1; i < arr.length; i++) {
    if (arr[i] < arr[1]) {
      [arr[i], arr[1]] = [arr[1], arr[i]]

      let lastParentIndex = Math.floor(k / 2)
      for (let j = lastParentIndex; j > 0; j--) {
        maxHeapify(arr, j, k)
      }
    }
  }
  return arr.slice(1, k + 1)
}

function buildMaxHeap(arr, size) {
  let lastParentIndex = Math.floor(size / 2)
  for (let i = lastParentIndex; i > 0; i--) {
    maxHeapify(arr, i, size)
  }
}

function maxHeapify(arr, i, size) {
  let left = 2 * i
  let right = left + 1

  //左右孩子中較大的一個
  let maxlr = -1

  //無左右孩子節點
  if (left > size && right > size) {
    return
  }
  //只有左孩子節點
  if (left <= size && right > size) {
    maxlr = left
  }
  //只有右孩子節點
  if (right <= size && left > size) {
    maxlr = right
  }
  //同時有左右孩子節點
  if (left <= size && right <= size) {
    maxlr = arr[left] < arr[right] ? right : left
  }

  if (arr[i] < arr[maxlr]) {
    [arr[i], arr[maxlr]] = [arr[maxlr], arr[i]]
    maxHeapify(arr, maxlr, size)
  }
}


var arr = [4, 5, 1, 6, 2, 7, 3, 8]
var result = getLeastNumbers(arr, 4)
console.log(result)
//[4,3,1,2]
複製代碼
相關文章
相關標籤/搜索