引言(文末有福利)?
演算法一直是大廠前端面試常問的一塊,而大家往往準備這方面的面試都是通過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 = 目標值
,那麼a
和b
對應的陣列下標就是我們想要的答案。
這種解法沒毛病,但有沒有優化的方案呢??
要知道兩層迴圈很多情況下都意味著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
中是否存在三個元素a
,b
,c
,使得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.特殊階段,帶好口罩,做好個人防護。