LeetCode題解:264. 醜數 II,二叉堆,JavaScript,詳細註釋

Lee_Chen86發表於2021-01-03

原題連線:https://leetcode-cn.com/problems/ugly-number-ii/

解題思路:

  1. 該題可使用解決,利用了堆能夠快速插入和取出元素,並始終能夠按要求排序的特點。
  2. 建立一個小頂堆,初始狀態下堆中儲存元素1,即為第一個醜數。第一次遍歷剛好可以計算出下一組醜數2、3、4。
  3. 因為堆中元素一直保持了從小到大排序,假設堆中已經儲存了所有醜數,那麼只需要從堆中取出n個數即可。
  4. 我們無需在每次執行時都計算出所有的醜數,再進行取出操作,每次只需要計算出足夠取出n個醜數的數量即可。
  5. 我們每次迴圈取出一個堆頂元素,將其分別乘以2、3、4,即可計算出一組醜數,但這一組醜數並不自然擁有從小到大的關係,因此只能插入堆中。
  6. 同時計算出的醜數可能是重複的,如23和32,因此需要使用Set標記當前的醜數是否已被儲存過。
  7. 迴圈計算並從堆中取出n次醜數,就得到了第n個醜數。
/**
 * @param {number} n
 * @return {number}
 */
var nthUglyNumber = function (n) {
  let heap = new BinaryHeap((a, b) => a - b); // 建立一個小頂堆
  let arr = [2, 3, 5]; // 將質因數儲存到陣列,方便進行遍歷計算
  let set = new Set(); // 由於計算結果可能重複,如2*3和3*2,因此需要用Set標記醜數是否已被儲存過
  let result = 1; // 1的質因數也可為2、3、4,因此設定初始結果為1
  let count = 0; // 統計計算出的醜數個數
  heap.insert(1); // 將1插入小頂堆,用於啟動遍歷

  // 迴圈計算n個醜數
  while (count < n) {
    // 堆頂元素即為醜數,每次取出醜數都比上一次取出的大
    // 由於堆的性質,取出之後依然會保持從小到大的排序
    // 退出迴圈後,result儲存的就是第n個醜數
    result = heap.deleteHead();
    count++; // 每取出一個醜數,計數一次

    // 分別將取出的醜數,乘以2、3、4,即為下一批的醜數
    for (let i = 0; i < arr.length; i++) {
      let product = result * arr[i]; // 計算出下一個醜數

      // 如果計算出的醜數沒有被儲存過,則將其插入堆
      if (!set.has(product)) {
        // 醜數插入堆後,自然按照從小到大排序
        heap.insert(product);
        // 標記當前醜數已被插入堆中
        set.add(product);
      }
    }
  }

  return result;
};

class BinaryHeap {
  constructor(compare) {
    this.data = []; // 使用陣列儲存堆
    this.compare = compare; // 堆元素的排序函式
  }

  // 獲取堆的元素數量
  size() {
    return this.data.length;
  }

  // 向堆中插入多個元素
  insertMultiple(arr) {
    for (let i = 0; i < arr.length; i++) {
      this.insert(arr[i]);
    }
  }

  // 向堆插入元素
  insert(value) {
    this.insertAt(this.data.length, value);
  }

  // 將元素插入到index位置
  insertAt(index, value) {
    // 先將元素插入到指定的位置
    this.data[index] = value;
    let fatherIndex = index;
    // 對比當前節點與其父節點,如果當前節點更小就交換它們
    // Math.floor((index - 1) / 2)是父節點在陣列中的索引
    while (
      index > 0 &&
      // 使用compare比較大小
      this.compare(
        value,
        this.data[(fatherIndex = Math.floor((index - 1) / 2))],
      ) < 0
    ) {
      // 將父節點移動到當前位置
      this.data[index] = this.data[fatherIndex];
      // 將插入的值移動到父節點位置
      this.data[fatherIndex] = value;
      // 更新索引為父節點索引,繼續下一次迴圈
      index = fatherIndex;
    }
  }

  // 刪除最大節點
  deleteHead() {
    return this.delete(0);
  }

  // 將指定位置的元素刪除
  delete(index) {
    // 如果堆為空,則不進行刪除操作
    if (this.data.length === 0) {
      return;
    }

    let value = this.data[index]; // 將要刪除的元素快取
    let parent = index; // 以當前元素為起始,向下整理堆

    // 不斷向子節點整理堆,每次迴圈將子節點中經過compare方法對比後較大者與父節點調換
    while (parent < this.data.length) {
      let left = parent * 2 + 1; // 左子節點索引
      let right = parent * 2 + 2; // 右子節點索引

      // 沒有左子節點,表示當前節點已經是最後一個節點
      if (left >= this.data.length) {
        break;
      }

      // 沒有右子節點,則直接將左子節點提前到父節點即可
      // 該左子節點即為最後一個節點
      if (right >= this.data.length) {
        this.data[parent] = this.data[left];
        parent = left;
        break;
      }

      // 使用compare方法比較左右子節點的大小,更大的補到父節點
      if (this.compare(this.data[left], this.data[right]) < 0) {
        // 由於被刪除的節點已儲存,此處只需要將子節點複製到當前父節點即可
        this.data[parent] = this.data[left];
        // 完成移動後將父節點指標移動到子節點,供下一次整理使用
        parent = left;
      } else {
        this.data[parent] = this.data[right];
        parent = right;
      }
    }

    // 檢視最後的空位是不是最後的葉子節點
    if (parent < this.data.length - 1) {
      // 如果還未整理到葉子節點,則繼續向下整理
      this.insertAt(parent, this.data.pop());
    } else {
      // 當完成整理時,最後一個節點即為多於元素,直接彈出陣列即可
      this.data.pop();
    }

    // 返回被刪除的元素
    return value;
  }

  // 刪除指定元素
  deleteItem(value) {
    // 查詢元素在堆中對應的索引
    const index = this.data.findIndex((item) => item === value);

    // 根據索引刪除相應元素
    if (typeof index === 'number') {
      this.delete(index);
    }
  }

  // 刪除指定元素
  deleteItem(value) {
    // 查詢元素在堆中對應的索引
    const index = this.data.findIndex((item) => item === value);

    // 根據索引刪除相應元素
    if (typeof index === 'number') {
      this.delete(index);
    }
  }

  // 讀取堆頂元素
  peek() {
    return this.data[0];
  }

  // 讀取所有堆元素
  getData() {
    return this.data;
  }
}

相關文章