LeetCode《算法入门》刷题笔记(31 题全)

LeetCode《算法入门》刷题笔记(31 题全)

二分查找

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. 第一个错误的版本

题目:278. 第一个错误的版本

_解法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 - 输入有序数组

题目:两数之和 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

题目:557. 反转字符串中的单词 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 个结点

题目:19. 删除链表的倒数第 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. 无重复的最长子串

题目:3. 无重复字符的最长子串

解法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]01

_解法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. 填充每个节点的下一个右侧节点指针

题目:116. 填充每个节点的下一个右侧节点指针

提示:

  • 树中节点的数量在 [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

题解:2种BFS,详解DP, 🤷‍♀️必须秒懂!

/**
 * 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
  • l1l2 均按 非递减顺序 排列

_解法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. 字母大小写全排列*

题目:784. 字母大小写全排列

解法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. 将复杂的原问题拆解成若干个简单的子问题
  2. 每个子问题仅仅解决 1 次,并保存它们的解
  3. 最后推导出原问题的解

可以用动态规划解决问题具备的特点:

  1. 最优子结构:通过求解子问题的最优解,可以获得原问题的最优解
  2. 无后效性:未来与过去无关

动态规划经验:

  1. 尝试为 dp 数组赋予一个合理的含义
  2. 尝试找到出 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. 三角形的最小路径和

题目:120. 三角形最小路径和

我的题解:动态规范、递归

我发现这种题目,如果明确告诉你使用什么方法,大多是比较好做的

如果做题前不知道属于什么方法,难度就上升了很多。。

解法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
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

萌宅鹿同学

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值