【算法面試】leetcode最多見的150道前端面試題 --- 中等題1(共80題)

兄弟姐妹們,中等題來了,本篇10道,剩下70道,每週更新10道!javascript

以前簡單題的連接以下:前端

2. 兩數相加

給你兩個 非空 的鏈表,表示兩個非負的整數。它們每位數字都是按照 逆序 的方式存儲的,而且每一個節點只能存儲 一位 數字。java

請你將兩個數相加,並以相同形式返回一個表示和的鏈表。git

你能夠假設除了數字 0 以外,這兩個數都不會以 0 開頭。面試

 

示例 1:算法

image.png

輸入:l1 = [2,4,3], l2 = [5,6,4]
輸出:[7,0,8]
解釋:342 + 465 = 807.
複製代碼

示例 2:編程

輸入:l1 = [0], l2 = [0]
輸出:[0]
複製代碼

示例 3:數組

輸入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
輸出:[8,9,9,9,0,0,0,1]
複製代碼

這個兩個數相加就跟咱們以前簡單題有一道叫作:加1,算法過程幾乎是如出一轍的.markdown

不過須要注意:作有關鏈表的題,有個隱藏技巧:添加一個虛擬頭結點(哨兵節點),幫助簡化邊界狀況的判斷 具體思路。ide

思路: 從最低位至最高位,逐位相加,若是和大於等於 10,則保留個位數字,同時向前一位進 1 若是最高位有進位,則需在最前面補 1。

var addTwoNumbers = function(l1, l2) {
    let carry= 0;
    let pre = point =  new ListNode();
    while(l1 || l2){
        point.next = new ListNode();
        point = point.next;
        let sum = 0;
        if(l1){
            sum += l1.val;
            l1 = l1.next;
        }
        if(l2){
            sum += l2.val;
            l2 = l2.next;
        }
        sum = sum + carry;
        point.val = sum % 10;
        carry = (sum / 10) | 0;
    }
    if(carry) point.next = new ListNode(carry);
    return pre.next;
};
複製代碼

3. 無重複字符的最長子串

題目以下: 給定一個字符串 s ,請你找出其中不含有重複字符的 最長子串 的長度。

示例 1:

輸入: s = "abcabcbb"
輸出: 3 
解釋: 由於無重複字符的最長子串是 "abc",因此其長度爲 3。
複製代碼

示例 2:

輸入: s = "bbbbb"
輸出: 1
解釋: 由於無重複字符的最長子串是 "b",因此其長度爲 1複製代碼

示例 3:

輸入: s = "pwwkew"
輸出: 3
解釋: 由於無重複字符的最長子串是 "wke",因此其長度爲 3。
     請注意,你的答案必須是 子串 的長度,"pwke" 是一個子序列,不是子串。
複製代碼

這個題是典型的滑動串口類的題目,咱們舉例來講明什麼是滑動窗口,

以下:好比字符串abcabcbb ,假設咱們已經到abc,此時下一個是a也就是abca,咱們以前維護了一個不重複的字符串序列是abc,如今接着又出現a了,說明不重複序列須要從新組織了,就編程了abca把第一個a刪掉,成爲bca,而後繼續向前遍歷,按照咱們剛纔說的規律去維護不重複的字符串序列,最後看這些序列誰最長。

┌─┐
  │a│b c a b c b b   # max = 0 , arr.length =1   取最大得:  max = 1  
  └─┘

  ┌───┐
  │a b│c a b c b b   # max = 1 , arr.length =2   取最大得:  max = 2
  └───┘

  ┌─────┐
  │a b c│a b c b b   # max = 2 , arr.length =3   取最大得:  max = 3
  └─────┘

    ┌─────┐
   a│b c a│b c b b   # max = 3 , arr.length =1   取最大得:  max = 3
    └─────┘

      ┌─────┐
   a b│c a b│c b b   # max = 3 , arr.length =1   取最大得:  max = 3
      └─────┘

        ┌─────┐
   a b c│a b c│b b    # max = 3 , arr.length =1   取最大得:  max = 3
        └─────┘

            ┌───┐
   a b c a b│c b│b    # max = 3 , arr.length =1   取最大得:  max = 3
            └───┘

                ┌─┐
   a b c a b c b│b│   # max = 3 , arr.length =1   取最大得:  max = 3
複製代碼

圖解pwwabw

┌─┐
 │p│w w a b w
 └─┘

 ┌───┐
 │p w│w a b w
 └───┘

     ┌─┐
  p w│w│a b w
     └─┘

     ┌───┐
  p w│w a│b w
     └───┘

     ┌─────┐
  p w│w a b│w
     └─────┘

       ┌─────┐
  p w w│a b w│
       └─────┘
複製代碼

因此咱們的代碼就出來了,解法有不少,我這個不是最優解,可是容易理解:

var lengthOfLongestSubstring = function(s) {
    if(s.length === 0) return 0;
    const map = {};
    // 這個指針就是指向最新維護不重複序列的最開始字母的下標
    let start = 0;
    let ret = 0;
    for(let i = 0; i < s.length; i++){
        // 若是map出現了相同的字母,而且以前出現的字母的下標大於等於不重複序列最開始的下標就更新下標
        // 這個是最難理解的地方,我也是想了一段時間才理解的,剛開始不理解不要緊
        if(map[s[i]] !== undefined && map[s[i]] >= start){
            start = map[s[i]] + 1
        }
        map[s[i]] = i;
        // 每次都更新結果,結果就會當前的下標減去最新的不重複序列的下標
        // +1是由於求長度,好比3到4的長度是2,就是4 - 3 + 1 = 2
        ret = Math.max(ret, i - start + 1)
    }
    return ret
};
複製代碼

在排序數組中查找元素的第一個和最後一個位置

題目以下:

給定一個按照升序排列的整數數組 nums,和一個目標值 target。找出給定目標值在數組中的開始位置和結束位置。

若是數組中不存在目標值 target,返回 [-1, -1]。

進階:

你能夠設計並實現時間複雜度爲 O(log n) 的算法解決此問題嗎?  

示例 1:

輸入:nums = [5,7,7,8,8,10], target = 8
輸出:[3,4]
複製代碼

示例 2:

輸入:nums = [5,7,7,8,8,10], target = 6
輸出:[-1,-1]
複製代碼

示例 3:

輸入:nums = [], target = 0
輸出:[-1,-1]
複製代碼

這道題,咱們可使用暴力解法:

  • 利用數組有序的特色從頭至尾遍歷一次數組
  • 在遍歷的時候,用一個數組記錄等於target的下標,最後取數組第一個和最後一個值

可是題目說如何在 O(log n) 的時間複雜度解決問題,這就不得不換個解法了,咱們採用二分法去解決這個題

在解決這個題以前咱們須要解決一個問題

  • 如何用2分法找到目標值target最左邊的值,好比
輸入:nums = [5,7,7,8,8,10]
複製代碼

如何找到最左邊的7,

咱們須要考慮3種狀況

    1. 若是target是4,也就是在左邊界5左邊,不在數組中
    1. 若是target是12,也就是在有邊界的右邊,不在數組中
    1. 若是target在數組中,好比target = 8

這3種狀況,咱們介紹一種方法,就是二分法一直二分,最後會有一規律,

  • 若是找數組中沒有的元素而且小於數組最左邊的元素,會返回數組下標0

  • 若是找數組中沒有的元素而且大於數組最右邊的元素,會返回數組長度-1(也就是最後一個元素的下標)

  • 若是找數組中有的元素,那麼會返回相同元素最左邊的元素

const findLeftBoundary = (nums, target) => {
    let left = 0;
    let right = nums.length - 1;
    while(left <= right){
         let mid = Math.floor((left + right) / 2);
        if(nums[mid] >= target){
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    return left;
}

findLeftBoundary([5,7,7,8,8,10], 4) // 0
findLeftBoundary([5,7,7,8,8,10], 7)  // 1
findLeftBoundary([5,7,7,8,8,10], 12) // 6
複製代碼

意思是這種一直二分的函數,會讓咱們找到左邊界,例如上面

  • target = 4,由於數組裏沒有4,因此左邊界就是最左邊的元素的下標
  • target = 7,由於數組裏有7,因此左邊界就是7在數組最左邊元素的下標
  • target = 12,由於數組沒有6,因此數組最右邊的下標

上面的函數還有一個功能就是找右邊界,也就是指

  • 若是找9,會返回下標5,由於10離9右邊最近
  • 若是找7.5,會返回下標3,由於8離7.5右邊最近
var searchRange = function (nums, target) {
      const findLeft = (nums, target) => {
        let left = 0;
        let right = nums.length - 1;
        while (left <= right) {
          let mid = Math.floor((left + right) / 2);
          if (nums[mid] >= target) {
            right = mid - 1;
          } else {
            left = mid + 1;
          }
        }
        return left;
      }
      if (nums[findLeft(nums, target)] !== target)
        return [-1, -1]
      else
        return [findLeft(nums, target), findLeft(nums, target + 1) - 1]
    };
複製代碼

5. 最長迴文子串

下面是一道動態規劃的題(也有其餘解法):

給你一個字符串 s,找到 s 中最長的迴文子串。

 

示例 1:

輸入: s = "babad"
輸出: "bab"
解釋: "aba" 一樣是符合題意的答案。
複製代碼

示例 2:

輸入: s = "cbbd"
輸出: "bb"
複製代碼

示例 3:

輸入: s = "a"
輸出: "a"
複製代碼

示例 4:

輸入: s = "ac"
輸出: "a"
複製代碼

思路:

    1. 肯定DP數組和下標的含義:dp[i][j] 表示 區間範圍 [i,j](左閉右閉)的字串是不是迴文串,若是是,則 dp[i][j]true;反之,爲 false
    1. 肯定遞推公式:
    • 若是 s[i] != s[j]dp[i][j]false
    • 若是 s[i] == s[j],則有三種狀況:
    • 當 下標i與 下標 j 相同,則 s[i]s[j] 是同一個字符,例如 a,這是迴文串
    • 當 下標i 與 下標 j 相差爲 1,例如 aa,也是迴文串
    • 當 下標i 與 下標 j 相差大於 1 時,例如 abcba,這時候就看bcb 是不是迴文串,bcb 的區間是 [i + 1, j - 1]
    • 若是 dp[i][j] 是迴文串,而且長度大於結果長度:咱們就更新結果
const longestPalindrome = function (s) {
  let result = s[0];
  const dp = [];
  for (let i = 0; i < s.length; i++) {
    dp[i] = [];
    for (let j = 0; j <= i; j++) {
      if (i - j === 0) {
        dp[i][j] = true;
      } else if (i - j === 1 && s[i] === s[j]) {
        dp[i][j] = true;
      } else if (s[i] === s[j] && dp[i - 1][j + 1]) dp[i][j] = true;
      if (dp[i][j] && i - j + 1 > result.length) {
        result = s.slice(j, i + 1);
      }
    }
  }
  return result;
};
複製代碼

盛最多水的容器

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

說明:你不能傾斜容器。

示例 1:

image.png

輸入:[1,8,6,2,5,4,8,3,7]
輸出:49 
解釋:圖中垂直線表明輸入數組 [1,8,6,2,5,4,8,3,7]。在此狀況下,容器可以容納水(表示爲藍色部分)的最大值爲 49複製代碼

示例 2:

輸入:height = [1,1]
輸出:1
複製代碼

示例 3:

輸入:height = [4,3,2,1,4]
輸出:16
複製代碼

思路:

  • 根據面積計算規則,面積是由兩個柱子的距離和柱子最低高度決定的。

  • 一開始先後指針指向第一根柱子和最後一根柱子,計算這兩根柱子的面積,此時他們距離是最大的。

  • 後面的柱子水平距離確定小於第一根柱子和最後一根柱子的距離,因此只有在高度上,兩根柱子更高才有機會比以前的大),再從新計算面積,並和前面的比較,取最大值

var maxArea = function(height) {
    let left = 0;
    let right = height.length - 1;
    let result = 0;
    while(left < right) {
        if(height[left] <= height[right]){
            result = Math.max(height[left] * (right - left), result);
            left++
        } else {
            result = Math.max(height[right] * (right - left), result);
            right--
        }
    }
    return result;
};
複製代碼

三數之和

給你一個包含 n 個整數的數組 nums,判斷 nums 中是否存在三個元素 a,b,c ,使得 a + b + c = 0 ?請你找出全部和爲 0 且不重複的三元組。

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

 

示例 1:

輸入:nums = [-1,0,1,2,-1,-4]
輸出:[[-1,-1,2],[-1,0,1]]
複製代碼

示例 2:

輸入:nums = []
輸出:[]
複製代碼

示例 3:

輸入:nums = [0]
輸出:[]
複製代碼

思路:排序 + 雙指針

  1. 排序

爲何要排序呢?我想是否是這樣:

  • 排序後相同的數會挨在一塊兒,對於去除重複的數字有幫助,好比說[-1,-1,0,2]其中兩個-1和0,2都能3數字之和爲0,咱們排序以後左邊相同的就直接忽略掉了。
  1. 遍歷
  • 使用三個指針 i、j 和 k 分別表明要找的三個數。

  • 經過枚舉 i 肯定第一個數,另外兩個指針 j,k 分別從左邊 i + 1 和右邊 n - 1 往中間移動,找到知足 nums[i] + nums[j] + nums[k] == 0 的全部組合。

  • jk 指針的移動邏輯,分狀況討論 sum = nums[i] + nums[j] + nums[k]

  • sum > 0:k 左移,使 sum 變小

  • sum < 0:j 右移,使 sum 變大

  • sum = 0:找到符合要求的答案,存起來

const threeSum = (nums) => {
    nums.sort((a, b) => a-b);
    const res = [];
        if(nums == null || nums.length< 3){
        return [];
    }
    for(let i = 0; i < nums.length - 2; i++){
        const curr = nums[i];
        if(curr > 0) break;
        if(i - 1 >= 0 && curr === nums[i-1]) continue;
        let left = i+1;
        let right = nums.length -1;
        while(left < right){
            let l = nums[left];let r = nums[right];
            if(curr + nums[left] + nums[right] === 0){
                res.push([curr, nums[left], nums[right]]);
                while(left < right && nums[left] === l) left++;
                while(left < right && nums[right] === r) right--;
            } else if (curr + nums[left] + nums[right] > 0){
                right--;
            } else {
                left++;
            }
        }
    }
    return res;
};
複製代碼

17 電話號碼的字母組合

給定一個僅包含數字 2-9 的字符串,返回全部它能表示的字母組合。答案能夠按 任意順序 返回。

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

image.png

示例 1:

輸入:digits = "23"
輸出:["ad","ae","af","bd","be","bf","cd","ce","cf"]
複製代碼

示例 2:

輸入:digits = ""
輸出:[]
複製代碼

示例 3:

輸入:digits = "2"
輸出:["a","b","c"]
複製代碼

思路: 這是一類叫全排列的算法類型,試着去理解解題的過程,好比

  • 當給定了輸入字符串,好比:"23",那麼整棵樹就構建完成了,以下:

image.png

var letterCombinations = function(digits) {
  if (digits.length == 0) return [];
   const res = [];
   const map = { '2': 'abc', '3': 'def', '4': 'ghi', '5': 'jkl', '6': 'mno', '7': 'pqrs', '8': 'tuv', '9': 'wxyz' };
     function dfs(str, deep){
       if(str.length === digits.length){
        res.push(str);
        return;
     }
     let curr= map[digits[deep]];
     for(let i =0; i < curr.length; i++){
      dfs(str + curr[i], deep + 1)
   }
  }
   dfs('',0)
   return res
 };
複製代碼

22. 括號生成

數字 n 表明生成括號的對數,請你設計一個函數,用於可以生成全部可能的而且 有效的 括號組合。

 

示例 1:

輸入: n = 3
輸出: ["((()))","(()())","(())()","()(())","()()()"]
複製代碼

示例 2:

輸入: n = 1
輸出: ["()"]
複製代碼
var generateParenthesis = function (n) {
  const res = [];

  const dfs = (lRemain, rRemain, str) => { // 左右括號所剩的數量,str是當前構建的字符串
    if (str.length == 2 * n) { // 字符串構建完成
      res.push(str);           // 加入解集
      return;                  // 結束當前遞歸分支
    }
    if (lRemain > 0) {         // 只要左括號有剩,就能夠選它,而後繼續作選擇(遞歸)
      dfs(lRemain - 1, rRemain, str + "(");
    }
    if (lRemain < rRemain) {   // 右括號比左括號剩的多,才能選右括號
      dfs(lRemain, rRemain - 1, str + ")"); // 而後繼續作選擇(遞歸)
    }
  };

  dfs(n, n, ""); // 遞歸的入口,剩餘數量都是n,初始字符串是空串
  return res;
};
複製代碼

29. 兩數相除

這道題,說實話,能夠列爲比較難的題,主要是邊界處理有個天坑(1073741824 << 1) 返回 -2147483648返回負數了,加倍不能使用 << 1來乘以2。也就是位移操做符乘法運算會有問題。(好比2進制00001111 左移4位,就是11110000,第一位表示符號位,正數是0,負數是1,因此正數就變爲負數了)

給定兩個整數,被除數 dividend 和除數 divisor。將兩數相除,要求不使用乘法、除法和 mod 運算符。

返回被除數 dividend 除以除數 divisor 獲得的商。

整數除法的結果應當截去(truncate)其小數部分,例如:truncate(8.345) = 8 以及 truncate(-2.7335) = -2

 

示例 1:

輸入: dividend = 10, divisor = 3
輸出: 3
解釋: 10/3 = truncate(3.33333..) = truncate(3) = 3
複製代碼

示例 2:

輸入: dividend = 7, divisor = -3
輸出: -2
解釋: 7/-3 = truncate(-2.33333..) = -2
複製代碼

思路:

  • 每次以除數做爲基數,不斷自加,當 sum 逼近到遞歸的被除數時,記錄當前的 count 和剩餘的值 (dividend-sum),繼續遞歸

  • divide(10,3) = recur(10,3) = 2 + recur(10-6,3) = 2 + 1 + recur(10-6-3, 3) = 3

  • 注意,遞歸函數中都是以雙整數位基礎的,因此最外層調用的時候,要根據入參值進行必定的調整

最後值不能超出 [-2^31,2^31-1]

var divide = function(dividend, divisor) {
    let flag = dividend > 0 && divisor < 0 || dividend < 0 && divisor > 0 ;
    dividend = Math.abs(dividend);
    divisor = Math.abs(divisor);
    function recur(dividend, divisor) {
        let count = 1;
        let nextDivisor = divisor;
        if(dividend < divisor) return 0;
        while((nextDivisor + nextDivisor) < dividend){
            count += count;
            nextDivisor = nextDivisor + nextDivisor;
        }
        return count + recur(dividend - nextDivisor, divisor);
    }
    const result =  flag ? -recur(dividend, divisor) : recur(dividend, divisor);
    const max = Math.pow(2, 31) - 1, min = -Math.pow(2, 31);
    if (result > max) return max
    if (result < min) return min
    return result;
};
複製代碼

字符串轉換整數 (atoi)

image.png

示例 1:

輸入:s = "42"
輸出:42
解釋:加粗的字符串爲已經讀入的字符,插入符號是當前讀取的字符。
第 1 步:"42"(當前沒有讀入字符,由於沒有前導空格)
         ^
第 2 步:"42"(當前沒有讀入字符,由於這裏不存在 '-' 或者 '+')
         ^
第 3 步:"42"(讀入 "42")
           ^
解析獲得整數 42 。
因爲 "42" 在範圍 [-231, 231 - 1] 內,最終結果爲 42複製代碼

示例 2:

輸入:s = " -42"
輸出:-42
解釋:
第 1 步:" -42"(讀入前導空格,但忽視掉)
            ^
第 2 步:" -42"(讀入 '-' 字符,因此結果應該是負數)
             ^
第 3 步:" -42"(讀入 "42")
               ^
解析獲得整數 -42 。
因爲 "-42" 在範圍 [-231, 231 - 1] 內,最終結果爲 -42複製代碼

示例 3:

輸入:s = "4193 with words"
輸出:4193
解釋:
第 1 步:"4193 with words"(當前沒有讀入字符,由於沒有前導空格)
         ^
第 2 步:"4193 with words"(當前沒有讀入字符,由於這裏不存在 '-' 或者 '+')
         ^
第 3 步:"4193 with words"(讀入 "4193";因爲下一個字符不是一個數字,因此讀入中止)
             ^
解析獲得整數 4193 。
因爲 "4193" 在範圍 [-231, 231 - 1] 內,最終結果爲 4193複製代碼

示例 4:

輸入:s = "words and 987"
輸出:0
解釋:
第 1 步:"words and 987"(當前沒有讀入字符,由於沒有前導空格)
         ^
第 2 步:"words and 987"(當前沒有讀入字符,由於這裏不存在 '-' 或者 '+')
         ^
第 3 步:"words and 987"(因爲當前字符 'w' 不是一個數字,因此讀入中止)
         ^
解析獲得整數 0 ,由於沒有讀入任何數字。
因爲 0 在範圍 [-231, 231 - 1] 內,最終結果爲 0複製代碼

這道題我用正則很快就解決了,不須要啥思路了。。。

var myAtoi = function(s) {
  let result = s.trim().match(/^(\-|\+)?\d+/g);
  let res = s.trim().match(/^(\-|\+)?\d+/g);
  return res ? Math.max(Math.min(Number(res[0]), 2**31-1), -(2**31)) : 0;
};

複製代碼
相關文章
相關標籤/搜索