leetcode之汇总考察频率低的题目

180 阅读7分钟

一、双指针 - 仅腾讯考过

392.判断子序列

题目 392.判断 s 是 t 的子序列

  1. 初始化两个指针longshort,分别指向字符串ts的初始位置;
  2. 匹配成功则longshort同时右移,匹配失败则long右移short不变,然后继续匹配;
  3. 最终判断short === s.length,等于则说明st是子序列。
const isSubsequence = function(s, t) {
  // 分别表示短字符串和长字符串的指针
  let short = 0, long = 0; 
  while (long < t.length) {
    if (s[short] === t[long]) {
      short++;
      long++;
    } else {
      long++;
    }
  }
  return short === s.length;
};

时间复杂度:O(m + n)。因为两个序列都要遍历一遍。

空间复杂度:O(1)。

344. 反转字符串

题目 344.反转字符串

要求原地修改输入数组,空间复杂度O(1)。 注意,题目中给出的输入是字符串数组,如果是字符串,则对于其内部的单个字符无法改变和增删。

const reverseString = function(s) {
  let left = 0;
  let right = s.length - 1;
  while (left < right) {
    [s[left], s[right]] = [s[right], s[left]];
    left++;
    right--;
  }
  return s;
}

时间复杂度:O(N)。

空间复杂度:O(1)。

31.下一个排列 - 两遍扫描 + 双指针

题目31. 下一个排列

「解题思路」

  1. 倒序查找,找出第一个相邻的 右 > 左 的位置,第一个待交换位置就是 的位置,记为 first
  2. 倒序查找,找出第一个 大于 nums[first] 的位置,记为 second,并将 first 和 second 的元素交换。
  3. first + 1 的位置开始,一直到 数组尾部,用双指针两两交换

解释说明

解释:对于第 1 步,找到第一对 “右 > 左” 的,这样交换才能实现 “数变大”。第 2 步,由于在 first 之后的元素,都是 “左 > 右” 的,我们倒序查找就能找到第一个比 first 元素大的。第 3 步,因为交换后, first + 1 及之后的元素都符合 “左 > 右”,所以我们通过双指针实现倒序。

const nextPermutation = function(nums) {
  let first = nums.length - 2;  // 第 1 个待交换位置
  // 找到待交换的第一个位置 first,逆向思维
  while (first >= 0 && nums[first + 1] <= nums[first]) {
    first--;
  }
  // 找出待交换的第二个位置 second,逆向思维
  if (first >= 0) {
    let second = nums.length - 1;
    while (second >= 0 && nums[first] >= nums[second]) {
      second--;
    }
    [nums[first], nums[second]] = [nums[second], nums[first]];  // 两数交换
  }
  // 交换后 first+1 之后一定是降序序列
  // 所以双指针实现升序序列,注意指针的初始值!
  let pos0 = first + 1, pos1 = nums.length - 1;
  while (pos0 < pos1) {
    [nums[pos0], nums[pos1]] = [nums[pos1], nums[pos0]];
    pos0++;
    pos1--;
  }
};

时间复杂度:O(n)。

空间复杂度:O(1)。 题目也要求只允许使用额外常数空间。

二、树

113. 路径总和II - 有难度

题目113.路径总和II

因为在js中数组是引用类型,如果直接push path;到result中的话,在result中的每个元素都是path的引用值,一旦path改变、result也会发生改变。所以 「用slice()先对path做拷贝」,然后再push进result

const pathSum = (root, sum) => {
  const res = [];  // 结果数组
  // path是每个叶子节点的路径
  // treeSum是对应的路径值
  const dfs = (root, path, treeSum) => {
    if (!root)  return null;
    path.push(root.val);
    treeSum += root.val;

    if (!root.left && !root.right) {
      if (treeSum == sum) {  // 注意,不能挪出去
        res.push(path.slice());  
      }
    } else {
      root.left && dfs(root.left, path, treeSum);
      root.right && dfs(root.right, path, treeSum);
    }
    path.pop();
  }
  
  dfs(root, [], 0);
  return res;
};

700. 二叉搜索树中的搜索

题目700.二叉搜索树中的搜索

根据二叉搜索树的性质,左子树所有节点的值都小于根节点值,右子树所有节点的值都大于根节点值。

const searchBST = function(root, val) {  
  if (!root)  return null;
  // 注意,root本身就是子树,可以直接返回。
  if (val == root.val)  return root;
  return searchBST(val < root.val ? root.left : root.right, val);
}

三、链表

92. 反转链表II - 有难度

题目92. 反转链表II

头插法。 这里的leftright是从下标 1 开始的。

image.png

  1. 我们定义根据参数left定义指针prev指向第一个要反转的节点的前面,且 「循环过程中不改变」,定义指针curr指向第一个要反转的节点的位置上。
  2. 然后删除curr后面的节点,然后再将该节点添加到prev的后面。
  3. 根据leftright重复过程2,最后返回。
const reverseBetween = function(head, left, right) {
  // 定义虚拟头节点
  let dummyHead = new ListNode(0);
  dummyHead.next = head;  
  // 初始化指针
  let prev = dummyHead;
  let curr = dummyHead.next;
  for (let i = 0; i < left - 1; i++) {  // 将指针移到相应的位置
    prev = prev.next;  // 注意是left - 1
    curr = curr.next;
  }

  // 头插法
  for (let i = 0; i < right - left; i++) {
    // 删除curr后面的节点removed
    let removed = curr.next;
    curr.next = curr.next.next;
    // 将删除的removed节点添加到prev的后面
    removed.next = prev.next;
    prev.next = removed;
  }
  return dummyHead.next;  // 实质的链表头节点
};

可以加上链表的结构,

const ListNode = function(val, next) {
  this.val = (this.val === undefined ? 0 : val);
  this.next = (this.next === undefined ? null : next);
};

时间复杂度:O(n)。

空间复杂度:O(1)。

876. 链表的中间结点 - 快慢指针

题目876. 链表的中间结点

用两个指针 fast 和 slow 一起遍历链表,slow 一次走一步,fast 一次走两步,那么当fast 到达链表的末尾时,slow 必然在中间的位置。

const middleNode = function(head) {
  let slow = head, fast = head;
  while (fast && fast.next) {
    slow = slow.next;
    fast = fast.next.next;
  }
  return slow;
}

86. 分隔链表

题目86. 分隔链表

模拟。 虚拟节点 + 链表指针移动。

我们用两个链表分别储存值大于 x 的节点和值小于 x 的节点,然后只要将两个链表连接起来即可。

设 small 和 large 节点指向当前链表的末尾节点,随着遍历不断更新。为了方便连接两个链表和处理头节点为空的边界条件,设 smallHead 和 largeHead 为两个链表的头节点,初始时 smallHead = small,largeHead = large

const partition = function(head, x) {
  let small = new ListNode(0);
  let large = new ListNode(0);
  let smallHead = small, largeHead = large;  // 注意虚拟头节点的定义
  // 维护两个链表small和large
  while (head) {
    if (head.val < x) {
      small.next = head;
      small = small.next;
    } else {
      large.next = head;
      large = large.next;
    }
    head = head.next;
  }
  large.next = null;  // 尾指针置空
  small.next = largeHead.next;  // 连接两个链表
  return smallHead.next;  // 返回头节点
}

时间复杂度:O(n)。

空间复杂度:O(1)。

234. 回文链表 - 几乎不考

题目234. 回文链表

要求空间复杂度O(1)。使用快慢指针。

  1. 定义两个指针 fast 和 slow,快指针一次走两步,慢指针一次走一次。遍历结束时,slow 要么在中点,要么在中点的第 2 个位置。

  2. 用 prev 保存 slow 的前一个节点,通过prev.next = null断成两个链表。

  3. 然后将后半段链表翻转,和前半段从头比对。

// 反转链表的迭代解法
const reverseList = function(head) {
  let prev = null;  // 当前节点的前一个节点
  let curr = head;  // 当前节点
  while (curr) {
    let next = curr.next;  // 储存当前节点的下一个节点
    curr.next = prev;
    prev = curr;
    curr = next;
  }
  return prev;
};

const isPalindrome = function (head) {
  if (!head || !head.next)  return true;
  // 快慢指针、prev初始化
  let slow = head, fast = head;  
  let prev;
  while (fast && fast.next) {  // 注意!
    fast = fast.next.next;
    prev = slow;
    slow = slow.next;
  }
  // 断成两个链表,head -> prev 和 slow -> 尾结点
  prev.next = null;
  // 反转
  let head2 = reverseList(slow);
  // 比对
  while (head && head2) {
    if (head.val !== head2.val) return false;
    head = head.next;
    head2 = head2.next;
  }
  return true;
};

时间复杂度:O(n)。

空间复杂度:O(1)。

四、栈

394. 字符串编码 - 有难度

题目394. 字符串编码

「思路」

该题的难点在于括号内嵌套括号,需要从内向外生成与拼接字符串。「数字放在数字栈,字符串放在字符串栈,遇到右括号时弹出一个数字栈,字符串栈弹到左括号为止」 逆波兰式。

  • 如果当前字符是数位,先转换为数字,这里需要考虑多位相邻的数字。
  • 如果当前字符是左括号,则把之前存的数字字符串分别插入numStackstrStack。并重置结果res和倍数num
  • 如果当前字符是右括号,字符串栈取出栈顶的字符串strStack.pop(),数字栈弹出倍数,并根据倍数和此时的res构造出新的字符串res.repeat(repeatTimes),两者相加赋值给结果res

abc3[cd]xyz举例,有res = abc ==> '3'转为数字3 ==> 遇到左括号,则numStack = [3]、strStack = ['abc']、res = ''、num = 0 ==> res = 'cd' ==> 遇到右括号,strStack弹出栈顶元素,numStack弹出栈顶元素并和res构造出新的字符串cdcdcd,然后两部分相加有res = abccdcdcd ==> res = abccdcdcdxyz

const decodeString = function(s) {
  const numStack = [];  // 倍数
  const strStack = [];  // 待拼接的字符
  let num = 0;  // 倍数
  let res = '';  // 结果

  for (let ch of s) {
    if (!isNaN(ch)) {  // 数位
      num = num * 10 + Number(ch);
    } else if (ch === '[') {  // 左括号
      strStack.push(res);
      numStack.push(num);
      res = '';
      num = 0;
    } else if (ch === ']') {  // 右括号
      let repeatTimes = numStack.pop();
      res = strStack.pop() + res.repeat(repeatTimes);   // 注意
    } else {  // 字母
      res += ch;
    }
  }
  return res;
}

时间复杂度:O(n)。

空间复杂度:O(n)。

227. 基本计算器II - 有难度

题目227. 基本计算机II

我们可以把所有运算都看作是加法运算。由于乘除优先于加减运算,因此遇到乘除号时,先进行乘除运算,并将这些乘除运算后的值放回原表达式的相应位置,那么此时整个表达式的值,就等于一系列整数加减后的值。

  • 遇到加号:将数字压入栈
  • 遇到减号:将它的负数压入栈
  • 遇到乘除号:让当前数乘(除)前一个数(也就是栈顶元素),并将栈顶元素替换为计算结果。
  • 遍历完字符串+string+后,将栈中元素累加,即为该字符串表达式的值。

「注意」 对第一个和最后一个元素均需要处理,字符串形式变成 +string+,这样才能将数字插到栈中。

const calculate = function(s) {
  let num = 0;  // 暂时保存数字
  let sign = '+';  // 对于第一个数字,其之前的运算符视为加号
  // 在字符串最后加一个加号,这样最后的数字才能插到栈中
  s = s + '+';  // 注意!!!
  const stack = [];
  let res = 0;  // 结果

  for (let i = 0; i < s.length; i++) {
    // Number参数是字符会返回NaN,参数是空则返回0
    // isNaN参数的数字返回false,其他返回true
    if (s[i] >= '0' && s[i] <= '9') {  // 数字 
      num = num * 10 + Number(s[i]);
    } else if (s[i] === ' ') {  // 空字符,跳过
      continue;
    } else {  // 运算符
      switch (sign) {
        case '+':
          stack.push(num);
          break;
        case '-':
          stack.push(-num);  // 注意
          break;
        case '*':
          stack[stack.length - 1] *= num;  // 注意
          break;
        default:
          stack[stack.length - 1] = stack[stack.length - 1] / num | 0;  // 注意
      }
      sign = s[i];  // 更新运算符
      num = 0;  // 重置
    }
  }
  while (stack.length) {  // 将栈中的元素累加
    res += stack.pop();
  }
  return res;
}

时间复杂度:O(n)。

空间复杂度:O(n)。 取决于栈的空间。

五、数学

470. 用Rand7()实现Rand10() - 拒绝采样

题目 470. 用Rand7()实现Rand10()

  1. rand7() - 1 能实现随机数[0, 6],(rand7() - 1) * 7可以实现随机数[0, 7, 14, 21, 28, 35, 42, 49],rand7() + (rand7() - 1) * 7就可以实现 1 到 49 的随机数。注意:不能用rand7() * rand7() 的乘法,因为结果的值会有重复。
  2. 随机产生 1 ~ 49之后,我们只取 1 ~ 40来做映射,让[1, 10] 中每个数的生成概率为 4/49。
  3. 另外如果用 1 ~ 40 直接取余 10,会产生0,因此 先减一, 用 0 ~ 39取余,最后再加一。
const rand10 = function() {
  let mul;
  do {
    mul = rand7() + (rand7() - 1) * 7;
  } while (mul > 40);
  return 1 + (mul - 1) % 10;
};

时间复杂度:O(1)。期望时间复杂度O(1),最坏情况为 无穷。

空间复杂度:O(1)。

补充22. IP地址和整数的转换

题目 补充题22. IP地址与整数的转换(腾讯前端爱考)

借助 「位运算」 实现,如IP "10.0.3.193",将 10 左移 24 位, 0 左移 16 位, 3 左移 8 位,193 左移 0 位。最后将 4 个 num 做 「或运算」,即为结果。

const ipToNumber = function(ip) {
  let ipList = ip.split('.');
  let num0 = Number(ipList[0]) << 24;
  let num1 = Number(ipList[1]) << 16;
  let num2 = Number(ipList[2]) << 8;
  let num3 = Number(ipList[3]);
  return num0 | num1 | num2 | num3;
};

时间复杂度:O(1)。

空间复杂度:O(1)。