1. 程式人生 > 其它 >🔥 LeetCode 熱題 HOT 100(11-20)

🔥 LeetCode 熱題 HOT 100(11-20)

20. 有效的括號

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

        for(int i = 0; i < s.length(); i++) {
            char ch = s.charAt(i);
            //ch為左括號,直接入棧
            if (!map.containsKey(ch)) {
                stack.push(ch);
            } else {
                //ch為右括號,檢查ch與棧頂元素是否配對
                if (stack.isEmpty() || stack.pop() != map.get(ch)) {
                    return false;
                }
            }
        }

        //遍歷完所有括號後,stack為空則說明有效
        return stack.isEmpty();
    }
}

21. 合併兩個有序連結串列

迭代版:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummyNode = new ListNode(-1);
        ListNode temp = dummyNode;
        
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                temp.next = l1;
                l1 = l1.next;
            } else {
                temp.next = l2;
                l2 = l2.next;
            }

            temp = temp.next;
        }

        if (l1 != null) temp.next = l1;
        if (l2 != null) temp.next = l2;

        return dummyNode.next;
    }
}

遞迴版:編寫遞迴程式時一定不要用自己腦袋去模擬遞迴棧,而是要根據已知函式定義來寫程式碼

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        //base case
        if (l1 == null) return l2;
        if (l2 == null) return l1;
        
        if (l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }
}

遞迴不理解的推薦題解:畫解演算法:21. 合併兩個有序連結串列

22. 括號生成

思路:對於只有一種括號的情況,要生成合法括號字串只需要滿足:

  • 左邊的的括號數不多於括號對數

  • 右邊的括號數不多於左邊的括號數

因此,只需要用回溯法生成所有組合,然後除去不符合條件的即可

class Solution {
    public List<String> generateParenthesis(int n) {
        char[] candidates = new char[] {'(', ')'};
        StringBuilder track = new StringBuilder();

        dfs(candidates, track, n, 0, 0);

        return res;
    }

    private List<String> res = new LinkedList<>();

    //left, right 分別記錄當前track中左括號和右括號的數量
    private void dfs(char[] candidates, StringBuilder track, int n, int left, int right) {
        //去除不符合條件的結果,剪枝
        if (left > n || right > left) return;

        if (track.length() == 2*n) {
            res.add(track.toString());
            return;
        }

        for (int i = 0; i < candidates.length; i++) {
            //選擇
            track.append(candidates[i]);

            if (candidates[i] == '(') {
                dfs(candidates, track, n, left + 1, right);
            } else if (candidates[i] == ')') {
                dfs(candidates, track, n, left, right + 1);
            }

            //撤銷
            track.deleteCharAt(track.length() - 1);
        }
    } 
}

23. 合併K個升序連結串列

思路一:迴圈依次合併連結串列

思路二:兩兩歸併

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if (lists == null || lists.length == 0) {
            return null;
        }

        return mergeKLists(lists, 0, lists.length - 1);
    }

    //合併陣列中下標在[left, right]之間的連結串列
    private ListNode mergeKLists(ListNode[] lists, int left, int right) {
        if (left == right) {
            return lists[left];
        }

        int mid = left + (right - left) / 2;
        ListNode leftList = mergeKLists(lists, left, mid);
        ListNode rightList = mergeKLists(lists, mid + 1, right);

        return mergeTwoList(leftList, rightList);
    }

    private ListNode mergeTwoList(ListNode l1, ListNode l2) {
        ListNode dummyNode = new ListNode(-1);
        ListNode temp = dummyNode;
        
        while (l1 != null && l2 != null) {
            if (l1.val < l2.val) {
                temp.next = l1;
                l1 = l1.next;
            } else {
                temp.next = l2;
                l2 = l2.next;
            }

            temp = temp.next;
        }

        if (l1 != null) temp.next = l1;
        if (l2 != null) temp.next = l2;

        return dummyNode.next;
    }
}

思路三:優先佇列

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if (lists == null || lists.length == 0) {
            return null;
        }

        //優先佇列,小的連結串列結點位於佇列前
        Queue<ListNode> pq = new PriorityQueue<>((l1, l2) -> l1.val - l2.val);
        for (int i = 0; i < lists.length; i++) {
            if (lists[i] != null) {
                pq.offer(lists[i]);
            }
        }

        ListNode dummyNode = new ListNode(-1);
        ListNode temp = dummyNode;
        while (!pq.isEmpty()) {
            ListNode minNode = pq.poll();

            temp.next = minNode;
            temp = minNode;

            if (minNode.next != null) {
                pq.offer(minNode.next);
            }
        }

        return dummyNode.next;
    }

}

31. 下一個排列

簡單來說,對於數字排列來說字典順序可以理解為升序。

class Solution {
    public void nextPermutation(int[] nums) {
        int len = nums.length;

        //從右往左找找第一對升序數的位置,i指向較大元素
        int i = len - 1;
        while (i >= 1 && nums[i] <= nums[i - 1]) {
            i--;
        }

        //存在升序對時:
        //最壞情況下,交換升序對 nums[i - 1]、 nums[i]即可找到下一個排列;
        //好點的情況下,升序對右側(低位)可能存在 nums[k] 大於 nums[i - 1],那麼交換他們即可;
        if (i >= 1) {
            for (int k = len - 1; k >= i; k--) {
                if (nums[k] > nums[i - 1]) {
                    swap(nums, k, i - 1);
                    break;
                }  
            }
        }

        reverse(nums, i, len - 1);
    }

    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }

    private void reverse(int[] nums, int i, int j) {
        while (i < j) {
            swap(nums, i, j);
            i++;
            j--;
        }
    }
}

32. 最長有效括號

思路:利用棧儲存下標,方便計算有效括號子串長度。

  • 首先將 -1 入棧墊底
  • 當字元為'(',直接將下標入棧
  • 當字元為')',彈出棧頂元素,若此時棧不為空,說明配對成功,然後用')'下標減去此時棧頂元素下標即為當前有效括號子串長度;若此時棧為空,說明未配對成功,直接將')'下標入棧墊底。
class Solution {
    public int longestValidParentheses(String s) {
        Deque<Integer> stack = new LinkedList<>();
        stack.push(-1);

        int maxLen = 0;
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == '(') {
                stack.push(i);
            } else if (s.charAt(i) == ')'){
                stack.pop();

                // ) 配對成功
                if (!stack.isEmpty()) {
                    maxLen = Math.max(maxLen, i - stack.peek());
                // ) 配對失敗
                } else {
                    stack.push(i);
                }
            }
        }

        return maxLen;
    }
}

33. 搜尋旋轉排序陣列

思路:可知旋轉後的陣列分為前後兩段,都為升序,且前面一段始終大於後面一段。可以利用二分法,始終拿 nums[mid]nums[right] 比較然後縮小範圍,具體如下:

  • nums[mid] 小於 nums[right], 說明 mid 位於後半段,那麼 nums[mid, right]有序;
  • nums[mid] 大於 nums[right], 說明 mid 位於前半段,那麼 nums[left, mid] 有序。找到有序區間後可以根據 target 值快速縮小區間。
class Solution {
    public int search(int[] nums, int target) {
        int left = 0;
        int right = nums.length - 1;

        //跳出迴圈時,left、 right 相鄰,且都可能為target
        while (left + 1 < right) {
            int mid = left + (right - left) / 2;

            //找到了
            if (nums[mid] == target) {
                return mid;
            // mid 位於後半段上升段
            } else if (nums[mid] < nums[right]) {
                // target 位於[mid, right]之間
                if (target >= nums[mid] && target <= nums[right]) {
                    left = mid;
                } else {
                    right = mid;
                }
            // mid 位於前半段上升段
            } else if (nums[mid] > nums[right]) {
                // target 位於 [left, mid]之間
                if (target >= nums[left] && target <= nums[mid]) {
                    right = mid;
                } else {
                    left = mid;
                }
            }
            // 由於 left + 1 < right,且nums不包含重複數,故不可能出現nums[mid] == nums[right]的情況
        }

        if (nums[left] == target) return left;
        if (nums[right] == target) return right;

        return -1;
    }
}

34. 在排序陣列中查詢元素的第一個和最後一個位置

思路:帶邊界二分查詢,利用 while (left + 1 < right)容易處理邊界條件。

class Solution {
    public int[] searchRange(int[] nums, int target) {
        if (nums.length <= 0) return new int[] {-1, -1};

        //左邊界
        int left = searchBorder(nums, target, Border.LEFT);
        if (left == -1) return new int[] {-1, -1};

        //右邊界
        int right = searchBorder(nums, target, Border.RIGHT);
        return new int[] {left, right};
    }

    private int searchBorder(int[] nums, int target, Border border) {
        int left = 0;
        int right = nums.length - 1;

        //跳出迴圈時,left、 right 相鄰,且都可能為target
        while (left + 1 < right) {
            int mid = left + (right - left) / 2;

            if (nums[mid] == target) {
                //查詢左邊界
                if (border == Border.LEFT) {
                    right = mid;
                //查詢右邊界
                } else if (border == Border.RIGHT) {
                    left = mid;
                }
            } else if (nums[mid] > target) {
                right = mid;
            } else if (nums[mid] < target) {
                left = mid;
            }
        }

        //沒找到
        if (nums[left] != target && nums[right] != target) {
            return -1;
        }

        //查詢左邊界,優先返回左邊元素
        if (border == Border.LEFT) {
            return nums[left] == target ? left : right;
        //查詢右邊界,優先返回右邊元素
        } else if (border == Border.RIGHT) {
            return nums[right] == target ? right : left;
        } else {
            throw new IllegalArgumentException("非法選項");
        }
    }
}

//使用列舉代替靜態變數
enum Border {
    LEFT, RIGHT
}

39. 組合總和

思路:回溯法

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        LinkedList<Integer> track = new LinkedList<>();
        Arrays.sort(candidates); //排序後可實現進一步剪枝
        dfs(candidates, track, target, 0);
        return res;
    }

    private List<List<Integer>> res = new LinkedList<>();

    private void dfs(int[] candidates, LinkedList<Integer> track, int target, int start) {
        //不會再出現target小於0的情況
        //base case
        if (target == 0) {
            res.add(new LinkedList<>(track));
            return;
        }

        //通過start改變可選列表
        for (int i = start; i < candidates.length; i++) {
            //由於此時candidates是有序的,candidates[i] 後面的元素都大於target,直接忽略
            if (target - candidates[i] < 0) {
                break;
            }
            
            track.offerLast(candidates[i]);
            dfs(candidates, track, target - candidates[i], i);
            track.pollLast();
        }
    }
}

推薦題解:回溯演算法 + 剪枝(回溯經典例題詳解)

42. 接雨水

推薦一種不使用單調棧而是使用備忘錄的解法:

思路一:在左邊找大於等於當前高度的最大值,右邊也找大於等於當前高度的最大值,兩者取最小值再減去當前高度即為當前下標所能接的雨水量。

class Solution {
    public int trap(int[] height) {
        if (height == null || height.length <= 2) {
            return 0;
        }
        int len = height.length;

        //分別記錄元素左邊和右邊的最大值
        int[] leftMax = new int[len];
        int[] rightMax = new int[len];

        //最左邊元素左邊的最大值
        leftMax[0] = height[0];
        //最右邊元素右邊的最大值
        rightMax[len - 1] = height[len - 1];

        for (int i = 1; i < len; i++) {
            leftMax[i] = Math.max(leftMax[i - 1], height[i]);
        }

        for (int i = len - 2; i >= 0; i--) {
            rightMax[i] = Math.max(rightMax[i + 1], height[i]);
        }

        int area = 0;
        for (int i = 1; i < len - 1; i++) {
            area += Math.min(leftMax[i], rightMax[i]) - height[i];
        }

        return area;
    }
}

思路二:單調棧

class Solution {
    public int trap(int[] height) {
        if (height == null || height.length <= 2) {
            return 0;
        }
        
        //用來儲存元素下標
        Deque<Integer> stack = new LinkedList<>();

        int area = 0;
        for (int i = 0; i < height.length; i++) {
            while(!stack.isEmpty() && height[i] > height[stack.peek()]) {
                int top = stack.pop();

                if (stack.isEmpty()) {
                    break;
                }

                int width = i - stack.peek() - 1;
                int high = Math.min(height[stack.peek()], height[i]) - height[top];

                area += width * high;
            }
            //入棧
            stack.push(i);
        }

        return area;
    }
}

推薦圖解幫助理解:【接雨水】單調遞減棧,簡潔程式碼,動圖模擬