1. 题目类型
1.1 动态规划
需要明确:
- 需要记录的状态是什么
- 如何从一个状态转移到下一个状态(状态转移方程)
1.2 双指针
1.3 快慢指针
思路:快指针先前进N步,随后一起移动,直到快指针先到尾部,此时慢指针位置就是目标节点的前驱节点
关键字:倒数第N个
模式识别:
- 涉及列表的特殊位置,考虑快慢指针
- 要删除列表节点,找到它的前驱
相关题目:10
1.4 二分搜索及其变种
关键字:排序,搜索
模式识别:
- 有序或者部分有序
2. 力扣top100
力扣top100:leetcode-cn.com/problem-lis…
以下编号是top100从上往下的编号,并非题目实际的编号
1. 两数之和
思路一:直接找 target-nums[i] 的值
var twoSum = function(nums, target) {
const result = [];
for (let i = 0; i < nums.length; i++) {
let idx = nums.indexOf(target - nums[i], i + 1);
if (idx > 0) {
result.push(i, idx);
break;
}
}
return result;
};
思路二:利用 Map 结构(边判断,边存储,循环一遍过)
var twoSum = function(nums, target) {
const map = new Map();
for (let i = 0; i < nums.length; i++) {
const num = target - nums[i];
if (map.has(num)) {
return [map.get(num), i];
}
map.set(nums[i], i);
}
return [];
};
2. 两数相加
思路一:直接处理,通过创建默认头结点、已结束列表对应位置值设置为0使代码更简洁
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
var addTwoNumbers = function(l1, l2) {
let node = new ListNode();
const head = node;
let addOne = 0;
while (addOne || l1 || l2) {
let num1 = l1 ? l1.val : 0; // l1?.val??0;
let num2 = l2 ? l2.val : 0;
let sum = num1 + num2 + addOne;
node.next = new ListNode(sum % 10);
node = node.next;
addOne = sum >= 10 ? 1 : 0;
if (l1) l1 = l1.next;
if (l2) l2 = l2.next;
}
return head.next;
};
3. 无重复字符的最长子串长度
思路:滑动窗口,注意左下标位置不能回退,i必须通过Math.max()进行判断(案例:'abba')
var lengthOfLongestSubstring = function(s) {
if (s.length === 0) return 0;
let i = 0, max = 0;
const map = new Map();
for (let j = 0; j < s.length; j++) {
if (map.has(s[j])) {
i = Math.max(i, map.get(s[j]) + 1);
}
map.set(s[j], j);
max = Math.max(max, j - i + 1);
}
return max;
};
4. 寻找两个有序数组的中位数
思路:二分查找、数组分割线
function findMedianSortedArrays(nums1: number[], nums2: number[]): number {
// 约定nums1长度小于nums2
if (nums1.length > nums2.length) {
return findMedianSortedArrays(nums2, nums1);
}
// m、n分别存储nums1、nums2的长度
let m = nums1.length, n = nums2.length;
// 分割线左边的所有元素需要满足个数 (m + n + 1) / 2【向下取整】
let totalLeft = Math.floor((m + n + 1) / 2);
// 在nums1的区间[0, m]中查找恰当的分割线,第二条分割线可根据totalLeft计算位置
// 约定i、j分别位于两个分割线的右边,也等于左侧被分割数组长度
// 分割线需要满足nums1[i - 1] <= nums2[j] && nums[j - 1] <= nums[i]
// 以较短的数组进行查找(第一个数组)
let left = 0, right = m;
while (left < right) {
let i = left + (right - left + 1) / 2;
}
};
5. 最长回文子串
思路一:暴力破解,最长子串为本身、小一个字符子串……进行查找,力扣会超时
function longestPalindrome(s: string): string {
// 判断字符串是否是回文字符串
const palindrome = (str: string): boolean => {
const reverseStr = str.split('').reverse().join('');
return str === reverseStr;
};
let maxLen = s.length;
while (maxLen > 0) {
for (let i = 0; i <= s.length - maxLen; i++) {
const subStr = s.slice(i, i + maxLen);
if (palindrome(subStr)) {
return subStr;
}
}
maxLen--;
}
return '';
};
思路二:动态规划(dp[i][j] = s[i] === s[j] && dp[i + 1][j - 1])
// 动态规划:dp[i][j] = s[i] === s[j] && dp[i+1][j-1]
function longestPalindrome(s: string): string {
const length = s.length;
if (length < 2) {
return s;
}
// 初始化
let dp = [];
for (let i = 0; i < length; i++) {
dp.push([]);
// dp[i][j]表示s[i...j]是否是回文串
dp[i][i] = true;
}
let maxLen = 1, begin = 0;
const charArr = s.split('');
// 从右向左按列填写
for (let j = 1; j < length; j++) {
// i,j分别表示串的左右下标
for (let i = 0; i < j; i++) {
if (charArr[i] !== charArr[j]) {
dp[i][j] = false;
}
else {
// 转移的状态子串长度小于2
if (j - i < 3) {
dp[i][j] = true;
}
else {
dp[i][j] = dp[i + 1][j - 1];
}
}
// 只要dp[i][j]===true成立,就表示子串s[i...j]是回文字符串,记录此时的长度和起始位置
let currentLen = j - i + 1;
if (dp[i][j] && currentLen > maxLen) {
maxLen = currentLen;
begin = i;
}
}
}
return s.substring(begin, begin + maxLen);
};
6. 正则表达式匹配
状态转移方程:
function isMatch(s: string, p: string): boolean {
let sLen = s.length, pLen = p.length;
let dp = [];
for (let i = 0; i <= sLen; i++) {
dp.push([]);
}
dp[0][0] = true;
// 计算机并不知道输入字符串和匹配串位置会随着走,而是遍历所有情况,存储状态
for (let i = 0; i <= sLen; i++) {
for (let j = 1; j <= pLen; j++) {
if (p.charAt(j - 1) === '*') {
dp[i][j] = dp[i][j - 2];
if (matches(s, p, i, j - 1)) {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
}
else {
if (matches(s, p, i, j)) {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
}
return !!dp[sLen][pLen];
};
// i, j分别是s, p的下标
function matches(s: string, p: string, i: number, j: number): boolean {
if (i === 0) {
return false;
}
if (p.charAt(j - 1) === '.') {
return true;
}
return s.charAt(i - 1) === p.charAt(j - 1);
}
7. 盛最多水的容器
模式识别:需要移动左右两头的问题可以考虑双指针
// 双指针:从两边出发记录面积,每次移动短的一边,一遍过
function maxArea(heights: number[]): number {
let max = 0, left = 0, right = heights.length - 1;
while (left < right) {
const width = right - left;
const height = Math.min(heights[left], heights[right]);
max = Math.max(max, width * height);
heights[left] < heights[right] ? left++ : right--;
}
return max;
};
8. 三数之和
// 排序 + 双指针
// 固定一个数,剩下两个可通过双指针进行查找
function threeSum(nums: number[]): number[][] {
const res: number[][] = [], length = nums.length;
nums.sort((a, b) => a - b);
for (let first = 0; first < length; first++) {
// 需要和上次枚举的数不同
if (first > 0 && nums[first] === nums[first - 1]) {
continue;
}
// 起始位置为最后一个元素,target可以为任意元素此处为0
let third = length - 1, target = 0 - nums[first];
for (let second = first + 1; second < length; second++) {
// 需要和上次枚举的数不同
if (second > first + 1 && nums[second] === nums[second - 1]) {
continue;
}
while (second < third && (nums[second] + nums[third]) > target) {
third--;
}
if (second === third) {
break;
}
if ((nums[second] + nums[third]) === target) {
res.push([nums[first], nums[second], nums[third]]);
}
}
}
return res;
};
js数组默认排序方式是按照字符串进行排序的,可传入回调函数按照指定规则排序
[11, 1, 2].sort(); // [1, 11, 2]
[11, 1, 2].sort((a, b) => a - b); // [1, 2, 11]
9. 电话号码的字母组合
回朔算法:
function letterCombinations(digits: string): string[] {
const combinations: string[] = [];
if (digits.length === 0) {
return combinations;
}
const keyMap = new Map();
keyMap.set('2', 'abc');
keyMap.set('3', 'def');
keyMap.set('4', 'ghi');
keyMap.set('5', 'jkl');
keyMap.set('6', 'mno');
keyMap.set('7', 'pqrs');
keyMap.set('8', 'tuv');
keyMap.set('9', 'wxyz');
backtrack(combinations, digits, keyMap, 0, []);
return combinations;
};
function backtrack(combinations: string[], digits: string, keyMap: Map<string, string>, index: number, buffer: string[]): void {
if (index === digits.length) {
combinations.push(buffer.join(''));
}
else {
const digit = digits.charAt(index);
const letters = keyMap.get(digit);
const lettersLen = letters.length;
for (let i = 0; i < lettersLen; i++) {
buffer.push(letters.charAt(i));
backtrack(combinations, digits, keyMap, index + 1, buffer);
buffer.splice(index, 1);
}
}
}
10. 删除链表的倒数第 N 个结点
直接分情况讨论:list.length=1(n=1), n=list.length, 1<n<list.length
function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
if (!head) {
return head;
}
const nodeList: Array<ListNode> = [];
let temp = head;
while (temp) {
nodeList.push(temp);
temp = temp.next;
}
if (nodeList.length === 1) {
head = null;
}
else if (n === nodeList.length) {
head.next = null;
head = nodeList[1];
}
else {
if (n === 1) {
const preNode = nodeList[nodeList.length - n - 1];
preNode.next = null;
}
else {
const preNode = nodeList[nodeList.length - n - 1];
let node: ListNode | null = nodeList[nodeList.length - n];
const nexNode = nodeList[nodeList.length - n + 1];
preNode.next = nexNode;
node.next = null;
// 释放引用
node = null;
}
}
// 释放引用
nodeList.length = 0;
return head;
};
快慢指针
function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null {
if (head) {
let runner = head, chaser = head;
// 快指针先移动N步
while(n > 0) {
runner = runner.next;
n--;
}
if (runner === null) {
return head.next;
}
while (runner.next !== null) {
runner = runner.next;
chaser = chaser.next;
}
chaser.next = chaser.next.next;
}
return head;
};
11. 有效的括号
栈:
function isValid(s: string): boolean {
const map = new Map([['(', ')'], ['[', ']'], ['{', '}']]);
const stack = [];
for (const c of s) {
if (stack.length === 0) stack.push(c);
else if (map.get(stack[stack.length - 1]) === c) stack.pop();
else stack.push(c)
}
return stack.length === 0;
};
12. 合并两个有序链表
思路:使用一个假节点作为头节点减少了很多判断
function mergeTwoLists(list1: ListNode | null, list2: ListNode | null): ListNode | null {
if (list1 === null || list2 === null) return list1 ? list1 : list2;
const head = new ListNode();
let node = head;
while (list1 && list2) {
if (list1.val < list2.val) {
node.next = list1;
list1 = list1.next;
}
else {
node.next = list2;
list2 = list2.next;
}
node = node.next;
}
node.next = list1 ? list1 : list2;
return head.next;
};
13. 括号生成
思路:深度优先遍历(回溯算法)+剪枝
function generateParenthesis(n: number): string[] {
const res: string[] = [];
dfs('', n, n, res);
return res;
}
// curStr->当前字符串,left->剩余左括号数量,right->剩余右括号数量,res->存储的结果集
function dfs(curStr, left, right, res): void {
if (left === 0 && right === 0) { // 到达叶子节点
res.push(curStr);
return;
}
// 左括号剩余数量严格大于右括号剩余数量,剪枝
if (left > right) return;
// 产生分支(左分支添加左括号,右分支添加右括号)
if (left > 0) dfs(curStr + '(', left - 1, right, res);
if (right > 0) dfs(curStr + ')', left, right - 1, res);
}
14. 合并K个升序链表
思路一:每次循环找到数组中值最小的节点,进行列表拼接(耗时较长)
function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
let node = new ListNode();
const head = node;
while (lists.some(list => list)) {
let minNode = new ListNode(1 * 10 ** 4 + 1), index = '';
// 遍历找到最小节点
for (let i in lists) {
if (lists[i] && lists[i].val < minNode.val) {
minNode = lists[i];
index = i;
}
}
node.next = minNode;
node = node.next;
minNode = minNode.next;
// 替换数组中的元素
lists[index] = minNode;
}
return head.next;
};
思路二:在合并二个列表的基础上合并多个
function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
let res = null;
for (const list of lists) {
res = mergeTwoList(res, list);
}
return res;
}
function mergeTwoList(list1: ListNode | null, list2: ListNode | null): ListNode | null {
if (!list1 || !list2) return list1 ? list1 : list2;
const head = new ListNode();
let tail = head;
while (list1 && list2) {
if (list1.val < list2.val) {
tail.next = list1;
list1 = list1.next;
}
else {
tail.next = list2;
list2 = list2.next;
}
tail = tail.next;
}
tail.next = list1 ? list1 : list2;
return head.next;
}
思路三:优化思路二,分而治之
function mergeKLists(lists: Array<ListNode | null>): ListNode | null {
return merge(lists, 0, lists.length - 1);
}
function merge(lists: Array<ListNode | null>, l: number, r: number): ListNode | null {
if (l === r) return lists[l];
if (l > r) return null;
const mid = (l + r) >> 1;
return mergeTwoList(merge(lists, l, mid), merge(lists, mid + 1, r));
}
function mergeTwoList(list1: ListNode | null, list2: ListNode | null): ListNode | null {
if (!list1 || !list2) return list1 ? list1 : list2;
const head = new ListNode();
let tail = head;
while (list1 && list2) {
if (list1.val < list2.val) {
tail.next = list1;
list1 = list1.next;
}
else {
tail.next = list2;
list2 = list2.next;
}
tail = tail.next;
}
tail.next = list1 ? list1 : list2;
return head.next;
}
15. 下一个排列
思路:两遍扫描(比较巧妙,包括倒序数组)
function nextPermutation(nums: number[]): void {
if (nums.length === 1) return;
let i = nums.length - 2;
while (i >= 0 && nums[i] >= nums[i + 1]) {
i--;
}
if (i >= 0) {
let j = nums.length - 1;
while (j > i && nums[j] <= nums[i]) {
j--;
}
swap(nums, i, j);
}
reverse(nums, i + 1);
};
function swap(nums: number[], i: number, j:number): void {
nums[i] = nums[i] + nums[j];
nums[j] = nums[i] - nums[j];
nums[i] = nums[i] - nums[j];
}
function reverse(nums: number[], start: number) : void {
let left = start, right = nums.length - 1;
while (left < right) {
swap(nums, left, right);
left++;
right--;
}
}
补充:交换两数
- 添加第三方临时变量
let a = 3, b = 5;
const temp = a;
a = b;
b = temp;
- 不使用临时变量(很巧妙)
let a = 3, b = 5;
a = a + b;
b = a - b;
a = a - b;
- 采用或运算特性,安全
let a = 3, b = 5;
a = a ^ b;
b = a ^ b;
a = a ^ b;
16. 最长有效括号
思路一:动态规划(时间复杂度->O(n), 空间复杂度O(n))
- 记录状态:
dp[i]表示以该位元素结尾的最长有效子字符串长度 - 确定状态转移方程
function longestValidParentheses(s: string): number {
let max = 0;
const length = s.length, dp = new Array(length).fill(0);
for (let i = 1; i < length; i++) {
if (s.charAt(i) === ')') {
if (s.charAt(i - 1) === '(') {
dp[i] = (i >= 2 ? dp[i - 2] : 0) + 2;
}
else if (i - dp[i - 1] > 0 && s.charAt(i - dp[i - 1] - 1) === '(') {
dp[i] = dp[i - 1] + (i - dp[i - 1] >= 2 ? dp[i - dp[i - 1] - 2] : 0) + 2;
}
max = Math.max(dp[i], max);
}
}
return max;
};
思路二:栈(时间复杂度->O(n), 空间复杂度O(n))
为了保持一致性,栈初始值放入 -1,表示子串是从0开始的
function longestValidParentheses(s: string): number {
let max = 0;
const stack = [-1];
for (let i = 0; i < s.length; i++) {
if (s.charAt(i) === '(') stack.push(i);
else {
stack.pop();
if (stack.length === 0) stack.push(i);
else max = Math.max(max, i - stack[stack.length - 1]);
}
}
return max;
};
思路三:正反向遍历(时间复杂度->O(n), 空间复杂度O(1))
function longestValidParentheses(s: string): number {
let left = 0, right = 0, max = 0;
const length = s.length;
// 正向遍历
for (let i = 0; i < length; i++) {
if (s.charAt(i) === '(') left++;
else right++;
if (left === right) max = Math.max(max, left * 2);
else if (right > left) left = right = 0;
}
// 反向遍历
left = right = 0;
for (let i = length - 1; i >= 0; i--) {
if (s.charAt(i) === '(') left++;
else right++;
if (left === right) max = Math.max(max, right * 2);
else if (left > right) left = right = 0;
}
return max;
};
17. 搜索旋转排序数组
类别:二分搜索变种
function search(nums: number[], target: number): number {
const length = nums.length;
if (length === 0) return -1;
if (length === 1) return nums[0] === target ? 0 : -1;
let left = 0, right = length - 1;
while (left <= right) {
let mid = (left + right) >> 1;
if (nums[mid] === target) return mid;
if (nums[0] <= nums[mid]) {
if (nums[0] <= target && target < nums[mid]) right = mid - 1;
else left = mid + 1;
}
else {
if (nums[mid] < target && target <= nums[length - 1]) left = mid + 1;
else right = mid - 1;
}
}
return -1;
};
3. 力扣others
24. 两两交换链表中的节点
思路:变量存储前后节点,一遍过
function swapPairs(head: ListNode | null): ListNode | null {
let prev: ListNode | null, curr: ListNode | null;
prev = new ListNode(), prev.next = head, curr = head, head = prev;
while (curr && curr.next !== null) {
const next = curr.next;
// 重整
curr.next = next.next;
next.next = curr;
prev.next = next;
// 移动
prev = curr;
curr = curr.next;
}
return head.next;
};
206. 反转链表
思路:通过变量,一遍过
function reverseList(head: ListNode | null): ListNode | null {
let curr = head, prev = null;
while (curr !== null) {
const next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
};
904. 水果成篮
思路:滑动窗口
关键点:counter记录篮子中存放水果的种类,records数组记录每种水果的数量
function totalFruit(fruits: number[]): number {
const length = fruits.length;
if (length <= 2) return length;
let left = 0, right = 0, max = 2, counter = 0;
// 记录每种水果的数目(下标->水果种类,值->水果数量)
const records = new Array(length).fill(0);
while (right < length) {
records[fruits[right]]++;
if (records[fruits[right]] === 1) counter++;
while (counter > 2) {
records[fruits[left]]--;
if (records[fruits[left]] === 0) counter--;
left++;
}
right++;
max = Math.max(max, right - left);
}
return max;
};