算法:前K個最大的元素

前幾天,阮一峯 和 winter 在前端九部組織了一個互面小組,目的是爲了分享和解答面試遇到的面試題,感興趣的能夠了解一下。前端

下面我就把我回答的一個問題整理出來分享給你們。git

問題描述

題目是:算法,前 K 個最大的元素。github

這個題目很是簡短,第一眼看上去可能不知道是什麼意思。翻譯一下:面試

給定一個數字類型的數組和一個正整數 K,找出數組中前 K 個最大的元素。算法

這個題目網速也有不少的講解,我也是根據網上提供的一些思路來實現的,下面就是我根據其中三種方法的實現:api

解答

解法一:

思路

最簡單的方法就是對數組進行排序,而後取前 K 位就能夠了。數組

實現

/** * 查找前 K 個最大的元素 * * @param {number[]} arr - 要查詢的數組 * @param {number} k - 最大個數 * * @return {number[]} */
const findKMax = (arr, k) => {
  return arr.sort((a, b) => b - a).slice(0, k);
}
複製代碼

解法二

思路

解法一用了 js 的 sort 來實現排序,可是複雜度比較高,數據量大的話會比較慢。仔細分析一下題目,找出前 K 個最大的元素,但並無要求對其排序,因此不用對全部的數都進行排序。分治法就會快不少:frontend

假設有 n 個數存在數組 S 中,從數組 S 中隨機找一個元素 X,遍歷數組,比 X 大的放在 S1 中,比 X 小的放在 S2 中,那麼會出現如下三種狀況:ui

S1 的數字個數等於 K,結束查找,返回 S1; S1 的數字個數大於 K,繼續在 S1 中找取最大的K個數字; S1 的數字個數小於 K,繼續在 S2 中找取最大的 K-S1.length 個數字,拼接在 S1 後; 這樣遞歸下去,就能夠找出答案來了。下面看具體的實現:spa

實現

/** * 分割數組 * * @typedef {Object} Partition * @property {number[]} Partition.maxarr * @property {number[]} Partition.minarr * * @param {number[]} arr - 要分割的數組 * * @returns {Partition} res - 返回結果 */
const partition = (arr) => {
  const length = arr.length; // 數組長度

  const mid = ~~(length / 2); // 取數組中間的位置,可隨機
  const middle = arr[mid]; // 數組中間的值
  const maxarr = []; // 比中間值大
  const minarr = []; // 比中間值小

  // 數組長度爲 2 的要特殊處理
  if (length === 2) {
    maxarr.push(Math.max(arr[0], arr[1]));
    minarr.push(Math.min(arr[0], arr[1]));
  } else {
    arr.forEach((v, i) => {
      if (i !== mid) {
        if (v >= middle) {
          maxarr.push(v);
        } else {
          minarr.push(v);
        }
      }
    })

    // 將中間值放到 maxarr 的最後一位
    maxarr.push(middle);
  }

  return { maxarr, minarr }
}

/** * 查找前 K 個最大的元素 * * @param {number[]} arr - 要查詢的數組 * @param {number} k - 最大個數 * * @return {number[]} */
const findKMax = (arr, k) => {

  if (arr.length < k) {
    return arr;
  }

  // 分割數組
  const { maxarr, minarr } = partition(arr);

  if (maxarr.length === k) {
    return maxarr;
  }

  if (maxarr.length > k) {
    return findKMax(maxarr, k);
  }

  if (maxarr.length < k) {
    return maxarr.concat(findKMax(minarr, k - maxarr.length));
  }
}
複製代碼

解法三

思路

能夠取數組的前 K 位構建一個小頂堆(也叫最小堆),這麼堆頂就是前 K 位最小的值,而後從 K+1 遍歷數組,若是小於堆頂,則將其交換,並從新構建堆,使堆頂最小,這麼遍歷結束後,堆就是最大的 K 位,堆頂是前 K 位的最小值。

實現

/** * 小頂堆葉子節點排序 * @param {number[]} arr - 堆 * @param {number} i = 父節點 * @param {length} i - 堆大小 */
const heapify = (arr, i, length) => {
  const left = 2 * i + 1; // 左孩子節點
  const right = 2 * i + 2; // 右孩子節點
  let minimum = i; // 假設最小的節點爲父結點

  // 肯定三個節點的最小節點
  if (left < length && arr[left] < arr[minimum]) {
    minimum = left;
  }

  if (right < length && arr[right] < arr[minimum]) {
    minimum = right;
  }

  // 若是父節點不是最小節點
  if (minimum !== i) {
    // 最小節點和父節點交換
    const tmp = arr[minimum];
    arr[minimum] = arr[i];
    arr[i] = tmp;

    // 對調整的結點作一樣的交換
    heapify(arr, minimum, length);
  }

}

/** * 構建小頂堆 * 從 n/2 個節點開始,依次構建堆,直到第一個節點 * * @param {number[]} arr */
const buildMinHeap = (arr) => {
  for (let i = Math.floor(arr.length / 2); i >= 0; i--) {
    heapify(arr, i, arr.length)
  }
  return arr;
}

/**· * 查找前 K 個最大的元素 * * @param {number[]} arr - 要查詢的數組 * @param {number} k - 最大個數 * * @return {number[]} */
const findKMax = (arr, k) => {
  // 取數組的前 K 位構建小頂堆
  const newArr = [...arr];
  const kMax = arr.slice(0, k)
  buildMinHeap(kMax);

  // 堆後面的進行遍歷,若是比堆頂大,則交換並從新構建堆
  for (let i = k; i < newArr.length; i++) {
    if (newArr[i] > kMax[0]) {
      const tmp = kMax[0];
      kMax[0] = newArr[i];
      newArr[i] = tmp;

      buildMinHeap(kMax);
    }
  }

  return kMax;
}
複製代碼

總結

上面就是我對這個題目的三種解法,其實還有幾種解法,由於精力緣由沒有探究,你們能夠本身去網上了解一下。

上述解法若是有問題還請指正。

相關文章
相關標籤/搜索