二分查找
1. 二分查找
题目:704. 二分查找
_解法1:二分搜索(迭代)
/**
* @param {number[]} nums
* @param {number} target
* @return {number}
*/
var search = function (nums, target) {
return binarySearch(nums, target, 0, nums.length - 1)
};
/**
* 在 nums 的 [left, right] 中搜索 target, 返回搜索到值的索引
* @param {number[]} nums 执行搜索的目标数组
* @param {number} target 搜索的目标数
* @param {number} left 搜索的左边界
* @param {number} right 搜索的右边界
* @return {number} 目标数的索引, 不存在返回 -1
*/
var binarySearch = function (nums, target, left, right) {
while (left <= right) {
let mid = (left + right) >> 1
if (nums[mid] > target)
right = mid - 1
else if (nums[mid] < target)
left = mid + 1
else return mid
}
return -1
}
解法2:二分搜索(递归)
var search = function (nums, target) {
return binarySearch(nums, target, 0, nums.length - 1)
};
var binarySearch = function (nums, target, left, right) {
if (left > right) return -1
let mid = (left + right) >> 1
if (nums[mid] == target) return mid;
return nums[mid] < target
? binarySearch(nums, target, mid + 1, right)
: binarySearch(nums, target, left, mid - 1)
}
2. 第一个错误的版本
_解法1:二分
~~
是 js 中 浮点数 转 整数 最快的方法:console.log(~~6.95) // 最快
/**
* @param {function} isBadVersion()
* @return {function}
*/
var solution = function (isBadVersion) {
/**
* @param {integer} n Total versions
* @return {integer} The first bad version
*/
return function (n) {
let left = 1, right = n
while (left < right) {
// 浮点数 --> 整数
let mid = ~~(left + (right - left) / 2)
isBadVersion(mid) ? right = mid : left = mid + 1
}
return left
};
};
3. 搜索插入位置
题目:35. 搜索插入位置
_解法1:二分
/**
* @param {function} isBadVersion()
* @return {function}
*/
var solution = function (isBadVersion) {
/**
* @param {integer} n Total versions
* @return {integer} The first bad version
*/
return function (n) {
let left = 1, right = n
while (left < right) {
let mid = ~~(left + (right - left) / 2)
isBadVersion(mid) ? right = mid : left = mid + 1
}
return left
};
};
双指针
4. 有序数组的平方
题目:977. 有序数组的平方
_解法1:map + sort
/**
* https://leetcode-cn.com/problems/squares-of-a-sorted-array/
* 有序数组的平方
* @param {number[]} nums
* @return {number[]}
*/
var sortedSquares = function (nums) {
return nums.map(n => n * n).sort((a, b) => a - b)
};
解法2:双指针
var sortedSquares = function (nums) {
let left = 0, right = nums.length - 1, idx = right
let res = new Array(nums.length)
while (left <= right) {
if (Math.pow(nums[left], 2) > Math.pow(nums[right], 2))
res[idx--] = Math.pow(nums[left++], 2)
else
res[idx--] = Math.pow(nums[right--], 2)
}
return res
};
5. 轮转数组
题目:189. 轮转数组
_解法1:额外空间
/**
* https://leetcode-cn.com/problems/rotate-array/
* 轮转数组
* @param {number[]} nums
* @param {number} k
* @return {void} Do not return anything, modify nums in-place instead.
*/
var rotate = function (nums, k) {
let len = nums.length
let res = new Array(len)
// 计算好索引, 放到新数组
for (let i = 0; i < len; i++)
res[(i + k) % len] = nums[i]
// 修改传入的数组
for (let i = 0; i < len; i++)
nums[i] = res[i]
};
以上写法中,“修改传入的数组” 这步可以利用 JS API 简化:
var rotate = function (nums, k) {
let len = nums.length
let res = new Array(len)
for (let i = 0; i < len; i++)
res[(i + k) % len] = nums[i]
nums.splice(0, len, ...res)
};
解法2:JS API
var rotate = function (nums, k) {
nums.splice(0, 0, ...nums.splice(-(k % nums.length)))
};
var rotate = function (nums, k) {
const len = nums.length
nums.unshift(...nums.splice(len - k % len))
};
解法3:反转 3 次数组
思路:
nums = "----->-->"; k =3
result = "-->----->";
reverse "----->-->" we can get "<--<-----"
reverse "<--" we can get "--><-----"
reverse "<-----" we can get "-->----->"
/**
* 反转 3 次数组
*/
var rotate = function (nums, k) {
k %= nums.length
reverse(nums, 0, nums.length - 1)
reverse(nums, 0, k - 1)
reverse(nums, k, nums.length - 1)
}
/**
* 反转数组
*/
let reverse = (nums, start, end) => {
while (start < end)
[nums[start++], nums[end--]] = [nums[end], nums[start]]
}
6. 移动零
题目:283. 移动零 - 力扣(LeetCode) (leetcode-cn.com)
解法1:双指针
常规思路:
- 遍历时将整数全部移动到前面, 剩下的补 0
- left 记录不为 0 的数字个数
var moveZeroes = function (nums) {
let left = 0, right = 0
while (right < nums.length) {
if (nums[right] != 0)
nums[left++] = nums[right]
right++
}
while (left < nums.length)
nums[left++] = 0
};
优化成一轮循环:
- 遍历时 right 遇到不为 0 的数字,就和前面为 0 的 left 交换
- 遇到为 0 的数字,left 不动,right 继续往前
var moveZeroes = function (nums) {
let left = 0, right = 0
while (right < nums.length) {
if (nums[right] != 0) {
if (nums[left] == 0) {
[nums[left], nums[right]] = [nums[right], nums[left]]
}
left++
}
right++
}
};
7. 两数之和 II - 输入有序数组
提示:
2 <= numbers.length <= 3 * 104
-1000 <= numbers[i] <= 1000
numbers
按 非递减顺序 排列-1000 <= target <= 1000
- 仅存在一个有效答案
_解法1:map
题目要求常量级的额外空间,因此这种做法不符合题意
思路:
- 通过一次遍历,建立 numbers 中元素 和 对应索引 的 映射
- 再次遍历,寻找
target - numbers[i]
是否在 map 的 key 中
/**
* @param {number[]} numbers
* @param {number} target
* @return {number[]}
*/
var twoSum = function (numbers, target) {
let map = new Map()
numbers.forEach((val, i) => map.set(val, i))
for (let i = 0; i < numbers.length; i++)
if (map.has(target - numbers[i]))
return [i + 1, map.get(target - numbers[i]) + 1]
return []
};
解法2:双指针
思路:
- 关键在于输入的是有序数组,可以定义两个指针,分别指向首尾
- 计算两个指针指向元素和,大了就
right--
,小了就left++
var twoSum = function (numbers, target) {
let left = 0, right = numbers.length - 1
while (left < right) {
let sum = numbers[left] + numbers[right]
if (sum == target) return [left + 1, right + 1]
else if (sum > target) right--;
else left++;
}
return []
};
_解法3:暴力 + 二分
/**
* 二分
*/
var twoSum = function (numbers, target) {
for (let i = 0; i < numbers.length; i++) {
let idx = leftBoundBinarySearch(numbers, target - numbers[i], i + 1, numbers.length - 1)
if (idx !== -1) return [i + 1, idx + 1]
}
return []
};
/**
* 寻找左边界的二分搜索
*/
const leftBoundBinarySearch = (nums, target, left, right) => {
while (left <= right) {
let mid = (left + right) >> 1
if (nums[mid] >= target)
right = mid - 1
else if (nums[mid] < target)
left = mid + 1
}
if (left >= nums.length || nums[left] != target)
return -1
return left
}
8. 反转字符串
题目:344. 反转字符串
正常都是使用库函数:
var reverseString = function (s) { s.reverse() };
_解法1:双指针
/**
* @param {character[]} s
* @return {void} Do not return anything, modify s in-place instead.
*/
var reverseString = function (s) {
let left = 0, right = s.length - 1
while (left <= right)
[s[left++], s[right--]] = [s[right], s[left]]
}
解法2:递归
var reverseString = function (s) {
var help = (s, left, right) => {
if (left >= right) return
[s[left], s[right]] = [s[right], s[left]]
help(s, ++left, --right)
}
help(s, 0, s.length - 1)
};
9. 反转字符串中的单词 III
_解法1:高阶函数 API
/**
* @param {string} s
* @return {string}
*/
var reverseWords = function (s) {
return s.split(" ").map(s => s.split("").reverse().join("")).join(" ")
};
10. 链表的中间结点
题目:876. 链表的中间结点
_解法1:哈希
var middleNode = function (head) {
let map = new Map(), idx = 0
while (head) {
map.set(idx++, head)
head = head.next
}
return map.get(Math.ceil(--idx / 2))
};
_解法2:快慢指针
/**
* 快慢指针
*/
var middleNode = function (head) {
let slow = head, fast = head
while (fast.next) {
slow = slow.next
fast = fast.next
fast.next && (fast = fast.next)
}
return slow
};
/**
* 快慢指针
*/
var middleNode = function (head) {
let slow = head, fast = head
while (fast && fast.next) {
slow = slow.next
fast = fast.next.next
}
return slow
};
11. 删除链表的倒数第 N 个结点
提示:
- 链表中结点的数目为
sz
1 <= sz <= 30
0 <= Node.val <= 100
1 <= n <= sz
_解法1:双指针
*/
var removeNthFromEnd = function (head, n) {
if (!head.next) return null
let slow = head, fast = head
while (n--)
fast = fast.next
if (!fast) return head.next
while (fast && fast.next) {
slow = slow.next
fast = fast.next
}
slow.next = slow.next.next
return head
};
/**
* @param {ListNode} head
* @param {number} n
* @return {ListNode}
*/
var removeNthFromEnd = function (head, n) {
if (!head.next) return null
let slow = head, fast = head
while (n--)
fast = fast.next
if (!fast) return head.next
while (fast && fast.next) {
slow = slow.next
fast = fast.next
}
slow.next = slow.next.next
return head
};
解法2:递归
Java 可以跑通,JS 就不能跑通,有点奇怪。。
class Solution {
int cur = 0;
public ListNode removeNthFromEnd(ListNode head, int n) {
if (head == null) return null;
head.next = removeNthFromEnd(head.next, n);
cur++;
if (cur == n) return head.next;
return head;
}
}
滑动窗口
12. 无重复的最长子串
解法1:滑动窗口
var lengthOfLongestSubstring = function (s) {
let set = new Set(), max = 0
let left = 0, right = 0
while (right < s.length) {
let c = s[right]
while (set.has(c))
set.delete(s[left++])
set.add(s[right])
max = Math.max(max, right - left + 1)
right++
}
return max
};
13. 字符串的排列
题目:567. 字符串的排列
解法1:滑动窗口 + 字典
class Solution {
public boolean checkInclusion(String s1, String s2) {
// 不可能的情况
int m = s1.length(), n = s2.length();
if (m > n) return false;
// cnt 数组: 记录 s1 中字母出现的次数
// cur 数组: 记录滑动的窗口中的字母出现的次数
int[] cnt = new int[26], cur = new int[26];
for (char c : s1.toCharArray())
cnt[c - 'a']++;
for (int i = 0; i < m; i++)
cur[s2.charAt(i) - 'a']++;
for (int i = m; i < n; i++) {
cur[s2.charAt(i) - 'a']++;
cur[s2.charAt(i - m) - 'a']--;
if (Arrays.equals(cnt, cur)) return true;
}
return false;
}
}
var checkInclusion = function (s1, s2) {
let m = s1.length, n = s2.length
if (m > n) return false
let cnt1 = new Array(26).fill(0), cnt2 = new Array(26).fill(0)
for (let c of s1)
cnt1[c.charCodeAt() - 'a'.charCodeAt()]++
for (let i = 0; i < m; i++)
cnt2[s2[i].charCodeAt() - 'a'.charCodeAt()]++
if (check(cnt1, cnt2)) return true
for (let i = m; i < n; i++) {
cnt2[s2[i].charCodeAt() - 'a'.charCodeAt()]++
cnt2[s2[i - m].charCodeAt() - 'a'.charCodeAt()]--
if (check(cnt1, cnt2)) return true
}
return false
}
let check = (nums1, nums2) => {
for (let i = 0; i < nums1.length; i++)
if (nums1[i] != nums2[i])
return false
return true
}
DFS / BFS
14. 图像渲染
题目:733. 图像渲染
提示:
m == image.length
n == image[i].length
1 <= m, n <= 50
0 <= image[i][j], newColor < 216
0 <= sr < m
0 <= sc < n
解法1:DFS
class Solution {
public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
dfs(image, sr, sc, newColor, image[sr][sc]);
return image;
}
/**
* @param image 二维数组
* @param sr 当前行
* @param sc 当前列
* @param newColor 新颜色
* @param originColor 旧颜色
*/
void dfs(int[][] image, int sr, int sc, int newColor, int originColor) {
// 越界, 停止遍历
if (sr < 0 || sr >= image.length || sc < 0 || sc >= image[0].length) return;
// 不满足上色要求, 或是已经上色过, 停止遍历
if (image[sr][sc] != originColor || image[sr][sc] == newColor) return;
// 上色
image[sr][sc] = newColor;
dfs(image, sr - 1, sc, newColor, originColor);
dfs(image, sr, sc + 1, newColor, originColor);
dfs(image, sr + 1, sc, newColor, originColor);
dfs(image, sr, sc - 1, newColor, originColor);
}
}
const floodFill = function (image, sr, sc, newColor) {
dfs(image, sr, sc, newColor, image[sr][sc])
return image
};
const dfs = (image, sr, sc, newColor, originColor) => {
// 越界
if (sr < 0 || sr >= image.length || sc < 0 || sc >= image[0].length) return
// 不满足上色要求, 或已经上色过
if (image[sr][sc] != originColor || image[sr][sc] == newColor) return;
// 上色
image[sr][sc] = newColor
dfs(image, sr - 1, sc, newColor, originColor)
dfs(image, sr, sc + 1, newColor, originColor)
dfs(image, sr + 1, sc, newColor, originColor)
dfs(image, sr, sc - 1, newColor, originColor)
}
解法2:BFS
class Solution {
int[] dx = { 1, 0, 0, -1 };
int[] dy = { 0, 1, -1, 0 };
public int[][] floodFill(int[][] image, int sr, int sc, int newColor) {
// 保存初始颜色
int originColor = image[sr][sc];
// 如果初始颜色和新上的颜色相同, 直接返回原数组
if (originColor == newColor) return image;
// 初始位置入队
Queue<int[]> queue = new LinkedList<>() {{ offer(new int[] { sr, sc }); }};
// 上色
image[sr][sc] = newColor;
while (!queue.isEmpty()) {
int[] cell = queue.poll();
int x = cell[0], y = cell[1];
for (int i = 0; i < 4; i++) {
int mx = x + dx[i], my = y + dy[i];
// 没有越界, 并且还未上过色, 才继续搜索
if (mx >= 0 && mx < image.length && my >= 0 && my < image[0].length
&& image[mx][my] == originColor) {
queue.offer(new int[] { mx, my });
image[mx][my] = newColor;
}
}
}
return image;
}
}
15. 岛屿的最大面积
题目:695. 岛屿的最大面积
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 50
grid[i][j]
为0
或1
_解法1:DFS
使用全局变量:
let area = 0
var maxAreaOfIsland = function (grid) {
let max = -1
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[0].length; j++) {
area = 0
dfs(grid, i, j)
max = Math.max(max, area)
}
}
return max
};
const dfs = (grid, cr, cc) => {
// 越界
if (cr < 0 || cr >= grid.length || cc < 0 || cc >= grid[0].length) return
// 已访问过, 或者不是岛屿
if (grid[cr][cc] === -1 || grid[cr][cc] === 0) return
// 面积+1, 并标记已经访问
area++
grid[cr][cc] = -1
dfs(grid, cr - 1, cc) // 上
dfs(grid, cr, cc + 1) // 右
dfs(grid, cr + 1, cc) // 下
dfs(grid, cr, cc - 1) // 左
}
不使用全局变量:
var maxAreaOfIsland = function (grid) {
let max = -1
for (let i = 0; i < grid.length; i++)
for (let j = 0; j < grid[0].length; j++)
max = Math.max(max, dfs(grid, i, j))
return max
};
const dfs = (grid, cr, cc) => {
// 越界
if (cr < 0 || cr >= grid.length || cc < 0 || cc >= grid[0].length) return 0
// 已访问过, 或者不是岛屿
if (grid[cr][cc] === -1 || grid[cr][cc] === 0) return 0
// 面积+1, 并标记已经访问
grid[cr][cc] = -1
let area = 1
area += dfs(grid, cr - 1, cc) // 上
area += dfs(grid, cr, cc + 1) // 右
area += dfs(grid, cr + 1, cc) // 下
area += dfs(grid, cr, cc - 1) // 左
return area
}
16. 合并二叉树
题目:617. 合并二叉树
_解法1:递归
/**
* @param {TreeNode} root1
* @param {TreeNode} root2
* @return {TreeNode}
*/
var mergeTrees = function (root1, root2) {
if (root1 && !root2) return root1
if (!root1 && root2) return root2
if (!root1 && !root2) return null
let root = new TreeNode(root1.val + root2.val)
root.left = mergeTrees(root1.left, root2.left)
root.right = mergeTrees(root1.right, root2.right)
return root
}
17. 填充每个节点的下一个右侧节点指针
提示:
- 树中节点的数量在 [0, 212 - 1] 范围内
-1000 <= node.val <= 1000
进阶:
- 你只能使用常量级额外空间。
- 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。
_解法1:递归
var connect = function (root) {
if (!root) return null
if (root.left) {
// 处理下一层
root.left.next = root.right
// 循环处理后面的层
let tmp1 = root.left, tmp2 = root.right
while (tmp1.right) {
tmp1.right.next = tmp2.left
tmp1 = tmp1.right
tmp2 = tmp2.left
}
}
connect(root.left)
connect(root.right)
return root
}
评论区看到更简洁的递归:
var connect = function (root) {
if (!root) return null
if (root.left) {
root.left.next = root.right
if (root.next)
root.right.next = root.next.left
}
connect(root.left)
connect(root.right)
return root
}
18. 0 1 矩阵**
题目:542. 01 矩阵
解法1:BFS
/**
* BFS
* 首先将所有的 0 入队, 并且将 1 的位置设置为 -1, 表示该位置未被访问过
* 遍历队列中的 0, 如果四邻域的点是 -1, 表示这个点是没有访问过的 1
* 这个点到 0 的距离更新成 matrix[x][y] + 1
*/
class Solution {
public int[][] updateMatrix(int[][] matrix) {
// 首先将所有的 0 入队, 并且将 1 的位置设置为 -1, 表示该位置未被访问过
int m = matrix.length, n = matrix[0].length;
Queue<int[]> queue = new LinkedList<>();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (matrix[i][j] == 0) queue.offer(new int[] { i, j });
else matrix[i][j] = -1;
}
}
int[] dx = new int[] { -1, 1, 0, 0 };
int[] dy = new int[] { 0, 0, -1, 1 };
while (!queue.isEmpty()) {
int[] point = queue.poll();
int x = point[0], y = point[1];
for (int i = 0; i < 4; i++) {
int newX = x + dx[i], newY = y + dy[i];
// 如果四邻域的点是 -1, 表示这个点是未被访问过的 1
// 所以这个点到 0 的距离可以更新成 matrix[x][y] + 1
if (newX >= 0 && newX < m && newY >= 0 && newY < n // 没有越界
&& matrix[newX][newY] == -1) { // 没有访问过
matrix[newX][newY] = matrix[x][y] + 1;
queue.offer(new int[] { newX, newY });
}
}
}
return matrix;
}
}
解法2:DFS
速度很慢。
class Solution {
private int min;
public int[][] updateMatrix(int[][] mat) {
int m = mat.length, n = mat[0].length;
boolean[][] visited = new boolean[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (mat[i][j] == 0) continue;
min = Integer.MAX_VALUE;
dfs(mat, i, j, 0, visited);
mat[i][j] = min;
}
}
return mat;
}
void dfs(int[][] mat, int i, int j, int dis, boolean[][] visited) {
if (i < 0 || i >= mat.length || j < 0 || j >= mat[0].length || visited[i][j])
return;
// 剪枝
if (dis > min) return;
if (mat[i][j] == 0) {
min = Math.min(min, dis);
return;
}
// 标记已访问
visited[i][j] = true;
dfs(mat, i - 1, j, dis + 1, visited);
dfs(mat, i, j + 1, dis + 1, visited);
dfs(mat, i + 1, j, dis + 1, visited);
dfs(mat, i, j - 1, dis + 1, visited);
// 回溯
visited[i][j] = false;
}
}
19. 腐烂的橘子**
题目:994. 腐烂的橘子
解法1:BFS
写法 1:不使用方向数组
import java.util.Queue;
class Solution {
public int orangesRotting(int[][] grid) {
int m = grid.length, n = grid[0].length;
Queue<int[]> queue = new LinkedList<>();
// 将腐烂的橘子放到队列
int count = 0; // 新鲜橘子的个数
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1)
count++;
else if (grid[i][j] == 2)
queue.offer(new int[] { i, j });
}
}
int round = 0; // 腐烂的分钟数
// 当没有新鲜橘子 或者 队列为空 停止循环
while (count > 0 && !queue.isEmpty()) {
round++;
int size = queue.size();
for (int i = 0; i < size; i++) {
int[] orange = queue.poll();
int x = orange[0], y = orange[1];
if (x - 1 >= 0 && grid[x - 1][y] == 1) { // 上
grid[x - 1][y] = 2;
count--; // 每感染一个, 减少新鲜橘子的数量
queue.add(new int[] { x - 1, y });
}
if (x + 1 < m && grid[x + 1][y] == 1) { // 下
grid[x + 1][y] = 2;
count--;
queue.add(new int[] { x + 1, y });
}
if (y - 1 >= 0 && grid[x][y - 1] == 1) { // 左
grid[x][y - 1] = 2;
count--;
queue.add(new int[] { x, y - 1 });
}
if (y + 1 < n && grid[x][y + 1] == 1) { // 右
grid[x][y + 1] = 2;
count--;
queue.add(new int[] { x, y + 1 });
}
}
}
return count > 0 ? -1 : round;
}
}
写法2:使用方向数组:
class Solution {
public int orangesRotting(int[][] grid) {
int m = grid.length, n = grid[0].length;
Queue<int[]> queue = new LinkedList<>();
// 将腐烂的橘子放到队列
int count = 0; // 新鲜橘子的个数
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] == 1)
count++;
else if (grid[i][j] == 2)
queue.offer(new int[] { i, j });
}
}
int[] dx = { 1, -1, 0, 0 };
int[] dy = { 0, 0, -1, 1 };
int round = 0; // 橘子腐烂的时间
// 当没有新鲜橘子 或者 队列为空 停止循环
while (count > 0 && !queue.isEmpty()) {
round++;
int size = queue.size();
for (int i = 0; i < size; i++) {
int[] orange = queue.poll();
int x = orange[0], y = orange[1];
// 边界内某个方向有新鲜橘子, 则遍历
for (int j = 0; j < 4; j++) {
int newX = x + dx[j], newY = y + dy[j];
if (newX >= 0 && newX < m && newY >= 0 && newY < n &&
grid[newX][newY] == 1) {
grid[newX][newY] = 2; // 新鲜橘子变腐烂
count--; // 新鲜橘子数量减少
queue.offer(new int[] { newX, newY });
}
}
}
}
// 能腐烂的橘子都腐烂后, 新鲜橘子的数量还 > 0, 说明无法腐烂, 返回 - 1
return count > 0 ? -1 : round;
}
}
递归 / 回溯
20. 合并两个有序链表
题目:21. 合并两个有序链表
提示:
- 两个链表的节点数目范围是
[0, 50]
-100 <= Node.val <= 100
l1
和l2
均按 非递减顺序 排列
_解法1:递归
递归1:需要创建新结点
/*
* 递归(需要创建新结点)
* 旧写法
*/
let mergeTwoLists = function (list1: ListNode | null, list2: ListNode | null): ListNode | null {
if (!list1 && !list2) return null
if (list1 && !list2) return list1
if (!list1 && list2) return list2
// list1 && list2 的情况
return list1.val <= list2.val
? new ListNode(list1.val, mergeTwoLists(list1.next, list2))
: new ListNode(list2.val, mergeTwoLists(list1, list2.next))
};
/**
* 递归(需要创建新结点)
* 新写法(更简洁)
*/
mergeTwoLists = function (list1: ListNode | null, list2: ListNode | null): ListNode | null {
if (!list2) return list1
if (!list1) return list2
return list1.val <= list2.val
? new ListNode(list1.val, mergeTwoLists(list1.next, list2))
: new ListNode(list2.val, mergeTwoLists(list1, list2.next))
};
递归2:不需要创建新结点
const mergeTwoLists = function (list1: ListNode | null, list2: ListNode | null): ListNode | null {
if (!list1) return list2
if (!list2) return list1
if (list1.val < list2.val) {
list1.next = mergeTwoLists(list1.next, list2)
return list1
} else {
list2.next = mergeTwoLists(list1, list2.next)
return list2
}
}
解法2:迭代
const mergeTwoLists = function (list1: ListNode | null, list2: ListNode | null): ListNode | null {
let res = new ListNode(0), cur = res
while (list1 && list2) {
if (list1.val <= list2.val) {
cur.next = list1
list1 = list1.next
} else {
cur.next = list2
list2 = list2.next
}
cur = cur.next
}
cur.next = list1 ?? list2;
return res.next
}
21. 反转链表
题目:206. 反转链表
解法1:递归
递归核心思想 :
1 -> 2 <- 3 <- 4 <- 5
| |
res node
let reverseList = function (head: ListNode | null): ListNode | null {
if (!head || !head.next) return head
let node = reverseList(head.next)
head.next.next = head
head.next = null
return node
}
解法2:迭代
/**
* 迭代, map
*/
const reverseList = function (head: ListNode | null): ListNode | null {
let map = new Map(), idx = 0
while (head) {
map.set(idx++, head.val)
head = head.next
}
let newHead = new ListNode(0), res = newHead
while (idx > 0) {
newHead.next = new ListNode(map.get(--idx))
newHead = newHead.next
}
return res.next
}
/**
* 迭代, 交换指针
* 1 -> 2 -> 3 -> 4 -> null
* null <- 1 <- 2 <- 3 <- 4
*/
const reverseList = function (head: ListNode | null): ListNode | null {
// prev 前节点, cur 当前节点
let prev = null, cur = head
// 每次循环, 将 当前节点 指向 前节点, 然后 当前节点 和 前节点 后移
while (cur) {
let tmpNode = cur.next // 保存 当前节点 的下一节点
cur.next = prev // 当前节点 指向 前节点
prev = cur // 前节点后移
cur = tmpNode // 当前节点后移
}
return prev
}
22. 组合**
题目:77. 组合
解法1:回溯
这道题属于组合问题,重点在于找对遍历的开始位置
class Solution {
List<List<Integer>> res = new ArrayList<>();
int k, n;
public List<List<Integer>> combine(int n, int k) {
this.k = k;
this.n = n;
dfs(1, new ArrayList<>());
return res;
}
/**
* @param begin 开始搜索的数字
* @param path 已经访问过的路径
*/
void dfs(int begin, List<Integer> path) {
// 剪枝: 当前访问过 path 长度 + [begin, n] 的长度 < k
// 不可能走到长度为 k 的 path
if (path.size() + (n - begin + 1) < k) return;
// 访问过 k 个数字, 加入结果
if (path.size() == k) {
res.add(new ArrayList<>(path));
return;
}
for (int i = begin; i <= n; i++) {
path.add(i);
dfs(i + 1, path);
// 回溯
path.remove(path.size() - 1);
}
}
}
let res: number[][]
function combine(n: number, k: number): number[][] {
res = new Array()
dfs(n, k, 1, new Array())
return res
};
const dfs = function (n: number, k: number, begin: number, path: number[]) {
// 剪枝
if (path.length + (n - begin + 1) < k) return;
if (path.length == k) {
res.push([...path]);
return;
}
for (let i = begin; i <= n; i++) {
path.push(i)
dfs(n, k, i + 1, path);
// 回溯
path.pop()
}
}
23. 全排列**
题 目:46. 全排列
_解法1:回溯
这道题属于排列问题,每次遍历整个数组,用 visited 记录访问过的位置
class Solution {
List<List<Integer>> res = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
boolean[] visited = new boolean[nums.length];
dfs(nums, new ArrayList<>(), visited);
return res;
}
void dfs(int[] nums, List<Integer> path, boolean[] visited) {
if (path.size() == nums.length) {
res.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
if (visited[i]) continue;
// 标记已访问
visited[i] = true;
path.add(nums[i]);
// 继续搜索
dfs(nums, path, visited);
// 回溯
visited[i] = false;
path.remove(path.size() - 1);
}
}
}
let res: number[][]
function permute(nums: number[]): number[][] {
res = new Array();
let visited = new Array();
dfs(nums, new Array(), visited)
return res;
};
const dfs = function (nums: number[], path: number[], visited: boolean[]) {
if (path.length == nums.length) {
res.push([...path]);
return;
}
for (let i = 0; i < nums.length; i++) {
if (visited[i]) continue;
// 标记已访问
visited[i] = true;
path.push(nums[i]);
// 继续搜索
dfs(nums, path, visited)
// 回溯
visited[i] = false;
path.pop()
}
}
24. 字母大小写全排列*
解法1:回溯
这道题属于组合问题,重点在于找对遍历的开始位置
/**
* https://leetcode-cn.com/problems/letter-case-permutation/
* 字母大小写全排列
*/
class Solution {
List<String> res = new ArrayList<>();
public List<String> letterCasePermutation(String s) {
char[] cs = s.toCharArray();
dfs(cs, 0);
return res;
}
/**
* @param cs 搜索的字符数组
* @param begin 开始搜索的位置
*/
void dfs(char[] cs, int begin) {
res.add(String.valueOf(cs));
for (int i = begin; i < cs.length; i++) {
// 数字, 则跳过
if (isDigit(cs[i])) continue;
// 大小写反转
cs[i] = changeLetter(cs[i]);
// 搜索
dfs(cs, i + 1);
// 回溯, 大小写反转回来
cs[i] = changeLetter(cs[i]);
}
}
/**
* 反转大小写
* 'A' --> 'a'
* 'a' --> 'A'
*/
public char changeLetter(char c) {
return (c >= 'a' && c <= 'z') ? (char) (c - 32) : (char) (c + 32);
}
/**
* 判断是否是数字 (此题中的字符, 非字母即数字)
*/
public boolean isDigit(char c) {
return c >= '0' && c <= '9';
}
}
function letterCasePermutation(s: string): string[] {
let res = new Array()
const dfs = function (s: string, idx: number) {
res.push(s)
for (let i = idx; i < s.length; i++) {
// 数字, 跳过此轮循环
if (isDigit(s[i])) continue;
// 字母, 反转字母大小写
s = s.substr(0, i) + changeLetter(s[i]) + s.substr(i + 1)
// 搜索
dfs(s, i + 1)
// 回溯, 将字母大小写反转回来
s = s.substr(0, i) + changeLetter(s[i]) + s.substr(i + 1)
}
}
dfs(s, 0)
return res
};
/**
* 反转字母大小写
*/
const changeLetter = function (s: String): String {
return s >= 'a' && s <= 'z' ? s.toUpperCase() : s.toLowerCase()
}
/**
* 判断是否是数字
*/
const isDigit = function (s: String): Boolean {
return s >= '0' && s <= '9'
}
动态规划
25.爬楼梯
题目:70. 爬楼梯
我的题解:递归、记忆化递归 、动态规划
先来个大家都会的,很基本的算法基础:
- 解法1:递归(超时)
const climbStairs = function (n: number): number {
if (n <= 2) return n
return climbStairs(n - 1) + climbStairs(n - 2)
};
然后来个有些人不知道的,递归的常见优化思路:
- 解法2:记忆化递归
const climbStairs = function (n: number): number {
if (n <= 2) return n
let memory = new Array(n + 1).fill(0)
memory[1] = 1
memory[2] = 2
recur(n, memory)
return recur(n, memory)
};
const recur = function (n: number, mem: number[]): number {
if (mem[n] == 0)
mem[n] = recur(n - 1, mem) + recur(n - 2, mem)
return mem[n]
}
接下来是个标准的动态规划的思想
- 解法3:动态规划
const climbStairs = function (n: number): number {
let dp = new Array(n + 1)
dp[1] = 1
dp[2] = 2
for (let i = 3; i <= n; i++)
dp[i] = dp[i - 1] + dp[i - 2]
return dp[n]
}
最后是一个时间和空间都很好的做法,本质上是对 滚动数组 的优化:
- 解法4:优化空间复杂度
const climbStairs = function (n: number): number {
if (n <= 2) return n
let first = 1, second = 2
for (let i = 3; i <= n; i++) {
second = first + second
first = second - first
}
return second
};
26. 打家劫舍
题目:198. 打家劫舍
我的题解
动态规划的思想:
- 将复杂的原问题拆解成若干个简单的子问题
- 每个子问题仅仅解决 1 次,并保存它们的解
- 最后推导出原问题的解
可以用动态规划解决问题具备的特点:
- 最优子结构:通过求解子问题的最优解,可以获得原问题的最优解
- 无后效性:未来与过去无关
动态规划经验:
- 尝试为 dp 数组赋予一个合理的含义
- 尝试找到出 dp[i] 与 dp[i-1] 的关系
这一步的难度往往取决于第一步。
标准的动态规划
const rob = function (nums: number[]): number {
// 处理特殊情况
if (nums.length == 1) return nums[0]
if (nums.length == 2) return Math.max(nums[0], nums[1])
// dp[i] 偷窃第 i 号房屋的最高金额
let dp = new Array(nums.length)
dp[0] = nums[0]
dp[1] = Math.max(nums[0], nums[1])
for (let i = 2; i < nums.length; i++)
// 偷窃第 i 家的最高金额 =
// max{ 偷窃第 i -1 家的最大值, 偷窃第 i -2 家的最大值 + nums[i] }
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i])
return dp[nums.length - 1]
}
27. 三角形的最小路径和
我的题解:动态规范、递归
我发现这种题目,如果明确告诉你使用什么方法,大多是比较好做的
如果做题前不知道属于什么方法,难度就上升了很多。。
解法1:从上往下的动态规划
- 这种做法比较恶心,需要考虑的边界情况不少,对于我这个 dp 入门选手,首先是写出了这种做法
public int minimumTotal(List<List<Integer>> triangle) {
int m = triangle.size(), n = triangle.get(triangle.size() - 1).size();
// 特殊情况
if (m == 1)
return triangle.get(0).get(0);
if (m == 2)
return triangle.get(0).get(0) + Math.min(triangle.get(1).get(0), triangle.get(1).get(1));
// dp[i][j] 矩阵 i, j 位置的三角形的最小路径和
int[][] dp = new int[m][n];
dp[0][0] = triangle.get(0).get(0);
dp[1][0] = dp[0][0] + triangle.get(1).get(0);
dp[1][1] = dp[0][0] + triangle.get(1).get(1);
for (int i = 2; i < m; i++) {
for (int j = 0; j <= i; j++) {
int curNum = triangle.get(i).get(j);
// 首元素特殊处理
if (j == 0) {
dp[i][j] = dp[i - 1][j] + curNum;
continue;
}
// 尾元素特殊处理
if (j == i) {
dp[i][j] = dp[i - 1][j - 1] + curNum;
continue;
}
dp[i][j] = curNum + Math.min(dp[i - 1][j - 1], dp[i - 1][j]);
}
}
int min = Integer.MAX_VALUE;
for (int i = 0; i < m; i++)
min = Math.min(min, dp[m - 1][i]);
return min;
}
解法2:从下往上的动态规划,这是评论区大佬们的思路膜拜一下 > 以后考虑思路要开阔,多方面都可以考虑一下,说不定有惊喜
public int minimumTotal(List<List<Integer>> triangle) {
// 加1可以不用初始化最后一层
int[][] dp = new int[triangle.size() + 1][triangle.size() + 1];
for (int i = triangle.size() - 1; i >= 0; i--) {
List<Integer> curTr = triangle.get(i);
for (int j = 0; j < curTr.size(); j++)
dp[i][j] = Math.min(dp[i + 1][j], dp[i + 1][j + 1]) + curTr.get(j);
}
return dp[0][0];
}
对解法2的优化:使用一维数组
public int minimumTotal1(List<List<Integer>> triangle) {
// 只需要记录每一层的最小值即可
int[] dp = new int[triangle.size() + 1];
for (int i = triangle.size() - 1; i >= 0; i--) {
List<Integer> curTr = triangle.get(i);
for (int j = 0; j < curTr.size(); j++)
// 这里的dp[j] 使用的时候默认是上一层的,赋值之后变成当前层
dp[j] = Math.min(dp[j], dp[j + 1]) + curTr.get(j);
}
return dp[0];
}
解法3:递归(超时) > 很多题目,递归不一定是最优解,却也有可能是最优解,但是无论如何,可以多多锻炼自己的递归思维
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
return dfs(triangle, 0, 0);
}
private int dfs(List<List<Integer>> triangle, int i, int j) {
if (i == triangle.size()) return 0;
return Math.min(dfs(triangle, i + 1, j), dfs(triangle, i + 1, j + 1)) + triangle.get(i).get(j);
}
}
对解法3的优化:记忆化搜索 > 这个方法必须熟悉,可以强行把递归优化成不错的效率,应该是所有递归都能优化吧?
class Solution1 {
Integer[][] memo;
public int minimumTotal(List<List<Integer>> triangle) {
memo = new Integer[triangle.size()][triangle.size()];
return dfs(triangle, 0, 0);
}
private int dfs(List<List<Integer>> triangle, int i, int j) {
if (i == triangle.size()) return 0;
if (memo[i][j] != null) return memo[i][j];
return memo[i][j] = Math.min(dfs(triangle, i + 1, j), dfs(triangle, i + 1, j + 1)) + triangle.get(i).get(j);
}
}
位运算
28. 2的幂
题目:231. 2 的幂
我的题解
解法1:先来个比较容易的想到的模拟思路
> Math.pow
的时间复杂度其实不低,应该尽量减少这种计算
public boolean isPowerOfTwo(int n) {
int i = 0;
while (Math.pow(2, i) <= n) {
if (Math.pow(2, i) == n)
return true;
i++;
}
return false;
}
所以可以将上面的代码优化一下(其实没有太多区别)
public boolean isPowerOfTwo0(int n) {
int i = 0;
double num = 1;
while (num <= n) {
if ((num = Math.pow(2, i)) == n)
return true;
i++;
}
return false;
}
解法2:位运算 位运算中有个技巧可以直接记住:n & n-1
可以消去最后一位的1 同时,如果 n 为 2 的幂,它的二进制中必然只有一个 1
举例:
n = 6 (不为 2 的幂,二进制中有多个 1)
n 二进制为:110
n - 1 二进制为:101
n & n - 1 == 100
n = 4 (为 2 的幂, 二进制中只有一个 1)
n 二进制为:100
n - 1 二进制为:011
n & n - 1 == 000
public boolean isPowerOfTwo(int n) {
if (n <= 0) return false;
return (n & (n - 1)) == 0;
}
29. 位1的个数
题目:191. 位1的个数
我的题解
思路1:常规模拟
- 注意循环结束条件得是
n != 0
而不能是n > 0
- 注意 Java 中要使用
>>>
进行无符号右移
public int hammingWeight(int n) {
int count = 0;
while (n != 0) {
if ((n & 1) == 1)
count++;
n >>>= 1;
}
return count;
}
思路2:使用位运算 n & (n - 1)
n & (n - 1)
可以用来去掉 n 的二进制位最后一个 1
因此很适合用来判断是否是 2 的幂,因为 2 的幂的二进制中只会有 1 个 1
n = 10
n 的二进制 1010
n - 1 的二进制 1001
n & (n - 1) 的二进制 1000
public int hammingWeight(int n) {
int count = 0;
while (n != 0) {
n &= (n - 1);
count++;
}
return count;
}
思路3:转成二进制字符串进行操作
转成二进制字符串后思路就很多了,但是不建议用,毕竟还是逃避了原问题的考察点
public int hammingWeight(int n) {
String binStr = Integer.toBinaryString(n);
int count = 0;
for (int i = 0; i < binStr.length(); i++)
if (binStr.charAt(i) == '1')
count++;
return count;
}
30. 颠倒二进制位
题目:190. 颠倒二进制位
我的题解:API、位运算
调 API 的做法:
public class Solution {
public int reverseBits(int n) {
return Integer.reverse(n);
}
}
使用位运算的做法:
public class Solution {
public int reverseBits(int n) {
int res = 0;
int i = 32;
while (i-- > 0) {
res <<= 1;
res += (n & 1);
n >>= 1;
}
return res;
}
}
31. 只出现一次的数字
我的题解:位运算
^
异或常见知识:
- 交换律:
a ^ b ^ c
<=>a ^ c ^ b
- 任何数于 0 异或 为 任何数
0 ^ n => n
- 相同的数异或为 0
n ^ n => 0
function singleNumber(nums: number[]): number {
let res = 0
for (let num of nums)
res ^= num
return res
};