「面試必問」leetcode高頻題精選

前端森林發表於2020-10-14

image

引言(文末有福利)?

演算法一直是大廠前端面試常問的一塊,而大家往往準備這方面的面試都是通過leetcode刷題。

我特地整理了幾道leetcode中「很有意思」而且非常「高頻」的演算法題目,分別給出了思路分析(帶圖解)和程式碼實現。

認真仔細的閱讀完本文,相信對於你在演算法方面的面試一定會有不小的幫助!?

兩數之和 ?

題目難度easy,涉及到的演算法知識有陣列、雜湊表

題目描述

給定一個整數陣列 nums  和一個目標值 target,請你在該陣列中找出和為目標值的那兩個整數,並返回他們的陣列下標。

你可以假設每種輸入只會對應一個答案。但是,陣列中同一個元素不能使用兩遍。

示例:

給定 nums = [2, 7, 11, 15], target = 9

因為 nums[0] + nums[1] = 2 + 7 = 9
所以返回 [0, 1]

思路分析

大多數同學看到這道題目,心中肯定會想:這道題目太簡單了,不就兩層遍歷嘛:兩層迴圈來遍歷同一個陣列;第一層迴圈遍歷的值記為a,第二層迴圈時遍歷的值記為b;若a+b = 目標值,那麼ab對應的陣列下標就是我們想要的答案。

這種解法沒毛病,但有沒有優化的方案呢??

要知道兩層迴圈很多情況下都意味著O(n^2) 的複雜度,這個複雜度非常容易導致你的演算法超時。即便沒有超時,在明明有一層遍歷解法的情況下,你寫了兩層遍歷,面試官也會對你的印象分大打折扣。?

其實我們可以在遍歷陣列的過程中,增加一個Map結構來儲存已經遍歷過的數字及其對應的索引值。然後每遍歷到一個新數字的時候,都回到Map裡去查詢targetNum與該數的差值是否已經在前面的數字中出現過了。若出現過,那麼答案已然顯現,我們就不必再往下走了。

我們就以本題中的例子結合圖片來說明一下上面提到的這種思路:

  • 這裡用物件diffs來模擬map結構:

    首先遍歷陣列第一個元素,此時key為 2,value為索引 0

  • 往下遍歷,遇到了 7:

    計算targetNum和 7 的差值為 2,去diffs中檢索 2 這個key,發現是之前出現過的值。那麼本題的答案就出來了!

程式碼實現

/**
 * @param {number[]} nums
 * @param {number} target
 * @return {number[]}
 */
const twoSum = function (nums, target) {
  const diffs = {};
  // 快取陣列長度
  const len = nums.length;
  // 遍歷陣列
  for (let i = 0; i < len; i++) {
    // 判斷當前值對應的 target 差值是否存在
    if (diffs[target - nums[i]] !== undefined) {
      // 若有對應差值,那麼得到答案
      return [diffs[target - nums[i]], i];
    }
    // 若沒有對應差值,則記錄當前值
    diffs[nums[i]] = i;
  }
};

三數之和 ?

題目難度medium,涉及到的演算法知識有陣列、雙指標

題目描述

給你一個包含n個整數的陣列nums,判斷nums中是否存在三個元素abc ,使得a + b + c = 0。請你找出所有滿足條件且不重複的三元組。

注意:答案中不可以包含重複的三元組。

示例:

給定陣列 nums = [-1, 0, 1, 2, -1, -4],

滿足要求的三元組集合為:
[
  [-1, 0, 1],
  [-1, -1, 2]
]

思路分析

和上面的兩數之和一樣,如果不認真思考,最快的方式可能就是多層遍歷了。但有了前車之鑑,我們同樣可以把求和問題變為求差問題:固定其中一個數,在剩下的數中尋找是否有兩個數的和這個固定數相加是等於 0 的。

這裡我們採用雙指標法來解決問題,相比三層迴圈,效率會大大提升。

雙指標法的適用範圍比較廣,一般像求和、比大小的都可以用它來解決。但是有一個前提:陣列必須有序

因此我們的第一步就是先將陣列進行排序:

// 給 nums 排序
nums = nums.sort((a, b) => {
  return a - b;
});

然後對陣列進行遍歷,每遍歷到哪個數字,就固定當前的數字。同時左指標指向該數字後面的緊鄰的那個數字,右指標指向陣列末尾。然後左右指標分別向中間靠攏:

每次指標移動一次位置,就計算一下兩個指標指向數字之和加上固定的那個數之後,是否等於 0。如果是,那麼我們就得到了一個目標組合;否則,分兩種情況來看:

  • 相加之和大於 0,說明右側的數偏大了,右指標左移
  • 相加之和小於 0,說明左側的數偏小了,左指標右移

程式碼實現

/**
 * @param {number[]} nums
 * @return {number[][]}
 */
const threeSum = function (nums) {
  // 用於存放結果陣列
  let res = [];
  // 目標值為0
  let sum = 0;
  // 給 nums 排序
  nums = nums.sort((a, b) => {
    return a - b;
  });
  // 快取陣列長度
  const len = nums.length;
  for (let i = 0; i < len - 2; i++) {
    // 左指標 j
    let j = i + 1;
    // 右指標k
    let k = len - 1;
    // 如果遇到重複的數字,則跳過
    if (i > 0 && nums[i] === nums[i - 1]) {
      continue;
    }
    while (j < k) {
      // 三數之和小於0,左指標前進
      if (nums[i] + nums[j] + nums[k] < 0) {
        j++;
        // 處理左指標元素重複的情況
        while (j < k && nums[j] === nums[j - 1]) {
          j++;
        }
      } else if (nums[i] + nums[j] + nums[k] > 0) {
        // 三數之和大於0,右指標後退
        k--;

        // 處理右指標元素重複的情況
        while (j < k && nums[k] === nums[k + 1]) {
          k--;
        }
      } else {
        // 得到目標數字組合,推入結果陣列
        res.push([nums[i], nums[j], nums[k]]);

        // 左右指標一起前進
        j++;
        k--;

        // 若左指標元素重複,跳過
        while (j < k && nums[j] === nums[j - 1]) {
          j++;
        }

        // 若右指標元素重複,跳過
        while (j < k && nums[k] === nums[k + 1]) {
          k--;
        }
      }
    }
  }

  // 返回結果陣列
  return res;
};

盛最多水的容器 ?

題目難度medium,涉及到的演算法知識有陣列、雙指標

題目描述

給你 n 個非負整數 a1,a2,...,an,每個數代表座標中的一個點  (i, ai) 。在座標內畫 n 條垂直線,垂直線 i  的兩個端點分別為  (i, ai) 和 (i, 0)。找出其中的兩條線,使得它們與  x  軸共同構成的容器可以容納最多的水。

說明:你不能傾斜容器,且  n  的值至少為 2。

圖中垂直線代表輸入陣列[1,8,6,2,5,4,8,3,7]。在此情況下,容器能夠容納水(表示為藍色部分)的最大值為 49。

示例:

輸入:[1,8,6,2,5,4,8,3,7]
輸出:49

思路分析

首先,我們能快速想到的一種方法:兩兩進行求解,計算可以承載的水量。 然後不斷更新最大值,最後返回最大值即可。

這種解法,需要兩層迴圈,時間複雜度是O(n^2)。這種相對來說比較暴力,對應就是暴力法

暴力法

/**
 * @param {number[]} height
 * @return {number}
 */
var maxArea = function (height) {
  let max = 0;
  for (let i = 0; i < height.length - 1; i++) {
    for (let j = i + 1; j < height.length; j++) {
      let area = (j - i) * Math.min(height[i], height[j]);
      max = Math.max(max, area);
    }
  }

  return max;
};

那麼有沒有更好的辦法呢?答案是肯定有。

其實有點類似雙指標的概念,左指標指向下標 0,右指標指向length-1。然後分別從左右兩側向中間移動,每次取小的那個值(因為水的高度肯定是以小的那個為準)。

如果左側小於右側,則i++,否則j--(這一步其實就是取所有高度中比較高的,我們知道面積等於長*寬)。對應就是雙指標 動態滑窗

雙指標 動態滑窗

/**
 * @param {number[]} height
 * @return {number}
 */
var maxArea = function (height) {
  let max = 0;
  let i = 0;
  let j = height.length - 1;
  while (i < j) {
    let minHeight = Math.min(height[i], height[j]);
    let area = (j - i) * minHeight;
    max = Math.max(max, area);
    if (height[i] < height[j]) {
      i++;
    } else {
      j--;
    }
  }
  return max;
};

爬樓梯 ?

題目難度easy,涉及到的演算法知識有斐波那契數列、動態規劃。

題目描述

假設你正在爬樓梯。需要 n  階你才能到達樓頂。

每次你可以爬 1 或 2 個臺階。你有多少種不同的方法可以爬到樓頂呢?

注意:給定 n 是一個正整數。

示例 1:

輸入: 2
輸出: 2
解釋: 有兩種方法可以爬到樓頂。
1.  1 階 + 1 階
2.  2 階

示例 2:

輸入: 3
輸出: 3
解釋: 有三種方法可以爬到樓頂。
1.  1 階 + 1 階 + 1 階
2.  1 階 + 2 階
3.  2 階 + 1 階

思路分析

這道題目是一道非常高頻的面試題目,也是一道非常經典的斐波那契數列型別的題目。

解決本道題目我們會用到動態規劃的演算法思想-可以分成多個子問題,爬第 n 階樓梯的方法數量,等於 2 部分之和:

  • 爬上n−1階樓梯的方法數量。因為再爬 1 階就能到第 n 階
  • 爬上n−2階樓梯的方法數量,因為再爬 2 階就能到第 n 階

可以得到公式:

climbs[n] = climbs[n - 1] + climbs[n - 2];

同時需要做如下初始化:

climbs[0] = 1;
climbs[1] = 1;

程式碼實現

/**
 * @param {number} n
 * @return {number}
 */
var climbStairs = function (n) {
  let climbs = [];
  climbs[0] = 1;
  climbs[1] = 1;
  for (let i = 2; i <= n; i++) {
    climbs[i] = climbs[i - 1] + climbs[i - 2];
  }
  return climbs[n];
};

環形連結串列 ?

題目難度easy,涉及到的演算法知識有連結串列、快慢指標。

題目描述

給定一個連結串列,判斷連結串列中是否有環。

為了表示給定連結串列中的環,我們使用整數 pos 來表示連結串列尾連線到連結串列中的位置(索引從 0 開始)。 如果 pos 是 -1,則在該連結串列中沒有環。

示例 1:

輸入:head = [3,2,0,-4], pos = 1
輸出:true
解釋:連結串列中有一個環,其尾部連線到第二個節點。

示例 2:

輸入:head = [1,2], pos = 0
輸出:true
解釋:連結串列中有一個環,其尾部連線到第一個節點。

示例 3:

輸入:head = [1], pos = -1
輸出:false
解釋:連結串列中沒有環。

思路分析

連結串列成環問題也是非常經典的演算法問題,在面試中也經常會遇到。

解決這種問題一般有常見的兩種方法:標誌法快慢指標法

標誌法

給每個已遍歷過的節點加標誌位,遍歷連結串列,當出現下一個節點已被標誌時,則證明單連結串列有環。

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function (head) {
  while (head) {
    if (head.flag) return true;
    head.flag = true;
    head = head.next;
  }
  return false;
};

快慢指標(雙指標法)

設定快慢兩個指標,遍歷單連結串列,快指標一次走兩步,慢指標一次走一步,如果單連結串列中存在環,則快慢指標終會指向同一個節點,否則直到快指標指向null時,快慢指標都不可能相遇。

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {boolean}
 */
var hasCycle = function (head) {
  if (!head || !head.next) {
    return false;
  }
  let slow = head,
    fast = head.next;
  while (slow !== fast) {
    if (!fast || !fast.next) return false;
    fast = fast.next.next;
    slow = slow.next;
  }
  return true;
};

有效的括號 ?

題目難度easy,涉及到的演算法知識有棧、雜湊表。

題目描述

給定一個只包括'('')''{''}''['']'  的字串,判斷字串是否有效。

有效字串需滿足:

1、左括號必須用相同型別的右括號閉合。
2、左括號必須以正確的順序閉合。

注意空字串可被認為是有效字串。

示例 1:

輸入: "()";
輸出: true;

示例  2:

輸入: "()[]{}";
輸出: true;

示例  3:

輸入: "(]";
輸出: false;

示例  4:

輸入: "([)]";
輸出: false;

示例  5:

輸入: "{[]}";
輸出: true;

思路分析

這道題可以利用結構。

思路大概是:遇到左括號,一律推入棧中,遇到右括號,將棧頂部元素拿出,如果不匹配則返回 false,如果匹配則繼續迴圈。

第一種解法是利用switch case

switch case

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function (s) {
  let arr = [];
  let len = s.length;
  if (len % 2 !== 0) return false;
  for (let i = 0; i < len; i++) {
    let letter = s[i];
    switch (letter) {
      case "(": {
        arr.push(letter);
        break;
      }
      case "{": {
        arr.push(letter);
        break;
      }
      case "[": {
        arr.push(letter);
        break;
      }
      case ")": {
        if (arr.pop() !== "(") return false;
        break;
      }
      case "}": {
        if (arr.pop() !== "{") return false;
        break;
      }
      case "]": {
        if (arr.pop() !== "[") return false;
        break;
      }
    }
  }
  return !arr.length;
};

第二種是維護一個map物件:

雜湊表map

/**
 * @param {string} s
 * @return {boolean}
 */
var isValid = function (s) {
  let map = {
    "(": ")",
    "{": "}",
    "[": "]",
  };
  let stack = [];
  let len = s.length;
  if (len % 2 !== 0) return false;
  for (let i of s) {
    if (i in map) {
      stack.push(i);
    } else {
      if (i !== map[stack.pop()]) return false;
    }
  }
  return !stack.length;
};

滑動視窗最大值 ⛵

題目難度hard,涉及到的演算法知識有雙端佇列。

題目描述

給定一個陣列 nums,有一個大小為  k  的滑動視窗從陣列的最左側移動到陣列的最右側。你只可以看到在滑動視窗內的 k  個數字。滑動視窗每次只向右移動一位。

返回滑動視窗中的最大值。

進階:你能線上性時間複雜度內解決此題嗎?

示例:

輸入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3
輸出: [3,3,5,5,6,7]
解釋:

  滑動視窗的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

提示:

  • 1 <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4
  • 1 <= k <= nums.length

思路分析

暴力求解

第一種方法,比較簡單。也是大多數同學很快就能想到的方法。

  • 遍歷陣列
  • 依次遍歷每個區間內的最大值,放入陣列中
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var maxSlidingWindow = function (nums, k) {
  let len = nums.length;
  if (len === 0) return [];
  if (k === 1) return nums;
  let resArr = [];
  for (let i = 0; i <= len - k; i++) {
    let max = Number.MIN_SAFE_INTEGER;
    for (let j = i; j < i + k; j++) {
      max = Math.max(max, nums[j]);
    }
    resArr.push(max);
  }
  return resArr;
};

雙端佇列

這道題還可以用雙端佇列去解決,核心在於在視窗發生移動時,只根據發生變化的元素對最大值進行更新。

結合上面動圖(圖片來源)我們梳理下思路:

  • 檢查隊尾元素,看是不是都滿足大於等於當前元素的條件。如果是的話,直接將當前元素入隊。否則,將隊尾元素逐個出隊、直到隊尾元素大於等於當前元素為止。(這一步是為了維持佇列的遞減性:確保隊頭元素是當前滑動視窗的最大值。這樣我們每次取最大值時,直接取隊頭元素即可。)
  • 將當前元素入隊
  • 檢查隊頭元素,看隊頭元素是否已經被排除在滑動視窗的範圍之外了。如果是,則將隊頭元素出隊。(這一步是維持佇列的有效性:確保佇列裡所有的元素都在滑動視窗圈定的範圍以內。)
  • 排除掉滑動視窗還沒有初始化完成、第一個最大值還沒有出現的特殊情況。
/**
 * @param {number[]} nums
 * @param {number} k
 * @return {number[]}
 */
var maxSlidingWindow = function (nums, k) {
  // 快取陣列的長度
  const len = nums.length;
  const res = [];
  const deque = [];
  for (let i = 0; i < len; i++) {
    // 隊尾元素小於當前元素
    while (deque.length && nums[deque[deque.length - 1]] < nums[i]) {
      deque.pop();
    }
    deque.push(i);

    // 當隊頭元素的索引已經被排除在滑動視窗之外時
    while (deque.length && deque[0] <= i - k) {
      // 隊頭元素出對
      deque.shift();
    }
    if (i >= k - 1) {
      res.push(nums[deque[0]]);
    }
  }
  return res;
};

每日溫度 ?

題目難度medium,涉及到的演算法知識有棧。

題目描述

根據每日氣溫列表,請重新生成一個列表,對應位置的輸出是需要再等待多久溫度才會升高超過該日的天數。如果之後都不會升高,請在該位置用  0 來代替。

例如,給定一個列表  temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的輸出應該是  [1, 1, 4, 2, 1, 1, 0, 0]。

提示:氣溫列表長度的範圍是  [1, 30000]。每個氣溫的值的均為華氏度,都是在  [30, 100]  範圍內的整數。

思路分析

看到這道題,大家很容易就會想到暴力遍歷法:直接兩層遍歷,第一層定位一個溫度,第二層定位離這個溫度最近的一次升溫是哪天,然後求出兩個溫度對應索引的差值即可。

然而這種解法需要兩層遍歷,時間複雜度是O(n^2),顯然不是最優解法。

本道題目可以採用棧去做一個優化。

大概思路就是:維護一個遞減棧。當遍歷過的溫度,維持的是一個單調遞減的態勢時,我們就對這些溫度的索引下標執行入棧操作;只要出現了一個數字,它打破了這種單調遞減的趨勢,也就是說它比前一個溫度值高,這時我們就對前後兩個溫度的索引下標求差,得出前一個溫度距離第一次升溫的目標差值。

程式碼實現

/**
 * @param {number[]} T
 * @return {number[]}
 */
var dailyTemperatures = function (T) {
  const len = T.length;
  const stack = [];
  const res = new Array(len).fill(0);
  for (let i = 0; i < len; i++) {
    while (stack.length && T[i] > T[stack[stack.length - 1]]) {
      const top = stack.pop();
      res[top] = i - top;
    }
    stack.push(i);
  }
  return res;
};

括號生成 ?

題目難度medium,涉及到的演算法知識有遞迴、回溯。

題目描述

數字 n 代表生成括號的對數,請你設計一個函式,用於能夠生成所有可能的並且 有效的 括號組合。

示例:

輸入:n = 3
輸出:[
       "((()))",
       "(()())",
       "(())()",
       "()(())",
       "()()()"
     ]

思路分析

這道題目通過遞迴去實現。

因為左右括號需要匹配、閉合。所以對應“(”和“)”的數量都是n,當滿足這個條件時,一次遞迴就結束,將對應值放入結果陣列中。

這裡有一個潛在的限制條件:有效的括號組合。對應邏輯就是在往每個位置去放入“(”或“)”前:

  • 需要判斷“(”的數量是否小於 n
  • “)”的數量是否小於“(”

程式碼實現

/**
 * @param {number} n
 * @return {string[]}
 */
var generateParenthesis = function (n) {
  let res = [];
  const generate = (cur, left, right) => {
    if (left === n && right === n) {
      res.push(cur);
      return;
    }
    if (left < n) {
      generate(cur + "(", left + 1, right);
    }
    if (right < left) {
      generate(cur + ")", left, right + 1);
    }
  };
  generate("", 0, 0);
  return res;
};

電話號碼的字母組合 ?

題目難度medium,涉及到的演算法知識有遞迴、回溯。

題目描述

給定一個僅包含數字 2-9 的字串,返回所有它能表示的字母組合。

給出數字到字母的對映如下(與電話按鍵相同)。注意 1 不對應任何字母。

示例:

輸入:"23"
輸出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].

思路分析

首先用一個物件map儲存數字與字母的對映關係,接下來遍歷對應的字串,第一次將字串存在結果陣列result中,第二次及以後的就雙層遍歷生成新的字串陣列。

程式碼實現

雜湊對映 逐層遍歷

/**
 * @param {string} digits
 * @return {string[]}
 */
var letterCombinations = function (digits) {
  let res = [];
  if (digits.length === 0) return [];
  let map = {
    2: "abc",
    3: "def",
    4: "ghi",
    5: "jkl",
    6: "mno",
    7: "pqrs",
    8: "tuv",
    9: "wxyz",
  };
  for (let num of digits) {
    let chars = map[num];
    if (res.length > 0) {
      let temp = [];
      for (let char of chars) {
        for (let oldStr of res) {
          temp.push(oldStr + char);
        }
      }
      res = temp;
    } else {
      res.push(...chars);
    }
  }
  return res;
};

遞迴

/**
 * @param {string} digits
 * @return {string[]}
 */
var letterCombinations = function (digits) {
  let res = [];
  if (!digits) return [];
  let map = {
    2: "abc",
    3: "def",
    4: "ghi",
    5: "jkl",
    6: "mno",
    7: "pqrs",
    8: "tuv",
    9: "wxyz",
  };
  function generate(i, str) {
    let len = digits.length;
    if (i === len) {
      res.push(str);
      return;
    }
    let chars = map[digits[i]];
    for (let j = 0; j < chars.length; j++) {
      generate(i + 1, str + chars[j]);
    }
  }
  generate(0, "");
  return res;
};

島嶼數量 ?

題目難度medium,涉及到的演算法知識有 DFS(深度優先搜尋)。

題目描述

給你一個由  '1'(陸地)和 '0'(水)組成的的二維網格,請你計算網格中島嶼的數量。

島嶼總是被水包圍,並且每座島嶼只能由水平方向或豎直方向上相鄰的陸地連線形成。

此外,你可以假設該網格的四條邊均被水包圍。

示例 1:

輸入: 11110;
11010;
11000;
00000;
輸出: 1;

示例  2:

輸入:
11000
11000
00100
00011
輸出: 3
解釋: 每座島嶼只能由水平和/或豎直方向上相鄰的陸地連線而成。

思路分析

如上圖,我們需要計算的就是圖中相連(只能是水平和/或豎直方向上相鄰)的綠色島嶼的數量。

這道題目一個經典的做法是沉島,大致思路是:採用DFS(深度優先搜尋),遇到 1 的就將當前的 1 變為 0,並將當前座標的上下左右都執行 dfs,並計數。

終止條件是:超出二維陣列的邊界或者是遇到 0 ,直接返回。

程式碼實現

/**
 * @param {character[][]} grid
 * @return {number}
 */
var numIslands = function (grid) {
  const rows = grid.length;
  if (rows === 0) return 0;
  const cols = grid[0].length;
  let res = 0;
  for (let i = 0; i < rows; i++) {
    for (let j = 0; j < cols; j++) {
      if (grid[i][j] === "1") {
        helper(grid, i, j, rows, cols);
        res++;
      }
    }
  }
  return res;
};
function helper(grid, i, j, rows, cols) {
  if (i < 0 || j < 0 || i > rows - 1 || j > cols - 1 || grid[i][j] === "0")
    return;

  grid[i][j] = "0";

  helper(grid, i + 1, j, rows, cols);
  helper(grid, i, j + 1, rows, cols);
  helper(grid, i - 1, j, rows, cols);
  helper(grid, i, j - 1, rows, cols);
}

分發餅乾 ?

題目難度easy,涉及到的演算法知識有貪心演算法。

題目描述

假設你是一位很棒的家長,想要給你的孩子們一些小餅乾。但是,每個孩子最多隻能給一塊餅乾。對每個孩子 i ,都有一個胃口值  gi ,這是能讓孩子們滿足胃口的餅乾的最小尺寸;並且每塊餅乾 j ,都有一個尺寸 sj 。如果 sj >= gi ,我們可以將這個餅乾 j 分配給孩子 i ,這個孩子會得到滿足。你的目標是儘可能滿足越多數量的孩子,並輸出這個最大數值。

注意:

你可以假設胃口值為正。
一個小朋友最多隻能擁有一塊餅乾。

示例  1:

輸入: [1,2,3], [1,1]

輸出: 1

解釋:
你有三個孩子和兩塊小餅乾,3個孩子的胃口值分別是:1,2,3。
雖然你有兩塊小餅乾,由於他們的尺寸都是1,你只能讓胃口值是1的孩子滿足。
所以你應該輸出1。

示例  2:

輸入: [1,2], [1,2,3]

輸出: 2

解釋:
你有兩個孩子和三塊小餅乾,2個孩子的胃口值分別是1,2。
你擁有的餅乾數量和尺寸都足以讓所有孩子滿足。
所以你應該輸出2.

思路分析

這道題目是一道典型的貪心演算法類。解題思路大概如下:

  • 優先滿足胃口小的小朋友的需求
  • 設最大可滿足的孩子數量為maxNum = 0
  • 胃口小的拿小的,胃口大的拿大的
  • 兩邊升序,然後一一對比

    • 餅乾j >= 胃口i 時,i++j++maxNum++
    • 餅乾j < 胃口i時,說明餅乾不夠吃,換更大的,j++
  • 到邊界後停止

程式碼實現

/**
 * @param {number[]} g
 * @param {number[]} s
 * @return {number}
 */
var findContentChildren = function (g, s) {
  g = g.sort((a, b) => a - b);
  s = s.sort((a, b) => a - b);
  let gLen = g.length,
    sLen = s.length,
    i = 0,
    j = 0,
    maxNum = 0;
  while (i < gLen && j < sLen) {
    if (s[j] >= g[i]) {
      i++;
      maxNum++;
    }
    j++;
  }
  return maxNum;
};

買賣股票的最佳時機 II ?

題目難度easy,涉及到的演算法知識有動態規劃、貪心演算法。

題目描述

給定一個陣列,它的第  i 個元素是一支給定股票第 i 天的價格。

設計一個演算法來計算你所能獲取的最大利潤。你可以儘可能地完成更多的交易(多次買賣一支股票)。

注意:你不能同時參與多筆交易(你必須在再次購買前出售掉之前的股票)。

示例 1:

輸入: [7,1,5,3,6,4]
輸出: 7
解釋: 在第 2 天(股票價格 = 1)的時候買入,在第 3 天(股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5-1 = 4 。
     隨後,在第 4 天(股票價格 = 3)的時候買入,在第 5 天(股票價格 = 6)的時候賣出, 這筆交易所能獲得利潤 = 6-3 = 3 。

示例 2:

輸入: [1,2,3,4,5]
輸出: 4
解釋: 在第 1 天(股票價格 = 1)的時候買入,在第 5 天 (股票價格 = 5)的時候賣出, 這筆交易所能獲得利潤 = 5-1 = 4 。
     注意你不能在第 1 天和第 2 天接連購買股票,之後再將它們賣出。
     因為這樣屬於同時參與了多筆交易,你必須在再次購買前出售掉之前的股票。

示例  3:

輸入: [7,6,4,3,1]
輸出: 0
解釋: 在這種情況下, 沒有交易完成, 所以最大利潤為 0。

提示:

  • 1 <= prices.length <= 3 * 10 ^ 4
  • 0 <= prices[i] <= 10 ^ 4

思路分析

其實這道題目思路也比較簡單:

  • 維護一個變數profit用來儲存利潤
  • 因為可以多次買賣,那麼就要後面的價格比前面的大,那麼就可以進行買賣
  • 因此,只要prices[i+1] > prices[i],那麼就去疊加profit
  • 遍歷完成得到的profit就是獲取的最大利潤

程式碼實現

/**
 * @param {number[]} prices
 * @return {number}
 */
var maxProfit = function (prices) {
  let profit = 0;
  for (let i = 0; i < prices.length - 1; i++) {
    if (prices[i + 1] > prices[i]) profit += prices[i + 1] - prices[i];
  }
  return profit;
};

不同路徑 ?

題目難度medium,涉及到的演算法知識有動態規劃。

題目描述

一個機器人位於一個 m x n 網格的左上角 (起始點在下圖中標記為“Start” )。

機器人每次只能向下或者向右移動一步。機器人試圖達到網格的右下角(在下圖中標記為“Finish”)。

問總共有多少條不同的路徑?

例如,上圖是一個 7 x 3 的網格。有多少可能的路徑?

示例  1:

輸入: m = 3, n = 2
輸出: 3
解釋:
從左上角開始,總共有 3 條路徑可以到達右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右

示例  2:

輸入: (m = 7), (n = 3);
輸出: 28;

思路分析

由題可知:機器人只能向右或向下移動一步,那麼從左上角到右下角的走法 = 從右邊開始走的路徑總數+從下邊開始走的路徑總數。

所以可推出動態方程為:dp[i][j] = dp[i-1][j]+dp[i][j-1]

程式碼實現

這裡採用Array(m).fill(Array(n).fill(1))進行了初始化,因為每一格至少有一種走法。
/**
 * @param {number} m
 * @param {number} n
 * @return {number}
 */
var uniquePaths = function (m, n) {
  let dp = Array(m).fill(Array(n).fill(1));
  for (let i = 1; i < m; i++) {
    for (let j = 1; j < n; j++) {
      dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
    }
  }
  return dp[m - 1][n - 1];
};

零錢兌換 ?

題目難度medium,涉及到的演算法知識有動態規劃。

題目描述

給定不同面額的硬幣 coins 和一個總金額 amount。編寫一個函式來計算可以湊成總金額所需的最少的硬幣個數。如果沒有任何一種硬幣組合能組成總金額,返回  -1。

示例  1:

輸入: (coins = [1, 2, 5]), (amount = 11);
輸出: 3;
解釋: 11 = 5 + 5 + 1;

示例 2:

輸入: (coins = [2]), (amount = 3);
輸出: -1;

說明:
你可以認為每種硬幣的數量是無限的。

思路分析

這道題目我們同樣採用動態規劃來解決。

假設給出的不同面額的硬幣是[1, 2, 5],目標是 60,問最少需要的硬幣個數?

我們需要先分解子問題,分層級找最優子結構。

dp[i]: 表示總金額為 i 的時候最優解法的硬幣數

我們想一下:求總金額 60 有幾種方法?一共有 3 種方式,因為我們有 3 種不同面值的硬幣。

  • 拿一枚面值為 1 的硬幣 + 總金額為 59 的最優解法的硬幣數量。即:dp[59] + 1
  • 拿一枚面值為 2 的硬幣 + 總金額為 58 的最優解法的硬幣數。即:dp[58] + 1
  • 拿一枚面值為 5 的硬幣 + 總金額為 55 的最優解法的硬幣數。即:dp[55] + 1

所以,總金額為 60 的最優解法就是上面這三種解法中最優的一種,也就是硬幣數最少的一種,我們下面用程式碼來表示一下:

dp[60] = Math.min(dp[59] + 1, dp[58] + 1, dp[55] + 1);

推匯出狀態轉移方程

dp[i] = Math.min(dp[i - coin] + 1, dp[i - coin] + 1, ...)
其中 coin 有多少種可能,我們就需要比較多少次,遍歷 coins 陣列,分別去對比即可

程式碼實現

/**
 * @param {number[]} coins
 * @param {number} amount
 * @return {number}
 */
var coinChange = function (coins, amount) {
  let dp = new Array(amount + 1).fill(Infinity);
  dp[0] = 0;
  for (let i = 0; i <= amount; i++) {
    for (let coin of coins) {
      if (i - coin >= 0) {
        dp[i] = Math.min(dp[i], dp[i - coin] + 1);
      }
    }
  }
  return dp[amount] === Infinity ? -1 : dp[amount];
};

福利

大多數前端同學對於演算法的系統學習,其實是比較茫然的,這裡我整理了一張思維導圖,算是比較全面的概括了前端演算法體系。

另外我還維護了一個github倉庫:https://github.com/Cosen95/js_algorithm,裡面包含了大量的leetcode題解,並且還在不斷更新中,感覺不錯的給個star哈!?

❤️ 愛心三連擊

1.如果覺得這篇文章還不錯,來個分享、點贊、在看三連吧,讓更多的人也看到~

2.關注公眾號前端森林,定期為你推送新鮮乾貨好文。

3.特殊階段,帶好口罩,做好個人防護。

相關文章