前端算法題目解析(二)

前言

雖然疫情仍是嚴峻,但總會過去。在此居家辦公之際,應該趁這個時機好好提高下自我,多讀書多看報,少吃零食多運動哈哈。git

最近看一些文章和題目有接觸一些算法題,我整理了一下收錄daily-question 的 algorithm 文件夾中,後續會繼續增長,本文分享我整理的十個算法題目。github

11-計算矩陣中的島個數

問題描述:算法

一個矩陣中只有 0 和 1 兩種值,每一個位置均可以和本身的上、下、左、右 四個位置相連,若是有一片 1 連在一塊兒,這個部分叫作一個島,求一個矩陣中有多少個島?數組

舉例: 下面這個矩陣中有4個島。bash

let arrIsland = [
	[0,0,1,0,1,0],
	[1,1,1,0,1,0],
	[1,0,0,1,0,0],
	[0,0,0,0,0,1]
]
// 四個島分別是 【(0,2)(1,0)(1,1)(1,2)(2,0)】 【(0,5),(1,5)】 【(2,3)】【(3,5)】
複製代碼

思路:dom

  1. 用遞歸與雙循環實現,循環中遞歸找到一個島(即找出 1 及其上下左右的 1),將此島標記(我標記爲2),而後重複依次找出剩下的島函數

  2. 注意邊界狀況及不等於1的狀況,此時應結束遞歸。post

參考答案:性能

function islandCount(arr){
	if (!arr || arr.length === 0) {
		return;
	};
	let N = arr.length, M = arr[0].length, res = 0;
	for(let i = 0; i < N; i++){
		for(let j = 0; j < M; j++){
			if (arr[i][j] === 1) {
				++res;
				infect(arr,i,j,N,M);
			}
		}
	}
	return res;
}
// 遞歸函數,傳入 數組arr, x座標 i, y座標j 數組長度N及數組內元素長度M
function infect(arr,i,j,N,M){
  // 處理邊界狀況及不爲1的狀況,此時結束遞歸
	if (i < 0 || j < 0 || i >= N || j >= M || arr[i][j] !== 1) {
		return;
	};
	arr[i][j] = 2; // 將找到的島元素標記,避免重複
	infect(arr,i,j-1,N,M); // 向左尋找
	infect(arr,i+1,j,N,M); // 向右尋找
	infect(arr,i,j+1,N,M); // 向下尋找
	infect(arr,i-1,j,N,M); // 向上尋找
}
let arrIsland = [
	[0,0,1,0,1,0],
	[1,1,1,0,1,0],
	[1,0,0,1,0,0],
	[0,0,0,0,0,1]
];
console.log(islandCount(arrIsland));  // 4
複製代碼

12-漢諾塔問題

關於漢諾塔:測試

漢諾塔:漢諾塔(又稱河內塔)問題是源於印度一個古老傳說的益智玩具。大梵天創造世界的時候作了三根金剛石柱子,在一根柱子上從下往上按照大小順序摞着64片黃金圓盤。大梵天命令婆羅門把圓盤從下面開始按大小順序從新擺放在另外一根柱子上。而且規定,在小圓盤上不能放大圓盤,在三根柱子之間一次只能移動一個圓盤。

思路:

  1. 遞歸解決:把問題轉化爲規模縮小了的同類問題的子問題;
  2. 明確遞歸結束的條件(base case):n == 1
  3. 其餘過程:from:來源地;to:目的地;help:輔助。

參考答案:

function hanoiProcess(n,from,to,help){
	if (n < 1) {
		return;
	}
	if (n == 1) {  // 最後一個從from移到to
		console.log("Move 1 from " + from + " to " + to);
	} else{
		hanoiProcess(n-1, from, help, to);  // 前n-1個從from移到help上,能夠藉助to
		console.log("Move "+ n +" from " + from + " to " + to);
		hanoiProcess(n-1, help, to, from);  // 再把n-1個從help移到to,能夠藉助from
	}
}
hanoiProcess(3, "左", "右", "中");
// Move 1 from 左 to 右
// Move 2 from 左 to 中
// Move 1 from 右 to 中
// Move 3 from 左 to 右
// Move 1 from 中 to 左
// Move 2 from 中 to 右
// Move 1 from 左 to 右
複製代碼

13-母牛生母牛

問題描述:

母牛每一年生一隻母牛,新出生的母牛成長三年後也能每一年生一隻母牛,假設不會死。求 N 年後,母牛的數量。

思路:

  1. 由於新生的母牛,只有等到第四年才能生小母牛。因此前 4 年,只有原來的一頭母牛每一年生一頭。

  2. 第一年:原始牛 共 1 頭

  3. 第二年:原始牛生了 牛 A 共兩頭

  4. 第三年: 原始牛生了 牛 A , 牛 B 共三頭

  5. 第四年: 原始牛生了 牛 A , 牛 B, 牛 C ,共四頭

  6. 第五年: 原始牛生了 牛 A , 牛 B, 牛 C,牛 D 共五頭, 牛 A 生了牛 A1 共兩頭,其中牛 A 多算了一次。咱們將牛 A 視爲一頭新的原始牛抽離出去,則原來的原始牛生了 牛 B, 牛 C,牛 D 共四頭, 新原始牛生了牛 A1 共兩頭,從這裏你會發現,第五年剩的數量 = 第四年生的的數量 + 第二年生的數量

  7. 接着算第六年:原始牛生了 牛 A , 牛 B, 牛 C,牛 D,牛 F 共六頭,牛 A 生了牛 A1,牛 A2 共三頭, 牛 B 生了牛 B1 共兩頭

單獨把牛 A 拿出來作原始牛,你會發現剩下的 原始牛生了 牛B, 牛C,牛D,牛F, 牛B生了牛B1共兩頭 與第五年的狀況 原始牛生了 牛A , 牛B, 牛C,牛D 共五頭, 牛A生了牛A1共兩頭及其類似,數量規則一致只是名字不同而已,那麼 第六年的數量 = 第五年的數量 + 第三年的數量,以此類推能夠得出 f(n) = f(n-1) + f(n-3), 當 n <= 4 時, f(n) = n, 是否是有點斐波那契數列的感受??能夠用遞歸實現!!

參考答案:

function cow(n) {
  if (n < 1) {
    return;
  }
  let count = 0;
  if (n > 4) {
    count = cow(n - 1) + cow(n - 3);
  } else {
    count = n;
  }
  return count;
}
let n = 7;
console.log(n + ' 年後,牛的數量是: ' + cow(n));
// 7 年後,牛的數量是: 13
複製代碼

14-找出字符串中出現最多的字母

例如字符串: (ababccdeajxac)

  • 最早想到的解法是用 map 紀錄每一個字符的次數,而後找出最多的便可:
function getMaxNumberOfChar(str) {
  return (str + '').split('').reduce(
    function(pre, cur, index, arr) {
      cur in pre ? pre[cur]++ : (pre[cur] = 1);
      pre[cur] > pre.value && ((pre.char = cur), (pre.value = pre[cur]));
      return pre;
    },
    { value: 0 }
  );
}
getMaxNumberOfChar('ababccdeajxac'); // Object {value: 4, a: 4, char: "a", b: 2, c: 3…}
複製代碼

此外,能夠考慮用正則來輔助處理:

function getMaxNumberOfChar(str) {
  return (str + '')
    .split('')
    .sort()
    .join('')
    .match(/(\w)\1*/g) // \1表示\w匹配到的字母 \1是匹配第一個分組匹配到的內容
    .reduce(
      function(pre, cur) {
        return cur.length > pre.value
          ? { value: cur.length, char: cur[0] }
          : pre;
      },
      { value: 0 }
    );
}
getMaxNumberOfChar('ababccdeajxac'); // Object {value: 4, char: "a"}
複製代碼

這裏拓展一下 reduce 函數的用法

// reduce 函數
// array.reduce(function(accumulator, currentValue, currentIndex, arr), initialValue)
// reducer回調函數自己接受幾個參數,第一個參數是 accumulator 累加器,第二個是數組中的 item,第三個參數是該項的索引,最後一個參數是原始數組的引用。
// initialValue 爲reduce初始值,不然視數組第一個值爲初始值,選填
const array1 = [1, 2, 3, 4];

// 1 + 2 + 3 + 4
console.log(
  array1.reduce((accumulator, currentValue) => {
    console.log(accumulator, currentValue);
    return accumulator + currentValue;
  })
);
複製代碼

15-解析 URL 參數爲對象

儘量的全面正確的解析一個任意 url 的全部參數爲 Object,注意邊界條件的處理

let url =
  'http://www.suporka.com/?user=suporka&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url);
/* 結果 { user: 'suporka', id: [ 123, 456 ], // 重複出現的 key 要組裝成數組,能被轉成數字的就轉成數字類型 city: '北京', // 中文需解碼 enabled: true, // 未指定值得 key 約定爲 true } */
複製代碼

解法

function parseParam(url) {
  const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 將 ? 後面的字符串取出來
  const paramsArr = paramsStr.split('&'); // 將字符串以 & 分割後存到數組中
  let paramsObj = {};
  // 將 params 存到對象中
  paramsArr.forEach(param => {
    if (/=/.test(param)) {
      // 處理有 value 的參數
      let [key, val] = param.split('='); // 分割 key 和 value
      val = decodeURIComponent(val); // 解碼
      val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判斷是否轉爲數字

      if (paramsObj.hasOwnProperty(key)) {
        // 若是對象有 key,則添加一個值
        paramsObj[key] = [].concat(paramsObj[key], val);
      } else {
        // 若是對象沒有這個 key,建立 key 並設置值
        paramsObj[key] = val;
      }
    } else {
      // 處理沒有 value 的參數
      paramsObj[param] = true;
    }
  });

  return paramsObj;
}
複製代碼

16. 走樓梯的動態規劃

題目:

樓梯臺階有 12 階,一步只能走 1 階或者 2 階,那麼,請問走完樓梯有多少走法?

這裏涉及到動態規劃,所謂動態規劃,意思就是說,大事化小,小事化了。術語的話,包含三個,最優子結構,邊界,狀態轉移公式

再來分析這道題目——

  1. 走到最後一個臺階的前一個狀況,只能有兩種,就是從第 11 臺階走一步上來,或者從 10 臺階走兩步上來,那麼無論有多少走法走到了 11 階假設是 X 種走法吧,假設是 Y 種走法走到了 10 階,那麼,走到 12 階的走法必定是 X+Y,這個是成立的吧。這就是最優子結構

  2. 那什麼是邊界呢?本例子中,走到第一個臺階,就一種走法吧,沒有臺階,那就 0 種走法吧,走到第二個臺階,也就 2 種走法,其實這就是邊界了。

  3. 那麼狀態轉移公式就水到渠成了, F(n) = F(n-1) + F(n-2),看起來是否是有點像斐波那契數列??

代碼以下:

function fun(n) {
  if (n < 0) {
    return 0;
  }
  if (n === 1) {
    return 1;
  }
  if (n === 2) {
    return 2;
  }
  return fun(n - 1) + fun(n - 2);
}
console.log('12臺階的走法 :' + fun(12));
複製代碼

咱們以前在斐波那契數列裏面講過,這種遞歸有性能問題,根據斐波那契數列的優化,改寫代碼以下:

function fun(n) {
  if (n < 0) {
    return 0;
  }
  if (n === 1) {
    return 1;
  }
  if (n === 2) {
    return 2;
  }
  var a = 1;
  var b = 2;
  var temp = 0;
  for (var i = 3; i <= n; i++) {
    temp = a + b;
    a = b;
    b = temp;
  }
  return temp;
}
console.log('12臺階的走法 :' + fun(12));
複製代碼

17-數組中找出和爲 M 的 N 個數

先來道簡單的題目:

給定一個整數數組 nums 和一個目標值 target,請你在該數組中找出和爲目標值的那 兩個 整數,並返回他們組成的數組。 你不能重複利用這個數組中一樣的元素。
複製代碼

比較容易想到的方法是用兩層循環,不斷遍歷找出和爲目標值的兩個元素,而後存進數組。

var nums = [8, 9, 2, 15, 7, 1];
var target = 9;
var twoSum = function(nums, target) {
  var result = [];
  for (var i = 0; i < nums.length; i++) {
    for (var j = i + 1; j < nums.length; j++) {
      if (nums[i] + nums[j] === target) {
        result.push([nums[i], nums[j]]);
      }
    }
  }
  return result;
};
console.log(twoSum(nums, target)); //[ [ 8, 1 ], [ 2, 7 ] ]
複製代碼

若是要求咱們使用遞歸,該如何實現呢?

這個和我上一個算法《走樓梯的動態規劃》有些類似,咱們也來動態規劃下:

假設數組和目標值以下

var nums = [8, 9, 2, 15, 7, 1];
var target = 9;
複製代碼
  1. 首先咱們拿出第一個元素 8 ,再從後面剩下的[9, 2, 15, 7 ,1]9-8 (即1), 找到與目標值差值(這裏是 1)則返回這個組合,找不到返回空數組

  2. 而後再從剩下的[9, 2, 15, 7 ,1]找出組合值等於目標值的數組,即重複 1 步驟

  3. 將 1,2 步驟合併就是咱們所求的組合

  4. 狀態轉移公式爲 f(n) = f(n 首項與目標值差值組合).concat(f(n - 1))

  5. 邊界。當數組長度小於取出元素個數時,返回空數組。當取出元素爲 1 時,返回目標值數組。

// 如下是代碼
var nums = [8, 9, 2, 15, 7, 1];
var target = 10;

function search(arr, m, n = 2) {
  if (arr.length < n) return [];

  if (n === 1) return arr.filter(i => i === m);
  return search(arr.slice(1), m - arr[0], 1)
    .map(item => [arr[0], item])
    .concat(search(arr.slice(1), m));
}
console.log(search(nums, target));
複製代碼

升級版

從一個數組中找出 N 個數,其和爲 M 的全部可能
複製代碼

從上面得知,若是使用循環,取出 2 個數就是兩層循環,3 個數就是三層循環,以此類推,n 越大循環越多,顯然不可取。因此選擇第二種方法,也就是遞歸。

上面已經爲咱們寫好了遞歸的雛形,如今對其進行改造

上面邊界 n === 1 其實還能夠降爲 0, 由於當 n === 0 && m === 0 時,上一步的 arr[0] 就是咱們要找的最後一個數,並且在 map 函數中,咱們已經將 arr[0] 置爲首位,此時只要返回一個長度爲 1 且首項爲空的數組([[]]),而且在 map 函數中將其 item([]) 展開便可

注:這裏要花點時間好好理解下,比較繞

// 代碼以下
function search(arr, m, n) {
  if (n === 0) return m === 0 ? [[]] : [];
  if (arr.length < n) return [];
  return search(arr.slice(1), m - arr[0], n - 1)
    .map(item => [arr[0], ...item])
    .concat(search(arr.slice(1), m, n));
}

// 測試一下
var nums = [8, 9, 2, 15, 7, 1];
var target = 10;

console.log(search(nums, target, 3)); // [[2,7,1]]
複製代碼

18-數組中找出和爲 M 的 N 個數(番外篇)

仍是一樣的問題:

從一個數組中找出 N 個數,其和爲 M 的全部可能
複製代碼

數組中選取不固定數值 N ,咱們能夠嘗試着使用標記的方式,咱們把 1 表示成選取狀態, 把 0 表示成未選取狀態。

假設數組 const arr=[1,2,3,4] ,對應着每一個元素都有標記 0 或者 1 。若是 N=4 ,也就是在這個數組中,須要選擇 4 個元素,那麼對應的標記就只有一種可能 1111 ,若是 N=3 ,那就有 4 種可能,分別是 1110 、 1101 、1011 以及 0111 (也就是 C4 取 3->4 ) 種可能。

標記中有幾個 1 就是表明選取了幾個數,而後再去遍歷這些 1 全部可能存在的排列方式,最後作一個判斷,這個判斷就是:每一種排列方式,都表明着數組中不一樣位置的被選中的數的組合,因此這裏就是將選中的這些數字,進行求和運算,而後判斷求出的和是否是等於 M 。

如何將數組和標記關聯

0101 明顯就是二進制嘛

對於 arr 來講,有 4 個元素,對應的選擇方式就是從 0000( N = 0 )到 1111( N = 4 )的全部可能。

而 1111 就是 15 的二進制,也就是說這全部的可能其實對應的就是 0 - 15 中全部數對應的二進制。

這裏的問題最終變成了如何從數組長度 4 推導出 0 - 15

這裏採用了位運算--左移運算, 1 << 4 的結果是 16 。 因此咱們能夠創建這樣一個迭代:

const arr = [1, 2, 3, 4];
let len = arr.length,
  bit = 1 << len;

// 這裏忽略了 0 的狀況(N = 0),取值就是 1 - 15
for (let i = 1; i < bit; i++) {
  // ...
}
複製代碼

如何從 1110 標記中取出 1 的個數

最簡單的方式:

const n = num => num.toString(2).replace(/0/g, '').length;
複製代碼

這其實也是一道算法常考題,由於位運算是不須要編譯的,確定速度最快。

PS: 若是不理解位運算爲什麼會提升性能的同窗,能夠自行搜索一下位運算。簡單點說就是:位運算直接用二進制進行表示,省去了中間過程的各類複雜轉換,提升了速度。

咱們嘗試使用 & 運算來解決這個問題

首先咱們確定知道 1 & 1 = 1; 1 & 0 = 0 這些結論的。因此咱們從 15 & 14 => 14 能夠推導出 1111 & 1110 => 1110 ,爲何能夠這樣推導呢,由於 15 的二進制就是 1111 ,14 同理。

咱們能夠看到,經過上面的操做消掉了最後的 1。

因此咱們能夠創建一個迭代,經過統計消除的次數,就能肯定最終有幾個 1 了。代碼以下:

const n = num => {
  let count = 0;
  while (num) {
    num &= num - 1;
    count++;
  }
  return count;
};
複製代碼

計算和等於 M

如今咱們已經能夠把全部的選取可能轉變爲遍歷一個數組,而後經過迭代數組中的每一個數對應的二進制,有幾個 1 來肯定選取元素的個數。

那麼,如今須要的最後一層判斷就是選取的這些數字和必須等於 M

這裏其實就是創建一個映射:

1110 到 [1, 2, 3, 4] 的映射,就表明選取了 2, 3, 4,而後判斷 2 + 3 + 4 與 M 。

這裏能夠這樣看:1110 中的左邊第一個 1 對應着數組 [1, 2, 3, 4] 中的 1 。

如今有一個問題,該如何創建這個映射關係呢?

咱們知道前者 1110 其實就是對應的外層遍歷中的 i = 14 的狀況。

再看看數組[1, 2, 3, 4] ,咱們能夠將元素及其位置分別映射爲 1000 0100 0010 0001。

實現方式也是經過位運算--左位移來實現:

1 << inx ,inx 爲數組的下標。
複製代碼

位掩碼介紹

位掩碼 不熟悉的童鞋會有點暈,這裏簡單科普下:

實質上,這裏的 1 << j ,是指使用 1 的移位來生成其中僅設置第 j 位的位掩碼。

好比:14 的二進制表示爲 1110,其表明(從右往左)選取了第 2 , 3 , 4 位。

那麼(下面故意寫成上下對應的方式):

// demo1
1110 & 0001 = 0000;

// demo2
1110 & 0010 = 0010;
複製代碼

PS: 經過上面代碼,咱們能夠看到上下對應的 0 和 1 在進行 & 運算之後,得出的結果和在 js 中進行相同條件下 & 運算的結果相同。

因此:

1 << 0 // 1 -> 0001
1 << 1 // 2 -> 0010
1 << 2 // 4 -> 0100
1 << 3 // 8 -> 1000

// 說白了,就是把左邊的值變成二進制形式,而後左移或者右移,超出補 0

// 因此, 1110 對應着 第一位沒有選取,那麼 1110 & 0001(設置爲第一位的位掩碼) = 0,若是 i & (1 << inx) !== 0 表明該位被選取了
for(let j = 0; j < arr.length; j++){
    if((i & (1 << j) !== 0) {
    // 表明這個數被選取了,咱們作累加求和就行
    }
}
複製代碼

複製代碼因此綜上所述,最終代碼實現以下:

// 參數依次爲目標數組、選取元素數目、目標和
const search = (arr, count, sum) => {
  // 計算某選擇狀況下有幾個 `1`,也就是選擇元素的個數
  const n = num => {
    let count = 0;
    while (num) {
      num &= num - 1;
      count++;
    }
    return count;
  };

  let len = arr.length,
    bit = 1 << len,
    res = [];

  // 遍歷全部的選擇狀況
  for (let i = 1; i < bit; i++) {
    // 知足選擇的元素個數 === count
    if (n(i) === count) {
      let s = 0,
        temp = [];

      // 每一種知足個數爲 N 的選擇狀況下,繼續判斷是否知足 和爲 M
      for (let j = 0; j < len; j++) {
        // 創建映射,找出選擇位上的元素
        if ((i & (1 << j)) !== 0) {
          s += arr[j];
          temp.push(arr[j]);
        }
      }

      // 若是這種選擇狀況知足和爲 M
      if (s === sum) {
        res.push(temp);
      }
    }
  }

  return res;
};
複製代碼

總結

這裏位操做思路和代碼都很棒。可是 bit 位移有溢出問題。當數組長度大於 30 的時候,位操做已經溢出不精準。所以僅供參考其思想,不能做爲其標準答案。

原文地址: 從一個數組中找出 N 個數,其和爲 M 的全部可能--最 nice 的解法

19-TOP-k 問題

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

比較簡單的是將這些數字組合成一個數組,而後進行從大到小進行排序,取前 K 個便可。

選擇算法

對整個數組進行排序有點浪費,咱們只是取前 K 個,後面剩下的不進行排序也行。所以,對此數組使用選擇算法獲取前 K 個數便可。

function getLargeNumber(array, k) {
  if (array == null || k <= 0 || k > array.length) {
    return [];
  }
  let length = array.length,
    i,
    j,
    maxIndex,
    maxValue,
    temp;
  for (i = 0; i < k; i++) {
    maxIndex = i;
    maxValue = array[maxIndex];
    for (j = i + 1; j < length; j++) {
      //經過循環選出最小的
      if (array[j] > maxValue) {
        maxIndex = j;
        maxValue = array[maxIndex];
      }
    }
    // 交換位置
    temp = array[i];
    array[i] = maxValue;
    array[maxIndex] = temp;
  }
  return array.slice(0, k);
}
// 測試一下
var nums = [8, 9, 2, 15, 7, 1];
console.log(getLargeNumber(nums, 3)); // [15,9,8]
複製代碼

快排算法

咱們能夠利用快排中 partion 函數的思想來作作題。

由於 partion 可使得序列分爲 2 部分:左邊的值都小於哨兵,右邊的值都大於哨兵。因此咱們只要找處處於第 k 位置的哨兵便可,也就是說找到第 k 大的值所在的位置便可,那麼它的左邊的 k-1 值都小於等於第 k 大值。顯然,前 k 個值即爲咱們所求的最小 k 個數。在咱們的劃分過程有 3 種狀況:

  1. 哨兵的位置大於 k,說明第 k 大的數在左邊,繼續遞歸處理左部分便可。
  2. 哨兵的位置小於 k,說明第 K 大的數在右邊,繼續遞歸處理有部分便可。
  3. 哨兵的位置等於 k,說明該哨兵即爲第 K 大的值,其左邊 k-1 個數都小於等於它,所以輸出前 k 個即爲所求的結果。
var nums = [8, 9, 2, 15, 7, 1, 13, 35, 24];

function getLargeNumber(array, k) {
  if (array == null || k <= 0 || k > array.length) {
    return [];
  }

  partition(array, 0, array.length - 1, k - 1);
  return array.slice(0, k);
}
function partition(array, low, high, k) {
  if (low < high) {
    // 獲取 low 至 high 之間一個隨機數
    let pivot = Math.floor(Math.random() * (high - low + 1)) + low;

    // 此隨機數對應的元素與最後一位暫時交換(後面會再交換一次),咱們先找到有多少個數大於此隨機數,大的話從左到右排列
    swap(array, pivot, high);
    let index = low;
    for (let i = low; i < high; i++) {
      if (array[i] > array[high]) {
        // 這裏即是一次選擇排序
        swap(array, i, index);
        index++;
      }
    }
    // 交換數組第index個和剛纔置於數組末尾的隨機數組元素,這樣array[index]左邊的數都比array[index]大
    swap(array, index, high);

    // 若是index > k,說明咱們剛纔排的範圍過大,應該縮小範圍再次遞歸尋找
    // 若是 index < k,說明咱們剛纔拍的範圍太小,應該擴大範圍再次遞歸尋找
    if (index > k) {
      partition(array, low, index - 1, k);
    } else if (index < k) {
      partition(array, index + 1, high, k);
    }
  }
}

function swap(array, one, two) {
  [array[one], array[two]] = [array[two], array[one]];
}
console.log(getLargeNumber(nums, 3)); // [35,24,15]
複製代碼

最小堆

咱們知道,最小堆的頂部結點爲該堆的最小值,所以咱們建立一個具備 K 的節點的最小堆(能夠先取該數組的前 K 個元素)調整爲最小堆。

以後將第 K + 1 個元素與堆頂對比,若是大於堆頂元素,則說明堆頂元素不是第 K 大的值,所以將堆頂元素替換爲第 K + 1 個元素,並調整此最小堆,以此類推至數組的最後一個元素,則最後整個最小堆即爲所求答案。

// 創建堆
function buildHeap(nums) {
  // 注意這裏的頭節點是從0開始的,因此最後一個非葉子結點是 parseInt(nums.length/2)-1
  let start = parseInt(nums.length / 2) - 1;
  let size = nums.length;
  // 從最後一個非葉子結點開始調整,直至堆頂。
  for (let i = start; i >= 0; i--) {
    adjustHeap(nums, i, size);
  }
  return nums;
}
// 調整最小堆,使index的值小於於左右節點
function adjustHeap(nums, index, size) {
  // 交換後可能會破壞堆結構,須要循環使得每個父節點都大於左右結點
  while (true) {
    let min = index;
    let left = index * 2 + 1; // 左節點
    let right = index * 2 + 2; // 右節點
    if (left < size && nums[min] > nums[left]) min = left;
    if (right < size && nums[min] > nums[right]) min = right;
    // 若是左右結點大於當前的結點則交換,並再循環一遍判斷交換後的左右結點位置是否破壞了堆結構(比左右結點小了)
    if (index !== min) {
      [nums[index], nums[min]] = [nums[min], nums[index]];
      index = min;
    } else {
      break;
    }
  }
}
// 獲取最大的前K個數
function getLargeNumber(nums, k) {
  // 建立一個具備 K 的節點的最小堆(能夠先取該數組的前 K 個元素)調整爲最小堆。
  let minHeap = buildHeap(nums.slice(0, k));
  for (let i = k; i < nums.length; i++) {
    // 將第 i 個元素與堆頂對比,若是大於堆頂元素,則說明堆頂元素不是第 K 大的值,所以將堆頂元素替換爲第 i 個元素
    if (minHeap[0] < nums[i]) {
      minHeap[0] = nums[i];
      // 替換後調整此最小堆
      adjustHeap(minHeap, 0, k);
    }
  }
  return minHeap;
}
var nums = [8, 9, 2, 15, 7, 1, 13, 35, 24];

console.log(getLargeNumber(nums, 4)); // [13,15,24,35]
複製代碼

20-判斷一個數是否爲醜數

何爲醜數?醜數就是隻包含質因數 2, 3, 5 的正整數。

試着寫出一個函數,判斷傳入的是否爲醜數?

function isUgly (num) {
  if (typeof +num !== 'number') return false
  if (num < 1) return false;
  // 從大往小除,先從5開始
  while(num % 5 === 0) {
      num /= 5;
  }
  while(num % 3 === 0) {
      num /= 3;
  }
  while(num % 2 === 0) {
      num /= 2;
  }
  return num === 1;
}

// 測試一下
isUgly(18) // true
isUgly(7) // false
複製代碼
相關文章
相關標籤/搜索