【算法面试】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;
};

复制代码
相关文章
相关标签/搜索