【算法面试】leetcode最常见的150道前端面试题 --- 中等题2(共80题)

4,438 阅读15分钟

兄弟姐妹们,中等题来了,本篇17道

之前简单题的链接如下:

378. 有序矩阵中第 K 小的元素(中等难度)

给你一个 n x n 矩阵 matrix ,其中每行和每列元素均按升序排序,找到矩阵中第 k 小的元素。 请注意,它是 排序后 的第 k 小元素,而不是第 k 个 不同 的元素。

 

示例 1:

输入:matrix = [[1,5,9],[10,11,13],[12,13,15]], k = 8
输出:13
解释:矩阵中的元素为 [1,5,9,10,11,12,13,13,15],第 8 小元素是 13

示例 2:

输入:matrix = [[-5]], k = 1
输出:-5

在刷leetcode时,我发现一类问题(Topk问题,只要问你前n个或者后n个最大最小值,我们讲的方法非常容易解决)可以用优先队列非常轻松的解决,优先队列可以用二叉堆这个数据结构来实现。

但是我们javascript没有这种数据结构,所以需要我们手写一个。

简单介绍一下二叉堆的特点:

  • 它是一颗完全的二叉树,表示树的每一层都有左侧和右侧子节点(除了最后一层的叶子节点),并且最后一层的叶节点尽可能都是左侧子节点
  • 二叉堆不是最小堆就是最大堆。最小堆允许你快速导出树的最小值,最大堆允许你快速导出树的最大值。所有的节点都大于等于最大堆或小于等于最小堆的每个它的子节点

以下是二叉堆示意图:

屏幕快照 2021-08-16 下午3.48.37.png 我们用数组来模拟这样一个二叉堆。

我们需要一个前置知识点,就是数组如何模拟二叉树。

屏幕快照 2021-08-16 下午3.49.41.png

对于给定的位置index的节点

  • 它的左侧子节点的位置是 2 * index + 1
  • 它的右侧子节点的位置是 2 * index + 2
  • 它的父节点的位置是 (index-1) / 2 如果位置可用

所以接下来,我们就用数组来写一个二叉堆:

以下是一个比较函数,比较两个数谁大谁小

const COMPARE_NUM = {
    less: -1,
    great: 1,
    equal: 0,
}
function defaultCompreFn(i, j){
  if(i - j === COMPARE_NUM.equal) return COMPARE_NUM.equal;
  return i > j ? COMPARE_NUM.great : COMPARE_NUM.less;
}

最小堆的数据部分,包括堆的数组表示,heap属性和比较函数compareFn

class MinHeap{
  constructor(compareFn = defaultCompreFn){
      this.heap = [];
      this.compareFn = compareFn;
  }

以下是获取节点左右子树和父节点下标的方法

  getLeftIndex(index){
      return 2 * index + 1;
  }
  
  getRightIndex(index){
      return 2 * index + 2;
  }
  
  getParentIndex(index){
      if(index === 0) return;
      return (index - 1) >> 1;
  }

以下是工具方法,用来交换数据

  swap(i, j){
      [this.heap[i], this.heap[j]] = [this.heap[j], this.heap[i]];
  }

以下是插入数据的方法

  insert(value){
      if(value !== null){
          // 往数组末尾添加数据
          this.heap.push(value);
          // 添加的数据上移到堆合适的位置
          this.siftUp(this.heap.length - 1);
          return true;
      }
      return false;
  }
  
  siftUp(index){
      let parentIndex = this.getParentIndex(index);
      // index > 0 非常必要,因为index等于0就到了小顶堆的顶部了
      while(index > 0 && this.compareFn(this.heap[index], this.heap[parentIndex]) === COMPARE_NUM.less) {
          this.swap(index, parentIndex);
          index = parentIndex;
          parentIndex = this.getParentIndex(parentIndex);
      }
  }

取出小顶堆的最小值

  extract(){
      if(this.heap.length === 0) return;
      if(this.heap.length === 1) return this.heap.shift();
      const removedValue = this.heap[0];
      this.heap[0] = this.heap.pop();
      // 下移从最后面放到第一位的元素
      this.siftDown(0);
      return removedValue;
  }

下移操作,主要思路就是比较叶子节点和自己谁更小,小的就上移

 siftDown(index) {
     let tempIndex = index;
     const leftIndex = this.getLeftIndex(index);
     const rightIndex = this.getRightIndex(index);
     if(leftIndex < this.heap.length && this.compareFn(this.heap[leftIndex], this.heap[tempIndex]) === COMPARE_NUM.less) {
         tempIndex = leftIndex;
     }
     if(rightIndex < this.heap.length && this.compareFn(this.heap[rightIndex],this.heap[tempIndex]) === COMPARE_NUM.less) {
         tempIndex = rightIndex;
     }
     if(tempIndex !== index){
         this.swap(tempIndex, index);
         this.siftDown(tempIndex);
     }
 }
}

好了,我们用这个数据结构来解题吧,非常好用!

好了,这种题,我们叫做TopK,意思是题目要求我们找矩阵中第k小元素,我们就用一个小顶堆把所有元素装进来,然后移除k个,就是答案啦!

下面的代码是不是很简单,这道题中等难度简直比简单题还简单!

var kthSmallest = function(matrix, k) {
    let result;
    const length = matrix.length;
    const heap = new MinHeap();
    for(let i = 0; i < length; i++){
        for(let j = 0; j < length; j++){
            heap.insert(matrix[i][j])
        }
    }
    while(k--){
        result = heap.extract();
    }

    return result;
};

删除链表的倒数第 N 个结点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

进阶: 你能尝试使用一趟扫描实现吗? image.png

输入:head = [1,2,3,4,5], n = 2 输出:[1,2,3,5] 示例 2:

输入:head = [1], n = 1 输出:[] 示例 3:

输入:head = [1,2], n = 1 输出:[1]

思路:

如何使用快慢指针找到倒数第n个节点?

  • 首先设置快指针先走n步,到达从头数,链表第n个节点
  • 慢指针从头开始走,跟快指针一起走,快指针到达链表尽头的时候,慢指针的下一个节点就是第n个节点
```javascript
var removeNthFromEnd = function(head, n) {
  let slow = slowCopy = fast = new ListNode();
  slow.next = head;
  while(n--){
      fast = fast.next;
  }
  while(fast.next){
      slow = slow.next;
      fast = fast.next;
  }
  slow.next = slow.next.next;
  return slowCopy.next;
};

36. 有效的数独

请你判断一个 9x9 的数独是否有效。只需要 根据以下规则 ,验证已经填入的数字是否有效即可。

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

注意:

  • 一个有效的数独(部分已被填充)不一定是可解的。
  • 只需要根据以上规则,验证已经填入的数字是否有效即可。

 

示例 1:

image.png

输入:board = 
[["5","3",".",".","7",".",".",".","."]
,["6",".",".","1","9","5",".",".","."]
,[".","9","8",".",".",".",".","6","."]
,["8",".",".",".","6",".",".",".","3"]
,["4",".",".","8",".","3",".",".","1"]
,["7",".",".",".","2",".",".",".","6"]
,[".","6",".",".",".",".","2","8","."]
,[".",".",".","4","1","9",".",".","5"]
,[".",".",".",".","8",".",".","7","9"]]
输出:true

示例 2:

输入:board = 
[["8","3",".",".","7",".",".",".","."]
,["6",".",".","1","9","5",".",".","."]
,[".","9","8",".",".",".",".","6","."]
,["8",".",".",".","6",".",".",".","3"]
,["4",".",".","8",".","3",".",".","1"]
,["7",".",".",".","2",".",".",".","6"]
,[".","6",".",".",".",".","2","8","."]
,[".",".",".","4","1","9",".",".","5"]
,[".",".",".",".","8",".",".","7","9"]]
输出:false

解释:除了第一行的第一个数字从 5 改为 8 以外,空格内其他数字均与 示例1 相同。 但由于位于左上角的 3x3 宫内有两个 8 存在, 因此这个数独是无效的。

思路:这道题就按照题目要求依次检查就行,横竖两个方向的检查很容易,就是3x3的格子,需要找到验证规律,如下图

image.png

3x3的验证关键就是横向坐标和竖直方向坐标都 %3 看是否 等于 0, 如果等于0,说明从这个位置开始,横向和纵向3x3就是我们要验证的区域。

var isExistInMap = function(
map, item) {
    if(map[item] && item !== '.'){
        return false;
    } else {
        map[item] = item;
        return true;
    }
};
var validateHV = function(nums) {
    const validateMap = {};
    return nums.every(num => isExistInMap(validateMap, num));
};

var validate3x3 = function(nums) {
    const validateMap = {};
    return nums.every(num => isExistInMap(validateMap, num));
};

var getVerticalNums = function(nums, row) {
    const result = [];
    for(let i = 0; i < nums.length; i++) result.push(nums[i][row])
    return result;
};

var get3x3 = function(nums, row, col) {
    const result = [];
    for(let i = col; i < col + 3; i++) {
        for(let j = row; j < row + 3; j++){
            result.push(nums[i][j]);
        } 
    }
    return result;
};

var isValidSudoku = function(board) {
    for(let i = 0; i < 9; i++) {
        if(!validateHV(board[i])) return false;
        if(!validateHV(getVerticalNums(board, i))) return false;
        for(let j = 0; j < 9; j++) {
            if(i % 3 === 0 && j % 3 === 0 && !validateHV(get3x3(board, i, j))) return false
        }
    }
    return true;
};

38. 外观数列

给定一个正整数 n ,输出外观数列的第 n 项。

「外观数列」是一个整数序列,从数字 1 开始,序列中的每一项都是对前一项的描述。

你可以将其视作是由递归公式定义的数字字符串序列:

  • countAndSay(1) = "1"
  • countAndSay(n) 是对 countAndSay(n-1) 的描述,然后转换成另一个数字字符串。

前五项如下:

1.     1
2.     11
3.     21
4.     1211
5.     111221
第一项是数字 1 
描述前一项,这个数是 1 即 “ 一 个 1 ”,记作 "11"
描述前一项,这个数是 11 即 “ 二 个 1 ” ,记作 "21"
描述前一项,这个数是 21 即 “ 一 个 2 + 一 个 1 ” ,记作 "1211"
描述前一项,这个数是 1211 即 “ 一 个 1 + 一 个 2 + 二 个 1 ” ,记作 "111221"

这个题思路主要是如下的generatorCount函数,这个函数的思路就是求任意数字,如何转化

  • 从第一个字符串开始,遍历字符串
  • 如果前一个字符和后一个字符相同,就加在一起,直到后一个字符和前一个字符不一样
  • 然后统计之前相同字符的个数,以此类推
var countAndSay = function(n) {
  if(n === 1) return '1';
  return generatorCount(countAndSay(n-1));
  
};
function generatorCount(n){
  let initStr = n[0];
  let result =''
  for(let i = 0; i < n.length; i++){
      if(n[i] === n[i+1]){
          initStr += n[i+1]
      }else{
          result += initStr.length + initStr[0];
          initStr = n[i+1];
      }
  }
  return result;
}

46. 全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

 

示例 1:

输入: nums = [1,2,3]
输出: [[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入: nums = [0,1]
输出: [[0,1],[1,0]]

示例 3:

输入: nums = [1]
输出: [[1]]

思路:

image.png

这道题是典型的回溯问题,全排列就是把各种组合全部打印出来,但是这个题需要去重。

主要思路就是按照上图的画法

  • 回溯中参数为原始数组nums,以及当前数组arr
  • 回溯的终止条件为,当前arr的个数已经等于原始数组nums
  • 回溯的处理,循环遍历原始数组nums,当前值不存在arr中时,将该值放入arr中,递归调用
var permute = function(nums) {
  const result = [];
  function dfs(partialResult){
      if(partialResult.length === nums.length){
          result.push(partialResult);
          return;
      }
      for(let i = 0, len = nums.length; i < len; i++){
          if(partialResult.includes(nums[i])) { continue };
          dfs(partialResult.concat(nums[i]));
      }
  }
  dfs([]);
  return result;
};

旋转图像

给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。

你必须在 原地 旋转图像,这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。

示例 1:

image.png

输入: matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出: [[7,4,1],[8,5,2],[9,6,3]]

示例 2:

image.png

输入:matrix = [[5,1,9,11],[2,4,8,10],[13,3,6,7],[15,14,12,16]]
输出:[[15,13,2,5],[14,3,4,1],[12,6,8,9],[16,7,10,11]]

示例 3:

输入:matrix = [[1]]
输出:[[1]]

示例 4:

输入:matrix = [[1,2],[3,4]]
输出:[[3,1],[4,2]]

思路

首先输入

1 2 3
4 5 6
7 8 9

通过交换matrix[i][j], matrix[j][i] 得到

1 4 7
2 5 8
3 6 9

最后将得到每组数组倒序排列即可

7 4 1
8 5 2
9 6 3

思路: 首先将输入

1 2 3
4 5 6
7 8 9

通过交换matrix[i][j], matrix[j][i] 得到

1 4 7
2 5 8
3 6 9

最后将得到每组数组倒序排列即可

7 4 1
8 5 2
9 6 3
var rotate = function(matrix) {
  for(let i = 0; i < matrix.length; i++){
      for(let j = i; j < matrix.length; j++){
          [matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]]
      }
  }
  return matrix.map(item => item.reverse());
};

49. 字母异位词分组

难度中等804

给定一个字符串数组,将字母异位词组合在一起。可以按任意顺序返回结果列表。

字母异位词指字母相同,但排列不同的字符串。

 

示例 1:

输入: strs = ["eat", "tea", "tan", "ate", "nat", "bat"]
输出: [["bat"],["nat","tan"],["ate","eat","tea"]]

示例 2:

输入: strs = [""]
输出: [[""]]

示例 3:

输入: strs = ["a"]
输出: [["a"]]

思路:

就是排序的字符串肯定相同,然后用一个哈希map发现排序的字符串相同就把他们加进去就行了

var groupAnagrams = function(strs) {
  const recordMap = {};
  const result = [];
  for(let str of strs){
      const sortStr = str.split('').sort().join('');
      if(recordMap[sortStr]){
          recordMap[sortStr].push(str);
      } else {
          recordMap[sortStr] = [str];
      }
  }
  for(let key in recordMap){
      result.push(recordMap[key])
  }
  return result;
};

2. 两数相加

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 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,算法过程几乎是一模一样的.

不过需要注意:做有关链表的题,有个隐藏技巧:添加一个虚拟头结点(哨兵节点),帮助简化边界情况的判断 具体思路。

思路: 从最低位至最高位,逐位相加,如果和大于等于 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;
};

395. 至少有 K 个重复字符的最长子串

给你一个字符串 s 和一个整数 k ,请你找出 s 中的最长子串, 要求该子串中的每一字符出现次数都不少于 k 。返回这一子串的长度。

示例 1:

输入: s = "aaabb", k = 3
输出: 3
解释: 最长子串为 "aaa" ,其中 'a' 重复了 3 次。

示例 2:

输入: s = "ababbc", k = 2
输出: 5
解释: 最长子串为 "ababb" ,其中 'a' 重复了 2 次, 'b' 重复了 3 次。