一、双指针 - 仅腾讯考过
392.判断子序列
- 初始化两个指针
long和short,分别指向字符串t和s的初始位置; - 匹配成功则
long和short同时右移,匹配失败则long右移、short不变,然后继续匹配; - 最终判断
short === s.length,等于则说明s是t是子序列。
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. 反转字符串
要求原地修改输入数组,空间复杂度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.下一个排列 - 两遍扫描 + 双指针
「解题思路」
- 倒序查找,找出第一个相邻的
右 > 左的位置,第一个待交换位置就是左的位置,记为first。 - 倒序查找,找出第一个
大于 nums[first]的位置,记为second,并将 first 和 second 的元素交换。 - 从
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 - 有难度
因为在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. 二叉搜索树中的搜索
根据二叉搜索树的性质,左子树所有节点的值都小于根节点值,右子树所有节点的值都大于根节点值。
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 - 有难度
头插法。 这里的
left和right是从下标 1 开始的。
- 我们定义根据参数
left定义指针prev指向第一个要反转的节点的前面,且 「循环过程中不改变」,定义指针curr指向第一个要反转的节点的位置上。 - 然后删除
curr后面的节点,然后再将该节点添加到prev的后面。 - 根据
left和right重复过程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. 链表的中间结点 - 快慢指针
用两个指针 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. 分隔链表
模拟。 虚拟节点 + 链表指针移动。
我们用两个链表分别储存值大于 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. 回文链表 - 几乎不考
要求空间复杂度O(1)。使用快慢指针。
-
定义两个指针 fast 和 slow,快指针一次走两步,慢指针一次走一次。遍历结束时,slow 要么在中点,要么在中点的第 2 个位置。
-
用 prev 保存 slow 的前一个节点,通过
prev.next = null断成两个链表。 -
然后将后半段链表翻转,和前半段从头比对。
// 反转链表的迭代解法
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. 字符串编码 - 有难度
「思路」
该题的难点在于括号内嵌套括号,需要从内向外生成与拼接字符串。「数字放在数字栈,字符串放在字符串栈,遇到右括号时弹出一个数字栈,字符串栈弹到左括号为止」 逆波兰式。
- 如果当前字符是数位,先转换为数字,这里需要考虑多位相邻的数字。
- 如果当前字符是左括号,则把之前存的数字和字符串分别插入
numStack和strStack。并重置结果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 - 有难度
我们可以把所有运算都看作是加法运算。由于乘除优先于加减运算,因此遇到乘除号时,先进行乘除运算,并将这些乘除运算后的值放回原表达式的相应位置,那么此时整个表达式的值,就等于一系列整数加减后的值。
- 遇到加号:将数字压入栈
- 遇到减号:将它的负数压入栈
- 遇到乘除号:让当前数乘(除)前一个数(也就是栈顶元素),并将栈顶元素替换为计算结果。
- 遍历完字符串
+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() - 拒绝采样
rand7() - 1能实现随机数[0, 6],(rand7() - 1) * 7可以实现随机数[0, 7, 14, 21, 28, 35, 42, 49],rand7() + (rand7() - 1) * 7就可以实现 1 到 49 的随机数。注意:不能用rand7() * rand7() 的乘法,因为结果的值会有重复。- 随机产生 1 ~ 49之后,我们只取 1 ~ 40来做映射,让[1, 10] 中每个数的生成概率为 4/49。
- 另外如果用 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地址和整数的转换
借助 「位运算」 实现,如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)。