Java - 高频 LeetCode 算法题

1 - 链表

1.1 反转链表 - easy

迭代法:使用三个指针,分别指向当前节点、前一个节点和后一个节点,逐个翻转节点的指向。

public class Solution {
    public ListNode ReverseList (ListNode head) {
        ListNode a = null;
        ListNode b = head;

        while(b != null) {
            ListNode c = b.next;
            b.next = a;
            a = b;
            b = c;
        }
        return a;
    }
}

1.4 合并两个排序的链表 - easy

双指针法:创建一个哑节点作为头节点,使用两个指针分别指向当前节点,比较节点值,将较小的节点加到新链表中,并移动指针。

  • 时间复杂度为 O(n+m),空间复杂度为 O(1)
public class Solution {
    public ListNode Merge (ListNode pHead1, ListNode pHead2) {
        ListNode dummy = new ListNode(0);
        ListNode tail = dummy;

        while (pHead1 != null && pHead2 != null) {
            if (pHead1.val < pHead2.val) {
                tail.next = pHead1;
                pHead1 = pHead1.next;
            } else {
                tail.next = pHead2;
                pHead2 = pHead2.next;
            }
            tail = tail.next;
        }
        tail.next = (pHead1 != null) ? pHead1 : pHead2;

        return dummy.next;
    }
}

1.7 链表中环的入口结点

可以使用双指针法,fast 每次移动两个节点,slow 每次移动一个节点,如果它们在途中相遇 ,说明这个链表有环。

如图,slow 走了 x + y 步,则 fast 走了 (x + y) * 2 步,所以 (x + y) * 2 = x + y + n * (y + z),令 n = 1解得 x = z。

public ListNode EntryNodeOfLoop(ListNode pHead) {
    if (pHead == null) return null;
    ListNode slow = pHead;
    ListNode fast = pHead;
    
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) {
            ListNode entry = pHead;
            while (entry != slow) {
                entry = entry.next;
                slow = slow.next;
            }
            return entry;
        }
    }
    return null;
}

1.8 链表中倒数最后k个结点 - easy

快慢指针法:先让快指针走k步,然后快慢指针一起走,直到快指针到达链表末尾,此时慢指针即为倒数第k个节点。

  • 时间复杂度为 O(n),空间复杂度为 O(1)
public class Solution {
    public ListNode FindKthToTail (ListNode pHead, int k) {
        ListNode slow = pHead;
        ListNode fast = pHead;

        for (int i = 0; i < k; i++) {
            if (fast == null) return null;
            fast = fast.next;
        }

        while (fast != null) {
            slow = slow.next;
            fast = fast.next;
        }
        return slow;
    }
}

1.10 两个链表的第一个公共结点 - easy

双指针法:同时遍历这两个链表,每次移动一个节点。如果在 ptr1 到达了链表 A 的末尾,将其重新指向链表 B 的头节点。

同样,如果 ptr2 到达了链表 B 的末尾,将其重新指向链表 A 的头节点。直到两个指针相遇,相遇点就是第一个公共节点。

  • 时间复杂度为 O(n),空间复杂度为 O(1)
public class Solution {
    public ListNode FindFirstCommonNode(ListNode pHead1, ListNode pHead2) {
        ListNode a = pHead1;
        ListNode b = pHead2;

        while (a != b) {
            a = (a == null) ? pHead2 : a.next;
            b = (b == null) ? pHead1 : b.next;
        }
        return a;
    }
}

1.11 链表相加(二) - medium

递归法:首先让指针指向相同位置的节点。然后递归地将相同位置的节点值相加,并考虑进位。最后处理进位和剩余的节点。

  • 时间复杂度为 O(max(n, m)),空间复杂度为 O(max(n, m))
public class Solution {
    public ListNode addInList (ListNode head1, ListNode head2) {
        if (head1 == null) return head2;
        if (head2 == null) return head1;
        
        head1 = reverseList(head1);     // 1.1 反转链表,略
        head2 = reverseList(head2);

        ListNode dummy = new ListNode(0);
        ListNode tail = dummy;
        int carry = 0;

        while (head1 != null || head2 != null) {
            int x = (head1 != null) ? head1.val : 0;
            int y = (head2 != null) ? head2.val : 0;
            int sum = x + y + carry;
            carry = sum / 10;
            tail.next = new ListNode(sum % 10);
            tail = tail.next;
            if (head1 != null) head1 = head1.next;
            if (head2 != null) head2 = head2.next;
        }
        return reverseList(dummy.next);
    }
}

1.12 单链表的排序 - medium

归并排序:将链表分成两半,递归地对每一半进行排序,然后合并两个已排序的链表。

  • 时间复杂度为 O(nlogn),空间复杂度为 O(1)
public class Solution {
    public ListNode sortInList(ListNode head) {
        if (head == null || head.next == null) return head;

        ListNode left = head;
        ListNode mid = head.next;
        ListNode right = head.next.next;

        // right 指针指向右段的末尾节点,mid 指针指向中间节点
        while (right != null && right.next != null) {
            left = left.next;
            mid = mid.next;
            right = right.next.next;
        }   
        // left 指针指向左段的末尾节点,从这里断开
        left.next = null;

        // 1.4 合并两个排序的链表,略
        return merge(sortInList(head), sortInList(mid));
    }
}

2 - 二分查找、排序

2.1 二分查找 - easy

双指针法:通过比较数组中间元素与目标值,根据比较结果调整搜索范围,直到找到目标值或搜索范围为空。

public class Solution {
    public int search (int[] nums, int target) {
        if (nums == null || nums.length == 0) return -1;

        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int mid = (left + right) / 2;
            if (nums[mid] < target) {
                left = mid + 1;
            } else if (nums[mid] > target){
                right = mid - 1;
            } else {
                return mid;
            }
        }
        return -1;
    }
}

2.3 寻找峰值 - median

二分查找:利用数组的特性,通过比较中间元素与相邻元素,确定峰值所在的区间,然后继续在该区间内查找。

public class Solution {
    public int findPeakElement (int[] nums) {
        if (nums.length == 1) return 0;

        int left = 0, right = nums.length - 1;
        while (left < right) {
            int mid = (left + right) / 2;
            if (nums[mid] < nums[mid + 1]) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        return left;
    }
}

2.4 数组中的逆序对 - median

归并排序:提供了一种优雅且高效的方式来统计逆序对,在归并的过程中统计逆序对的数量。

  • 时间复杂度为 O(nlogn),空间复杂度为 O(n)
public class Solution {
    private long count = 0;

    public int InversePairs (int[] nums) {
        mergeSort(nums, 0, nums.length - 1);
        return (int) (count % 1000000007);
    }

    private void mergeSort(int[] nums, int left, int right) {
        if (left < right) {
            int mid = (left + right) / 2;
            mergeSort(nums, left, mid);
            mergeSort(nums, mid + 1, right);
            merge(nums, left, mid, right);
        }
    }

    private void merge(int[] nums, int left, int mid, int right) {
        // temp 数组用于保存合并后的有序结果
        int[] temp = new int[right - left + 1];
        int i = left, j = mid + 1, k = 0;

        while (i <= mid && j <= right) {
            if (nums[i] > nums[j]) {
                // nums[i] 右侧的所有元素与 nums[j] 形成逆序对
                count += (mid - i + 1);
                temp[k++] = nums[j++];
            } else {
                temp[k++] = nums[i++];
            }
        }
        while (i <= mid) temp[k++] = nums[i++];
        while (j <= right) temp[k++] = nums[j++];

        for (int r = left; r <= right; r++) {
            nums[r] = temp[r - left];
        }
    }
}

2.5 旋转数组的最小数字 - easy

二分查找:通过比较中间元素与首尾元素,确定最小值所在的区间,然后继续在该区间内查找。

  • 时间复杂度为 O(logn),空间复杂度为 O(1)
public class Solution {
    public int minNumberInRotateArray (int[] nums) {
        if (nums.length == 1) return nums[0];

        int left = 0, right = nums.length - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (nums[mid] < nums[right]) {
                right = mid;
            } else if (nums[mid] > nums[right]) {
                left = mid + 1;
            } else {
                right--;
            }
        }
        return nums[left];
    }
}

2.6 比较版本号 - median

逐个比较:逐个字符比较版本号,遇到 '.' 时分隔出一个修订号进行比较。

  • 时间复杂度为 O(n + m),其中 n 和 m 分别是两个版本号的长度
public class Solution {
    public int compare (String version1, String version2) {
        String[] v1 = version1.split("\\.");
        String[] v2 = version2.split("\\.");
        int maxLen = Math.max(v1.length, v2.length);

        for (int i = 0; i < maxLen; i++) {
            int x = (i < v1.length) ? Integer.parseInt(v1[i]) : 0;
            int y = (i < v2.length) ? Integer.parseInt(v2[i]) : 0;
            if (x < y) {
                return -1;
            } else if (x > y){
                return 1;
            }
        }
        return 0;
    }
}

3 - 二叉树

3.1 二叉树的前序遍历 - easy

递归法:前序遍历的顺序是 “根左右”。可以定义一个递归函数,首先访问根节点,然后递归地遍历左子树,最后递归地遍历右子树。

public class Solution {
    public int[] preorderTraversal (TreeNode root) {
        List<Integer> result = new ArrayList<>();
        preorder(root, result);
        
        int[] res = new int[result.size()];
        for (int i = 0; i < result.size(); i++) {
            res[i] = result.get(i);
        }
        return res;
    }

    private void preorder(TreeNode node, List<Integer> result) {
        if (node == null) return;
        result.add(node.val);
        preorder(node.left, result);
        preorder(node.right, result);
    }
}

3.2 二叉树的中序遍历 - easy

递归法:中序遍历的顺序是 “左根右”。可以定义一个递归函数,首先递归地遍历左子树,然后访问根节点,最后递归地遍历右子树。

public class Solution {
    public int[] inorderTraversal (TreeNode root) {
        List<Integer> result = new ArrayList<>();
        inorder(root, result);
        
        int[] res = new int[result.size()];
        for (int i = 0; i < result.size(); i++) {
            res[i] = result.get(i);
        }
        return res;
    }

    private void inorder(TreeNode node, List<Integer> result) {
        if (node == null) return;
        inorder(node.left, result);
        result.add(node.val);
        inorder(node.right, result);
    }
}

3.3 二叉树的后序遍历 - easy

递归法:后序遍历的顺序是 “左右根”。可以定义一个递归函数,首先递归地遍历左子树,然后递归地遍历右子树,最后访问根节点。

public class Solution {
    public int[] postorderTraversal (TreeNode root) {
        List<Integer> result = new ArrayList<>();
        postorder(root, result);
        
        int[] res = new int[result.size()];
        for (int i = 0; i < result.size(); i++) {
            res[i] = result.get(i);
        }
        return res;
    }

    private void postorder(TreeNode node, List<Integer> result) {
        if (node == null) return;
        postorder(node.left, result);
        postorder(node.right, result);
        result.add(node.val);
    }
}

3.6 二叉树的最大深度 - easy

递归法:深度优先搜索(DFS),递归地计算左右子树的最大深度,然后取两者的最大值加 1(当前节点)。

public class Solution {
    public int maxDepth (TreeNode root) {
        if (root == null) return 0;
        int leftDepth = maxDepth(root.left);
        int rightDepth = maxDepth(root.right);
        return Math.max(leftDepth, rightDepth) + 1;
    }
}

3.8 二叉搜索树与双向链表


3.10 合并二叉树 - easy

递归法:前序遍历两棵树,对于每个节点,如果两棵树都有对应的节点,则将它们的值相加并创建一个新的节点;

如果只有一棵树有节点,则直接使用那个节点;如果两棵树都没有节点,则返回空。

public class Solution {
    public TreeNode mergeTrees (TreeNode t1, TreeNode t2) {
        if (t1 == null) return t2;
        if (t2 == null) return t1;
        
        TreeNode node = new TreeNode(t1.val + t2.val);
        node.left = mergeTrees(t1.left, t2.left);
        node.right = mergeTrees(t1.right, t2.right);
        return node;
    }
}

4 - 堆、栈、队列

4.1 用两个栈实现队列 - easy

一个栈用于入队操作,另一个栈用于出队操作。当出队时,如果出队栈为空,将入队栈的所有元素移动到出队栈中。

public class Solution {
    Stack<Integer> stack1 = new Stack<Integer>();
    Stack<Integer> stack2 = new Stack<Integer>();

    public void push(int node) {
        stack1.push(node);
    }

    public int pop() {
        if (stack2.isEmpty()) {
            while (!stack1.isEmpty()) {
                stack2.push(stack1.pop());
            }
        }
        return stack2.pop();
    }
}

4.2 包含min函数的栈 - easy

使用两个栈,一个存储所有元素,另一个辅助栈存储当前的最小值。

每次push操作时,将新元素与辅助栈顶部元素比较,更新辅助栈。pop和min操作分别对应两个栈的相应操作。

public class Solution {
    Stack<Integer> stack = new Stack<>();
    Stack<Integer> minStack = new Stack<>();

    public void push(int node) {
        if (minStack.isEmpty() || node <= minStack.peek()) {
            minStack.push(node);
        }
        stack.push(node);
    }

    public void pop() {
        if (stack.peek().equals(minStack.peek())) {
            minStack.pop();
        }
        stack.pop();
    }

    public int top() {
        return stack.peek();
    }

    public int min() {
        return minStack.peek();
    }
}

4.3 有效括号序列 - easy

使用一个栈存储右括号。遍历字符串,左括号时压栈,右括号时匹配栈顶元素。最后检查栈是否为空,空则有效,非空则无效。

public class Solution {
    public boolean isValid (String s) {
        Stack<Character> stack = new Stack<>();
        Map<Character, Character> map = new HashMap<>();
        map.put(')', '(');
        map.put(']', '[');
        map.put('}', '{');

        for (char c : s.toCharArray()) {
            if (map.containsKey(c)) {
                if (stack.isEmpty() || stack.pop() != map.get(c)) {
                    return false;
                }
            } else {
                stack.push(c);
            }
        }
        return stack.isEmpty();
    }
}

4.4 滑动窗口的最大值 - hard

使用双指针定义窗口,结合双端队列存储窗口内的最大值。随着窗口的滑动,更新双端队列以保持最大值在队列头部。

public class Solution {
    public ArrayList<Integer> maxInWindows (int[] num, int size) {
        ArrayList<Integer> result = new ArrayList<>();
        if (size == 0 || size > num.length) return result;
        Deque<Integer> deque = new LinkedList<>();

        for (int i = 0; i < num.length; i++) {
            // 移除比当前元素小的元素
            while (!deque.isEmpty() && num[i] > num[deque.peekLast()]) {
                deque.pollLast();
            }
            deque.offer(i);
            // 维护窗口大小 [i - size + 1, i]
            if (deque.peek() < i - size + 1) {
                deque.poll();
            }
            // 窗口大小到达要求时,添加最大值
            if (i >= size - 1) {
                result.add(num[deque.peek()]);
            }
        }
        return result;
    }
}

4.5 最小的 K 个数 - medium

使用最大堆,维护数组中最小的K个数。遍历数组,将元素与堆顶比较,必要时替换堆顶并调整堆。

public class Solution {
    public ArrayList<Integer> GetLeastNumbers_Solution (int[] input, int k) {
        PriorityQueue<Integer> q = new PriorityQueue<>((x, y) -> y - x);
        ArrayList<Integer> result = new ArrayList<>();
        if (k == 0) return result;

        for (int i = 0; i < input.length; i++) {
            q.offer(input[i]);
            if (q.size() > k) {
                q.poll();
            }
        }
        for (int i = 0; i < k; i++) {
            result.add(q.poll());
        }
        return result;
    }
}

4.7 数据流中的中位数 - medium

一个最大堆存储较小一半的元素,一个最小堆存储较大一半的元素。根据元素数量的奇偶性,调整两个堆的大小,并计算中位数。

import java.util.*;

public class Solution {
    private PriorityQueue<Integer> maxHeap = new PriorityQueue<>(Collections.reverseOrder());
    private PriorityQueue<Integer> minHeap = new PriorityQueue<>();

    public void Insert(Integer num) {
        if (maxHeap.isEmpty() || num < maxHeap.peek()) {
            maxHeap.offer(num);
        } else {
            minHeap.offer(num);
        }
        if (maxHeap.size() > minHeap.size() + 1) {      // 保持两个堆的平衡,确保最大堆的大小最多只比最小堆大1
            minHeap.offer(maxHeap.poll());
        } else if (minHeap.size() > maxHeap.size()) {
            maxHeap.offer(minHeap.poll());
        }
    }

    public Double GetMedian() {
        if (maxHeap.size() > minHeap.size()) {          // 如果最大堆的元素个数多,中位数就是最大堆的堆顶
            return (double) maxHeap.peek();
        } else {                                        // 如果最小堆的元素个数多,中位数是两个堆顶的平均值
            return (maxHeap.peek() + minHeap.peek()) / 2.0;
        }
    }
}

5 - 哈希、贪心

5.2 数组中出现次数超过一半的数字 - easy

使用哈希表来记录每个数字出现的次数,然后遍历哈希表找到出现次数超过一半的数字。

public class Solution {
    public int MoreThanHalfNum_Solution (int[] numbers) {
        HashMap<Integer, Integer> map = new HashMap<>();
        
        for (int num : numbers) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }

        for (int num : numbers) {
            if (map.get(num) > numbers.length / 2) {
                return num;
            }
        }
        return 0;
    }
}

5.3 数组中只出现一次的两个数字 - medium

使用哈希表记录每个数字出现的次数,然后找到出现次数为1的两个数字。

public class Solution {
    public int[] FindNumsAppearOnce (int[] nums) {
        HashMap<Integer, Integer> map = new HashMap<>();
        List<Integer> result = new ArrayList<>();

        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }
        for (int num : nums) {
            if (map.get(num) == 1) {
                result.add(num);
            }
        }
        if (result.get(0) < result.get(1)) {
            return new int[] {result.get(0), result.get(1)};
        } else {
            return new int[] {result.get(1), result.get(0)};
        }
    }
}

5.4 缺失的第一个正整数 - medium

使用哈希表记录数组中所有数字的出现,然后从1开始依次枚举正整数,并判断其是否在哈希表中。

public class Solution {
    public int minNumberDisappeared(int[] nums) {
        Set<Integer> set = new HashSet<>();

        // 将数组中的所有数字加入哈希表
        for (int num : nums) {
            set.add(num);
        }
        // 枚举正整数,查找缺失的第一个正整数
        for (int i = 1; i <= nums.length; i++) {
            if (!set.contains(i)) {
                return i;
            }
        }
        // 如果所有从1到n的正整数都存在,那么缺失的数就是n+1
        return nums.length + 1;
    }
}

6 - 递归、回溯

6.1 没有重复项的全排列 - medium

回溯法:使用一个数组来存储当前的排列,然后递归地尝试数字,如果该数字没有被使用过,就添加到排列中,并继续递归。

如果当前排列的长度等于数组的长度,则将其添加到结果中。然后回溯,移除最后一个添加的数字,尝试下一个数字。

public class Solution {
    ArrayList<ArrayList<Integer>> result = new ArrayList<>();

    public ArrayList<ArrayList<Integer>> permute(int[] num) {
        backtrack(new ArrayList<>(), num);
        return result;
    }

    private void backtrack(ArrayList<Integer> res, int[] num) {
        if (res.size() == num.length) {
            result.add(new ArrayList<>(res));
            return;
        }
        for (int i = 0; i < num.length; i++) {
            if (res.contains(num[i])) continue;
            res.add(num[i]);
            backtrack(res, num);
            res.remove(res.size() - 1);
        }
    }
}

6.2 有重复项的全排列 - medium

回溯法:这个问题与没有重复项的全排列类似。在递归之前,对数组进行排序,并且在递归中添加一个判断,

如果当前数字与前一个数字相同,并且前一个数字已经被使用过,则跳过当前数字,以避免重复的排列。

public class Solution {
    ArrayList<ArrayList<Integer>> result = new ArrayList<>();

    public ArrayList<ArrayList<Integer>> permuteUnique (int[] num) {
        Arrays.sort(num);
        backtrack(new ArrayList<>(), num, new boolean[num.length]);
        return result;
    }

    private void backtrack(ArrayList<Integer> res, int[] num, boolean[] used) {
        if (res.size() == num.length) {
            result.add(new ArrayList<>(res));
            return;
        }

        for (int i = 0; i < num.length; i++) {
            if (used[i]) continue;
            if (i > 0 && num[i] == num[i - 1] && !used[i - 1]) continue;
            res.add(num[i]);
            used[i] = true;
            backtrack(res, num, used);
            res.remove(res.size() - 1);
            used[i] = false;
        }
    }
}

6.3 岛屿数量 - medium

深度优先搜索(DFS):遍历每一个单元格,如果是岛屿的一部分,则使用DFS来标记所有相连的岛屿单元格,同时计数器加一。

public class Solution {
    public int solve (char[][] grid) {
        int count = 0;
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[0].length; j++) {
                if (grid[i][j] == '1') {
                    dfs(i, j, grid);
                    count++;
                }
            }
        }
        return count;
    }

    private void dfs(int x, int y, char[][] grid) {
        if (x < 0 || y < 0 || x >= grid.length || y >= grid[0].length) return;
        if (grid[x][y] == '0') return;

        grid[x][y] = '0';
        dfs(x + 1, y, grid);
        dfs(x - 1, y, grid);
        dfs(x, y + 1, grid);
        dfs(x, y - 1, grid);
    }
}

6.4 字符串的排列 - medium

回溯法:使用一个字符串来存储当前的排列,然后递归地尝试字符,如果该字符没有被使用过,就添加到当前排列中,并继续递归。

如果当前排列的长度等于原始字符串的长度,则将其添加到结果中。然后回溯,移除最后一个添加的字符,尝试下一个字符。

public class Solution {
    ArrayList<String> result = new ArrayList<>();

    public ArrayList<String> Permutation(String str) {
        char[] charStr = str.toCharArray();
        Arrays.sort(charStr);
        backtrack(new StringBuffer(), charStr, new boolean[charStr.length]);
        return result;
    }

    private void backtrack(StringBuffer res, char[] str, boolean[] used) {
        if (res.length() == str.length) {
            result.add(res.toString());
            return;
        }
        for (int i = 0; i < str.length; i++) {
            if (used[i]) continue;
            if (i > 0 && str[i] == str[i - 1] && !used[i - 1]) continue;
            
            res.append(str[i]);
            used[i] = true;
            backtrack(res, str, used);
            res.deleteCharAt(res.length() - 1);
            used[i] = false;
        }
    }
}

6.6 括号生成 - medium

回溯法:使用一个字符串来存储当前的括号组合,然后递归地尝试添加左括号或右括号。

如果左括号的数量大于右括号,就添加一个左括号;反正亦然。如果相等,说明找到了一个有效的括号组合,则添加到结果中。

public class Solution {
    ArrayList<String> result = new ArrayList<>();

    public ArrayList<String> generateParenthesis (int n) {
        backtrack("", n, 0, 0);
        return result;
    }

    private void backtrack(String res, int n, int x, int y) {
        if (res.length() == n * 2) {
            result.add(res);
            return;
        }
        if (x < n) backtrack(res + '(', n, x + 1, y);
        if (y < x) backtrack(res + ')', n, x, y + 1);
    }
}

7 - 动态规划

7.1 斐波那契数列

题目已经把递推公式直接给我们了,即状态转移方程 dp[i] = dp[i - 1] + dp[i - 2]

public int Fibonacci (int n) {
    // write code here
    if(n == 1 || n == 2) return 1;
    return Fibonacci(n - 1) + Fibonacci(n - 2);
}

7.2 跳台阶

① 确定含义:爬到第 i 层楼梯,有 dp[i] 种方法;② 递推公式: dp[i] = dp[i - 1] + dp[i - 2]

③ 初始化:dp[1] = 1,dp[2] = 2;④ 遍历顺序:从前往后

public int jumpFloor (int n) {
    if(n == 1) return 1;
    
    int[] dp = new int[n + 1];
    dp[1] = 1;
    dp[2] = 2;
    for(int i = 3; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }
    return dp[n];
}

7.3 最小花费爬楼梯 - easy

状态转移方程:将问题分解为子问题,即到达第 i 个台阶的最小花费,可以通过第 i-1 个台阶或第 i-2 个台阶到达。

public class Solution {
    public int minCostClimbingStairs (int[] cost) {
        int[] dp = new int[cost.length + 1];

        for (int i = 2; i <= cost.length; i++) {
            dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
        }
        return dp[cost.length];
    }
}

7.4 最长公共子序列(二)- medium

状态转移方程:构建一个二维数组,dp[i][j] 表示 s1 的前 i 个字符和 s2 的前 j 个字符的最长公共子序列长度。

public class Solution {
    public String LCS (String s1, String s2) {
        int m = s1.length(), n = s2.length();
        int[][] dp = new int[m + 1][n + 1];
        
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                } else {
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
                }
            }
        }

        StringBuilder sb = new StringBuilder();
        int i = m, j = n;
        while (i > 0 && j > 0) {
            if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
                sb.insert(0, s1.charAt(i - 1));
                i--;
                j--;
            } else if (dp[i - 1][j] > dp[i][j - 1]) {
                i--;
            } else {
                j--;
            }
        }
        return sb.toString().isEmpty() ? "-1" : sb.toString();
    }
}

7.5 最长公共子串 - medium

状态转移方程:与最长公共子序列类似,但是要求子串必须是连续的。

public class Solution {
    public String LCS (String str1, String str2) {
        int m = str1.length(), n = str2.length();
        int len = 0, end = 0;
        int[][] dp = new int[m + 1][n + 1];
        
        // 1. 遍历两个字符串,填充 dp 数组
        for (int i = 1; i < m + 1; i++) {
            for (int j = 1; j < n + 1; j++) {
                if (str1.charAt(i - 1) == str2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1] + 1;
                    if (dp[i][j] > len) {
                        len = dp[i][j];
                        end = i;
                    }
                }
            }
        }
        // 2. 根据结束索引和最长长度,截取最长公共子串
        return str1.substring(end - len, end);
    }
}

7.7 矩阵的最小路径和 - medium

最优问题:使用一个与原矩阵同样大小的 dp 数组,dp[i][j] 表示到达矩阵 (i,j) 位置的最小路径和。

public class Solution {
    public int minPathSum (int[][] matrix) {
        int m = matrix.length, n = matrix[0].length;
        int[][] dp = new int[m][n];
        dp[0][0] = matrix[0][0];

        for (int i = 1; i < m; i++) {
            dp[i][0] = dp[i - 1][0] + matrix[i][0];
        }
        for (int j = 1; j < n; j++) {
            dp[0][j] = dp[0][j - 1] + matrix[0][j];
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + matrix[i][j];
            }
        }
        return dp[m - 1][n - 1];
    }
}

7.10 最长上升子序列(一)- medium

优化问题:使用二分查找来优化查找下一个更大元素的过程,从而减少时间复杂度。

public class Solution {
    public int LIS (int[] arr) {
        if (arr.length == 0) return 0;
        int[] dp = new int[arr.length];
        Arrays.fill(dp, 1);

        int maxLen = 1;
        for (int i = 1; i < arr.length; i++) {
            for (int j = 0; j < i; j++) {
                if (arr[j] < arr[i]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
            maxLen = Math.max(maxLen, dp[i]);
        }
        return maxLen;
    }
}

7.17 打家劫舍(一)- medium

边界问题:构建状态转移方程,dp[i] 表示到第 i 家时的最大抢劫金额,需要考虑是否抢劫当前家,以及是否抢劫前一家。

public class Solution {
    public int rob (int[] nums) {
        int[] dp = new int[nums.length + 1];
        dp[1] = nums[0];

        // 前 i - 1 个房子最多能偷到的金额 dp[i - 1]
        // 前 i - 2 个房子最多能偷到的金额 dp[i - 2]
        // 第 i 个房子的金额 nums[i - 1]
        
        for (int i = 2; i <= nums.length; i++) {
            dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i - 1]);
        }
        return dp[nums.length];
    }
}

8 - 字符串、双指针

8.1 字符串变形 - medium

StringBuilder:首先对每个字符进行大小写转换,然后翻转整个字符串,接着以空格为界,对每个单词进行二次翻转,最终返回。

public class Solution {
    public String trans (String s, int n) {
        if (n == 1) return s;
        StringBuilder sb = new StringBuilder();

        for (char c : s.toCharArray()) {
            if (c <= 'Z' && c >= 'A') {
                sb.append((char)(c - 'A' + 'a'));
            } else if (c >= 'a' && c <= 'z') {
                sb.append((char)(c - 'a' + 'A'));
            } else {
                sb.append(c);
            }
        }
        sb.reverse();
        for (int i = 0; i < n; i++) {
            int j = i;
            while (j < n && sb.charAt(j) != ' ') j++;
            StringBuilder str = new StringBuilder(sb.substring(i, j));
            sb.replace(i, j, str.reverse().toString());
            i = j;
        } 
        return sb.toString();
    }
}

8.2 最长公共前缀 - easy

纵向扫描:将字符串数组看作一个二维空间。每一次从第一列开始确定字符,逐层扫描后面每一列,遇到不同字符停止扫描。

public class Solution {
    public String longestCommonPrefix (String[] strs) {
        if (strs.length == 0) return "";
        int m = strs.length, n = strs[0].length();

        for (int i = 0; i < n; i++) {
            char c = strs[0].charAt(i);
            for (int j = 1; j < m; j++) {
                if (i >= strs[j].length() || strs[j].charAt(i) != c) {
                    return strs[0].substring(0, i);
                }
            }
        }
        return strs[0];
    }
}

8.9 反转字符串 - easy

双指针法:通过双指针从两端向中间遍历,交换字符来实现。

public class Solution {
    public String solve (String str) {
        char[] chars = str.toCharArray();
        int left = 0, right = chars.length - 1;

        while (left < right) {
            char temp = chars[left];
            chars[left] = chars[right];
            chars[right] = temp;
            left++;
            right--;
        }
        return new String(chars);
    }
}

8.10 最长无重复子数组 - medium

滑动窗口:通过维护一个窗口来不断更新最长无重复子数组的长度。

public class Solution {
    public int maxLength (int[] arr) {
        int n = arr.length;
        if (n == 0 || n == 1) return n;
        Map<Integer, Integer> map = new HashMap<>();

        int maxLen = 0, left = 0;
        for (int right = 0; right < n; right++) {
            int num = arr[right];
            if (map.containsKey(num)) {
                left = Math.max(left, map.get(num) + 1);
            }
            map.put(num, right);
            maxLen = Math.max(maxLen, right - left + 1);
        }
        return maxLen;
    }
}

8.11 盛水最多的容器 - medium

双指针法:通过维护两个指针来遍历数组,计算在不同位置的 “雨水” 量。

public class Solution {
    public int maxArea (int[] height) {
        int maxArea = 0, left = 0, right = height.length - 1;
        
        while (left < right) {
            int h1 = height[left], h2 = height[right];
            int area = Math.min(h1, h2) * (right - left);
            maxArea = Math.max(maxArea, area);
            
            if (h1 < h2) left++;
            else right--;
        }
        return maxArea;
    }
}

9 - 模拟

9.3 顺时针旋转矩阵 - medium

通过交换矩阵的上三角和下三角元素,将矩阵转置。然后对每一行的元素进行左右翻转。

public class Solution {
    public int[][] rotateMatrix (int[][] mat, int n) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < i; j++) {
                int temp = mat[i][j];
                mat[i][j] = mat[j][i];
                mat[j][i] = temp;
            }
        }
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n / 2; j++) {
                int temp = mat[i][j];
                mat[i][j] = mat[i][n - j - 1];
                mat[i][n - j - 1] = temp;
            }
        }
        return mat;
    }
}

9.4 设计 LRU 缓存结构 - hard

使用 HashMap 来存储缓存的键值对,它会按照访问顺序排序,方便快速定位最近访问的元素。

使用 LinkedHashSet 来存储缓存的键,它的元素有序,且不重复,可以快速定位最老的元素。

public class Solution {
    private int capacity;
    Map<Integer, Integer> map = new HashMap<>();
    Set<Integer> set = new LinkedHashSet<>();

    public Solution(int capacity) {
        this.capacity = capacity;
    }

    public int get(int key) {
        if (map.containsKey(key)) {
            set.remove(key);                // 更新 Set,保证最近使用的元素在末尾
            set.add(key);
            return map.get(key);
        }
        return -1;
    }
    public void set(int key, int value) {
        if (!map.containsKey(key) && map.size() == capacity) {  // 如果缓存已满,则移除最老的元素
            int oldestKey = set.iterator().next();
            map.remove(oldestKey);
            set.remove(oldestKey);
        }
        map.put(key, value);                // 新增或更新 Map
        set.add(key);                       // 新增 Set
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容