1. 移动零
1.1 思路一:暴力法
找出所有 0 的下标,然后删除掉所有 0 元素,再 push 相应的 0 的个数到末尾。
/**
* @param {number[]} nums
* @return {void} Do not return anything, modify nums in-place instead.
*/
var moveZeroes = function(nums) {
const len = nums.length
let i = 0;
while(i < len) {
if(nums[i] === 0) {
const n = nums.splice(i,1)
nums.push(n)
} else i ++
}
};
1.2 思路二:双指针
慢指针 j 从 0 开始,当快指针 i 遍历到非 0 元素的时候,i 和 j 位置的元素交换,然 后把 j + 1;
也就是说,快指针 i 遍历完毕后, [0, j) 区间就存放着所有非 0 元素,而剩余的[j, n]区间再遍历一次,用 0 填充满即可。
优化: 双指针 在上面的算法里,快指针遍历完成后,还要遍历慢指针到末尾来填充 0。实际上这题只要遇 到非 0 元素,就把当前位置的值和慢指针位置 j 的值交换,然后只有此时 j 才 + 1,即 可完成。
var moveZeroes = function (nums) {
let j = 0
for (let i = 0; i < nums.length; i++) {
if (nums[i] !== 0) {
swap(nums, i, j)
j++
}
}
}
function swap(nums, i, j) {
let temp = nums[i]
nums[i] = nums[j]
nums[j] = temp
}
2. 每日温度
给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
var dailyTemperatures = function (temperatures) {
let answer = new Array(temperatures.length).fill(0); // 初始化
let monotone = []; //栈数组
for (let i = 0; i < temperatures.length; i++) {
if (i > 0 && temperatures[i - 1] < temperatures[i]) {
// 从后往前循环数组模拟从栈顶到栈低
for (let j = monotone.length - 1; j >= 0; j--) {
if (temperatures[i] > temperatures[monotone[j]]) {
// 给answer数组中的对应元素赋值为两个元素下标差值
answer[monotone[j]] = i - monotone[j];
// 出栈
monotone.pop();
}
}
}
// 入栈当前元素下标
monotone.push(i);
}
return answer;
};
3. 旋转图像
给定一个 n × n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。
输入: matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出: [[7,4,1],[8,5,2],[9,6,3]]
3.1 方法一:使用辅助数组
const rotate = (matrix) => {
const n = matrix.length;
const matrix_new = new Array(n).fill(0).map(() => new Array(n).fill(0));
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
matrix_new[j][n - i - 1] = matrix[i][j];
}
}
for (let i = 0; i < n; i++) {
for (let j = 0; j < n; j++) {
matrix[i][j] = matrix_new[i][j];
}
}
return matrix_new
}
3.2 用翻转代替旋转
var rotate = function(matrix) {
const n = matrix.length;
// 水平翻转
for (let i = 0; i < Math.floor(n / 2); i++) {
for (let j = 0; j < n; j++) {
[matrix[i][j], matrix[n - i - 1][j]] = [matrix[n - i - 1][j], matrix[i][j]];
}
}
// 主对角线翻转
for (let i = 0; i < n; i++) {
for (let j = 0; j < i; j++) {
[matrix[i][j], matrix[j][i]] = [matrix[j][i], matrix[i][j]];
}
}
return matrix
};
4. 验证IP地址
给定一个字符串 queryIP。如果是有效的 IPv4 地址,返回 "IPv4" ;如果是有效的 IPv6 地址,返回 "IPv6" ;如果不是上述类型的 IP 地址,返回 "Neither" 。
有效的IPv4地址 是 “x1.x2.x3.x4” 形式的IP地址。 其中 0 <= xi <= 255 且 xi 不能包含 前导零。例如: “192.168.1.1” 、 “192.168.1.0” 为有效IPv4地址, “192.168.01.1” 为无效IPv4地址; “192.168.1.00” 、 “192.168@1.1” 为无效IPv4地址。
一个有效的IPv6地址 是一个格式为“x1:x2:x3:x4:x5:x6:x7:x8” 的IP地址,其中:
1 <= xi.length <= 4 xi 是一个 十六进制字符串 ,可以包含数字、小写英文字母( 'a' 到 'f' )和大写英文字母( 'A' 到 'F' )。 在 xi 中允许前导零。 示例 1:
输入:queryIP = "172.16.254.1"
输出:"IPv4"
解释:有效的 IPv4 地址,返回 "IPv4"
示例 2:
输入:queryIP = "172.16.254.1"
输出:"IPv4"
解释:有效的 IPv4 地址,返回 "IPv4"
输入:queryIP = "2001:0db8:85a3:0:0:8A2E:0370:7334"
输出:"IPv6"
解释:有效的 IPv6 地址,返回 "IPv6"
示例 3:
输入:queryIP = "256.256.256.256"
输出:"Neither"
解释:既不是 IPv4 地址,又不是 IPv6 地址
4.1 方法一:正则
const validIPAddress = (queryIP) =>{
return queryIP.match(/^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)($|(?!\.$)\.)){4}$/) ? "IPv4":
queryIP.match(/^(([\da-fA-F]{1,4})($|(?!:$):)){8}$/) ? "IPv6" : "Neither";
}
4.2 方法二:依次判断
var validIPAddress = function(queryIP) {
if (queryIP.indexOf('.') >= 0) {
// IPv4
let last = -1;
for (let i = 0; i < 4; ++i) {
const cur = (i === 3 ? queryIP.length : queryIP.indexOf('.', last + 1));
if (cur < 0) {
return "Neither";
}
if (cur - last - 1 < 1 || cur - last - 1 > 3) {
return "Neither";
}
let addr = 0;
for (let j = last + 1; j < cur; ++j) {
if (!isDigit(queryIP[j])) {
return "Neither";
}
addr = addr * 10 + (queryIP[j].charCodeAt() - '0'.charCodeAt());
}
if (addr > 255) {
return "Neither";
}
if (addr > 0 && queryIP[last + 1].charCodeAt() === '0'.charCodeAt()) {
return "Neither";
}
if (addr === 0 && cur - last - 1 > 1) {
return "Neither";
}
last = cur;
}
return "IPv4";
} else {
// IPv6
let last = -1;
for (let i = 0; i < 8; ++i) {
const cur = (i === 7 ? queryIP.length : queryIP.indexOf(':', last + 1));
if (cur < 0) {
return "Neither";
}
if (cur - last - 1 < 1 || cur - last - 1 > 4) {
return "Neither";
}
for (let j = last + 1; j < cur; ++j) {
if (!isDigit(queryIP[j]) && !('a' <= queryIP[j].toLowerCase() && queryIP[j].toLowerCase() <= 'f')) {
return "Neither";
}
}
last = cur;
}
return "IPv6";
}
};
const isDigit = (ch) => {
return parseFloat(ch).toString() === "NaN" ? false : true;
}
5. 三数之和
/**
* @param {number[]} nums
* @return {number[][]}
*/
const threeSum = (nums) => {
let ans = [];
const len = nums.length;
if(nums == null || len < 3) return ans;
nums.sort((a, b) => a - b); // 排序
for (let i = 0; i < len ; i++) {
if(nums[i] > 0) break; // 如果当前数字大于0,则三数之和一定大于0,所以结束循环
if(i > 0 && nums[i] == nums[i-1]) continue; // 去重
let L = i+1;
let R = len-1;
while(L < R){
const sum = nums[i] + nums[L] + nums[R];
if(sum == 0){
ans.push([nums[i],nums[L],nums[R]]);
while (L<R && nums[L] == nums[L+1]) L++; // 去重
while (L<R && nums[R] == nums[R-1]) R--; // 去重
L++;
R--;
}
else if (sum < 0) L++;
else if (sum > 0) R--;
}
}
return ans;
};
理解去重:
while (L<R && nums[L] == nums[L+1]) L++; // 去重
while (L<R && nums[R] == nums[R-1]) R--; // 去重
L++;
R--;
可以替换为下面的
while (L < R && nums[L] == nums[++L]);
while (L < R && nums[R] == nums[--R]);
解释:
arr = [2, 3, 3, 3, 3, 6, 7, 8]
当 L = 1 时
即 arr[L] = 3, arr[L + 1] = 3,
arr[L] 等于 arr[L + 1] 此时将 L++, L 的值就跳过一个
重复这个过程直到 arr[L + 1] 不是 3,跳出 while 此时 L + 1 的下标指向的值是 6,但 L 指向的是最后一个 3
所以在原四行的代码的第三行 才会 L++ 让 L 指向不是 3 的那个数。
原有四行的代码中
while (L<R && nums[L] == nums[L+1]) L++; // 去重,这个地方跳出的时候,L 是最后一个相同数字的下标
while (L<R && nums[R] == nums[R-1]) R--; // 去重
L++; // 这个地方又往后跳一个才是不相同数字的下标
R--;
而在两行的代码中
while (L < R && nums[L] == nums[++L]); // ++L 找到的是跳过所有相同的数字,遇到的第一个不同的数字的下标
while (L < R && nums[R] == nums[--R]);
6. 寻找两个正序数组的中位数
var findMedianSortedArrays = function(nums1, nums2) {
let len1 = nums1.length;
let len2 = nums2.length;
let middle;
let flag;
if ((len1 + len2) % 2 === 0) {
middle = parseInt((len1+len2)/2);
flag = 0;
} else {
middle = parseInt((len1+len2)/2) + 1;
flag = 1;
}
let i=0, j=0;
let arr = [];
while(i < len1 && j < len2) {
if (nums1[i] < nums2[j]) {
arr.push(nums1[i]);
i++;
} else {
arr.push(nums2[j]);
j++;
}
}
if (i === len1) {
arr = arr.concat(nums2.slice(j));
}
if (j === len2) {
arr = arr.concat(nums1.slice(i));
}
if (flag === 0) {
return parseFloat((arr[middle-1] + arr[middle]) / 2);
} else {
return arr[middle-1];
}
};
7. 和为K的子数组
var subarraySum = function(nums, k) {
const mp = new Map();
mp.set(0, 1);
let count = 0, pre = 0;
for (const x of nums) {
pre += x;
if (mp.has(pre - k)) {
count += mp.get(pre - k);
}
if (mp.has(pre)) {
mp.set(pre, mp.get(pre) + 1);
} else {
mp.set(pre, 1);
}
}
return count;
};
8. 不同路径
8.1 方法一:动态规划
思路与算法
我们用 f(i,j) 表示从左上角走到 (i,j) 的路径数量,其中 i 和 j 的范围分别是 [0,m) 和 [0,n)。
由于我们每一步只能从向下或者向右移动一步,因此要想走到 (i,j),如果向下走一步,那么会从 (i−1,j) 走过来;如果向右走一步,那么会从 (i,j−1) 走过来。因此我们可以写出动态规划转移方程:f(i,j)=f(i−1,j)+f(i,j−1)
需要注意的是,如果 i=0,那么 f(i−1,j) 并不是一个满足要求的状态,我们需要忽略这一项;同理,如果 j=0,那么 f(i,j−1) 并不是一个满足要求的状态,我们需要忽略这一项。初始条件为 f(0,0)=1,即从左上角走到左上角有一种方法。
最终的答案即为 f(m−1,n−1)。
细节
为了方便代码编写,我们可以将所有的f(0,j) 以及 f(i,0) 都设置为边界条件,它们的值均为 1。
var uniquePaths = function(m, n) {
const f = new Array(m).fill(0).map(() => new Array(n).fill(0));
for (let i = 0; i < m; i++) {
f[i][0] = 1;
}
for (let j = 0; j < n; j++) {
f[0][j] = 1;
}
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
f[i][j] = f[i - 1][j] + f[i][j - 1];
}
}
return f[m - 1][n - 1];
};
8.2 方法二:组合数学
思路与算法
从左上角到右下角的过程中,我们需要移动 m+n−2 次,其中有 m−1 次向下移动,n−1 次向右移动。因此路径的总数,就等于从 m+n−2 次移动中选择 m−1 次向下移动的方案数,即组合数:
因此我们直接计算出这个组合数即可。计算的方法有很多种:
-
如果使用的语言有组合数计算的 API,我们可以调用 API 计算;
-
如果没有相应的 API,我们可以使用 (m−1)!(m+n−2)(m+n−3)⋯n进行计算。
var uniquePaths = function(m, n) {
let ans = 1;
for (let x = n, y = 1; y < m; ++x, ++y) {
ans = Math.floor(ans * x / y);
}
return ans;
};
9. 岛屿数量
给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
9.1 方法1.dfs
思路:循环网格,深度优先遍历每个坐标的四周,注意坐标不要越界,遇到陆地加1,并沉没四周的陆地,这样就不会重复计算 复杂度:时间复杂度O(mn), m和n是行数和列数。空间复杂度是O(mn),最坏的情况下所有网格都需要递归,递归栈深度达到m * n
const numIslands = (grid) => {
let count = 0
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[0].length; j++) {//循环网格
if (grid[i][j] === '1') {//如果为陆地,count++,
count++
turnZero(i, j, grid)
}
}
}
return count
}
function turnZero(i, j, grid) {//沉没四周的陆地
if (i < 0 || i >= grid.length || j < 0
|| j >= grid[0].length || grid[i][j] === '0') return //检查坐标的合法性
grid[i][j] = '0'//让四周的陆地变为海水
turnZero(i, j + 1, grid)
turnZero(i, j - 1, grid)
turnZero(i + 1, j, grid)
turnZero(i - 1, j, grid)
}
9.2 方法2.bfs
思路:循环网格,广度优先遍历坐标的四周,遇到陆地加1,沉没四周的陆地,不重复计算陆地数 复杂度:时间复杂度O(mn),m和n是行数和列数。空间复杂度是O(min(m,n)),队列的长度最坏的情况下需要能容得下m和n中的较小者
class Solution {
public int numIslands(char[][] grid) {
if (grid == null || grid.length == 0) {
return 0;
}
int nr = grid.length;
int nc = grid[0].length;
int num_islands = 0;
for (int r = 0; r < nr; ++r) {
for (int c = 0; c < nc; ++c) {
if (grid[r][c] == '1') {
++num_islands;
grid[r][c] = '0';
Queue<Integer> neighbors = new LinkedList<>();
neighbors.add(r * nc + c);
while (!neighbors.isEmpty()) {
int id = neighbors.remove();
int row = id / nc;
int col = id % nc;
if (row - 1 >= 0 && grid[row-1][col] == '1') {
neighbors.add((row-1) * nc + col);
grid[row-1][col] = '0';
}
if (row + 1 < nr && grid[row+1][col] == '1') {
neighbors.add((row+1) * nc + col);
grid[row+1][col] = '0';
}
if (col - 1 >= 0 && grid[row][col-1] == '1') {
neighbors.add(row * nc + col-1);
grid[row][col-1] = '0';
}
if (col + 1 < nc && grid[row][col+1] == '1') {
neighbors.add(row * nc + col+1);
grid[row][col+1] = '0';
}
}
}
}
}
return num_islands;
}
}
10. 子数组的最小值之和
var sumSubarrayMins = function(arr) {
const n = arr.length;
let monoStack = [];
const left = new Array(n).fill(0);
const right = new Array(n).fill(0);
for (let i = 0; i < n; i++) {
while (monoStack.length !== 0 && arr[i] <= arr[monoStack[monoStack.length - 1]]) {
monoStack.pop();
}
left[i] = i - (monoStack.length === 0 ? -1 : monoStack[monoStack.length - 1]);
monoStack.push(i);
}
monoStack = [];
for (let i = n - 1; i >= 0; i--) {
while (monoStack.length !== 0 && arr[i] < arr[monoStack[monoStack.length - 1]]) {
monoStack.pop();
}
right[i] = (monoStack.length === 0 ? n : monoStack[monoStack.length - 1]) - i;
monoStack.push(i);
}
let ans = 0;
const MOD = 1000000007;
for (let i = 0; i < n; i++) {
ans = (ans + left[i] * right[i] * arr[i]) % MOD;
}
return ans;
};
复杂度分析
-
时间复杂度:O(n),其中 n 为数组的长度。利用单调栈求出每个元素为最小值的子序列长度需要的时间为 O(n),求出连续子数组的最小值的总和需要的时间为 O(n),因此总的时间复杂度为 O(n)。
-
空间复杂度:O(n)。其中 n 为数组的长度。我们需要保存以每个元素为最小元素的子序列长度,所需的空间为 O(n)。