1. 字符串相加:
/**
* @param {string} num1
* @param {string} num2
* @return {string}
*/
var addStrings = function(num1, num2) {
if (num1 === num2 && num1 === "0") {
return num1
}
// 切割成数组倒过来,倒过来是因为可能存在长度不一的字符串
num1 = num1.split("").reverse();
num2 = num2.split("").reverse();
const len = Math.max(num1.length, num2.length);
let flag = 0;
const result = [];
for (let i = 0; i < len; i++) {
const n1 = +num1[i] || 0;
const n2 = +num2[i] || 0;
let sum = n1 + n2 + flag;
flag = 0;
// 进1
if (sum > 9) {
sum -= 10
flag = 1
}
result.push(sum);
}
// 仍存在进1标志,手动进1
if (flag) {
result.push(flag)
}
return result.reverse().join("");
};
2. 翻转字符串里的单词
// 翻转字符串里的单词
const reverseWords = function(s) {
return s.trim().split(/\s+/).reverse().join(' ');
};
3. 二分查找
过程:
- 设定左右指针
- 找出中间位置,并判断该位置值是否等于 target
- nums[mid] == target 则返回该位置下标
- nums[mid] > target 则右侧指针移到中间
- nums[mid] < target 则左侧指针移到中间
- 时间复杂度:O(logN)
var search = function(nums, target) {
let left = 0, right = nums.length - 1;
while (left <= right) {
const mid = Math.floor((right - left) / 2) + left;
const num = nums[mid];
if (num === target) {
return mid;
} else if (num > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1;
};
4. 二叉树的中序遍历
4.1 方法一:递归
思路与算法
首先我们需要了解什么是二叉树的中序遍历:按照访问左子树——根节点——右子树的方式遍历这棵树,而在访问左子树或者右子树的时候我们按照同样的方式遍历,直到遍历完整棵树。因此整个遍历过程天然具有递归的性质,我们可以直接用递归函数来模拟这一过程。
定义 inorder(root) 表示当前遍历到root 节点的答案,那么按照定义,我们只要递归调用 inorder(root.left) 来遍历 root 节点的左子树,然后将 root 节点的值加入答案,再递归调用inorder(root.right) 来遍历 root 节点的右子树即可,递归终止的条件为碰到空节点。
var inorderTraversal = function(root) {
const res = [];
const inorder = (root) => {
if (!root) {
return;
}
inorder(root.left);
res.push(root.val);
inorder(root.right);
}
inorder(root);
return res;
};
4.2 方法二:迭代
思路与算法
方法一的递归函数我们也可以用迭代的方式实现,两种方式是等价的,区别在于递归的时候隐式地维护了一个栈,而我们在迭代的时候需要显式地将这个栈模拟出来,其他都相同.
var inorderTraversal = function(root) {
const res = [];
const stk = [];
while (root || stk.length) {
while (root) {
stk.push(root);
root = root.left;
}
root = stk.pop();
res.push(root.val);
root = root.right;
}
return res;
};
5. 两个数组的交集
5.1 方法一:两个集合
计算两个数组的交集,直观的方法是遍历数组 nums1,对于其中的每个元素,遍历数组 nums2 判断该元素是否在数组 nums2 中,如果存在,则将该元素添加到返回值。假设数组 nums1 和 nums2 的长度分别是 m 和 n,则遍历数组 nums1 需要 O(m) 的时间,判断 nums1 中的每个元素是否在数组 nums2 中需要 O(n) 的时间,因此总时间复杂度是 O(mn)。
如果使用哈希集合存储元素,则可以在 O(1) 的时间内判断一个元素是否在集合中,从而降低时间复杂度。
首先使用两个集合分别存储两个数组中的元素,然后遍历较小的集合,判断其中的每个元素是否在另一个集合中,如果元素也在另一个集合中,则将该元素添加到返回值。该方法的时间复杂度可以降低到 O(m+n)。
const set_intersection = (set1, set2) => {
if (set1.size > set2.size) {
return set_intersection(set2, set1);
}
const intersection = new Set();
for (const num of set1) {
if (set2.has(num)) {
intersection.add(num);
}
}
return [...intersection];
}
var intersection = function(nums1, nums2) {
const set1 = new Set(nums1);
const set2 = new Set(nums2);
return set_intersection(set1, set2);
};
5.2 方法二:排序 + 双指针
如果两个数组是有序的,则可以使用双指针的方法得到两个数组的交集。
首先对两个数组进行排序,然后使用两个指针遍历两个数组。可以预见的是加入答案的数组的元素一定是递增的,为了保证加入元素的唯一性,我们需要额外记录变量 pre 表示上一次加入答案数组的元素。
初始时,两个指针分别指向两个数组的头部。每次比较两个指针指向的两个数组中的数字,如果两个数字不相等,则将指向较小数字的指针右移一位,如果两个数字相等,且该数字不等于 pre ,将该数字添加到答案并更新 pre 变量,同时将两个指针都右移一位。当至少有一个指针超出数组范围时,遍历结束。
var intersection = function(nums1, nums2) {
nums1.sort((x, y) => x - y);
nums2.sort((x, y) => x - y);
const length1 = nums1.length, length2 = nums2.length;
let index1 = 0, index2 = 0;
const intersection = [];
while (index1 < length1 && index2 < length2) {
const num1 = nums1[index1], num2 = nums2[index2];
if (num1 === num2) {
// 保证加入元素的唯一性
if (!intersection.length || num1 !== intersection[intersection.length - 1]) {
intersection.push(num1);
}
index1++;
index2++;
} else if (num1 < num2) {
index1++;
} else {
index2++;
}
}
return intersection;
};
复杂度分析:
时间复杂度:O(mlogm+nlogn),其中 m 和 n 分别是两个数组的长度。对两个数组排序的时间复杂度分别是O(mlogm) 和 O(nlogn),双指针寻找交集元素的时间复杂度是 O(m+n),因此总时间复杂度是 O(mlogm+nlogn)。
空间复杂度: O(logm+logn),其中 m 和 n 分别是两个数组的长度。空间复杂度主要取决于排序使用的额外空间。
6. 两数之和
给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。
const twoSum = (nums, target) => {
const prevNums = {}; // 存储出现过的数字,和对应的索引
for (let i = 0; i < nums.length; i++) { // 遍历元素
const curNum = nums[i]; // 当前元素
const targetNum = target - curNum; // 满足要求的目标元素
const targetNumIndex = prevNums[targetNum]; // 在prevNums中获取目标元素的索引
if (targetNumIndex !== undefined) { // 如果存在,直接返回 [目标元素的索引,当前索引]
return [targetNumIndex, i];
} else { // 如果不存在,说明之前没出现过目标元素
prevNums[curNum] = i; // 存入当前的元素和对应的索引
}
}
}
7. 一个链表的头节点 head ,判断链表中是否有环
双指针
- 普通指针:两指针同一方向或不同方向
- 对撞指针:两指针互相靠拢
- 快慢指针:一快一慢
7.1 方法1.哈希表或set:
思路:准备一个map或者set,然后循环链表,每次遍历到一个节点的时候,判断当前节点是否在map中存在,如果不存在就把当前节点加入map中,如果存在的话说明之前访问过此节点,也就说明了这条链表有环。 复杂度分析:时间复杂度O(n),n是链表的数量,最差的情况下每个节点都要遍历。空间复杂度O(n),n是存储遍历过的节点的map或者set
var hasCycle = (head) => {
let map = new Map();
while (head) {
if (map.has(head)) return true;//如果当前节点在map中存在就说明有环
map.set(head, true);//否则就加入map
head = head.next;//迭代节点
}
return false;//循环完成发现没有重复节点,说明没环
};
7.2 方法2.快慢指针
思路:准备两个指针fast和slow,循环链表,slow指针初始也指向head,每次循环向前走一步,fast指针初始指向head,每次循环向前两步,如果没有环,则快指针会抵达终点,如果有环,那么快指针会追上慢指针 复杂度:时间复杂度O(n),空间复杂度O(1)
var hasCycle = function (head) {
//设置快慢指针
let slow = head;
let fast = head;
//如果没有环,则快指针会抵达终点,否则继续移动双指针
while (fast && fast.next) {
slow = slow.next;
fast = fast.next.next;
//快慢指针相遇,说明含有环
if (slow == fast) {
return true;
}
}
return false;
};
8. 最大子序和
8.1 方法一:动态规划
思路和算法
假设 nums 数组的长度是 n,下标从 0 到 n−1。 我们用 f(i) 代表以第 i 个数结尾的「连续子数组的最大和」,那么很显然我们要求的答案就是:
max {f(i)}
因此我们只需要求出每个位置的 f(i),然后返回 f 数组中的最大值即可。那么我们如何求 f(i) 呢?我们可以考虑nums[i] 单独成为一段还是加入 f(i−1) 对应的那一段,这取决于 f(i−1)+nums[i] 的大小,我们希望获得一个比较大的,于是可以写出这样的动态规划转移方程:
f(i)=max{f(i−1)+nums[i],nums[i]}
var maxSubArray = function(nums) {
let pre = 0, maxAns = nums[0];
nums.forEach((x) => {
pre = Math.max(pre + x, x);
maxAns = Math.max(maxAns, pre);
});
return maxAns;
};
8.2 分治:
function Status(l, r, m, i) {
this.lSum = l;
this.rSum = r;
this.mSum = m;
this.iSum = i;
}
const pushUp = (l, r) => {
const iSum = l.iSum + r.iSum;
const lSum = Math.max(l.lSum, l.iSum + r.lSum);
const rSum = Math.max(r.rSum, r.iSum + l.rSum);
const mSum = Math.max(Math.max(l.mSum, r.mSum), l.rSum + r.lSum);
return new Status(lSum, rSum, mSum, iSum);
}
const getInfo = (a, l, r) => {
if (l === r) {
return new Status(a[l], a[l], a[l], a[l]);
}
const m = (l + r) >> 1;
const lSub = getInfo(a, l, m);
const rSub = getInfo(a, m + 1, r);
return pushUp(lSub, rSub);
}
var maxSubArray = function(nums) {
return getInfo(nums, 0, nums.length - 1).mSum;
};
复杂度分析
假设序列 a 的长度为 n。
- 时间复杂度:假设我们把递归的过程看作是一颗二叉树的先序遍历,那么这颗二叉树的深度的渐进上界为 O(logn),这里的总时间相当于遍历这颗二叉树的所有节点,故总时间的渐进上界是 O(∑i=1logn2i−1)=O(n),故渐进时间复杂度为 O(n)。
- 空间复杂度:递归会使用 O(logn) 的栈空间,故渐进空间复杂度为 O(logn)。
9. 反转链表
9.1 方法一:迭代
在遍历链表时,将当前节点的 next 指针改为指向前一个节点。由于节点没有引用其前一个节点,因此必须事先存储其前一个节点。在更改引用之前,还需要存储后一个节点。最后返回新的头引用。
var reverseList = function(head) {
let prev = null;
let curr = head;
while (curr) {
const next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
};
9.2 方法二:递归
var reverseList = function(head) {
if (head == null || head.next == null) {
return head;
}
const newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
};
10. 基本计算器
方法一:括号展开 + 栈 由于字符串除了数字与括号外,只有加号和减号两种运算符。因此,如果展开表达式中所有的括号,则得到的新表达式中,数字本身不会发生变化,只是每个数字前面的符号会发生变化。
因此,我们考虑使用一个取值为 { − 1 , + 1 } {−1,+1} 的整数 sign sign 代表「当前」的符号。根据括号表达式的性质,它的取值:
- 与字符串中当前位置的运算符有关;
- 如果当前位置处于一系列括号之内,则也与这些括号前面的运算符有关:每当遇到一个以
−号开头的括号,则意味着此后的符号都要被「翻转」。
考虑到第二点,我们需要维护一个栈 ops,其中栈顶元素记录了当前位置所处的每个括号所「共同形成」的符号。例如,对于字符串 1+2+(3-(4+5)) 1+2+(3-(4+5)):
- 扫描到 1+2 时,由于当前位置没有被任何括号所包含,则栈顶元素为初始值 +1;
- 扫描到 1+2+(3 1+2+(3 时,当前位置被一个括号所包含,该括号前面的符号为 + 号,因此栈顶元素依然 +1;
- 扫描到1+2+(3-(4 时,当前位置被两个括号所包含,分别对应着 + 号和 − 号,由于 + 号和 − 号合并的结果为 − 号,因此栈顶元素变为 −1。
在得到栈 ops 之后, sign 的取值就能够确定了:如果当前遇到了 + 号,则更新 sign←ops.top();如果遇到了遇到了 − 号,则更新 sign←−ops.top()。
然后,每当遇到(时,都要将当前的 sign 取值压入栈中;每当遇到)时,都从栈中弹出一个元素。这样,我们能够在扫描字符串的时候,即时地更新 ops 中的元素。
var calculate = function(s) {
const ops = [1];
let sign = 1;
let ret = 0;
const n = s.length;
let i = 0;
while (i < n) {
if (s[i] === ' ') {
i++;
} else if (s[i] === '+') {
sign = ops[ops.length - 1];
i++;
} else if (s[i] === '-') {
sign = -ops[ops.length - 1];
i++;
} else if (s[i] === '(') {
ops.push(sign);
i++;
} else if (s[i] === ')') {
ops.pop();
i++;
} else {
let num = 0;
while (i < n && !(isNaN(Number(s[i]))) && s[i] !== ' ') {
num = num * 10 + s[i].charCodeAt() - '0'.charCodeAt();
i++;
}
ret += sign * num;
}
}
return ret;
};