前端算法进阶 --005

138 阅读3分钟

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 次向下移动的方案数,即组合数:

image.png

因此我们直接计算出这个组合数即可。计算的方法有很多种:

  • 如果使用的语言有组合数计算的 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)。