java刷代码随想录

数组

704. 二分查找

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1
示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

提示:

  1. 你可以假设 nums 中的所有元素是不重复的。
  2. n 将在 [1, 10000]之间。
  3. nums 的每个元素都将在 [-9999, 9999]之间。
class Solution {
    public int search(int[] nums, int target) {
        int left = 0, right = nums.length - 1;
        while (left <= right) {
            int middle = (left + right) / 2;
            if (target == nums[middle]) {
                return middle;
            } else if (nums[middle] > target) {
                right = middle - 1;
            } else {
                left = middle + 1;
            }
        }
        return -1;
    }
}

35.搜索插入位置

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

请必须使用时间复杂度为 O(log n) 的算法。

示例 1:

输入: nums = [1,3,5,6], target = 5
输出: 2

示例 2:

输入: nums = [1,3,5,6], target = 2
输出: 1

示例 3:

输入: nums = [1,3,5,6], target = 7
输出: 4

提示:

  • 1 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • nums无重复元素升序 排列数组
  • -104 <= target <= 104
class Solution {
    public int searchInsert(int[] nums, int target) {
        if (target > nums[nums.length - 1]) return nums.length;
        if (target < nums[0]) return 0;
        int left = 0;
        int right = nums.length - 1;
        int mid = 0;
        while (left <= right) {
            mid = (right + left) / 2;
            if (nums[mid] == target) return mid;
            if (nums[mid] > target) right = mid - 1;
            else left = mid + 1;
        }
        if (nums[mid] > target) return mid;
        else return mid + 1;
    }
}

27. 移除元素

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

示例 1:

输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。

示例 2:

输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,4,0,3]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

提示:

  • 0 <= nums.length <= 100
  • 0 <= nums[i] <= 50
  • 0 <= val <= 100
class Solution {
    public int removeElement(int[] nums, int val) {
        int left = 0;
        for (int right = 0; right < nums.length; right++) {
            if(nums[right]!=val){
                nums[left]=nums[right];
                left++;
            }
        }
        return left;
    }
}

26.删除排序数组中的重复项

给你一个 升序排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。

考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:

  • 更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
  • 返回 k

示例 1:

输入:nums = [1,1,2]
输出:2, nums = [1,2,_]
解释:函数应该返回新的长度 2 ,并且原数组 nums 的前两个元素被修改为 1, 2 。不需要考虑数组中超出新长度后面的元素。

示例 2:

输入:nums = [0,0,1,1,1,2,2,3,3,4]
输出:5, nums = [0,1,2,3,4]
解释:函数应该返回新的长度 5 , 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4 。不需要考虑数组中超出新长度后面的元素。

提示:

  • 1 <= nums.length <= 3 * 104
  • -104 <= nums[i] <= 104
  • nums 已按 升序 排列
class Solution {
    public int removeDuplicates(int[] nums) {
        int left=1;
        int right = 1;
        while(right< nums.length){
            //由于数组是有序的,重复的元素只能相邻
            if(nums[left-1]!=nums[right]){
                nums[left]=nums[right];
                left++;
            }
            right++;
        }
        return left;
    }
}

977.有序数组的平方

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

示例 1:

输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]

示例 2:

输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]

提示:

  • 1 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • nums 已按 非递减顺序 排序
class Solution {
    public int[] sortedSquares(int[] nums) {
        int[] result = new int[nums.length];
        int left = 0, right = nums.length - 1, target = nums.length - 1;
        while (left <= right)
            //比较数组的两端
            if (nums[left] * nums[left] > nums[right] * nums[right]) {
                result[target--] = nums[left] * nums[left];
                left++;
            } else {
                result[target--] = nums[right] * nums[right];
                right--;
            }
        return result;
    }
}

209.长度最小的子数组

给定一个含有 n 个正整数的数组和一个正整数 target

找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度如果不存在符合条件的子数组,返回 0

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0

提示:

  • 1 <= target <= 109
  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 105
class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        //滑动窗口
        int left = 0, right = 0;
        int lrsum = 0;
        int result = nums.length + 1;

        while (right < nums.length) {
            lrsum += nums[right];
            while (lrsum >= target) {
                result = Math.min(result, right - left + 1);
                lrsum -= nums[left];
                left++;
            }
            right++;
        }
        return result == nums.length + 1 ? 0 : result;
    }
}

59.螺旋矩阵II

给你一个正整数 n ,生成一个包含 1n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix

示例 1:

img

输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]

示例 2:

输入:n = 1
输出:[[1]]

提示:

  • 1 <= n <= 20
class Solution {
    public int[][] generateMatrix(int n) {
        int[][] mat = new int[n][n];
        int l = 0, r = n - 1, t = 0, b = n - 1;
        int num = 1, tar = n * n;
        while (num <= tar) {
            for (int i = l; i <= r; i++) {
                mat[t][i] = num++;
            }
            t++;
            for (int i = t; i <= b; i++) {
                mat[i][r] = num++;
            }
            r--;
            for (int i = r; i >= l; i--) {
                mat[b][i] = num++;
            }
            b--;
            for (int i = b; i >= t; i--) {
                mat[i][l] = num++;
            }
            l++;
        }
        return mat;
    }
}

54.螺旋矩阵

给你一个 mn 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。

示例 1:

img

输入:matrix = [[1,2,3],[4,5,6],[7,8,9]]
输出:[1,2,3,6,9,8,7,4,5]

示例 2:

img

输入:matrix = [[1,2,3,4],[5,6,7,8],[9,10,11,12]]
输出:[1,2,3,4,8,12,11,10,9,5,6,7]

提示:

  • m == matrix.length
  • n == matrix[i].length
  • 1 <= m, n <= 10
  • -100 <= matrix[i][j] <= 100
class Solution {
    public List<Integer> spiralOrder(int[][] matrix) {
        int m = matrix.length;
        int n = matrix[0].length;
        List<Integer> list = new ArrayList<>();
        int tar = m * n, count = 1;
        int l = 0, r = n - 1, t = 0, b = m - 1;
        while (true) {
            for (int i = l; i <= r; i++) {
                list.add(matrix[t][i]);
                count++;
                if (count > tar) {
                    return list;
                }
            }
            t++;
            for (int i = t; i <= b; i++) {
                list.add(matrix[i][r]);
                count++;
                if (count > tar) {
                    return list;
                }
            }
            r--;
            for (int i = r; i >= l; i--) {
                list.add(matrix[b][i]);
                count++;
                if (count > tar) {
                    return list;
                }
            }
            b--;
            for (int i = b; i >= t; i--) {
                list.add(matrix[i][l]);
                count++;
                if (count > tar) {
                    return list;
                }
            }
            l++;
        }

    }
}

1365.有多少小于当前数字的数字

给你一个数组 nums,对于其中每个元素 nums[i],请你统计数组中比它小的所有数字的数目。

换而言之,对于每个 nums[i] 你必须计算出有效的 j 的数量,其中 j 满足 j != i nums[j] < nums[i]

以数组形式返回答案。

示例 1:

输入:nums = [8,1,2,2,3]
输出:[4,0,1,1,3]
解释: 
对于 nums[0]=8 存在四个比它小的数字:(1,2,2 和 3)。 
对于 nums[1]=1 不存在比它小的数字。
对于 nums[2]=2 存在一个比它小的数字:(1)。 
对于 nums[3]=2 存在一个比它小的数字:(1)。 
对于 nums[4]=3 存在三个比它小的数字:(1,2 和 2)。

示例 2:

输入:nums = [6,5,4,8]
输出:[2,1,0,3]

示例 3:

输入:nums = [7,7,7,7]
输出:[0,0,0,0]

提示:

  • 2 <= nums.length <= 500
  • `0 <= nums[i] <= 100
class Solution {
    public int[] smallerNumbersThanCurrent(int[] nums) {
        HashMap<Integer, Integer> map = new HashMap<>();
        int[] res = Arrays.copyOf(nums, nums.length);
        Arrays.sort(res);
        //根据排序确定当前数字的要求值,hashMap可以去除重复数字
        for (int i = 0; i < nums.length; i++) {
            if (!map.containsKey(res[i])) {
                map.put(res[i], i);
            }
        }
        //根据key取value
        for (int i = 0; i < nums.length; i++) {
            res[i] = map.get(nums[i]);
        }
        return res;
    }
}

941.有效的山脉数组

给定一个整数数组 arr,如果它是有效的山脉数组就返回 true,否则返回 false

让我们回顾一下,如果 arr 满足下述条件,那么它是一个山脉数组:

  • arr.length >= 3
  • 0 < i < arr.length - 1条件下,存在i,使得:
    • arr[0] < arr[1] < ... arr[i-1] < arr[i]
    • arr[i] > arr[i+1] > ... > arr[arr.length - 1]

img

示例 1:

输入:arr = [2,1]
输出:false

示例 2:

输入:arr = [3,5,5]
输出:false

示例 3:

输入:arr = [0,3,2,1]
输出:true

提示:

  • 1 <= arr.length <= 104
  • 0 <= arr[i] <= 104
class Solution {
    public boolean validMountainArray(int[] arr) {
        if (arr.length < 3) return false;
        if (arr[1] <= arr[0]) return false;
        //峰顶的下一个位置
        int pos = 0;
        for (int i = 2; i < arr.length; i++) {
            //找到峰顶
            if (arr[i] < arr[i - 1]) {
                pos = i;
                break;
            } else if (arr[i] == arr[i - 1]) return false;//出现平地不是山脉
        }
        if (pos == 0) return false;//没有峰顶不是山脉
        for (int i = pos; i < arr.length; i++) {
            if (arr[i] >= arr[i - 1]) return false;//再次出现峰顶不是山脉
        }
        return true;
    }
}

1207.独一无二的出现次数

给你一个整数数组 arr,请你帮忙统计数组中每个数的出现次数。

如果每个数的出现次数都是独一无二的,就返回 true;否则返回 false

示例 1:

输入:arr = [1,2,2,1,1,3]
输出:true
解释:在该数组中,1 出现了 3 次,2 出现了 2 次,3 只出现了 1 次。没有两个数的出现次数相同。

示例 2:

输入:arr = [1,2]
输出:false

示例 3:

输入:arr = [-3,0,1,-3,1,1,1,-3,10,0]
输出:true

提示:

  • 1 <= arr.length <= 1000
  • -1000 <= arr[i] <= 1000
class Solution {
    public boolean uniqueOccurrences(int[] arr) {
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int j : arr) {
            map.compute(j, (k, v) -> {
                if (v == null) return 1;
                return ++v;
            });
        }
        Collection<Integer> counts = map.values();
        int[] counts1 = counts.stream().mapToInt(a -> a).toArray();
        HashMap<Integer, Integer> map1 = new HashMap<>();

        for (int count : counts1) {
            if (!map1.containsKey(count)) {
                map1.put(count, 1);
            } else return false;
        }
        return true;
    }
}
class Solution {
    public boolean uniqueOccurrences(int[] arr) {
        int[] count = new int[2002];
        //使用Hash表计算每个数字出现的次数,Key:数字,value:次数
        for (int j : arr) {
            count[j + 1000]++;
        }   
        boolean[] flag = new boolean[1002];
        //使用Hash表计算次数是否重复,Key:次数,value:是否重复
        for (int i = 0; i < 2001; i++) {
            if (count[i] > 0) {
                if (!flag[count[i]]) flag[count[i]] = true;
                else return false;
            }
        }
        return true;
    }
}

283. 移动零

给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。

请注意 ,必须在不复制数组的情况下原地对数组进行操作。

示例 1:

输入: nums = [0,1,0,3,12]
输出: [1,3,12,0,0]

示例 2:

输入: nums = [0]
输出: [0]

提示:

  • 1 <= nums.length <= 104
  • -231 <= nums[i] <= 231 - 1
class Solution {
    public void moveZeroes(int[] nums) {
        int left = 0;
        for (int right = 0; right < nums.length; right++) {
            if (nums[right] != 0) {
                nums[left] = nums[right];
                left++;
            }

        }
        for (int i = left; i < nums.length; i++) {
            nums[i] = 0;
        }
    }
}

链表

203.移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

示例 1:

img

输入:head = [1,2,6,3,4,5,6], val = 6
输出:[1,2,3,4,5]

示例 2:

输入:head = [], val = 1
输出:[]

示例 3:

输入:head = [7,7,7,7], val = 7
输出:[]

提示:

  • 列表中的节点数目在范围 [0, 104]
  • 1 <= Node.val <= 50
  • 0 <= val <= 50
class Solution {
    //带虚拟头结点
    public ListNode removeElements(ListNode head, int val) {
        ListNode dummyNode=new ListNode(val-1);
        dummyNode.next = head;
        ListNode pre = dummyNode;

        while (pre.next != null) {
            if (pre.next.val == val) {
                pre.next = pre.next.next;
            } else {
                pre = pre.next;
            }
        }
        return dummyNode.next;
    }
}

707.设计链表

设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:valnextval 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。

在链表类中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

示例:

MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2);   //链表变为1-> 2-> 3
linkedList.get(1);            //返回2
linkedList.deleteAtIndex(1);  //现在链表是1-> 3
linkedList.get(1);            //返回3

提示:

  • 0 <= index, val <= 1000
  • 请不要使用内置的 LinkedList 库。
  • get, addAtHead, addAtTail, addAtIndexdeleteAtIndex 的操作次数不超过 2000
class MyLinkedList {
    //定义虚拟头节点
    private ListNode head;
    private ListNode tail;
    private int size;

    public MyLinkedList() {
        this.head = new ListNode();
        this.tail = this.head;
        this.size = 0;
    }

    public int get(int index) {
        if (index > size - 1) {
            return -1;
        }
        ListNode cur = head.next;
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        return cur.val;
    }

    public void addAtHead(int val) {
        this.size++;
        ListNode node = new ListNode(val);
        node.next = head.next;
        head.next = node;
        if (size == 1) {
            tail = node;
        }
    }

    public void addAtTail(int val) {
        this.size++;
        ListNode node = new ListNode(val);
        tail.next = node;
        tail = node;
    }

    public void addAtIndex(int index, int val) {
        if (index > size) {
            return;
        }
        if (size == index) {
            addAtTail(val);
            return;
        }
        if (index < 0) {
            addAtHead(val);
            return;
        }
        ListNode cur = head;
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        ListNode node = new ListNode(val);
        node.next = cur.next;
        cur.next = node;
        this.size++;
    }

    public void deleteAtIndex(int index) {
        if (index > size - 1 || index < 0) {
            return;
        }
        ListNode cur = head;
        for (int i = 0; i < index; i++) {
            cur = cur.next;
        }
        if (cur.next == tail) {
            tail = cur;
        }
        if (cur.next != null) {
            cur.next = cur.next.next;
        }
        this.size--;
    }

    static class ListNode {
        int val;
        ListNode next;

        ListNode() {
        }

        ListNode(int val) {
            this.val = val;
        }
    }


}

206.反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例 1:

img

输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]

示例 2:

img

输入:head = [1,2]
输出:[2,1]

示例 3:

输入:head = []
输出:[]

提示:

  • 链表中节点的数目范围是 [0, 5000]
  • -5000 <= Node.val <= 5000
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode prev = null;
        ListNode cur = head;

        while (cur != null) {
            ListNode node = cur.next;//暂存当前节点的下一个节点,保证链条不断裂
            cur.next = prev;//当前节点指向当前节点的前一个节点
            prev = cur;//改变前一节点
            cur = node;//改变当前节点
        }
        return prev;
    }
}

24.两两交换链表中的节点

给你一个链表,两两交换其中相邻的节点,并返回交换后链表的头节点。你必须在不修改节点内部的值的情况下完成本题(即,只能进行节点交换)。

示例 1:

img

输入:head = [1,2,3,4]
输出:[2,1,4,3]

示例 2:

输入:head = []
输出:[]

示例 3:

输入:head = [1]
输出:[1]

提示:

  • 链表中节点的数目在范围 [0, 100]
  • 0 <= Node.val <= 100
class Solution {
    public ListNode swapPairs(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        //定义虚拟头结点
        ListNode pre = new ListNode();
        pre.next = head.next;

        //定义一个二节点栈
        ListNode bottom;
        ListNode top;

        //当前栈
        ListNode cur = head;
        //保存上组节点的bottom
        ListNode temp = new ListNode();

        while (cur != null && cur.next != null) {
            //1.将当前两节点进栈
            bottom = cur;
            top = cur.next;

            //2.移至下组节点
            cur = top.next;

            //3.翻转当前栈中两节点
            top.next = bottom;

            //4.将上一个栈底节点指向当前栈顶节点
            temp.next = top;
            //5.保存栈底节点
            temp = bottom;
        }
        //将最后一个栈底节点指向最后一个节点,如果为空指向空,如果有一个节点,指向该节点
        temp.next = cur;
        return pre.next;
    }
}

19.删除链表的倒数第N个节点

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

示例 1:

img

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]

提示:

  • 链表中结点的数目为 sz
  • 1 <= sz <= 30
  • 0 <= Node.val <= 100
  • 1 <= n <= sz
class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode pre=new ListNode(0,head);
        int ListLen = getLength(head);
        ListNode cur=pre;
        //找到倒数第n-1个节点
        for (int i = 0; i < ListLen-n; i++) {
            cur=cur.next;
        }
        //跳过第n个节点
        cur.next=cur.next.next;
        return pre.next;
    }
	//获取链表的长度
    public int getLength(ListNode head) {
        int size = 1;
        ListNode cur = head;
        while (cur.next != null) {
            cur = cur.next;
            size++;
        }
        return size;
    }
}

160.链表相交

给你两个单链表的头节点 headAheadB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null

图示两个链表在节点 c1 开始相交

img

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

自定义评测:

评测系统 的输入如下(你设计的程序 不适用 此输入):

  • intersectVal - 相交的起始节点的值。如果不存在相交节点,这一值为 0
  • listA - 第一个链表
  • listB - 第二个链表
  • skipA - 在 listA 中(从头节点开始)跳到交叉节点的节点数
  • skipB - 在 listB 中(从头节点开始)跳到交叉节点的节点数

评测系统将根据这些输入创建链式数据结构,并将两个头节点 headAheadB 传递给你的程序。如果程序能够正确返回相交节点,那么你的解决方案将被 视作正确答案

示例 1:

img

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,6,1,8,4,5], skipA = 2, skipB = 3
输出:Intersected at '8'
解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,6,1,8,4,5]。
在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
— 请注意相交节点的值不为 1,因为在链表 A 和链表 B 之中值为 1 的节点 (A 中第二个节点和 B 中第三个节点) 是不同的节点。换句话说,它们在内存中指向两个不同的位置,而链表 A 和链表 B 中值为 8 的节点 (A 中第三个节点,B 中第四个节点) 在内存中指向相同的位置。

示例 2:

img

输入:intersectVal = 2, listA = [1,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Intersected at '2'
解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。
从各自的表头开始算起,链表 A 为 [1,9,1,2,4],链表 B 为 [3,2,4]。
在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:

img

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。
由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
这两个链表不相交,因此返回 null 。

提示:

  • listA 中节点数目为 m
  • listB 中节点数目为 n
  • 1 <= m, n <= 3 * 104
  • 1 <= Node.val <= 105
  • 0 <= skipA <= m
  • 0 <= skipB <= n
  • 如果 listAlistB 没有交点,intersectVal0
  • 如果 listAlistB 有交点,intersectVal == listA[skipA] == listB[skipB]
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) return null;
        //链表A的长度:m+n
        //链表B的长度:h+n,其中n为公共部分
        ListNode curA = headA;
        ListNode curB = headB;
        
        while (curA != curB) {
            //每个链表走完都从另一个链表的头部再走一次,即都走m+n+h
            curA = curA == null ? headB : curA.next;
            curB = curB == null ? headA : curB.next;
        }
        return curA;
    }
}

142.环形链表II

给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos-1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

不允许修改 链表。

示例 1:

img

输入:head = [3,2,0,-4], pos = 1
输出:返回索引为 1 的链表节点
解释:链表中有一个环,其尾部连接到第二个节点。

示例 2:

img

输入:head = [1,2], pos = 0
输出:返回索引为 0 的链表节点
解释:链表中有一个环,其尾部连接到第一个节点。

示例 3:

img

输入:head = [1], pos = -1
输出:返回 null
解释:链表中没有环。

提示:

  • 链表中节点的数目范围在范围 [0, 104]
  • -105 <= Node.val <= 105
  • pos 的值为 -1 或者链表中的一个有效索引

img假设快慢指针在\(x+y\)个节点出相遇,慢指针走了\(x+y\)个节点,快指针走了\(x+y+n(y+z)\)个节点,\(n\)为快指针比慢指针多走的圈数

\[2(x+y)=x+y+n(y+z)\Rightarrow x=(n-1)(y+z)+z \]

\(n=1\),则\(x=z\),在相遇处只需要将快指针从头节点重新走,每次走一个节点,慢节点继续走将在\(x\)个节点出相遇

public class Solution {
    public ListNode detectCycle(ListNode head) {
        //定义快慢指针
        ListNode slow = head;
        ListNode fast = head;
        //快指针一次移动两个位置,慢指针每次移动一个位置
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (slow == fast) {//说明有环
                fast = head;//头节点
                //两个指针,从头结点和相遇节点开始各走一步,直到相遇,相遇节点即为环入口节点
                while (fast != slow) {
                    fast = fast.next;
                    slow = slow.next;
                }
                return fast;
            }
        }
        return null;
    }
}

哈希表

242.有效的字母异位词

给定两个字符串 st ,编写一个函数来判断 t 是否是 s 的字母异位词。

注意:st 中每个字符出现的次数都相同,则称 st 互为字母异位词。

示例 1:

输入: s = "anagram", t = "nagaram"
输出: true

示例 2:

输入: s = "rat", t = "car"
输出: false

提示:

  • 1 <= s.length, t.length <= 5 * 104
  • st 仅包含小写字母
class Solution {
    public boolean isAnagram(String s, String t) {
        if (s.length() != t.length()) return false;
        int[] hash = new int[26];
        for (int i = 0; i < s.length(); i++) {
            hash[s.charAt(i) - 'a']++;
            hash[t.charAt(i) - 'a']--;
        }
        for (int i = 0; i < 26; i++) {
            if (hash[i] != 0) return false;
        }
        return true;
    }
}

349. 两个数组的交集

给定两个数组 nums1nums2 ,返回 它们的交集 。输出结果中的每个元素一定是 唯一 的。我们可以 不考虑输出结果的顺序

示例 1:

输入:nums1 = [1,2,2,1], nums2 = [2,2]
输出:[2]

示例 2:

输入:nums1 = [4,9,5], nums2 = [9,4,9,8,4]
输出:[9,4]
解释:[4,9] 也是可通过的

提示:

  • 1 <= nums1.length, nums2.length <= 1000
  • 0 <= nums1[i], nums2[i] <= 1000
class Solution {
    public int[] intersection(int[] nums1, int[] nums2) {
        int[] hash1 = new int[1001];
        int[] hash2 = new int[1001];
        ArrayList<Integer> res = new ArrayList<>();

        for (int num1 : nums1) {
            hash1[num1]++;
        }

        for (int num2 : nums2) {
            hash2[num2]++;
            if (hash1[num2] > 0 && hash2[num2] < 2) res.add(num2);
        }


        return res.stream().mapToInt(a->a).toArray();
    }
}

383. 赎金信

给你两个字符串:ransomNotemagazine ,判断 ransomNote 能不能由 magazine 里面的字符构成。

如果可以,返回 true ;否则返回 false

magazine 中的每个字符只能在 ransomNote 中使用一次。

示例 1:

输入:ransomNote = "a", magazine = "b"
输出:false

示例 2:

输入:ransomNote = "aa", magazine = "ab"
输出:false

示例 3:

输入:ransomNote = "aa", magazine = "aab"
输出:true

提示:

  • 1 <= ransomNote.length, magazine.length <= 105
  • ransomNotemagazine 由小写英文字母组成
class Solution {
    public boolean canConstruct(String ransomNote, String magazine) {
        int[] record = new int[26];
        for (int i = 0; i < ransomNote.length(); i++) {
            record[ransomNote.charAt(i) - 'a']--;
        }
        for (int i = 0; i < magazine.length(); i++) {
            record[magazine.charAt(i) - 'a']++;
        }
        for (int count : record) {
            if (count < 0) return false;
        }
        return true;
    }
}

202快乐数

编写一个算法来判断一个数 n 是不是快乐数。

「快乐数」 定义为:

  • 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
  • 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
  • 如果这个过程 结果为 1,那么这个数就是快乐数。

如果 n快乐数 就返回 true ;不是,则返回 false

示例 1:

输入:n = 19
输出:true
解释:
12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1

示例 2:

输入:n = 2
输出:false

提示:

  • 1 <= n <= 231 - 1
class Solution {
    public boolean isHappy(int n) {
        //记录已出现的数,避免出现循环
        Set<Integer> record = new HashSet<>();
        while (n != 1 && !record.contains(n)) {
            record.add(n);
            n = getNextNumber(n);
        }
        return n==1;
    }

    //判断数是否为快乐数
    public int getNextNumber(int n) {
        int res = 0;
        while (n > 0) {
            int tem = n % 10;
            res += tem * tem;
            n = n / 10;
        }
        return res;
    }
}

1. 两数之和

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]

提示:

  • 2 <= nums.length <= 104
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109
  • 只会存在一个有效答案
class Solution {
    public int[] twoSum(int[] nums, int target) {
        int[] res = new int[2];
        if(nums==null||nums.length == 0){
            return res;
        }
        Map<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int temp=target-nums[i];
            if(map.containsKey(temp)){
                res[0]=i;
                res[1] =map.get(temp);
                break;
            }
            map.put(nums[i],i);
        }
        return res;
    }
}

454.四数相加II

给你四个整数数组 nums1nums2nums3nums4 ,数组长度都是 n ,请你计算有多少个元组 (i, j, k, l) 能满足:

  • 0 <= i, j, k, l < n
  • nums1[i] + nums2[j] + nums3[k] + nums4[l] == 0

示例 1:

输入:nums1 = [1,2], nums2 = [-2,-1], nums3 = [-1,2], nums4 = [0,2]
输出:2
解释:
两个元组如下:
1. (0, 0, 0, 1) -> nums1[0] + nums2[0] + nums3[0] + nums4[1] = 1 + (-2) + (-1) + 2 = 0
2. (1, 1, 0, 0) -> nums1[1] + nums2[1] + nums3[0] + nums4[0] = 2 + (-1) + (-1) + 0 = 0

示例 2:

输入:nums1 = [0], nums2 = [0], nums3 = [0], nums4 = [0]
输出:1

提示:

  • n == nums1.length
  • n == nums2.length
  • n == nums3.length
  • n == nums4.length
  • 1 <= n <= 200
  • -228 <= nums1[i], nums2[i], nums3[i], nums4[i] <= 228
class Solution {
    public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
        Map<Integer, Integer> countAB = new HashMap<>();
        for (int a : nums1) {
            for (int b : nums2) {
                countAB.put(a + b, countAB.getOrDefault(a + b, 0) + 1);
            }
        }
        int res = 0;
        for (int c : nums3) {
            for (int d : nums4) {
                if (countAB.containsKey(-c - d)) {
                    res += countAB.get(-c - d);
                }
            }
        }
        return res;
    }
}

15. 三数之和

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i != ji != kj != k ,同时还满足 nums[i] + nums[j] + nums[k] == 0 。请

你返回所有和为 0 且不重复的三元组。

注意:答案中不可以包含重复的三元组。

示例 1:

输入:nums = [-1,0,1,2,-1,-4]
输出:[[-1,-1,2],[-1,0,1]]
解释:
nums[0] + nums[1] + nums[2] = (-1) + 0 + 1 = 0 。
nums[1] + nums[2] + nums[4] = 0 + 1 + (-1) = 0 。
nums[0] + nums[3] + nums[4] = (-1) + 2 + (-1) = 0 。
不同的三元组是 [-1,0,1] 和 [-1,-1,2] 。
注意,输出的顺序和三元组的顺序并不重要。

示例 2:

输入:nums = [0,1,1]
输出:[]
解释:唯一可能的三元组和不为 0 。

示例 3:

输入:nums = [0,0,0]
输出:[[0,0,0]]
解释:唯一可能的三元组和为 0 。

提示:

  • 3 <= nums.length <= 3000
  • -105 <= nums[i] <= 105
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        List<List<Integer>> result = new ArrayList<>();
        if (nums == null || nums.length < 3) return result;
        //对数组进行排序,便于计算
        Arrays.sort(nums);
        for (int i = 0; i < nums.length; i++) {
            //确定满足条件的最左边的数字,因为它为最下值,所以一定为负数
            if (nums[i] > 0) break;
            //跳过重复的数字
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            //确定第二个数字
            int L = i + 1;
            //确定第三个数字
            int R = nums.length - 1;
            while (L < R) {
                int sum = nums[i] + nums[L] + nums[R];
                //如果当前三个数字满足条件
                if (sum == 0) {
                    result.add(Arrays.asList(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 R--;
            }
        }
        return result;
    }
}

18. 四数之和

给你一个由 n 个整数组成的数组 nums ,和一个目标值 target 。请你找出并返回满足下述全部条件且不重复的四元组 [nums[a], nums[b], nums[c], nums[d]] (若两个四元组元素一一对应,则认为两个四元组重复):

  • 0 <= a, b, c, d < n
  • abcd 互不相同
  • nums[a] + nums[b] + nums[c] + nums[d] == target

你可以按 任意顺序 返回答案 。

示例 1:

输入:nums = [1,0,-1,0,-2,2], target = 0
输出:[[-2,-1,1,2],[-2,0,0,2],[-1,0,0,1]]

示例 2:

输入:nums = [2,2,2,2,2], target = 8
输出:[[2,2,2,2]]

提示:

  • 1 <= nums.length <= 200
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109
class Solution {
    public List<List<Integer>> fourSum(int[] nums, int target) {
        List<List<Integer>> res = new ArrayList<>();
        if (nums == null || nums.length < 4) return res;
        Arrays.sort(nums);
        for (int i = 0; i < nums.length; i++) {
            //跳过上一个已经计算过的第一个数字,i>0表示已经计算过第一个数字一次
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            for (int j = i + 1; j < nums.length; j++) {
                //跳过上一步已经计算过的第二个数字,j-i》1表示已经计算过i基础上j一次
                if (j - i > 1 && nums[j] == nums[j - 1]) continue;
                //确定第三个数字
                int L = j + 1;
                //确定第四个数字
                int R = nums.length - 1;
                while (L < R) {
                    long sum = (long)nums[i] + nums[j] + nums[L] + nums[R];
                   if (sum == target) {
                        res.add(Arrays.asList(nums[i], nums[j], 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 < target) L++;
                    else R--;
                }
            }
        }
        return res;
    }
}

哈希表总结

两数组中元素出现个数

有效的字母异位词、[两个数组的交集](#349. 两个数组的交集)、[赎金信](#383. 赎金信)hash表中key为元素,value为元素出现的个数

多数组的元素求和

字符串

344.反转字符串

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。

不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

示例 1:

输入:s = ["h","e","l","l","o"]
输出:["o","l","l","e","h"]

示例 2:

输入:s = ["H","a","n","n","a","h"]
输出:["h","a","n","n","a","H"]

提示:

  • 1 <= s.length <= 105
  • s[i] 都是 ASCII 码表中的可打印字符
class Solution {
    public void reverseString(char[] s) {
        int L=0;
        int R=s.length-1;
        while (L< R) {
            char temp;
            temp=s[L];
            s[L]=s[R];
            s[R]=temp;
            L++;
            R--;
        }
    }
}

541. 反转字符串II

给定一个字符串 s 和一个整数 k,从字符串开头算起,每计数至 2k 个字符,就反转这 2k 字符中的前 k 个字符。

  • 如果剩余字符少于 k 个,则将剩余字符全部反转。
  • 如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。

示例 1:

输入:s = "abcdefg", k = 2
输出:"bacdfeg"

示例 2:

输入:s = "abcd", k = 2
输出:"bacd"

提示:

  • 1 <= s.length <= 104
  • s 仅由小写英文组成
  • 1 <= k <= 104
class Solution {
    public String reverseStr(String s, int k) {
        char[] cs = s.toCharArray();
        int n = s.length();
        for (int l = 0; l < n; l = l + 2 * k) {
            int r = l + k - 1;
            reverseStr(cs, l, Math.min(r, n - 1));
        }
        return String.valueOf(cs);
    }
	//翻转字符串指定长度的部分
    public void reverseStr(char[] s,int L,int R) {
        while (L < R) {
            char temp = s[L];
            s[L] = s[R];
            s[R] = temp;
            L++;
            R--;
        }
    }
}

剑指Offer 05.替换空格

请实现一个函数,把字符串 s 中的每个空格替换成"%20"。

示例 1:

输入:s = "We are happy."
输出:"We%20are%20happy."

限制:

0 <= s 的长度 <= 10000
class Solution {
    public String replaceSpace(String s) {
        int count = 0;
        //计算空格数量
        for (int i = 0; i < s.length(); i++) {
            if (s.charAt(i) == ' ') count++;
        }
        //构建结果数组
        char[] result = new char[s.length() + 2 * count];
        int i = 0;
        int j = 0;
        while (i < result.length) {
            //替换空格
            if (s.charAt(j) == ' ') {
                result[i] = '%';
                i++;
                result[i] = '2';
                i++;
                result[i] = '0';
                i++;
                j++;
            } else {
                //复制
                result[i] = s.charAt(j);
                i++;
                j++;
            }

        }
        return new String(result);
    }
}

151.翻转字符串里的单词

给你一个字符串 s ,请你反转字符串中 单词 的顺序。

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。

示例 1:

输入:s = "the sky is blue"
输出:"blue is sky the"

示例 2:

输入:s = "  hello world  "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。

示例 3:

输入:s = "a good   example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。

提示:

  • 1 <= s.length <= 104
  • s 包含英文大小写字母、数字和空格 ' '
  • s至少存在一个 单词
class Solution {
    public String reverseWords(String s) {
        //去除多余空格
        StringBuilder sb = trimSpaces(s);

        // 翻转字符串
        reverse(sb, 0, sb.length() - 1);

        // 翻转每个单词
        reverseEachWord(sb);

        return sb.toString();
    }

    public StringBuilder trimSpaces(String s) {
        int left = 0, right = s.length() - 1;
        // 去掉字符串开头的空白字符
        while (left <= right && s.charAt(left) == ' ') {
            ++left;
        }

        // 去掉字符串末尾的空白字符
        while (left <= right && s.charAt(right) == ' ') {
            --right;
        }

        // 将字符串间多余的空白字符去除
        StringBuilder sb = new StringBuilder();
        while (left <= right) {
            char c = s.charAt(left);

            if (c != ' ') {
                sb.append(c);
            } else if (sb.charAt(sb.length() - 1) != ' ') {//当stringBuffer的最后一个字符为空格且新添加的字符依然为空格时,丢弃新的空格
                sb.append(c);
            }

            ++left;
        }
        return sb;
    }

    public void reverse(StringBuilder sb, int left, int right) {
        while (left < right) {
            char tmp = sb.charAt(left);
            sb.setCharAt(left++, sb.charAt(right));
            sb.setCharAt(right--, tmp);
        }
    }

    //翻转每个单词,得到结果字符串的翻转字符串
    public void reverseEachWord(StringBuilder sb) {
        int n = sb.length();
        int start = 0, end = 0;

        while (start < n) {
            // 循环至单词的末尾
            while (end < n && sb.charAt(end) != ' ') {
                ++end;
            }
            // 翻转单词
            reverse(sb, start, end - 1);
            // 更新start,去找下一个单词
            start = end + 1;
            ++end;
        }
    }
}

剑指Offer58-II.左旋转字符串

字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。

示例 1:

输入: s = "abcdefg", k = 2
输出: "cdefgab"

示例 2:

输入: s = "lrloseumgh", k = 6
输出: "umghlrlose"

限制:

  • 1 <= k < s.length <= 10000
class Solution {
    public String reverseLeftWords(String s, int n) {

        char[] cs = s.toCharArray();
        String head = new String(cs, 0, n);
        String tail = new String(cs, n, cs.length - n);
        return tail + head;
    }
}

二叉28. 实现 strStr()树

给你两个字符串 haystackneedle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1

示例 1:

输入:haystack = "sadbutsad", needle = "sad"
输出:0
解释:"sad" 在下标 0 和 6 处匹配。
第一个匹配项的下标是 0 ,所以返回 0 。

示例 2:

输入:haystack = "leetcode", needle = "leeto"
输出:-1
解释:"leeto" 没有在 "leetcode" 中出现,所以返回 -1 。

提示:

  • 1 <= haystack.length, needle.length <= 104
  • haystackneedle 仅由小写英文字符组成

KMP 解法

KMP 算法是一个快速查找匹配串的算法,它的作用其实就是本题问题:如何快速在「原字符串」中找到「匹配字符串」。

上述的朴素解法,不考虑剪枝的话复杂度是 O(mn) 的,而 KMP 算法的复杂度为 O(m+n)。

KMP 之所以能够在 *O*(*m*+*n*) 复杂度内完成查找,是因为其能在「非完全匹配」的过程中提取到有效信息进行复用,以减少「重复匹配」的消耗。

你可能不太理解,没关系,我们可以通过举 🌰 来理解 KMP。

1. 匹配过程

在模拟 KMP 匹配过程之前,我们先建立两个概念:

  • 前缀:不包含最后一个字符的所有以第一个字符开头的连续子串
  • 后缀:不包含第一个字符的所有以最后一个字符结尾的连续子串

然后我们假设原串为 abeababeabf,匹配串为 abeabf

image.png

我们可以先看看如果不使用 KMP,会如何进行匹配(不使用 substring 函数的情况下)。

首先在「原串」和「匹配串」分别各自有一个指针指向当前匹配的位置。

首次匹配的「发起点」是第一个字符 a。显然,后面的 abeab 都是匹配的,两个指针会同时往右移动(黑标)。

在都能匹配上 abeab 的部分,「朴素匹配」和「KMP」并无不同。

直到出现第一个不同的位置(红标):

image.png

接下来,正是「朴素匹配」和「KMP」出现不同的地方:

  • 先看下「朴素匹配」逻辑:

1. 将原串的指针移动至本次「发起点」的下一个位置(b 字符处);匹配串的指针移动至起始位置。

2. 尝试匹配,发现对不上,原串的指针会一直往后移动,直到能够与匹配串对上位置。

如图:

image.png

也就是说,对于「朴素匹配」而言,一旦匹配失败,将会将原串指针调整至下一个「发起点」,匹配串的指针调整至起始位置,然后重新尝试匹配。

这也就不难理解为什么「朴素匹配」的复杂度是 O(mn) 了。

  • 然后我们再看看「KMP 匹配」过程:

首先匹配串会检查之前已经匹配成功的部分中里是否存在相同的「前缀」和「后缀」。如果存在,则跳转到「前缀」的下一个位置继续往下匹配:

9364346F937803F03CD1A0AE645EA0F1.jpg

跳转到下一匹配位置后,尝试匹配,发现两个指针的字符对不上,并且此时匹配串指针前面不存在相同的「前缀」和「后缀」,这时候只能回到匹配串的起始位置重新开始:

image.png

到这里,你应该清楚 KMP 为什么相比于朴素解法更快:

  • 因为 KMP 利用已匹配部分中相同的「前缀」和「后缀」来加速下一次的匹配。
  • 因为 KMP 的原串指针不会进行回溯(没有朴素匹配中回到下一个「发起点」的过程)。

第一点很直观,也很好理解。

我们可以把重点放在第二点上,原串不回溯至「发起点」意味着什么?

其实是意味着:随着匹配过程的进行,原串指针的不断右移,我们本质上是在不断地在否决一些「不可能」的方案。

当我们的原串指针从 i 位置后移到 j 位置,不仅仅代表着「原串」下标范围为 [*i*,*j*) 的字符与「匹配串」匹配或者不匹配,更是在否决那些以「原串」下标范围为 [*i*,*j*) 为「匹配发起点」的子集。

2. 分析实现

到这里,就结束了吗?要开始动手实现上述匹配过程了吗?

我们可以先分析一下复杂度。如果严格按照上述解法的话,最坏情况下我们需要扫描整个原串,复杂度为 O(n)。同时在每一次匹配失败时,去检查已匹配部分的相同「前缀」和「后缀」,跳转到相应的位置,如果不匹配则再检查前面部分是否有相同「前缀」和「后缀」,再跳转到相应的位置 ... 这部分的复杂度是 O(m2) ,因此整体的复杂度是 O(nm2),而我们的朴素解法是 O(mn) 的。

说明还有一些性质我们没有利用到。

显然,扫描完整原串操作这一操作是不可避免的,我们可以优化的只能是「检查已匹配部分的相同前缀和后缀」这一过程。

再进一步,我们检查「前缀」和「后缀」的目的其实是「为了确定匹配串中的下一段开始匹配的位置」。

同时我们发现,对于匹配串的任意一个位置而言,由该位置发起的下一个匹配点位置其实与原串无关。

举个 🌰,对于匹配串 abcabd 的字符 d 而言,由它发起的下一个匹配点跳转必然是字符 c 的位置。因为字符 d 位置的相同「前缀」和「后缀」字符 ab 的下一位置就是字符 c

可见从匹配串某个位置跳转下一个匹配位置这一过程是与原串无关的,我们将这一过程称为找 next 点。

显然我们可以预处理出 next 数组,数组中每个位置的值就是该下标应该跳转的目标位置( next 点)。

当我们进行了这一步优化之后,复杂度是多少呢?

预处理 next 数组的复杂度未知,匹配过程最多扫描完整个原串,复杂度为 O(n)。

因此如果我们希望整个 KMP 过程是 O(m+n) 的话,那么我们需要在 O(m) 的复杂度内预处理出 next数组。

所以我们的重点在于如何在 *O*(*m*) 复杂度内处理处 next 数组。

next数组的作用是确定匹配串与与原串在某个位置不匹配,指针应该往前跳转的位置。

3. pat字符串的next 数组构建

接下来,我们看看 next 数组是如何在 O(m) 的复杂度内被预处理出来的。

假设有匹配串 aaabbab,我们来看看对应的 next 是如何被构建出来的。

next[i]为以i位置为字符串末尾的最长公共前后缀的长度j=next[i-1]i-1处最大公共前后缀的长度

步骤1:要计算next[i],由于已经知道next[i-1],只需要比较pat[i]是否等于pat[j]

kmp步骤1

步骤2:如果pat[i]=pat[next[j]]i位置的最长公共前后缀长度在前一个字符的基础上加一,next[i]=next[i-1]+1=j+1

步骤3:如果pat[i]!=pat[next[j]],比较pat[i]是否等于pat[next[j]],下图可知框中为相同前后缀,重复步骤1和2

kmp步骤2

这就是整个 next 数组的构建过程,时空复杂度均为 O(m)。

使用next数组来匹配

有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。

注意next数组是新前缀表(旧前缀表统一减一了)。

匹配过程动画如下:

KMP精讲4

至此整个 KMP 匹配过程复杂度是 O(m+n) 的。

class Solution {
    public int strStr(String haystack, String needle) {
        if (needle.isEmpty()) return 0;
        int n = haystack.length();
        int m = needle.length();
        char[] ch = haystack.toCharArray();
        char[] cs = needle.toCharArray();
		
        //构造next数组,
        //next[i]为以i为字符串末尾最长公共前后缀的长度,j=next[i-1]为i-1处最大公共前后缀的长度
        //next数组只在匹配串上进行
        int[] next = new int[m];        
        for (int i = 1, j = 0; i < m; i++) {
            while (j > 0 && cs[i] != cs[j]) j = next[j - 1];
            if (cs[i] == cs[j]) j++;
            next[i] = j;
        }
        //使用next数组进行匹配,i为原串指针,j为匹配串指针
        for (int i = 0, j = 0; i < n; i++) {
            //如果当前ch[i]!=ch[j],查找匹配串前一个字符的`next[j]`,再次比较
            while (j > 0 && ch[i] != cs[j]) j = next[j - 1];
            if (ch[i] == cs[j]) j++;
            if (j == m) return i - m+1;
        }
        return -1;
    }
}

459.重复的子字符串

给定一个非空的字符串 s ,检查是否可以通过由它的一个子串重复多次构成。

示例 1:

输入: s = "abab"
输出: true
解释: 可由子串 "ab" 重复两次构成。

示例 2:

输入: s = "aba"
输出: false

示例 3:

输入: s = "abcabcabcabc"
输出: true
解释: 可由子串 "abc" 重复四次构成。 (或子串 "abcabc" 重复两次构成。)

提示:

  • 1 <= s.length <= 104
  • s 由小写英文字母组成
class Solution {
    public boolean repeatedSubstringPattern(String s) {
        int[] next = new int[s.length()];
        //构造next数组
        for (int i = 1, j = 0; i < s.length(); i++) {
            while (j > 0 && s.charAt(i) != s.charAt(j)) j = next[j - 1];
            if (s.charAt(i) == s.charAt(j)) j++;
            next[i] = j;
        }
        return s.length() % (s.length() - next[s.length() - 1]) == 0 && next[s.length() - 1] != 0;
    }
}

双指针法

[数组-27. 移除元素](#27. 移除元素)

字符串-344.反转字符串

[字符串-替换空格](#剑指Offer 05.替换空格)

字符串-151.翻转字符串里的单词

链表-206.反转链表

链表-19.删除链表的倒数第N个节点

链表-160.链表相交

链表-142.环形链表II

[哈希表-15. 三数之和](#15. 三数之和)

[哈希表-18. 四数之和](#18. 四数之和)

栈与队列

232.用栈实现队列

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty):

实现 MyQueue 类:

  • void push(int x) 将元素 x 推到队列的末尾
  • int pop() 从队列的开头移除并返回元素
  • int peek() 返回队列开头的元素
  • boolean empty() 如果队列为空,返回 true ;否则,返回 false

说明:

  • 只能 使用标准的栈操作 —— 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。
  • 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。

示例 1:

输入:
["MyQueue", "push", "push", "peek", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 1, 1, false]
class MyQueue {
    Deque<Integer> inStack;
    Deque<Integer> outStack;
	
    //定义一个入栈和出栈队列
    public MyQueue() {
        inStack = new ArrayDeque<>();
        outStack = new ArrayDeque<>();
    }

    //向入栈队列压栈
    public void push(int x) {
        inStack.push(x);
    }

    
    public int pop() {
        if (outStack.isEmpty()) {
            in2out();
        }
        return outStack.pop();
    }

    public int peek() {
        if (outStack.isEmpty()) {
            in2out();
        }
        return outStack.peek();
    }

    public boolean empty() {
        return inStack.isEmpty() && outStack.isEmpty();
    }

    //把入栈压入出栈
    private void in2out() {
        while (!inStack.isEmpty()) {
            outStack.push(inStack.pop());
        }
    }
}

225. 用队列实现栈

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushtoppopempty)。

实现 MyStack 类:

  • void push(int x) 将元素 x 压入栈顶。
  • int pop() 移除并返回栈顶元素。
  • int top() 返回栈顶元素。
  • boolean empty() 如果栈是空的,返回 true ;否则,返回 false

注意:

  • 你只能使用队列的基本操作 —— 也就是 push to backpeek/pop from frontsizeis empty 这些操作。
  • 你所使用的语言也许不支持队列。 你可以使用 list (列表)或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。

示例:

输入:
["MyStack", "push", "push", "top", "pop", "empty"]
[[], [1], [2], [], [], []]
输出:
[null, null, null, 2, 2, false]

解释:
MyStack myStack = new MyStack();
myStack.push(1);
myStack.push(2);
myStack.top(); // 返回 2
myStack.pop(); // 返回 2
myStack.empty(); // 返回 False

提示:

  • 1 <= x <= 9
  • 最多调用100pushpoptopempty
  • 每次调用 poptop 都保证栈不为空
class MyStack {
    Queue<Integer> queue1;
    Queue<Integer> queue2;

    public MyStack() {
        queue1 = new LinkedList<>();
        queue2 = new LinkedList<>();
    }

    public void push(int x) {
        //先把压栈的元素加入队列2
        queue2.offer(x);
        //将队列1中的元素从倒序添加至队列2,并清空队列1
        while (!queue1.isEmpty()) {
            queue2.offer(queue1.poll());
        }
        
        Queue<Integer> temp=queue1;
        queue1=queue2;
        //清空队列2
        queue2=temp;
    }

    public int pop() {
        return queue1.poll();
    }

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

    public boolean empty() {
        return queue1.isEmpty();
    }
}

20. 有效的括号

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

示例 1:

输入:s = "()"
输出:true

示例 2:

输入:s = "()[]{}"
输出:true

示例 3:

输入:s = "(]"
输出:false

提示:

  • 1 <= s.length <= 104
  • s 仅由括号 '()[]{}' 组成
class Solution {
    public boolean isValid(String s) {
        if (s.length() % 2 != 0) {
            return false;
        }
        Map<Character, Character> map = new HashMap<>();
        //用Hash表处理匹配问题
        map.put('{', '}');
        map.put('[', ']');
        map.put('(', ')');
        Deque<Character> stack = new ArrayDeque<>(s.length() / 2);
        char[] cs = s.toCharArray();
        for (char c : cs) {
            if (map.containsKey(c)) {
                stack.push(c);//将左括号压栈
            } else if (stack.isEmpty()||map.get(stack.pop()) != c) {//如果是右括号,匹配最近的左括号
                return false;
            }
        }
        return stack.isEmpty();
    }
}

1047. 删除字符串中的所有相邻重复项

给出由小写字母组成的字符串 S重复项删除操作会选择两个相邻且相同的字母,并删除它们。

在 S 上反复执行重复项删除操作,直到无法继续删除。

在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。

示例:

输入:"abbaca"
输出:"ca"
解释:
例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。

提示:

  1. 1 <= S.length <= 20000
  2. S 仅由小写英文字母组成。
class Solution {
    public String removeDuplicates(String s) {
        char[] cs = s.toCharArray();
        Deque<Character> stack = new ArrayDeque<>();
        //利用栈删除重复元素
        for (char c : cs) {
            if (!stack.isEmpty() && stack.peek() == c) {
                stack.pop();
            } else {
                stack.push(c);
            }
        }
        //栈转化为字符串
        StringBuilder res=new StringBuilder();
        int len = stack.size();
        for (int i = 0; i < len; i++) {
            res.append(stack.pollLast());
        }
        return res.toString().equals("null") ?"":res.toString();
    }
}

150. 逆波兰表达式求值

给你一个字符串数组 tokens ,表示一个根据 逆波兰表示法 表示的算术表达式。

请你计算该表达式。返回一个表示表达式值的整数。

注意:

  • 有效的算符为 '+''-''*''/'
  • 每个操作数(运算对象)都可以是一个整数或者另一个表达式。
  • 两个整数之间的除法总是 向零截断
  • 表达式中不含除零运算。
  • 输入是一个根据逆波兰表示法表示的算术表达式。
  • 答案及所有中间计算结果可以用 32 位 整数表示。

示例 1:

输入:tokens = ["2","1","+","3","*"]
输出:9
解释:该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9

示例 2:

输入:tokens = ["4","13","5","/","+"]
输出:6
解释:该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6

示例 3:

输入:tokens = ["10","6","9","3","+","-11","*","/","*","17","+","5","+"]
输出:22
解释:该算式转化为常见的中缀算术表达式为:
  ((10 * (6 / ((9 + 3) * -11))) + 17) + 5
= ((10 * (6 / (12 * -11))) + 17) + 5
= ((10 * (6 / -132)) + 17) + 5
= ((10 * 0) + 17) + 5
= (0 + 17) + 5
= 17 + 5
= 22

提示:

  • 1 <= tokens.length <= 104
  • tokens[i] 是一个算符("+""-""*""/"),或是在范围 [-200, 200] 内的一个整数

逆波兰表达式:

逆波兰表达式是一种后缀表达式,所谓后缀就是指算符写在后面。

  • 平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 )
  • 该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * )

逆波兰表达式主要有以下两个优点:

  • 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。
  • 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中
class Solution {
    	//只需简单的将数字压入栈中,遇到运算符,弹出两个数据,进行计算
        public int evalRPN(String[] tokens) {
            Deque<Integer> stack = new ArrayDeque<>();
            for (String s : tokens) {
                switch (s) {
                    case "+" -> stack.push(stack.pop() + stack.pop());
                    case "-" -> stack.push(-stack.pop() + stack.pop());
                    case "*" -> stack.push(stack.pop() * stack.pop());
                    case "/" -> {
                        int temp1 = stack.pop();
                        int temp2 = stack.pop();
                        stack.push(temp2 / temp1);
                    }
                    default -> stack.push(Integer.valueOf(s));
                }
            }
            return stack.pop();
        }
}

239. 滑动窗口最大值

给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。

返回 滑动窗口中的最大值

示例 1:

输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置                最大值
---------------               -----
[1  3  -1] -3  5  3  6  7       3
 1 [3  -1  -3] 5  3  6  7       3
 1  3 [-1  -3  5] 3  6  7       5
 1  3  -1 [-3  5  3] 6  7       5
 1  3  -1  -3 [5  3  6] 7       6
 1  3  -1  -3  5 [3  6  7]      7

示例 2:

输入:nums = [1], k = 1
输出:[1]

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104
  • 1 <= k <= nums.length
class Solution {
    public int[] maxSlidingWindow(int[] nums, int k) {
        if(nums == null || nums.length < 2) {
            return nums;
        }
        // 双向队列 保存当前窗口最大值的数组位置 保证队列中数组位置的数值按从大到小排序
        LinkedList<Integer> queue = new LinkedList();
        // 结果数组
        int[] result = new int[nums.length-k+1];
        // 遍历nums数组
        for(int i = 0;i < nums.length;i++){
            // 保证从大到小 如果前面数小则需要依次弹出,直至满足要求
            while(!queue.isEmpty() && nums[queue.peekLast()] <= nums[i]){
                queue.pollLast();
            }
            // 添加当前值对应的数组下标
            queue.addLast(i);
            // 判断当前队列中队首的值是否有效
            if(queue.peek() <= i-k){
                queue.poll();
            }
            // 当窗口长度为k时 保存当前窗口中最大值
            if(i+1 >= k){
                result[i+1-k] = nums[queue.peek()];
            }
        }
        return result;
    }
}

347.前 K 个高频元素

给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。

示例 1:

输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]

示例 2:

输入: nums = [1], k = 1
输出: [1]

提示:

  • 1 <= nums.length <= 105
  • k 的取值范围是 [1, 数组中不相同的元素的个数]
  • 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的
class Solution {
    public int[] topKFrequent(int[] nums, int k) {
        Map<Integer, Integer> map = new HashMap<>();
        //计算数组中元素出现的频率
        for (int num : nums) {
            map.put(num, map.getOrDefault(num, 0) + 1);
        }
        //构建一个从大到小的优先队列
        PriorityQueue<int[]> queue = new PriorityQueue<>((o1, o2) -> o2[1] - o1[1]);
        //将Hash表按照评率从大到小添加至优先队列
        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            queue.add(new int[]{entry.getKey(), entry.getValue()});
        }
        int[] result = new int[k];
        for (int i = 0; i < k; i++) {
            result[i] = queue.poll()[0];
        }
        return result;
    }
}

二叉树

二叉树理论

二叉树种类

  • 满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
img
  • 完全二叉树:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h1开始),则该层包含 $[1,2^{h-1}] $个节点。
image-20230616162738775
  • 二叉搜索树:
    • 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
    • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
    • 它的左、右子树也分别为二叉排序树

img

  • 平衡二叉搜索树(AVL(Adelson-Velsky and Landis)树):它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
img

144.前序遍历

递归遍历

class Solution {
    //递归遍历
    List<Integer> list = new ArrayList<>();

    public List<Integer> preorderTraversal(TreeNode root) {
        if (root == null) {
            return list;
        }
        help(root);
        return list;
    }

    public void help(TreeNode root) {
        list.add(root.val);
        if (root.left != null) {
            help(root.left);
        }
        if (root.right != null) {
            help(root.right);
        }
    }
}

迭代遍历

class Solution {
    //迭代遍历
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if (root == null) return list;
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()){
            TreeNode node = stack.pop();
            list.add(node.val);
            if(node.right!=null) stack.push(node.right);
            if (node.left != null) stack.push(node.left);
        }
        return list;
    }
}

统一迭代遍历

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        if (root != null) stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            if (node != null) {
                if (node.right != null) stack.push(node.right);
                if (node.left != null) stack.push(node.left);
                stack.push(node);
                stack.push(null);
            } else {
                node = stack.pop();
                list.add(node.val);
            }
        }
        return list;
    }
}

94.中序遍历

递归遍历

class Solution {
    List<Integer> list= new ArrayList<>();
    public List<Integer> inorderTraversal(TreeNode root) {
        if (root==null) return list;
        inOrder(root);
        return list;
    }
    void inOrder(TreeNode root){
        if (root.left != null) inOrder(root.left);
        list.add(root.val);
        if (root.right != null) this.inOrder(root.right);
    }
}

迭代遍历

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if (root == null) return list;
        Stack<TreeNode> stack = new Stack<>();
        while (root != null||!stack.isEmpty()) {
            if(root!=null){
                stack.push(root);
                root = root.left;
            }else {
                root=stack.pop();
                list.add(root.val);
                root=root.right;
            }
        }
        return list;
    }
}

统一迭代遍历

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        if (root != null) stack.push(root);
        while (!stack.isEmpty()) {
            TreeNode node = stack.pop();
            if (node != null) {
                if (node.right != null) stack.push(node.right);
                stack.push(node);
                stack.push(null);
                if (node.left != null) stack.push(node.left);
            } else {
                node = stack.pop();
                list.add(node.val);
            }
        }
        return list;
    }
}

145.后序遍历

递归遍历

class Solution {
    //递归遍历
    List<Integer> list = new ArrayList<>();

    public List<Integer> postorderTraversal(TreeNode root) {
        if (root == null) {
            return list;
        }
        help(root);
        return list;
    }

    public void help(TreeNode root) {
        if (root.left != null) {
            help(root.left);
        }
        if (root.right != null) {
            help(root.right);
        }
        list.add(root.val);
    }
}

迭代遍历

class Solution {
    //迭代遍历
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        if (root == null) return list;
        Stack<TreeNode> stack = new Stack<>();
        stack.push(root);
        while (!stack.isEmpty()){
            TreeNode node = stack.pop();
            list.add(node.val);
            if (node.left != null) stack.push(node.left);
            if(node.right!=null) stack.push(node.right);
        }
        Collections.reverse(list);
        return list;
    }
}

统一迭代遍历

class Solution {
    //迭代遍历
    public List<Integer> postorderTraversal(TreeNode root) {
        List<Integer> list = new ArrayList<>();
        Stack<TreeNode> stack = new Stack<>();
        if(root!=null) stack.push(root);
        while (!stack.isEmpty()){
            TreeNode node = stack.pop();
            if(node!=null){
                stack.push(node);
                stack.push(null);
                if(node.right!=null) stack.push(node.right);
                if (node.left != null) stack.push(node.left);
            }else {
                node=stack.pop();
                list.add(node.val);
            }
        }
        return list;
    }
}

二叉树的层序遍历

给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。

示例 1:

img

输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]

示例 2:

输入:root = [1]
输出:[[1]]

示例 3:

输入:root = []
输出:[]

提示:

  • 树中节点数目在范围 [0, 2000]
  • -1000 <= Node.val <= 1000
class Solution {
    public List<List<Integer>> levelOrder(TreeNode root) {
        List<List<Integer>> ret = new ArrayList<>();//初始化返回的数据格式,为二维数组
        if (root == null) {
            return ret;
        }

        Queue<TreeNode> queue = new LinkedList<>();//初始化当前层
        queue.offer(root);

        while (!queue.isEmpty()) {
            List<Integer> level = new ArrayList<>();//初始化当前层数据

            int currentLevelSize = queue.size();//获取当前层的节点数
            for (int i = 0; i < currentLevelSize; i++) {//对于当前层每个节点
                TreeNode node = queue.poll();//将节点值取出,并弹出该节点
                level.add(node.val);

                if (node.left != null) {
                    queue.offer(node.left);//保存当前节点的左子节点
                }
                if (node.right != null) {
                    queue.offer(node.right);//保存当前节点的右子节点
                }
            }
            ret.add(level);
        }
        return ret;
    }
}

层序遍历:

  • 定义当前层
  • 获取当前层的节点数
  • 遍历当前层每个节点,将每个节点的子节点存入下一层,复用当前链表作为下一层链表

104.二叉树的最大深度

二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。

说明: 叶子节点是指没有子节点的节点。

示例:
给定二叉树 [3,9,20,null,null,15,7]

    3
   / \
  9  20
    /  \
   15   7

返回它的最大深度 3 。

采用递归的深度优先算法

class Solution {
    //递归,深度优先算法
    public int maxDepth(TreeNode root) {
        if(root==null)return 0;
        int leftHeight=maxDepth(root.left);//深度搜索到左树的最底端
        int rightHeight=maxDepth(root.right);//深度搜索到右树的最底端
        return Math.max(leftHeight,rightHeight)+1;//返回同层左右子树的层数大值加1
    }
}

广度优先搜索

class Solution {
    //广度优先算法(利用层序遍历每层迭代)
    public int maxDepth(TreeNode root) {
        if(root==null)return 0;
        Deque<TreeNode> queue=new ArrayDeque<>();//存放当前层的所有节点的队列
        queue.offer(root);
        int ans=0;
        while (!queue.isEmpty()){
            int size=queue.size();//获取当前层节点个数
            while (size>0){
                TreeNode node = queue.poll();//弹出当前层的节点
                if (node.left!=null) queue.offer(node.left);//将当前节点的左子节点放入队列
                if (node.right != null) queue.offer(node.right);//将当前节点的右子节点放入队列
                size--;//节点数减1
            }
            ans++;//遍历完当前层所有节点后层数加1
        }
        return ans;
    }
}

111.二叉树的最小深度

给定一个二叉树,找出其最小深度。

最小深度是从根节点到最近叶子节点的最短路径上的节点数量。

说明:叶子节点是指没有子节点的节点。

示例 1:

img

输入:root = [3,9,20,null,null,15,7]
输出:2

示例 2:

输入:root = [2,null,3,null,4,null,5,null,6]
输出:5

提示:

  • 树中节点数的范围在 [0, 105]
  • -1000 <= Node.val <= 1000

采用递归的深度优先算法

class Solution {
    //递归法,深度优先
    public int minDepth(TreeNode root) {
        if (root == null) return 0;//如果节点为空返回0
        if (root.left == null && root.right == null) return 1;//该节点为叶子节点,直接返回1
        int leftHeight = minDepth(root.left);
        int rightHeight = minDepth(root.right);
        //如果某节点只有一个子树,直接返回子树的深度
        if (root.left == null || root.right == null) return leftHeight + rightHeight + 1;
        //如果某节点有两个子树,返回子树的最小深度
        return Math.min(leftHeight, rightHeight) + 1;
    }
}

广度优先搜索

class Solution {
    public int minDepth(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int ans = 0;
        LinkedList<TreeNode> list = new LinkedList<>();//定义当前层
        list.add(root);
        while (!list.isEmpty()) {
            ans++;
            int currentLevelSize = list.size();//获取当前层的节点数
            for (int i = 0; i < currentLevelSize; i++) {
                TreeNode node = list.poll();
                if (node.left == null && node.right == null) {
                    return ans;
                }
                if (node.left != null) {
                    list.add(node.left);
                }
                if (node.right != null) {
                    list.add(node.right);
                }//将当前层的节点的子节点放入下一层

            }
        }
        return ans;
    }
}

222.完全二叉树的节点个数

给你一棵 完全二叉树 的根节点 root ,求出该树的节点个数。

完全二叉树 的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2h 个节点。

示例 1:

img

输入:root = [1,2,3,4,5,6]
输出:6

示例 2:

输入:root = []
输出:0

示例 3:

输入:root = [1]
输出:1

提示:

  • 树中节点的数目范围是[0, 5 * 104]
  • 0 <= Node.val <= 5 * 104
  • 题目数据保证输入的树是 完全二叉树

基于递归的深度优先搜索

class Solution {
    //基于递归的的深度优先搜索
    public int countNodes(TreeNode root) {
        if (root == null) return 0;
        int leftNum=countNodes(root.left);//左子树节点数
        int rightNum = countNodes(root.right);//右子树节点数
        return leftNum+rightNum+1;
    }
}

广度优先搜索

class Solution {
    public int countNodes(TreeNode root) {
        if (root == null) {
            return 0;
        }
        int ans = 1;
        LinkedList<TreeNode> list = new LinkedList<>();
        list.add(root);
        while (!list.isEmpty()) {
            int currentLevelSIze = list.size();
            for (int i = 0; i < currentLevelSIze; i++) {
                TreeNode node = list.poll();
                if (node.left != null) {
                    list.add(node.left);
                    ans++;
                }
                if (node.right != null) {
                    list.add(node.right);
                    ans++;
                }
            }
        }
        return ans;
    }
}

110.平衡二叉树

给定一个二叉树,判断它是否是高度平衡的二叉树。

本题中,一棵高度平衡二叉树定义为:

一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1 。

示例 1:

img

输入:root = [3,9,20,null,null,15,7]
输出:true

示例 2:

img

输入:root = [1,2,2,3,3,null,null,4,4]
输出:false

示例 3:

输入:root = []
输出:true

提示:

  • 树中的节点数在范围 [0, 5000]
  • -104 <= Node.val <= 104

基于递归的深度优先搜索

class Solution {
    public static int getHeight(TreeNode root) {
        if (root == null) return 0;
        int leftHeight = getHeight(root.left);
        if (leftHeight == -1) return -1;//如果当前节点的左子节点的高度为-1,返回当前节点的高度,否则返回-1;
        int rightHeight = getHeight(root.right);
        if (rightHeight == -1) return -1;//如果当前节点的右子节点的高度为-1,返回当前节点的高度,否则返回-1;
        if (Math.abs(leftHeight - rightHeight) > 1) return -1;//如果当前以当前节点为根节点的树不是一个完全二叉树,返回-1.
        return Math.max(leftHeight, rightHeight) + 1;//计算当前节点所在的层数
    }

    //基于递归的深度优先搜索
    public boolean isBalanced(TreeNode root) {
        return getHeight(root) != -1;
    }
}

257.二叉树的所有路径

给你一个二叉树的根节点 root ,按 任意顺序 ,返回所有从根节点到叶子节点的路径。

叶子节点 是指没有子节点的节点。

示例 1:

img

输入:root = [1,2,3,null,5]
输出:["1->2->5","1->3"]

示例 2:

输入:root = [1]
输出:["1"]

提示:

  • 树中节点的数目在范围 [1, 100]
  • -100 <= Node.val <= 100

递归法深度优先遍历所有路径

class Solution {
    ArrayList<String> paths;
    public List<String> binaryTreePaths(TreeNode root) {
        this.paths=new ArrayList<>();
        constructPaths(root, "");
        return paths;
    }

    public void constructPaths(TreeNode root, String path) {
        if (root != null) {
            //将父节点的所在路径放入StringBuilder
            StringBuilder pathStrBuilder = new StringBuilder(path);
            //将当前节点添加至当前路径的末尾
            pathStrBuilder.append(root.val);
            if (root.left == null && root.right == null) {
                //如果当前节点是叶子节点,将当前路径添加至路径列表
                paths.add(pathStrBuilder.toString());
            } else {
                //如果当前节点不是叶子节点,在该节点后添加箭头符号以满足题目要求
                pathStrBuilder.append("->");
                //递归左子节点
                constructPaths(root.left, pathStrBuilder.toString());
                //递归右子节点
                constructPaths(root.right, pathStrBuilder.toString());
            }
        }
    }
}

广度优先遍历所有路径

class Solution {
    //广度优先遍历所有节点
    public List<String> binaryTreePaths(TreeNode root) {
        ArrayList<String> paths = new ArrayList<>();//返回List数组
        if (root == null) return paths;
        ArrayDeque<TreeNode> nodeQueue = new ArrayDeque<>();//节点队列
        ArrayDeque<String> pathQueue = new ArrayDeque<>();//路径队列
        nodeQueue.offer(root);
        pathQueue.offer(Integer.toString(root.val));
        while (!nodeQueue.isEmpty()) {
            TreeNode node = nodeQueue.poll();//取出队首节点
            String path = pathQueue.poll();//取出队首路径
            if (node.left == null && node.right == null) paths.add(path);//如果当前节点为叶子节点,将当前路径添加至返回数组
            else {
                if (node.left != null) {
                    nodeQueue.offer(node.left);//将左子节点添加至节点队列
                    pathQueue.offer(path + "->" + node.left.val);//将左子节点的值添加至当前路径的末尾,并将当前路径添加至路径队列
                }
                if (node.right != null) {
                    nodeQueue.offer(node.right);//将右子节点添加至节点队列
                    pathQueue.offer(path + "->" + node.right.val);//将右子节点的值添加至当前路径的末尾,并将当前路径添加至路径队列
                }
            }
        }
        return paths;
    }
}

404.左叶子之和

给定二叉树的根节点 root ,返回所有左叶子之和。

示例 1:

img

输入: root = [3,9,20,null,null,15,7] 
输出: 24 
解释: 在这个二叉树中,有两个左叶子,分别是 9 和 15,所以返回 24

示例 2:

输入: root = [1]
输出: 0

提示:

  • 节点数在 [1, 1000] 范围内
  • -1000 <= Node.val <= 1000

深度优先搜索

class Solution {
    int sumLeft = 0;

    public int sumOfLeftLeaves(TreeNode root) {
        dfs(root);
        return sumLeft;
    }

    public void dfs(TreeNode root) {
        if (root == null) {
            return;
        }
        if (root.left != null) {
            //如果当前节点的左节点为叶子节点,添加改节点左节点的值
            if (root.left.left == null && root.left.right == null) {
                sumLeft += root.left.val;
            } else {
                dfs(root.left);
            }
        }
        dfs(root.right);
    }
}

广度优先搜索

class Solution {
    //广度优先搜索
    public int sumOfLeftLeaves(TreeNode root) {
        ArrayDeque<TreeNode> queue = new ArrayDeque<>();
        queue.add(root);
        int ans=0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            while (size > 0) {
                TreeNode node = queue.poll();
                if (node.left != null) {
                    if (node.left.left == null && node.left.right == null) ans += node.left.val;
                    else queue.add(node.left);
                }
                if (node.right != null) queue.add(node.right);
                size--;
            }
        }
        return ans;
    }
}

513.找树左下角的值

给定一个二叉树的 根节点 root,请找出该二叉树的 最底层 最左边 节点的值。

假设二叉树中至少有一个节点。

示例 1:

img

输入: root = [2,1,3]
输出: 1

示例 2:

img

输入: [1,2,3,4,null,5,6,null,null,7]
输出: 7

提示:

  • 二叉树的节点个数的范围是 [1,104]
  • -231 <= Node.val <= 231 - 1

深度优先搜索

class Solution {
    //深度优先搜索
    static int Deep = -1;//定义当前到达的深度
    static int val = 0;//定义当前深度最左侧的值

    public static void findLeftVale(TreeNode root, int deep) {
        if (root == null) return;
        if (root.left == null && root.right == null) {
            //深度增加时,为下一层的最左边的节点
            if (deep > Deep) {
                val = root.val;
                Deep = deep;
            }
        }
        if (root.left != null) findLeftVale(root.left, deep + 1);
        if (root.right != null) findLeftVale(root.right, deep + 1);
    }

    public int findBottomLeftValue(TreeNode root) {
        val = root.val;
        findLeftVale(root, 0);
        return val;
    }
}

广度优先搜索

class Solution {
    //广度优先搜索
    public int findBottomLeftValue(TreeNode root) {
        LinkedList<TreeNode> queue = new LinkedList<>();
        queue.offer(root);
        int res = 0;
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                TreeNode poll = queue.poll();
                if (i == 0) res = poll.val;//将该行的最左侧元素赋予返回结果
                if (poll.left != null) queue.offer(poll.left);
                if (poll.right != null) queue.offer(poll.right);
            }
        }
        return res;
    }
}

112.路径总和

递归实现

给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false

叶子节点 是指没有子节点的节点。

示例 1:

img

输入:root = [5,4,8,11,null,13,4,7,2,null,null,null,1], targetSum = 22
输出:true
解释:等于目标和的根节点到叶节点路径如上图所示。

示例 2:

img

输入:root = [1,2,3], targetSum = 5
输出:false
解释:树中存在两条根节点到叶子节点的路径:
(1 --> 2): 和为 3
(1 --> 3): 和为 4
不存在 sum = 5 的根节点到叶子节点的路径。

示例 3:

输入:root = [], targetSum = 0
输出:false
解释:由于树是空的,所以不存在根节点到叶子节点的路径。

提示:

  • 树中节点的数目在范围 [0, 5000]
  • -1000 <= Node.val <= 1000
  • -1000 <= targetSum <= 1000
class Solution {
    public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null) return false;//如果根节点为空,返回false
        //如果已经到达叶子节点,判断削减后的目标值是否和该叶子节点的值相同
        if (root.left == null && root.right == null) return root.val == targetSum;
        //如果该节点存在左右子节点,先削减目标值,递归进入
        return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val);
    }
}

深度优先搜索总结

  1. 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
  2. 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
  3. 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。

广度优先搜索总结

步骤1:定义存放当前层所有节点的队列

步骤2:存入第一层即根节点

步骤3:获取当前层节点个数

步骤4:将当前层所有节点的子节点放入队列

106.从中序与后序遍历序列构造二叉树

给定两个整数数组 inorderpostorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树

示例 1:

img

输入:inorder = [9,3,15,20,7], postorder = [9,15,7,20,3]
输出:[3,9,20,null,null,15,7]

示例 2:

输入:inorder = [-1], postorder = [-1]
输出:[-1]

提示:

  • 1 <= inorder.length <= 3000
  • postorder.length == inorder.length
  • -3000 <= inorder[i], postorder[i] <= 3000
  • inorderpostorder 都由 不同 的值组成
  • postorder 中每一个值都在 inorder
  • inorder 保证是树的中序遍历
  • postorder 保证是树的后序遍历
class Solution {
    //递归
    int post_idx;
    int[] postorder;
    int[] inorder;
    //中序遍历的hashmap
    Map<Integer, Integer> idx_map = new HashMap<Integer, Integer>();

     public TreeNode buildTree(int[] inorder, int[] postorder) {
        this.postorder = postorder;
        this.inorder = inorder;
        post_idx = postorder.length - 1;
        int idx = 0;
         //将中序遍历放入map中
        for (Integer val : inorder) idx_map.put(val, idx++);
        return helper(0, inorder.length - 1);
    }
    
    public TreeNode helper(int in_left, int in_right) {
        if (in_left > in_right) return null;
		//根节点为后序遍历的最后一个值
        int root_val = postorder[post_idx];
		//构建一个以上述根节点为根节点的树
        TreeNode root = new TreeNode(root_val);
		//获取中序遍历中该节点的位置
        int index = idx_map.get(root_val);
		//后序遍历节点逆推1个为根节点右子树的根节点
        post_idx--;
		//根节点的右子节点为中序遍历该节点右侧的数
        root.right = helper(index + 1, in_right);
        //根节点的左子节点为中序遍历该节点左侧的数
        root.left = helper(in_left, index - 1);
        return root;
    }
}

654.最大二叉树

给定一个不重复的整数数组 nums最大二叉树 可以用下面的算法从 nums 递归地构建:

  1. 创建一个根节点,其值为 nums 中的最大值。
  2. 递归地在最大值 左边子数组前缀上 构建左子树。
  3. 递归地在最大值 右边子数组后缀上 构建右子树。

返回 nums 构建的 *最大二叉树*

示例 1:

img

输入:nums = [3,2,1,6,0,5]
输出:[6,3,5,null,2,0,null,null,1]
解释:递归调用如下所示:
- [3,2,1,6,0,5] 中的最大值是 6 ,左边部分是 [3,2,1] ,右边部分是 [0,5] 。
    - [3,2,1] 中的最大值是 3 ,左边部分是 [] ,右边部分是 [2,1] 。
        - 空数组,无子节点。
        - [2,1] 中的最大值是 2 ,左边部分是 [] ,右边部分是 [1] 。
            - 空数组,无子节点。
            - 只有一个元素,所以子节点是一个值为 1 的节点。
    - [0,5] 中的最大值是 5 ,左边部分是 [0] ,右边部分是 [] 。
        - 只有一个元素,所以子节点是一个值为 0 的节点。
        - 空数组,无子节点。

示例 2:

img

输入:nums = [3,2,1]
输出:[3,null,2,null,1]

提示:

  • 1 <= nums.length <= 1000
  • 0 <= nums[i] <= 1000
  • nums 中的所有整数 互不相同
class Solution {
    public TreeNode constructMaximumBinaryTree(int[] nums) {
        return helper(nums, 0, nums.length - 1);
    }

    public TreeNode helper(int[] nums, int left, int right) {
        if (right < left) return null;
        int lagerst = left;
        //找到最大值
        for (int i = left+1; i <= right; i++) {
            if (nums[i] > nums[lagerst]) lagerst = i;
        }
        TreeNode node = new TreeNode(nums[lagerst]);
        node.left = helper(nums, left, lagerst - 1);
        node.right = helper(nums, lagerst + 1, right);
        return node;
    }
}

从两种遍历顺序构造二叉树总结

步骤一:根据后序遍历确定根节点

步骤二:根据根节点确定左右两颗子树

步骤三:确定左右边界及边界条件

步骤四:根据左右边界确定根节点的左右子树

步骤五:确定递归终止条件

617.合并二叉树

class Solution {
    public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
        if (root1 == null && root2 == null) return null;
        int root1val;
        int root2val;
        TreeNode leftNode1;
        TreeNode leftNode2;
        TreeNode rightNode1;
        TreeNode rightNode2;
        if (root1 != null) {
            root1val = root1.val;
            leftNode1 = root1.left == null ? null : root1.left;
            rightNode1 = root1.right == null ? null : root1.right;
        } else {
            root1val = 0;
            leftNode1 = null;
            rightNode1 = null;
        }
        if (root2 != null) {
            root2val = root2.val;
            leftNode2 = root2.left == null ? null : root2.left;
            rightNode2 = root2.right == null ? null : root2.right;
        } else {
            root2val = 0;
            leftNode2 = null;
            rightNode2 = null;
        }

        TreeNode node = new TreeNode(root1val + root2val);
        node.left = mergeTrees(leftNode1, leftNode2);
        node.right = mergeTrees(rightNode1, rightNode2);
        return node;
    }
}
class Solution {
    public TreeNode mergeTrees(TreeNode root1, TreeNode root2) {
        //如果两个节点有一个为空,直接返回以另一个节点为根节点的子树
        if(root1==null) return root2;
        if(root2==null) return root1;

        TreeNode node = new TreeNode(root1.val + root2.val);
        node.left = mergeTrees(root1.left, root2.left);
        node.right = mergeTrees(root1.right, root2.right);
        return node;
    }
}

700.二叉搜索树中的搜索

给定二叉搜索树(BST)的根节点 root 和一个整数值 val

你需要在 BST 中找到节点值等于 val 的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 null

示例 1:

img

输入:root = [4,2,7,1,3], val = 2
输出:[2,1,3]

示例 2:

img

输入:root = [4,2,7,1,3], val = 5
输出:[]

提示:

  • 数中节点数在 [1, 5000] 范围内
  • 1 <= Node.val <= 107
  • root 是二叉搜索树
  • 1 <= val <= 107
class Solution {
    public TreeNode searchBST(TreeNode root, int val) {
        //二叉搜索树为顺序的中序遍历数
        if (root.val > val && root.left != null) root = root.left;
        else if (root.val < val && root.right != null) root = root.right;
        else if (root.val == val) return root;
        else return null;
        return searchBST(root, val);
    }

}

98.验证二叉搜索树

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

示例 1:

img

输入:root = [2,1,3]
输出:true

示例 2:

img

输入:root = [5,1,4,null,null,3,6]
输出:false
解释:根节点的值是 5 ,但是右子节点的值是 4 。

提示:

  • 树中节点数目范围在[1, 104]
  • -231 <= Node.val <= 231 - 1
class Solution {
    boolean isLeft = true;
    boolean isRight = true;
    List<Integer> list = new ArrayList<>();

    public boolean isValidBST(TreeNode root) {
        //采用中序遍历暴力求解
        inOrder(root);
        int pos = list.get(0);
        List<Integer> listsub=list.subList(1,list.size());
        for (int o : listsub) {
            if (o > pos) pos = o;
            else return false;
        }
        return true;
    }

    public void inOrder(TreeNode root) {
        if (root.left != null) inOrder(root.left);
        list.add(root.val);
        if (root.right != null) inOrder(root.right);
    }
}
class Solution {
    public boolean isValidBST(TreeNode root) {
        return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
    }

    public boolean isValidBST(TreeNode root, long left, long right) {
        if (root == null) return true;
        if (root.val <= left || root.val >= right) return false;
        //根节点的左子树的值需要小于根节点的值,根节点的右子树的值需要大于根节点的值
        return isValidBST(root.left, left, root.val) && isValidBST(root.right, root.val, right);
    }
}

530.二叉搜索树的最小绝对差

给你一个二叉搜索树的根节点 root ,返回 树中任意两不同节点值之间的最小差值

差值是一个正数,其数值等于两值之差的绝对值。

示例 1:

img

输入:root = [4,2,6,1,3]
输出:1

示例 2:

img

输入:root = [1,0,48,null,null,12,49]
输出:1

提示:

  • 树中节点的数目范围是 [2, 104]
  • 0 <= Node.val <= 105
class Solution {
    int ans;
    int pre;
    public int getMinimumDifference(TreeNode root) {
        ans = Integer.MAX_VALUE;
        pre = -1;
        inOrder(root);
        return ans;
    }

    void inOrder(TreeNode node) {
        //采用中序遍历计算
        if (node == null) return;
        inOrder(node.left);
        if (pre != -1) ans = Math.min(ans, node.val - pre);
        pre = node.val;
        inOrder(node.right);
    }
}

501.二叉搜索树中的众数

给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。

如果树中有不止一个众数,可以按 任意顺序 返回。

假定 BST 满足如下定义:

  • 结点左子树中所含节点的值 小于等于 当前节点的值
  • 结点右子树中所含节点的值 大于等于 当前节点的值
  • 左子树和右子树都是二叉搜索树

示例 1:

img

输入:root = [1,null,2,2]
输出:[2]

示例 2:

输入:root = [0]
输出:[0]

提示:

  • 树中节点的数目在范围 [1, 104]
  • -105 <= Node.val <= 105
class Solution {
    ArrayList<Integer> ans=new ArrayList<>();
    int base, count, maxCount;

    public int[] findMode(TreeNode root) {
        inOrder(root);
        int[] answers=new int[ans.size()];
        for (int i = 0; i < ans.size(); i++) {
            answers[i]=ans.get(i);
        }
        return answers;
    }

    public void inOrder(TreeNode root) {
        if (root == null) return;
        inOrder(root.left);
        update(root.val);
        inOrder(root.right);
    }

    //更新最大计数和对应的数
    public void update(int x) {
        if (x == base) ++count;
        else {
            count = 1;
            base = x;
        }
        if (count == maxCount) ans.add(base);
        if (count > maxCount) {
            maxCount = count;
            ans.clear();
            ans.add(base);
        }
    }
}

236. 二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

示例 1:

img

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出:3
解释:节点 5 和节点 1 的最近公共祖先是节点 3 。

示例 2:

img

输入:root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出:5
解释:节点 5 和节点 4 的最近公共祖先是节点 5 。因为根据定义最近公共祖先节点可以为节点本身。

示例 3:

输入:root = [1,2], p = 1, q = 2
输出:1

提示:

  • 树中节点数目在范围 [2, 105] 内。
  • -109 <= Node.val <= 109
  • 所有 Node.val 互不相同
  • p != q
  • pq 均存在于给定的二叉树中。
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root == p || root == q || root == null) return root;
        //搜索左子树
        TreeNode left = lowestCommonAncestor(root.left, p, q);
        //搜索右子树
        TreeNode right = lowestCommonAncestor(root.right, p, q);
        //如果左子树和右子树都不为空,说明两个节点分别在该节点的左右子树,则返回该节点
        if (left != null && right != null) return root;
        //如果左右子树有一个为空,则返回非空的根节点
        if (left == null) return right;
        return left;
    }
}

235. 二叉搜索树的最近公共祖先

给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。

示例 1:

输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6 
解释: 节点 2 和节点 8 的最近公共祖先是 6。

示例 2:

输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。

说明:

  • 所有节点的值都是唯一的。
  • p、q 为不同节点且均存在于给定的二叉搜索树中。
class Solution {
    public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) {
        if (root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q);
        if (root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q);
        return root;
    }
}

701.二叉搜索树中的插入操作

给定二叉搜索树(BST)的根节点 root 和要插入树中的值 value ,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据 保证 ,新值和原始二叉搜索树中的任意节点值都不同。

注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回 任意有效的结果

示例 1:

img

输入:root = [4,2,7,1,3], val = 5
输出:[4,2,7,1,3,5]
解释:另一个满足题目要求可以通过的树是:

示例 2:

输入:root = [40,20,60,10,30,50,70], val = 25
输出:[40,20,60,10,30,50,70,null,null,25]

示例 3:

输入:root = [4,2,7,1,3,null,null,null,null,null,null], val = 5
输出:[4,2,7,1,3,5]

提示:

  • 树中的节点数将在 [0, 104]的范围内。
  • -108 <= Node.val <= 108
  • 所有值 Node.val独一无二 的。
  • -108 <= val <= 108
  • 保证 val 在原始BST中不存在。
class Solution {
    public TreeNode insertIntoBST(TreeNode root, int val) {
        if (root == null) return new TreeNode(val);
        if (root.val < val) root.right = insertIntoBST(root.right, val);
        if (root.val > val) root.left = insertIntoBST(root.left, val);
        return root;
    }
}

450.删除二叉搜索树中的节点

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

一般来说,删除节点可分为两个步骤:

  1. 首先找到需要删除的节点;
  2. 如果找到了,删除它。

示例 1:

img
输入:root = [5,3,6,2,4,null,7], key = 3
输出:[5,4,6,2,null,null,7]
解释:给定需要删除的节点值是 3,所以我们首先找到 3 这个节点,然后删除它。
一个正确的答案是 [5,4,6,2,null,null,7], 如下图所示。
另一个正确答案是 [5,2,6,null,4,null,7]。

示例 2:

输入: root = [5,3,6,2,4,null,7], key = 0
输出: [5,3,6,2,4,null,7]
解释: 二叉树不包含值为 0 的节点

示例 3:

输入: root = [], key = 0
输出: []

提示:

  • 节点数的范围 [0, 104].
  • -105 <= Node.val <= 105
  • 节点值唯一
  • root 是合法的二叉搜索树
  • -105 <= key <= 105
class Solution {
    public TreeNode deleteNode(TreeNode root, int key) {
        if (root == null) return null;
        if (root.val == key) {
            if (root.right == null && root.left == null) return null;
            if (root.right != null && root.left != null) {
                //如果要删除的节点左右都有子节点,需要旋转子节点,将该节点的左子树接到该节点右子树的左下角
                if (root.right.left != null) {
                    TreeNode pos = root.right.left;
                    while (pos.left != null) pos = pos.left;
                    pos.left = root.left;
                } else root.right.left = root.left;
                return root.right;
            }
            if (root.left == null) return root.right;
            return root.left;
        }
        if (root.val > key) root.left = deleteNode(root.left, key);
        if (root.val < key) root.right = deleteNode(root.right, key);
        return root;
    }
}

669. 修剪二叉搜索树

给你二叉搜索树的根节点 root ,同时给定最小边界low 和最大边界 high。通过修剪二叉搜索树,使得所有节点的值在[low, high]中。修剪树 不应该 改变保留在树中的元素的相对结构 (即,如果没有被移除,原有的父代子代关系都应当保留)。 可以证明,存在 唯一的答案

所以结果应当返回修剪好的二叉搜索树的新的根节点。注意,根节点可能会根据给定的边界发生改变。

示例 1:

img
输入:root = [1,0,2], low = 1, high = 2
输出:[1,null,2]

示例 2:

img
输入:root = [3,0,4,null,2,null,null,1], low = 1, high = 3
输出:[3,2,null,1]

提示:

  • 树中节点数在范围 [1, 104]
  • 0 <= Node.val <= 104
  • 树中每个节点的值都是 唯一
  • 题目数据保证输入是一棵有效的二叉搜索树
  • 0 <= low <= high <= 104
class Solution {
    public TreeNode trimBST(TreeNode root, int low, int high) {
        if (root == null) return null;
        //步骤1:递归找到边界外的值
        if (root.val >= low) root.right = trimBST(root.right, low, high);
        if (root.val <= high) root.left = trimBST(root.left, low, high);
        //步骤2:删除边界内的值
        //删除左边界外左子树的值
        if (root.val < low) return trimBST(root.right, low, high);
        //删除右边界右外右子树的值
        if (root.val > high) return trimBST(root.left, low, high);
        return root;
    }
}

108.将有序数组转换为二叉搜索树

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。

高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。

示例 1:

img

输入:nums = [-10,-3,0,5,9]
输出:[0,-3,9,-10,null,5]
解释:[0,-10,5,null,-3,null,9] 也将被视为正确答案:

示例 2:

img

输入:nums = [1,3]
输出:[3,1]
解释:[1,null,3] 和 [3,1] 都是高度平衡二叉搜索树。

提示:

  • 1 <= nums.length <= 104
  • -104 <= nums[i] <= 104
  • nums严格递增 顺序排列
class Solution {
    //将数组转化为list进行操作
    public TreeNode sortedArrayToBST(int[] nums) {
        //将数组转化为list
        List<Integer> list = Arrays.stream(nums).boxed().toList();
        return helper(list);
    }

    public TreeNode helper(List<Integer> list) {
        int cen = list.size() / 2;
        if (cen > list.size() - 1) return null;
        TreeNode root = new TreeNode(list.get(cen));
        if(cen == 0) return root;
        root.left = helper(list.subList(0, cen));
        root.right = helper(list.subList(cen + 1, list.size()));
        return root;
    }
}
class Solution {
    //直接对数组进行操作
    public TreeNode sortedArrayToBST(int[] nums) {
        return sortedArrayToBST(nums, 0, nums.length);
    }

    public TreeNode sortedArrayToBST(int[] nums, int left, int right) {
        if (left >= right) {
            return null;
        }
        if (right - left == 1) {
            return new TreeNode(nums[left]);
        }
        int mid = left + (right - left) / 2;
        //创建根节点
        TreeNode root = new TreeNode(nums[mid]);
        root.left = sortedArrayToBST(nums, left, mid);
        root.right = sortedArrayToBST(nums, mid + 1, right);
        return root;
    }
}

538.把二叉搜索树转换为累加树

给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。

示例 1:

img

输入:[4,1,6,0,2,5,7,null,null,null,3,null,null,null,8]
输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8]

示例 2:

输入:root = [0,null,1]
输出:[1,null,1]

示例 3:

输入:root = [1,0,2]
输出:[3,3,2]

示例 4:

输入:root = [3,2,4,1]
输出:[7,9,4,10]

提示:

  • 树中的节点数介于 0104 之间。
  • 每个节点的值介于 -104104 之间。
  • 树中的所有值 互不相同
  • 给定的树为二叉搜索树
class Solution {
    int sum;
    public TreeNode convertBST(TreeNode root) {
        sum=0;
        helper(root);
        return root;
    }
    //反中序遍历
    public void helper(TreeNode root){
        if (root==null) return;
        helper(root.right);
        sum+= root.val;
        root.val=sum;
        helper(root.left);
    }
}

回溯算法

77.组合

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

示例 2:

输入:n = 1, k = 1
输出:[[1]]

提示:

  • 1 <= n <= 20
  • 1 <= k <= n

77.组合4

class Solution {
    LinkedList<Integer> path = new LinkedList<>();
    List<List<Integer>> result = new ArrayList<>();

    public List<List<Integer>> combine(int n, int k) {
        hepler(n, k, 1);
        return result;
    }

    public void hepler(int n, int k, int startIndex) {
        //返回条件:如果剩下的元素数量等于组合的数量,则直接添加到结果数组中
        if (path.size() == k) {
            result.add(new ArrayList<>(path));
            return;
        }
        //i <= n - (k - path.size()) + 1:减枝处理,表示开始的位置需要保证后面的元素个数不小于组合个数
        //纵向遍历
        for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) {
            //结果添加
            path.add(i);
            //横向遍历
            hepler(n, k, i + 1);
            //回溯
            path.removeLast();
        }
    }
}

剪枝原理:

  1. 已经选择的元素个数:path.size()
  2. 还需要的元素个数为: k - path.size();
  3. 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历

216.组合总和III

找出所有相加之和为 nk 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

示例 1:

输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。

示例 2:

输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。

示例 3:

输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。

提示:

  • 2 <= k <= 9
  • 1 <= n <= 60

216.组合总和III1

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> combinationSum3(int k, int n) {
        helper(n, k, 0, 1);
        return result;
    }

    public void helper(int target, int k, int targetSum, int startIndex) {
        //返回条件
        if (path.size() == k) {
            if (target == targetSum) result.add(new ArrayList<>(path));
            return;
        }
		//纵向递归
        for (int i = startIndex; i < 10; i++) {
            //剪枝
            if (targetSum + i > target) break;
            //结果添加
            path.add(i);
            //横向递归
            helper(target, k, targetSum + i, i + 1);
            //回溯
            path.removeLast();
        }
    }
}

17.电话号码的字母组合

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

img

示例 1:

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

示例 2:

输入:digits = ""
输出:[]

示例 3:

输入:digits = "2"
输出:["a","b","c"]

提示:

  • 0 <= digits.length <= 4
  • digits[i] 是范围 ['2', '9'] 的一个数字。

17. 电话号码的字母组合

//步骤1:定义数字与字符串的映射
//步骤2:顺序找到给出数字对应的字符串
//步骤3:回溯

class Solution {
    List<String> result = new ArrayList<>();
    StringBuffer temp = new StringBuffer();

    public List<String> letterCombinations(String digits) {
        //定义数字与字符串的映射关系
        String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
        helper(digits, numString, 0);
        return result;
    }

    public void helper(String digits, String[] numString, int startIndex) {
        //如果达到数字字符串的长度,返回条件
        if (startIndex == digits.length()) {
            result.add(temp.toString());
            return;
        }
		//定义字符串所在的下标
        String str = numString[digits.charAt(startIndex) - '0'];
        //纵向遍历
        for (int i = 0; i < str.length(); i++) {
            //结果添加
            temp.append(str.charAt(i));
            //横向遍历
            helper(digits, numString, startIndex + 1);
            //回溯
            temp.deleteCharAt(temp.length() - 1);
        }
    }
}

组合问题总结

  1. 确定横向遍历传递参数
  2. 在路径中添加当前元素
  3. 递归横向遍历
  4. 去除当前元素,纵向遍历下一个元素

39. 组合总和

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:

输入: candidates = [2], target = 1
输出: []

提示:

  • 1 <= candidates.length <= 30
  • 2 <= candidates[i] <= 40
  • candidates 的所有元素 互不相同
  • 1 <= target <= 40

39.组合总和1

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        Arrays.sort(candidates);
        helper(target, candidates, 0, 0);
        return result;
    }

    public void helper(int target, int[] candidates, int targetSum, int startIndex) {
        if (targetSum == target) {
            result.add(new ArrayList<>(path));
        }

        for (int i = startIndex; i < candidates.length; i++) {
            if (targetSum + candidates[i] > target) {
                break;
            }
            path.add(candidates[i]);
            helper(target, candidates, targetSum + candidates[i], i);
            path.removeLast();
        }
    }
}

40.组合总和II

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次

注意:解集不能包含重复的组合。

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

提示:

  • 1 <= candidates.length <= 100
  • 1 <= candidates[i] <= 50
  • 1 <= target <= 30

40.组合总和II

class Solution {
        List<List<Integer>> result = new ArrayList<>();
        LinkedList<Integer> path = new LinkedList<>();
        boolean[] used;

        public List<List<Integer>> combinationSum2(int[] candidates, int target) {
            used = new boolean[candidates.length];
            Arrays.fill(used, false);
            Arrays.sort(candidates);
            helper(target, candidates, 0, 0);
            return result;
        }

        public void helper(int target, int[] candidates, int targetSum, int startIndex) {
            //返回结果
            if (targetSum == target) result.add(new ArrayList<>(path));
            //纵向遍历
            for (int i = startIndex; i < candidates.length; i++) {
                //剪枝
                if (targetSum + candidates[i] > target) break;
                //将数组排序后,如果当前位置与上一位置相同并且上一个位置已经使用过便跳过该位置的值
                if (i > 0 && candidates[i] == candidates[i - 1] & !used[i - 1]) continue;
                used[i] = true;
                //结果添加
                path.add(candidates[i]);
                //横向递归
                helper(target, candidates, targetSum + candidates[i], i + 1);
                path.removeLast();
                used[i]=false;
            }
        }
    }

131.分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。

回文串 是正着读和反着读都一样的字符串。

示例 1:

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

示例 2:

输入:s = "a"
输出:[["a"]]

提示:

  • 1 <= s.length <= 16
  • s 仅由小写英文字母组成

131.分割回文串

class Solution {
    List<List<String>> result = new ArrayList<>();
    LinkedList<String> path = new LinkedList<>();

    public List<List<String>> partition(String s) {
        helper(s, 0);
        return result;
    }

    public void helper(String s, int startIndex) {
        //返回条件
        if (startIndex >= s.length()) {
            result.add(new ArrayList<>(path));
            return;
        }
        //纵向遍历
        for (int i = startIndex; i < s.length(); i++) {
            //结果添加
            if (isPalindrome(s, startIndex, i)) {
                String str = s.substring(startIndex, i+1);
                path.add(str);
            } else continue;
            //横向递归
            helper(s, i + 1);
            //回溯
            path.removeLast();
        }
    }

    //判断是否时回文
    public boolean isPalindrome(String s, int startIndex, int end) {
        for (int i = startIndex, j = end; i < j; i++, j--) {
            if (s.charAt(i) != s.charAt(j)) return false;
        }
        return true;
    }
}

93.复原IP地址

有效 IP 地址 正好由四个整数(每个整数位于 0255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

  • 例如:"0.1.2.201" "192.168.1.1"有效 IP 地址,但是 "0.011.255.245""192.168.1.312""192.168@1.1"无效 IP 地址。

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

示例 1:

输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

示例 2:

输入:s = "0000"
输出:["0.0.0.0"]

示例 3:

输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]

提示:

  • 1 <= s.length <= 20
  • s 仅由数字组成

93.复原IP地址

class Solution {
    List<String> result = new ArrayList<>();
    LinkedList<String> path = new LinkedList<>();

    public List<String> restoreIpAddresses(String s) {
        helper(s, 0);
        return result;
    }

    public void helper(String s, int startIndex) {
        //返回条件
        if (startIndex >= s.length()) {
            if (path.size() == 4) {
                result.add(String.join(".", path));
            }
            return;
        }

        //纵向遍历
        for (int i = startIndex; i < s.length(); i++) {
            //结果添加
            if (isValid(s, startIndex, i) && path.size() < 4) {
                String str = s.substring(startIndex, i + 1);
                path.add(str);
            } else {
                //如果该段的数字不符合条件,后面再添加一定不符合条件,直接跳出循环
                break;
            }
            //横向递归
            helper(s, i + 1);
            path.removeLast();
        }
    }

    public boolean isValid(String s, int startIndex, int end) {
        String str = s.substring(startIndex, end + 1);
        int num = Integer.parseInt(str);
        return num <= 255 && num >= Math.pow(10, str.length() - 1) || num == 0 && str.length() == 1;
    }
}

78.子集

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10
  • nums 中的所有元素 互不相同

78.子集

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> subsets(int[] nums) {
        helper(nums, 0);
        return result;
    }

    public void helper(int[] nums, int startIndex) {
        result.add(new ArrayList<>(path));
        //返回条件
        if (startIndex >= nums.length) {
            return;
        }

        //纵向遍历
        for (int i = startIndex; i < nums.length; i++) {
            //结果添加
            path.add(nums[i]);
            //横向递归
            helper(nums, i + 1);
            //回溯
            path.removeLast();
        }
    }
}

90.子集II

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

示例 1:

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

提示:

  • 1 <= nums.length <= 10
  • -10 <= nums[i] <= 10

90.子集II

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    boolean[] used;

    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        used = new boolean[nums.length];
        Arrays.fill(used, false);
        helper(nums, 0);
        return result;
    }

    public void helper(int[] nums, int startIndex) {
        result.add(new ArrayList<>(path));
        //返回条件
        if (startIndex >= nums.length) return;


        //纵向遍历
        for (int i = startIndex; i < nums.length; i++) {
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) continue;
            //结果添加
            path.add(nums[i]);
            used[i]=true;
            //横向递归
            helper(nums, i + 1);
            //回溯
            path.removeLast();
            used[i] = false;
        }
    }
}

491.递增子序列

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

示例 1:

输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]

示例 2:

输入:nums = [4,4,3,2,1]
输出:[[4,4]]

提示:

  • 1 <= nums.length <= 15
  • -100 <= nums[i] <= 100

491. 递增子序列1

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();

    public List<List<Integer>> findSubsequences(int[] nums) {
        helper(nums, 0);
        return result;
    }

    public void helper(int[] nums, int startIndex) {
        //如果序列的长度大于1,则添加到结果中
        if (path.size() > 1) {
            result.add(new ArrayList<>(path));
        }
        //通过数组映射确保每层的重复元素不被重复使用
        int[] used = new int[201];
        //纵向遍历
        for (int i = startIndex; i < nums.length; i++) {
            //剪枝:递增且未重复
            if (!path.isEmpty() && nums[i] < path.get(path.size() - 1) || used[nums[i] + 100] == 1) continue;
            used[nums[i] + 100] = 1;
            //结果添加
            path.add(nums[i]);
            //横向递归
            helper(nums, i + 1);
            //回溯
            path.removeLast();
        }
    }
}

46.全排列

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]
输出:[[1]]

提示:

  • 1 <= nums.length <= 6
  • -10 <= nums[i] <= 10
  • nums 中的所有整数 互不相同

46.全排列

class Solution {
    List<List<Integer>> result = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    boolean[] used;
    public List<List<Integer>> permute(int[] nums) {
        used = new boolean[nums.length];
        Arrays.fill(used, false);
        helper(nums, 0);
        return result;
    }

    public void helper(int[] nums, int startIndex) {
        //返回条件
        if (path.size()== nums.length) {
            result.add(new ArrayList<>(path));
            return;
        }

        //纵向遍历
        for (int i = 0; i < nums.length; i++) {
            //剪枝
            if (used[i]) continue;
            used[i] = true;
            path.add(nums[i]);
            //纵向递归
            helper(nums, startIndex + 1);
            //回溯
            path.removeLast();
            used[i]=false;
        }
    }
}

332.重新安排行程

给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。

所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

  • 例如,行程 ["JFK", "LGA"]["JFK", "LGB"] 相比就更小,排序更靠前。

假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次且只能用一次。

示例 1:

img
输入:tickets = [["MUC","LHR"],["JFK","MUC"],["SFO","SJC"],["LHR","SFO"]]
输出:["JFK","MUC","LHR","SFO","SJC"]

示例 2:

img
输入:tickets = [["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]]
输出:["JFK","ATL","JFK","SFO","ATL","SFO"]
解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"] ,但是它字典排序更大更靠后。

提示:

  • 1 <= tickets.length <= 300
  • tickets[i].length == 2
  • fromi.length == 3
  • toi.length == 3
  • fromitoi 由大写英文字母组成
  • fromi != toi

332.重新安排行程1

class Solution {
    List<String> result = new ArrayList<>();
    LinkedList<String> path = new LinkedList<>();
    boolean[] used;

    public List<String> findItinerary(List<List<String>> tickets) {
        used = new boolean[tickets.size()];
        Arrays.fill(used, false);
        tickets.sort(Comparator.comparing(a -> a.get(1)));
        path.add("JFK");
        helper(tickets);
        return result;
    }

    public boolean helper(List<List<String>> tickets) {
        //返回条件
        if (path.size() == tickets.size() + 1) {
            result = new ArrayList<>(path);
            return true;
        }
        //纵向遍历
        for (int i = 0; i < tickets.size(); i++) {
            //剪枝
            if (!used[i] && tickets.get(i).get(0).equals(path.getLast())) {
                path.add(tickets.get(i).get(1));
                used[i] = true;
                //纵向递归
                if (helper(tickets)) {
                    return true;
                }
                //如果纵向递归没有找到结果则进行回溯,反之直接不进行回溯
                path.removeLast();
                used[i] = false;
            }
        }
        return false;
    }
}

51. N皇后

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。

示例 1:

img

输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。

示例 2:

输入:n = 1
输出:[["Q"]]

提示:

  • 1 <= n <= 9

51.N皇后

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

    public List<List<String>> solveNQueens(int n) {
        char[][] chessboard = new char[n][n];
        for (char[] c : chessboard) Arrays.fill(c, '.');
        helper(n, 0, chessboard);
        return result;
    }

    public void helper(int n, int row, char[][] chessboard) {
        //返回条件
        if (row == n) {
            result.add(Array2List(chessboard));
            return;
        }
        //纵向遍历
        for (int col = 0; col < n; ++col) {
            if (isValid(row, col, n, chessboard)) {
                chessboard[row][col] = 'Q';
                helper(n, row + 1, chessboard);
                //回溯
                chessboard[row][col] = '.';
            }
        }
    }

    //将二维数组转化为List
    private List<String> Array2List(char[][] chessboard) {
        List<String> list = new ArrayList<>();
        for (char[] c : chessboard) list.add(String.copyValueOf(c));
        return list;
    }

    public boolean isValid(int row, int col, int n, char[][] chessboard) {

        //检查列
        for (int i = 0; i < row; i++) {
            if (chessboard[i][col] == 'Q') return false;
        }
        //检查45度对角线
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (chessboard[i][j] == 'Q') return false;
        }
        //检查135度对角线
        for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            if (chessboard[i][j] == 'Q') return false;
        }
        return true;
    }
}

37. 解数独

编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则

  1. 数字 1-9 在每一行只能出现一次。
  2. 数字 1-9 在每一列只能出现一次。
  3. 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)

数独部分空格内已填入了数字,空白格用 '.' 表示。

示例 1:

img

输入:board = [
["5","3",".",".","7",".",".",".","."],
["6",".",".","1","9","5",".",".","."],
[".","9","8",".",".",".",".","6","."],
["8",".",".",".","6",".",".",".","3"],
["4",".",".","8",".","3",".",".","1"],
["7",".",".",".","2",".",".",".","6"],
[".","6",".",".",".",".","2","8","."],
[".",".",".","4","1","9",".",".","5"],
[".",".",".",".","8",".",".","7","9"]
]
输出:[
["5","3","4","6","7","8","9","1","2"],
["6","7","2","1","9","5","3","4","8"],
["1","9","8","3","4","2","5","6","7"],
["8","5","9","7","6","1","4","2","3"],
["4","2","6","8","5","3","7","9","1"],
["7","1","3","9","2","4","8","5","6"],
["9","6","1","5","3","7","2","8","4"],
["2","8","7","4","1","9","6","3","5"],
["3","4","5","2","8","6","1","7","9"]
]

提示:

  • board.length == 9
  • board[i].length == 9
  • board[i][j] 是一位数字或者 '.'
  • 题目数据 保证 输入数独仅有一个解

37.解数独

class Solution {
    public void solveSudoku(char[][] board) {
        helper(board);
    }

    public boolean helper(char[][] board) {
        //「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列, 
        // 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」 
        for (int i = 0; i < 9; i++) {// 遍历行 
            for (int j = 0; j < 9; j++) {// 遍历列 
                if (board[i][j] != '.') {// 跳过原始数字 
                    // 数独部分空格内已填入了数字,空白格用 '.' 表示 
                    continue;
                }
                for (char k = '1'; k <= '9'; k++) {
                    // (i, j) 这个位置放k是否合适 
                    if (isValid(i, j, k, board)) {
                        board[i][j] = k;
                        if (helper(board)) {// 如果找到合适一组立刻返回 布尔类型 
                            return true;
                        }
                        board[i][j] = '.';// 回溯 
                    }
                }
                // 9个数都试完了,都不行,那么就返回false 
                return false;
                // 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解! 
                // 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」 
            }
        }
        // 遍历完没有返回false,说明找到了合适棋盘位置了 
        return true;
    }

    public boolean isValid(int row, int col, char val, char[][] board) {
        //检查行
        for (int i = 0; i < 9; i++) {
            if (board[row][i] == val) return false;
        }
        //检查列
        for (int i = 0; i < 9; i++) {
            if (board[i][col] == val) return false;
        }
        //9宫格是否重复
        int startRow = (row / 3) * 3;
        int starrCol = (col / 3) * 3;
        for (int i = startRow; i < startRow + 3; i++) {
            for (int j = starrCol; j < starrCol + 3; j++) {
                if (board[i][j] == val) return false;
            }
        }
        return true;
    }
}

回溯算法总结

对于重新安排行程、[解数独](#37. 解数独)这种只有一个解的问题,需要在找到解后立即返回,打断回溯。

贪心算法

455.分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。

对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

示例 1:

输入: g = [1,2,3], s = [1,1]
输出: 1
解释: 
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。

示例 2:

输入: g = [1,2], s = [1,2,3]
输出: 2
解释: 
你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。
你拥有的饼干数量和尺寸都足以让所有孩子满足。
所以你应该输出2.

提示:

  • 1 <= g.length <= 3 * 104
  • 0 <= s.length <= 3 * 104
  • 1 <= g[i], s[j] <= 231 - 1
class Solution {
    public int findContentChildren(int[] g, int[] s) {
        Arrays.sort(g);
        Arrays.sort(s);
        int start = 0;
        int count = 0;
        for (int i = 0; i < s.length && start < g.length; i++) {
            if (s[i] >= g[start]) {
                count++;
                start++;
            }
        }
        return count;
    }
}

376. 摆动序列

如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为 摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

  • 例如, [1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值 (6, -3, 5, -7, 3) 是正负交替出现的。
  • 相反,[1, 4, 7, 2, 5][1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

子序列 可以通过从原始序列中删除一些(也可以不删除)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums 中作为 摆动序列最长子序列的长度

示例 1:

输入:nums = [1,7,4,9,2,5]
输出:6
解释:整个序列均为摆动序列,各元素之间的差值为 (6, -3, 5, -7, 3) 。

示例 2:

输入:nums = [1,17,5,10,13,15,10,5,16,8]
输出:7
解释:这个序列包含几个长度为 7 摆动序列。
其中一个是 [1, 17, 10, 13, 10, 16, 8] ,各元素之间的差值为 (16, -7, 3, -3, 6, -8) 。

示例 3:

输入:nums = [1,2,3,4,5,6,7,8,9]
输出:2

提示:

  • 1 <= nums.length <= 1000
  • 0 <= nums[i] <= 1000
class Solution {
    public int wiggleMaxLength(int[] nums) {
        if (nums.length <= 1) return nums.length;
        int preDiff = 0;
        int curDiff;
        int count = 1;
        for (int i = 1; i < nums.length; i++) {
            curDiff = nums[i] - nums[i - 1];
            if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) {
                count++;
                preDiff = curDiff;
            }
        }
        return count;
    }
}

53. 最大子序和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:

输入:nums = [1]
输出:1

示例 3:

输入:nums = [5,4,-1,7,8]
输出:23

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104
class Solution {
    public int maxSubArray(int[] nums) {
        if (nums.length == 1) return nums[0];
        int sum = Integer.MIN_VALUE;
        int count = 0;
        for (int i = 0; i < nums.length; i++) {
            count += nums[i];
            sum = Math.max(sum, count);//更新最大子序列和
            if (count <= 0) count = 0;//如果当前序列和小于零,重新开始
        }
        return sum;
    }
}

122.买卖股票的最佳时机II

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

示例 1:

输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
     总利润为 4 + 3 = 7 。

示例 2:

输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
     总利润为 4 。

示例 3:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。

提示:

  • 1 <= prices.length <= 3 * 104
  • 0 <= prices[i] <= 104
class Solution {
    public int maxProfit(int[] prices) {
        int count;
        int sum=0;
        for (int i = 1; i < prices.length; i++) {
            count=prices[i]-prices[i-1];
            if(count>0) sum+=count;
        }
        return sum;
    }
}

55. 跳跃游戏

给定一个非负整数数组 nums ,你最初位于数组的 第一个下标

数组中的每个元素代表你在该位置可以跳跃的最大长度。

判断你是否能够到达最后一个下标。

示例 1:

输入:nums = [2,3,1,1,4]
输出:true
解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。

示例 2:

输入:nums = [3,2,1,0,4]
输出:false
解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。

提示:

  • 1 <= nums.length <= 3 * 104
  • 0 <= nums[i] <= 105
class Solution {
    public boolean canJump(int[] nums) {
        if (nums.length == 1) return true;
        int coverRange = 0;
        for (int i = 0; i <= coverRange; i++) {
            //coverRange为上个位置的最大覆盖范围,i+num[i]为当前位置的最大覆盖范围,取大值作为下一个最大覆盖范围
            coverRange = Math.max(coverRange, i + nums[i]);
            if (coverRange >= nums.length - 1) return true;
        }
        return false;
    }
}

45.跳跃游戏II

给定一个长度为 n0 索引整数数组 nums。初始位置为 nums[0]

每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:

  • 0 <= j <= nums[i]
  • i + j < n

返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]

示例 1:

输入: nums = [2,3,1,1,4]
输出: 2
解释: 跳到最后一个位置的最小跳跃数是 2。
     从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。

示例 2:

输入: nums = [2,3,0,1,4]
输出: 2

提示:

  • 1 <= nums.length <= 104
  • 0 <= nums[i] <= 1000
  • 题目保证可以到达 nums[n-1]
class Solution {
    public int jump(int[] nums) {
        if (nums == null || nums.length == 0 || nums.length == 1) {
            return 0;
        }
        //当前跳跃次数
        int count = 0;
        //当前最大覆盖区域
        int curDistance = 0;
        //最大覆盖区域
        int maxDistance = 0;
        for (int i = 0; i < nums.length; i++) {
            //在可覆盖区域更新最大覆盖区域
            maxDistance = Math.max(maxDistance, nums[i] + i);
            //如果最大覆盖区域已经覆盖了终点,t数加1,结束循环
            if (maxDistance >= nums.length - 1) {
                count++;
                break;
            }
            //如果已经走到当前区域的最大范围,curDistance更新为下一步的最大覆盖区域,跳数加1
            if (i == curDistance) {
                curDistance = maxDistance;
                count++;
            }
        }
        return count;
    }
}

1005.K次取反后最大化的数组和

给你一个整数数组 nums 和一个整数 k ,按以下方法修改该数组:

  • 选择某个下标 i 并将 nums[i] 替换为 -nums[i]

重复这个过程恰好 k 次。可以多次选择同一个下标 i

以这种方式修改数组后,返回数组 可能的最大和

示例 1:

输入:nums = [4,2,3], k = 1
输出:5
解释:选择下标 1 ,nums 变为 [4,-2,3] 。

示例 2:

输入:nums = [3,-1,0,2], k = 3
输出:6
解释:选择下标 (1, 2, 2) ,nums 变为 [3,1,0,2] 。

示例 3:

输入:nums = [2,-3,-1,5,-4], k = 2
输出:13
解释:选择下标 (1, 4) ,nums 变为 [2,3,-1,5,4] 。

提示:

  • 1 <= nums.length <= 104
  • -100 <= nums[i] <= 100
  • 1 <= k <= 104
class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        //将数组元素按绝对值从大到小排序
        nums = IntStream.of(nums)
                .boxed()
                .sorted(((o1, o2) -> Math.abs(o2) - Math.abs(o1)))
                .mapToInt(Integer::intValue).toArray();
        int len = nums.length;
        //将绝对值大的负值全部转化为正值
        for (int i = 0; i < len; i++) {
            if (nums[i] < 0 && k > 0) {
                nums[i] = -nums[i];
                k--;
            }
        }
        //如果所有的值都变成了正值,将有多余的转化次数放在最小的值上
        if (k % 2 == 1) nums[len - 1] = -nums[len - 1];
        return Arrays.stream(nums).sum();
    }
}

134. 加油站

在一条环路上有 n 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

给定两个整数数组 gascost ,如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1 。如果存在解,则 保证 它是 唯一 的。

示例 1:

输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2]
输出: 3
解释:
从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油
开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油
开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油
开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油
开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油
开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。
因此,3 可为起始索引。

示例 2:

输入: gas = [2,3,4], cost = [3,4,3]
输出: -1
解释:
你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。
我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油
开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油
开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油
你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。
因此,无论怎样,你都不可能绕环路行驶一周。

提示:

  • gas.length == n
  • cost.length == n
  • 1 <= n <= 105
  • 0 <= gas[i], cost[i] <= 104
class Solution {
    public int canCompleteCircuit(int[] gas, int[] cost) {
        int sum = 0;
        int min = 0;
        int pos = 0;
        for (int i = 0; i < gas.length; i++) {
            sum += (gas[i] - cost[i]);
            min = Math.min(sum, min);
            //保存存油最小的位置
            if (min == sum) pos = i;
        }
        if (sum < 0) return -1;//油不够
        if (min == 0) return 0;//
        else return pos == gas.length - 1 ? 0 : pos + 1;
    }
}

135. 分发糖果

n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

你需要按照以下要求,给这些孩子分发糖果:

  • 每个孩子至少分配到 1 个糖果。
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。

请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目

示例 1:

输入:ratings = [1,0,2]
输出:5
解释:你可以分别给第一个、第二个、第三个孩子分发 2、1、2 颗糖果。

示例 2:

输入:ratings = [1,2,2]
输出:4
解释:你可以分别给第一个、第二个、第三个孩子分发 1、2、1 颗糖果。
     第三个孩子只得到 1 颗糖果,这满足题面中的两个条件。

提示:

  • n == ratings.length
  • 1 <= n <= 2 * 104
  • 0 <= ratings[i] <= 2 * 104
class Solution {
    public int candy(int[] ratings) {
        int[] candyVec = new int[ratings.length];
        candyVec[0] = 1;
        for (int i = 1; i < candyVec.length; i++) {
            candyVec[i] = ratings[i] > ratings[i - 1] ? candyVec[i - 1] + 1 : 1;
        }
        for (int i = ratings.length - 2; i >= 0; i--) {
            if (ratings[i] > ratings[i + 1]) candyVec[i] = Math.max(candyVec[i], candyVec[i + 1] + 1);
        }
        return Arrays.stream(candyVec).sum();
    }
}

860.柠檬水找零

在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false

示例 1:

输入:bills = [5,5,5,10,20]
输出:true
解释:
前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。
第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。
第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。
由于所有客户都得到了正确的找零,所以我们输出 true。

示例 2:

输入:bills = [5,5,10,10,20]
输出:false
解释:
前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。
对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。
对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。
由于不是每位顾客都得到了正确的找零,所以答案是 false。

提示:

  • 1 <= bills.length <= 105
  • bills[i] 不是 5 就是 10 或是 20
class Solution {
    public boolean lemonadeChange(int[] bills) {
        int five = 0;
        int ten = 0;
        for (int bill : bills) {
            if (bill == 5) five++;
            else if (bill == 10) {
                five--;
                ten++;
            } else if (bill == 20) {
                if (ten > 0) {
                    ten--;
                    five--;
                } else five -= 3;
            }
            if (five < 0 || ten < 0) return false;
        }
        return true;
    }
}

406.根据身高重建队列

假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好ki 个身高大于或等于 hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

示例 1:

输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]]
输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]]
解释:
编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。
编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。
编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。
编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。
编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。
因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。

示例 2:

输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]]
输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]]

提示:

  • 1 <= people.length <= 2000
  • 0 <= hi <= 106
  • 0 <= ki < people.length
  • 题目数据确保队列可以被重建
class Solution {
    public int[][] reconstructQueue(int[][] people) {
        //身高从大到小排序
        Arrays.sort(people, (a, b) -> a[0] == b[0] ? a[1] - b[1] : b[0] - a[0]);
        LinkedList<int[]> list = new LinkedList<>();
        for (int[] p : people) list.add(p[1], p);
        return list.toArray(new int[people.length][]);
    }
}

452. 用最少数量的箭引爆气球

有一些球形气球贴在一堵用 XY 平面表示的墙面上。墙面上的气球记录在整数数组 points ,其中points[i] = [xstart, xend] 表示水平直径在 xstartxend之间的气球。你不知道气球的确切 y 坐标。

一支弓箭可以沿着 x 轴从不同点 完全垂直 地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 x``startx``end, 且满足 xstart ≤ x ≤ x``end,则该气球会被 引爆 。可以射出的弓箭的数量 没有限制 。 弓箭一旦被射出之后,可以无限地前进。

给你一个数组 points返回引爆所有气球所必须射出的 最小 弓箭数

示例 1:

输入:points = [[10,16],[2,8],[1,6],[7,12]]
输出:2
解释:气球可以用2支箭来爆破:
-在x = 6处射出箭,击破气球[2,8]和[1,6]。
-在x = 11处发射箭,击破气球[10,16]和[7,12]。

示例 2:

输入:points = [[1,2],[3,4],[5,6],[7,8]]
输出:4
解释:每个气球需要射出一支箭,总共需要4支箭。

示例 3:

输入:points = [[1,2],[2,3],[3,4],[4,5]]
输出:2
解释:气球可以用2支箭来爆破:
- 在x = 2处发射箭,击破气球[1,2]和[2,3]。
- 在x = 4处射出箭,击破气球[3,4]和[4,5]。

提示:

  • 1 <= points.length <= 105
  • points[i].length == 2
  • -231 <= xstart < xend <= 231 - 1

452.用最少数量的箭引爆气球

class Solution {
    public int findMinArrowShots(int[][] points) {
        Arrays.sort(points, Comparator.comparingInt(a -> a[0]));
        int count = 1;
        for (int i = 1; i < points.length; i++) {
            //没有交叉箭数加1
            if (points[i - 1][1] < points[i][0]) count++;
            //有交叉更新该气球的右边界,左边气球右边界最大覆盖的区域
            else points[i][1] = Math.min(points[i - 1][1], points[i][1]);
        }
        return count;
    }
}

435. 无重叠区间

给定一个区间的集合 intervals ,其中 intervals[i] = [starti, endi] 。返回 需要移除区间的最小数量,使剩余区间互不重叠

示例 1:

输入: intervals = [[1,2],[2,3],[3,4],[1,3]]
输出: 1
解释: 移除 [1,3] 后,剩下的区间没有重叠。

示例 2:

输入: intervals = [ [1,2], [1,2], [1,2] ]
输出: 2
解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。

示例 3:

输入: intervals = [ [1,2], [2,3] ]
输出: 0
解释: 你不需要移除任何区间,因为它们已经是无重叠的了。

提示:

  • 1 <= intervals.length <= 105
  • intervals[i].length == 2
  • -5 * 104 <= starti < endi <= 5 * 104
class Solution {
    public int eraseOverlapIntervals(int[][] intervals) {
        Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));
        int count = 1;
        for (int i = 1; i < intervals.length; i++) {
            if (intervals[i - 1][1] <= intervals[i][0]) count++;
            else intervals[i][1] = Math.min(intervals[i - 1][1], intervals[i][1]);
        }
        return intervals.length-count;
    }
}

763.划分字母区间

给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。

注意,划分结果需要满足:将所有划分结果按顺序连接,得到的字符串仍然是 s

返回一个表示每个字符串片段的长度的列表。

示例 1:

输入:s = "ababcbacadefegdehijhklij"
输出:[9,7,8]
解释:
划分结果为 "ababcbaca"、"defegde"、"hijhklij" 。
每个字母最多出现在一个片段中。
像 "ababcbacadefegde", "hijhklij" 这样的划分是错误的,因为划分的片段数较少。 

示例 2:

输入:s = "eccbbbbdec"
输出:[10]

提示:

  • 1 <= s.length <= 500
  • s 仅由小写英文字母组成

763.划分字母区间

class Solution {
    public List<Integer> partitionLabels(String s) {
        LinkedList<Integer> list = new LinkedList<>();
        int[] edge = new int[26];
        char[] chars = s.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            //获取字母在字符串中最远的距离
            edge[chars[i] - 'a'] = i;
        }
        int idx = 0;
        int last = -1;
        for (int i = 0; i < chars.length; i++) {
            //获取当前位置字母的最远位置,如果刚好为最远位置,可以截断
            idx = Math.max(idx, edge[chars[i] - 'a']);
            if (i == idx) {
                list.add(i - last);
                last = i;
            }
        }
    }
}

56. 合并区间

以数组 intervals 表示若干个区间的集合,其中单个区间为 intervals[i] = [starti, endi] 。请你合并所有重叠的区间,并返回 一个不重叠的区间数组,该数组需恰好覆盖输入中的所有区间

示例 1:

输入:intervals = [[1,3],[2,6],[8,10],[15,18]]
输出:[[1,6],[8,10],[15,18]]
解释:区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].

示例 2:

输入:intervals = [[1,4],[4,5]]
输出:[[1,5]]
解释:区间 [1,4] 和 [4,5] 可被视为重叠区间。

提示:

  • 1 <= intervals.length <= 104
  • intervals[i].length == 2
  • 0 <= starti <= endi <= 104
class Solution {
    public int[][] merge(int[][] intervals) {
        Arrays.sort(intervals, Comparator.comparingInt(a -> a[0]));
        LinkedList<int[]> list = new LinkedList<>();
        int start = intervals[0][0];
        int right = intervals[0][1];
        for (int i = 1; i < intervals.length; i++) {
            if (intervals[i][0] > right) {
                list.add(new int[]{start, right});
                start = intervals[i][0];
                right = intervals[i][1];
            } else right = Math.max(right, intervals[i][1]);
        }
        list.add(new int[]{start, right});
        return list.toArray(new int[list.size()][]);
    }
}

738.单调递增的数字

当且仅当每个相邻位数上的数字 xy 满足 x <= y 时,我们称这个整数是单调递增的。

给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增

示例 1:

输入: n = 10
输出: 9

示例 2:

输入: n = 1234
输出: 1234

示例 3:

输入: n = 332
输出: 299

提示:

  • 0 <= n <= 109
class Solution {
    public int monotoneIncreasingDigits(int n) {
        String s = String.valueOf(n);
        char[] chars = s.toCharArray();
        int pos = s.length();
        //从后遍历,如果后面的数字大于前面的,前面的数字减一,并记录后面数字的位置
        for (int i = s.length() - 2; i >= 0; i--) {
            if (chars[i] > chars[i + 1]) {
                chars[i]--;
                pos = i + 1;
            }
        }
        //将记录数字后的数字全部置为9
        for (int i = pos; i < s.length(); i++) {
            chars[i]='9';
        }
        return Integer.parseInt(String.valueOf(chars));
    }
}

968.监控二叉树

给定一个二叉树,我们在树的节点上安装摄像头。

节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。

计算监控树的所有节点所需的最小摄像头数量。

示例 1:

img

输入:[0,0,null,0,0]
输出:1
解释:如图所示,一台摄像头足以监控所有节点。

示例 2:

img

输入:[0,0,null,0,null,0,null,null,0]
输出:2
解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。
**提示:**
  1. 给定树的节点数的范围是 [1, 1000]
  2. 每个节点的值都是 0。
class Solution {
    int res = 0;

    /**
     * 节点的状态值:
     * 0 表示无覆盖
     * 1 表示有摄像头
     * 2 表示有覆盖
     * 后序遍历,根据左右节点的情况,来判读自己的状态
     */
    public int helper(TreeNode root) {
        if (root == null) return 2;
        int left = helper(root.left);
        int right = helper(root.right);
        //左右节点都被覆盖,当前节点为无覆盖
        if (left == 2 && right == 2) return 0;
        //左右节点存在无覆盖,在此节点添加摄像头
        else if (left == 0 || right == 0) {
            res++;
            return 1;
        } else return 2;//其他情况表示该节点被覆盖
    }

    public int minCameraCover(TreeNode root) {
        //如果根节点无覆盖,添加摄像头
        if (helper(root) == 0) res++;
        return res;
    }
}

动态规划

509. 斐波那契数

斐波那契数 (通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 01 开始,后面的每一项数字都是前面两项数字的和。也就是:

F(0) = 0,F(1) = 1
F(n) = F(n - 1) + F(n - 2),其中 n > 1

给定 n ,请计算 F(n)

示例 1:

输入:n = 2
输出:1
解释:F(2) = F(1) + F(0) = 1 + 0 = 1

示例 2:

输入:n = 3
输出:2
解释:F(3) = F(2) + F(1) = 1 + 1 = 2

示例 3:

输入:n = 4
输出:3
解释:F(4) = F(3) + F(2) = 2 + 1 = 3

提示:

  • 0 <= n <= 30
//动态规划
class Solution {
    public int fib(int n) {
        if (n < 2) return n;
        int num1 = 0;
        int num2 = 1;
        int sum;
        for (int i = 2; i <= n; i++) {
            sum = num1 + num2;
            num1 = num2;
            num2 = sum;
        }
        return num2;
    }
}
//递归
class Solution {
    public int fib(int n) {
        if (n == 0) return 0;
        if (n == 1) return 1;
        return fib(n - 1) + fib(n - 2);
    }
}

70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:

输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

示例 2:

输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

提示:

  • 1 <= n <= 45
class Solution {
    public int climbStairs(int n) {
        //初始化
        int front1 = 1, front2 = 2, temp;
        if (n < 3) return n;
        for (int i = 3; i <= n; i++) {
            //递推公式
            temp = front2;
            front2 = front1 + front2;
            front1 = temp;
        }
        return front2;
    }
}

746. 使用最小花费爬楼梯

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

示例 1:

输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。

示例 2:

输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 。

提示:

  • 2 <= cost.length <= 1000
  • 0 <= cost[i] <= 999
class Solution {
    public int minCostClimbingStairs(int[] cost) {
        //初始化
        int cost1 = 0;
        int cost2 = 0;
        int temp;
        for (int i = 1; i < cost.length; i++) {
            //递推公式
            temp = cost2;
            cost2 = Math.min(cost1 + cost[i - 1], cost2 + cost[i]);
            cost1 = temp;
        }
        return cost2;
    }
}

62.不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

img

输入:m = 3, n = 7
输出:28

示例 2:

输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

输入:m = 7, n = 3
输出:28

示例 4:

输入:m = 3, n = 3
输出:6

提示:

  • 1 <= m, n <= 100
  • 题目数据保证答案小于等于 2 * 109
class Solution {
    public int uniquePaths(int m, int n) {
        int[][] dp = new int[m][n];
        //初始化
        for (int i = 0; i < m; i++) dp[i][0] = 1;
        for (int i = 0; i < n; i++) dp[0][i] = 1;
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                //递推公式
                dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
            }
        }
        return dp[m-1][n-1];
    }
}

63.不同路径 II

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 10 来表示。

示例 1:

img

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

示例 2:

img

输入:obstacleGrid = [[0,1],[0,0]]
输出:1

提示:

  • m == obstacleGrid.length
  • n == obstacleGrid[i].length
  • 1 <= m, n <= 100
  • obstacleGrid[i][j]01
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        int m = obstacleGrid.length;
        int n = obstacleGrid[0].length;
        int[][] dp = new int[m][n];
        //如果起点和终点出现了障碍,直接返回0
        if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) return 0;
        //初始化
        for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
            dp[i][0] = 1;
        }
        for (int i = 0; i < n && obstacleGrid[0][i] == 0; i++) {
            dp[0][i] = 1;
        }
        for (int i = 1; i < m; i++) {
            for (int j = 1; j < n; j++) {
                //递推公式,如果遇到障碍点该点直接设为0,否则递推
                dp[i][j] = (obstacleGrid[i][j] == 0 ? dp[i - 1][j] + dp[i][j - 1] : 0);
            }
        }
        return dp[m - 1][n - 1];
    }
}

343. 整数拆分

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积

示例 1:

输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

示例 2:

输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

提示:

  • 2 <= n <= 58
class Solution {
    public int integerBreak(int n) {
        int[] dp = new int[n + 1];
        //初始化,起始值为2
        dp[2] = 1;
        for (int i = 3; i <= n; i++) {
            for (int j = 1; j <= i - j; j++) {
                //对于整数i,其最大拆分积有三种情况:
                //1、自身
                //2、拆成两部分的乘积
                //3、拆成两部分,其中一部分为该部分乘积最大值
                dp[i] = Math.max(dp[i], Math.max(j * (i - j), j * dp[i - j]));
            }
        }
        return dp[n];
    }
}

96.不同的二叉搜索树

给你一个整数 n ,求恰由 n 个节点组成且节点值从 1n 互不相同的 二叉搜索树 有多少种?返回满足题意的二叉搜索树的种数。

示例 1:

img

输入:n = 3
输出:5

示例 2:

输入:n = 1
输出:1

提示:

  • 1 <= n <= 19
class Solution {
    public int numTrees(int n) {
        int[] dp = new int[n + 1];
        //初始化
        dp[0] = 1;
        dp[1] = 1;
        for (int i = 2; i <= n; i++) {
            for (int j = 1; j <= i; j++) {
                //已根节点左右子树的节点个数递归,将左右节点不同个数下的情况的乘积和作为该整数下的情况,由二叉搜索树的性质保证递归的有效性
                dp[i] += dp[j - 1] * dp[i - j];
            }
        }
        return dp[n];
    }
}

0-1背包问题

二维数组

public class Package {
    public static void main(String[] args) {
        int[] weight = {4, 3, 1};
        int[] value = {30, 20, 15};
        int bagSize = 4;
        testWeightBagProblem(weight, value, bagSize);
    }

    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize) {
        //物品的数量
        int goods = weight.length;
        //dp[i][j]表示从下标0-i的物品里面任意取,放进容量为j的背包,价值总和最大为多少
        int[][] dp = new int[goods][bagSize + 1];
        //将下表为0的物品放入背包,背包容纳重量从下标为0的物品重量到背包可容纳重量的价值总和为物品0的价值
        for (int j = weight[0]; j <= bagSize; j++) {
            dp[0][j] = value[0];
        }
        //物品从1开始
        for (int i = 1; i < weight.length; i++) {
            for (int j = 1; j <= bagSize; j++) {
                //如果当前背包容量没有当前物品大时,前i-1个物品的最大价值就是当前情况的最大价值
                if (j < weight[i]) {
                    dp[i][j] = dp[i - 1][j];
                } else {
                    //如果当前背包容量可以放下物品i,将不放物品i和放物品i的最大价值的大值作为前i个物品放入容量为j的背包的最大价值
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
                }
            }
        }
        for (int i = 0; i < goods; i++) {
            for (int j = 0; j <= bagSize; j++) {
                System.out.print(dp[i][j] + " ");
            }
            System.out.print("\n");
        }
    }
}

滚动数组

public class Package {
    public static void main(String[] args) {
        int[] weight = {4, 3, 1};
        int[] value = {30, 20, 15};
        int bagSize = 4;
        testWeightBagProblem(weight, value, bagSize);
    }

    public static void testWeightBagProblem(int[] weight, int[] value, int bagSize) {
        //定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
        int[] dp = new int[bagSize + 1];
        //遍历顺序:先遍历物品,再遍历背包容量
        for (int i = 0; i < weight.length; i++){
            for (int j = bagSize; j >= weight[i]; j--){//反向遍历背包容量
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
            }
            //打印dp数组
            for (int j = 0; j <= bagSize; j++){
                System.out.print(dp[j] + " ");
            }
            System.out.println();
        }
    }
}

416. 分割等和子集

给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。

注意: 每个数组中的元素不会超过 100 数组的大小不会超过 200

示例 1:

  • 输入: [1, 5, 11, 5]
  • 输出: true
  • 解释: 数组可以分割成 [1, 5, 5] 和 [11].

示例 2:

  • 输入: [1, 2, 3, 5]
  • 输出: false
  • 解释: 数组不能分割成两个元素和相等的子集.

提示:

  • 1 <= nums.length <= 200
  • 1 <= nums[i] <= 100
class Solution {
    public boolean canPartition(int[] nums) {
        if (nums == null || nums.length == 0) return false;
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if (sum % 2 != 0) return false;
        //背包容量sum/2
        int target = sum / 2;
        int[] dp = new int[target + 1];
        for (int num : nums) {
            for (int j = target; j >= num; j--) {
                dp[j] = Math.max(dp[j], dp[j - num] + num);
            }
        }
        return dp[target] == target;
    }
}

1049.最后一块石头的重量II

有一堆石头,每块石头的重量都是正整数。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:

如果 x == y,那么两块石头都会被完全粉碎;

如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。

最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。

示例:

  • 输入:[2,7,4,1,8,1]
  • 输出:1

解释:

  • 组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
  • 组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
  • 组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
  • 组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

提示:

  • 1 <= stones.length <= 30
  • 1 <= stones[i] <= 1000
class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum = 0;
        for (int stone : stones) {
            sum += stone;
        }
        int target = sum/2;
        int[] dp = new int[target + 1];
        for (int stone : stones) {
            for (int j = target; j >= stone; j--) {
                dp[j] = Math.max(dp[j], dp[j - stone] + stone);
            }
        }
        return sum - 2 * dp[target];
    }
}

494.目标和

给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。

返回可以使最终数组和为目标数 S 的所有添加符号的方法数。

示例:

  • 输入:nums: [1, 1, 1, 1, 1], S: 3
  • 输出:5

解释:

  • -1+1+1+1+1 = 3
  • +1-1+1+1+1 = 3
  • +1+1-1+1+1 = 3
  • +1+1+1-1+1 = 3
  • +1+1+1+1-1 = 3

一共有5种方法让最终目标和为3。

提示:

  • 数组非空,且长度不会超过 20 。
  • 初始的数组的和不会超过 1000 。
  • 保证返回的最终结果能被 32 位整数存下。
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int num : nums) {
            sum += num;
        }
        if (target < 0 && sum < -target) return 0;
        if ((target + sum) % 2 != 0) return 0;
        int size = (target + sum) / 2;
        if (size < 0) size = -size;
        //表示填满背包j,有dp[j]种方法
        int[] dp = new int[size + 1];
        //组合问题初始化
        dp[0] = 1;
        for (int num : nums) {
            for (int j = size; j >= num; j--) {
                //组合方法的迭代公式
                dp[j] += dp[j - num];
            }
        }
        return dp[size];
    }
}

474.一和零

给你一个二进制字符串数组 strs 和两个整数 m 和 n 。

请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。

如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。

示例 1:

  • 输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
  • 输出:4
  • 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。

示例 2:

  • 输入:strs = ["10", "0", "1"], m = 1, n = 1
  • 输出:2
  • 解释:最大的子集是 {"0", "1"} ,所以答案是 2 。

提示:

  • 1 <= strs.length <= 600
  • 1 <= strs[i].length <= 100
  • strs[i] 仅由 '0' 和 '1' 组成
  • 1 <= m, n <= 100
class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        //dp[i][j]表示i个0和j个1时的最大子集
        int[][] dp = new int[m + 1][n + 1];
        int oneNum, zeroNum;
        //计算每个字符中0和1的个数作为物品的重量
        for (String str : strs) {
            oneNum = 0;
            zeroNum = 0;
            for (char ch : str.toCharArray()) {
                if (ch == '0') zeroNum++;
                else oneNum++;
            }
            //进行二维的0-1背包迭代
            for (int i = m; i >= zeroNum; i--) {
                for (int j = n; j >= oneNum; j--) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
                }
            }
        }
        return dp[m][n];
    }
}

0-1背包总结

首先,所有背包问题都采用滚动数组的方式来处理;

基本0-1背包问题求解

第一步:确定背包容量:bagSize

第二步:确定物品的价值和重量

第三步:确定初始条件:dp=0

第四步:遍历物品:for(weight : weights)

第五步:反向遍历背包:for(j = bagSize; j > weight; j- - )

第六步:确定容量j下的最大物品价值 :dp[j] = Max(dp[j] , dp[j-weight]+value[i])

第七步:确定返回结果

[分割等和子集](#416. 分割等和子集):

第一步:首先确定背包容量为总和的一半:bagSize = sum/2

第二步:确定物品的价值和重量

第三步:确定初始条件:dp=0

第四步:遍历物品:for(weight : weights)

第五步:反向遍历背包:for(j = bagSize; j > weight; j- - )

第六步:确定容量j下的最大物品价值 :dp[j] = Max(dp[j] , dp[j-weight]+value[i])

第七步:确定返回结果:bagSize-dp[end]

最后一块石头的重量II

第一步:首先确定背包容量为总和的一半:bagSize = sum/2

第二步:确定物品的价值和重量

第三步:确定初始条件:dp=0

第四步:遍历物品:for(weight : weights)

第五步:反向遍历背包:for(j = bagSize; j > weight; j- - )

第六步:确定容量j下的最大物品价值 :dp[j] = Max(dp[j] , dp[j-weight]+value[i])

第七步:确定返回结果:sum-2*dp[end]


目标和(组合问题)

第一步:首先确定背包容量:

\[\left\{ \begin{aligned} &x-y=s\\ &x+y=sum \end{aligned} \right. \Rightarrow x=\frac{1}{2}(sum+s) \]

第二步:确定物品的重量和价值

第三步:确定初始条件:dp=0

第四步:遍历物品:for(weight : weights)

第五步:反向遍历背包:for(j = bagSize; j > weight; j- - )

第六步:确定容量j下的最大物品价值 :dp[j] = dp[j]+dp[j-weight]

第七步:确定返回结果:dp[end]


一和零(二维0-1背包问题)

第一步:确定背包容量:

​ 维度一:0的数量

​ 维度二:1的数量

第二步:确定物品的重量,重量为字符串中0和1的数量,价值定值1

第三步:确定初始条件:dp=0

第四步:遍历物品:for(weight : weights)

第五步:反向遍历背包:for(j = bagSize; j > weight; j- - )

第六步:确定容量[i,j]下的最大物品价值 :dp[j] = Max(dp[i][j] , dp[i-weighti][j-weightj]+value[i][j])

第七步:确定返回结果:dp[end][end]

完全背包理论

先遍历物品再遍历背包

public class PackageAll {
    public static void main(String[] args) {
        int[] weight = {1, 3, 4};
        int[] value = {15, 20, 30};
        int bagWeight = 4;
        int[] dp = new int[bagWeight + 1];
        for (int i = 0; i < weight.length; i++){ // 遍历物品
            for (int j = weight[i]; j <= bagWeight; j++){ // 正向遍历背包容量
                dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
            }
        }
        for (int maxValue : dp){
            System.out.println(maxValue + "   ");
        }
    }
}

先遍历背包再遍历物品

private static void testCompletePackAnotherWay(){
    int[] weight = {1, 3, 4};
    int[] value = {15, 20, 30};
    int bagWeight = 4;
    int[] dp = new int[bagWeight + 1];
    for (int i = 1; i <= bagWeight; i++){ // 遍历背包容量
        for (int j = 0; j < weight.length; j++){ // 遍历物品
            if (i - weight[j] >= 0){
                dp[i] = Math.max(dp[i], dp[i - weight[j]] + value[j]);
            }
        }
    }
    for (int maxValue : dp){
        System.out.println(maxValue + "   ");
    }
}

518.零钱兑换II

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。

示例 1:

  • 输入: amount = 5, coins = [1, 2, 5]
  • 输出: 4

解释: 有四种方式可以凑成总金额:

  • 5=5
  • 5=2+2+1
  • 5=2+1+1+1
  • 5=1+1+1+1+1

示例 2:

  • 输入: amount = 3, coins = [2]
  • 输出: 0
  • 解释: 只用面额2的硬币不能凑成总金额3。

示例 3:

  • 输入: amount = 10, coins = [10]
  • 输出: 1

注意,你可以假设:

  • 0 <= amount (总金额) <= 5000
  • 1 <= coin (硬币面额) <= 5000
  • 硬币种类不超过 500 种
  • 结果符合 32 位符号整数
class Solution {
    public int change(int amount, int[] coins) {
        //完全背包问题,组合
        //凑成总金额j的货币组合数为dp[j]
        int[] dp = new int[amount + 1];
        //组合问题初始化
        dp[0] = 1;
        for (int coin : coins) {
            for (int j = coin; j <= amount; j++) {
                dp[j] += dp[j - coin];
            }
        }
        return dp[amount];
    }
}

377. 组合总和 Ⅳ

给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

  • nums = [1, 2, 3]
  • target = 4

所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)

请注意,顺序不同的序列被视作不同的组合。

因此输出为 7。

class Solution {
    public int combinationSum4(int[] nums, int target) {
        //考虑遍历顺序的完全背包组合问题
        //凑成目标正整数为i的排列个数为dp[i]
        int[] dp = new int[target + 1];
        dp[0] = 1;
        for (int i = 0; i <= target; i++) {
            for (int num : nums) {
                if (i >= num) {
                    dp[i] += dp[i - num];
                }
            }
        }
        return dp[target];
    }
}

70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

示例 1: 输入: 2 输出: 2 解释: 有两种方法可以爬到楼顶。

  1. 1 阶 + 1 阶
  2. 2 阶

示例 2: 输入: 3 输出: 3 解释: 有三种方法可以爬到楼顶。

  1. 1 阶 + 1 阶 + 1 阶
  2. 1 阶 + 2 阶
  3. 2 阶 + 1 阶
class Solution {
    public int climbStairs(int n) {
        //完全背包、组合遍历顺序
        int[] nums = {1, 2};
        //爬到有i个台阶的楼顶,有dp[i]种方法
        int[] dp = new int[n + 1];
        dp[0] = 1;
        for (int i = 0; i <= n; i++) {
            for (int num : nums) {
                if (i >= num) dp[i] += dp[i - num];
            }
        }
        return dp[n];
    }
}

322. 零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

你可以认为每种硬币的数量是无限的。

示例 1:

  • 输入:coins = [1, 2, 5], amount = 11
  • 输出:3
  • 解释:11 = 5 + 5 + 1

示例 2:

  • 输入:coins = [2], amount = 3
  • 输出:-1

示例 3:

  • 输入:coins = [1], amount = 0
  • 输出:0

示例 4:

  • 输入:coins = [1], amount = 1
  • 输出:1

示例 5:

  • 输入:coins = [1], amount = 2
  • 输出:2

提示:

  • 1 <= coins.length <= 12
  • 1 <= coins[i] <= 2^31 - 1
  • 0 <= amount <= 10^4
class Solution {
    public int coinChange(int[] coins, int amount) {
        int max = Integer.MAX_VALUE;
        //凑足总额为i所需金币的最小个数
        int[] dp = new int[amount + 1];
        for (int i = 0; i <= amount; i++) {
            dp[i] = max;
        }
        dp[0] = 0;
        for (int coin : coins) {
            for (int i = coin; i <= amount; i++) {
                if (dp[i - coin] != max) {
                    //如果当前当前所需金币数减去当前金币值所需的金币数没有
                    dp[i] = Math.min(dp[i], dp[i - coin] + 1);
                }
            }
        }
        return dp[amount] == max ? -1 : dp[amount];
    }
}

279.完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。

给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。

示例 1:

  • 输入:n = 12
  • 输出:3
  • 解释:12 = 4 + 4 + 4

示例 2:

  • 输入:n = 13
  • 输出:2
  • 解释:13 = 4 + 9

提示:

  • 1 <= n <= 10^4
class Solution {
    public int numSquares(int n) {
        int max = Integer.MAX_VALUE;
        int[] dp = new int[n + 1];
        for (int i = 0; i <= n; i++) {
            dp[i] = max;
        }
        dp[0] = 0;
        for (int i = 1; i * i <= n; i++) {
            for (int j = i * i; j <= n; j++) {
                if (dp[j - i * i] != max) {
                    dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
                }
            }
        }
        return dp[n];
    }
}

139.单词拆分

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。

说明:

拆分时可以重复使用字典中的单词。

你可以假设字典中没有重复的单词。

示例 1:

  • 输入: s = "leetcode", wordDict = ["leet", "code"]
  • 输出: true
  • 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。

示例 2:

  • 输入: s = "applepenapple", wordDict = ["apple", "pen"]
  • 输出: true
  • 解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。
  • 注意你可以重复使用字典中的单词。

示例 3:

  • 输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
  • 输出: false
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        HashSet<String> set = new HashSet<>(wordDict);
        //字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词
        boolean[] dp = new boolean[s.length() + 1];
        dp[0] = true;
        for (int i = 1; i <= s.length(); i++) {
            for (int j = 0; j < i && !dp[i]; j++) {//当前容量下不考虑新物品没有物品可以填满
                if (set.contains(s.substring(j, i)) && dp[j]) {//添加新物品,且当前容量下去除新物品有物品可以填满
                    dp[i] = true;
                    break;
                }
            }
        }
        return dp[s.length()];
    }
}

完全背包总结

先遍历物品再遍历背包

第一步:确定背包容量:bagSize

第二步:确定物品的价值和重量

第三步:确定初始条件:dp=0

第四步:遍历物品:for(weight : weights)

第五步:正向遍历背包:for(j = weight; j<= bagSize; j++ )

第六步:确定容量j下的最大物品价值 :dp[j] = Max(dp[j] , dp[j-weight]+value[i])

第七步:确定返回结果dp[end]

先遍历背包再遍历物品

第一步:确定背包容量:bagSize

第二步:确定物品的价值和重量

第三步:确定初始条件:dp=0

第四步:正向遍历背包:for(j = 1; j<= bagSize; j++ )

第五步:遍历物品:for(weight : weights)

第六步:判断背包容量是否可以装下当前物品

第七步:如果背包容量是否可以装下当前物品,确定容量j下的最大物品价值 :dp[j] = Max(dp[j] , dp[j-weight]+value[i]);反之,增加背包容量

第八步:确定返回结果dp[end]


零钱兑换II(组合问题,不考虑遍历顺序,先遍历物品再遍历背包)

第一步:确定背包容量:bagSize

第二步:确定物品的重量和价值

第三步:确定初始条件:dp[0]=1

第四步:遍历物品:for(weight : weights)

第五步:正向遍历背包:for(j = weight; j<= bagSize; j++ )

第六步:确定容量j下的填充种数:dp[j] = dp[j]+dp[j-weight]

第七步:确定返回结果dp[end]


[组合总和 Ⅳ](#377. 组合总和 Ⅳ)(组合数问题,考虑遍历顺序,先遍历背包再遍历物品)

第一步:确定背包容量:bagSize

第二步:确定物品的价值和重量

第三步:确定初始条件:dp[0]=1

第四步:正向遍历背包:for(j = 1; j<= bagSize; j++ )

第五步:遍历物品:for(weight : weights)

第六步:判断背包容量是否可以装下当前物品

第七步:如果背包容量是否可以装下当前物品,确定容量j下的填充种数:dp[j] = dp[j]+dp[j-weight];反之,增加背包容量

第八步:确定返回结果dp[end]

[ 爬楼梯](70. 爬楼梯(完全背包、组合、遍历顺序))(组合数问题,考虑遍历顺序,先遍历背包再遍历物品)

第一步:确定背包容量:bagSize

第二步:确定物品的价值和重量

第三步:确定初始条件:dp[0]=1

第四步:正向遍历背包:for(j = 1; j<= bagSize; j++ )

第五步:遍历物品:for(weight : weights)

第六步:判断背包容量是否可以装下当前物品

第七步:如果背包容量是否可以装下当前物品,确定容量j下的填充种数:dp[j] = dp[j]+dp[j-weight];反之,增加背包容量

第八步:确定返回结果dp[end]


[零钱兑换](#322. 零钱兑换)(最少物品数,不考虑遍历顺序,先遍历物品再遍历背包)

第一步:确定背包容量:bagSize

第二步:确定物品的重量和价值,价值为常量1

第三步:确定初始条件:dp=MAXVALUE

第四步:遍历物品:for(weight : weights)

第五步:正向遍历背包:for(j = weight; j<= bagSize; j++ )

第六步:判断当前容量下去除物品重量后的最小金币数是否计算

第七步:如果已经计算,更新容量j下的最小物品数dp[j]=MAX(dp[j],dp[j-weight]+1)

第七步:确定返回结果:如果有物品组合填满背包返回dp[end];反之,返回-1

完全平方数(最少物品数,不考虑遍历顺序,先遍历物品再遍历背包)

第一步:确定背包容量:bagSize

第二步:确定物品的重量和价值,价值为常量1

第三步:确定初始条件:dp=MAXVALUE

第四步:遍历物品:for(weight : weights)

第五步:正向遍历背包:for(j = weight; j<= bagSize; j++ )

第六步:判断当前容量下去除物品重量后的最小金币数是否计算

第七步:如果已经计算,更新容量j下的最小物品数dp[j]=MAX(dp[j],dp[j-weight]+1)

第七步:确定返回结果:如果有物品组合填满背包返回dp[end];反之,返回-1


单词拆分(组合问题,考虑遍历顺序,先遍历背包再遍历物品)

多重背包问题

\(N\)种物品和一个容量为\(V\)的背包。第\(i\)种物品最多有\(Mi\)件可用,每件耗费的空间是\(Ci\) ,价值是\(Wi\) 。求解将哪些物品装入背包可使这些物品的耗费的空间总和不超过背包容量,且价值总和最大。

多重背包和0-1背包是非常像的, 为什么和0-1背包像呢?

每件物品最多有\(Mi\)件可用,把\(Mi\)件摊开,其实就是一个0-1背包问题了。

198.打家劫舍

  • 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

    给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

    示例 1:

    输入:[1,2,3,1]
    输出:4
    解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
         偷窃到的最高金额 = 1 + 3 = 4 。
    

    示例 2:

    输入:[2,7,9,3,1]
    输出:12
    解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
         偷窃到的最高金额 = 2 + 9 + 1 = 12 。
    

    提示:

    • 1 <= nums.length <= 100
    • 0 <= nums[i] <= 400
class Solution {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        if (nums.length == 1) return nums[0];

        int[] dp = new int[nums.length];
        dp[0] = nums[0];
        dp[1] = Math.max(dp[0], nums[1]);

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

213.打家劫舍II

  • 你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

    给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

    示例 1:

    输入:nums = [2,3,2]
    输出:3
    解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。
    

    示例 2:

    输入:nums = [1,2,3,1]
    输出:4
    解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
         偷窃到的最高金额 = 1 + 3 = 4 。
    

    示例 3:

    输入:nums = [1,2,3]
    输出:3
    

    提示:

    • 1 <= nums.length <= 100
    • 0 <= nums[i] <= 1000
class Solution {
    public int rob(int[] nums) {
        if (nums == null || nums.length == 0) return 0;
        if (nums.length == 1) return nums[0];
        return Math.max(rob(nums, 0, nums.length - 1), rob(nums, 1, nums.length));

    }

    public int rob(int[] nums, int start, int end) {
        int[] dp = new int[2];
        int res=0;
        for (int i = start; i < end; i++) {
            dp[1] = res;
            res = Math.max(dp[0] + nums[i], dp[1]);
            dp[0] = dp[1];
        }
        return res;
    }
} 

337.打家劫舍 III

小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为 root

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额

示例 1:

img

输入: root = [3,2,3,null,3,null,1]
输出: 7 
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7

示例 2:

img

输入: root = [3,4,5,1,3,null,1]
输出: 9
解释: 小偷一晚能够盗取的最高金额 4 + 5 = 9

提示:

  • 树的节点数在 [1, 104] 范围内
  • 0 <= Node.val <= 104
class Solution {
    //后序递归遍历
    public int rob(TreeNode root) {
        if (root == null) return 0;
        int money = root.val;
        //计算根节点的左节点的子节点价值
        if (root.left != null) {
            money += rob(root.left.left) + rob(root.left.right);
        }
        //计算根节点的右节点的子节点价值
        if (root.right != null) {
            money += rob(root.right.left) + rob(root.right.right);
        }
		//返回根节点加上根节点的左右子节点的子节点的价值与根节点的左右节点的值
        return Math.max(money, rob(root.left) + rob(root.right));
    }

}
class Solution {
    //树形动态规划
    public int rob(TreeNode root) {
        int[] res = rob1(root);
        return Math.max(res[0], res[1]);
    }

    public int[] rob1(TreeNode root) {
        //状态值,表示偷与不偷的价值
        int[] res = new int[2];
        if (root == null) return res;
        //左节点偷与不偷的价值
        int[] left = rob1(root.left);
        //右节点偷与不偷的价值
        int[] right = rob1(root.right);
		//不偷的价值为左右节点偷或者不偷的大值的和
        res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]);
        //偷的价值为当前节点的值与不偷当前节点左右子节点的值
        res[1] = root.val + left[0] + right[0];
        return res;
    }
}

打家劫舍总结

打家劫舍

步骤一:初始化dp为第一和第二家的价值

步骤二:遍历家数,用dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);确定下一家的价值。

步骤三:返回最后一家的价值

打家劫舍II

环状家数,分成两种情况:不要最后一家和不要弟一家,最后采用打家劫舍的方法取这两者的大值。

[打家劫舍 III](#337.打家劫舍 III)

树形动态规划,状态为当前节点偷与不偷的价值

121. 买卖股票的最佳时机

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。

你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。

返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0

示例 1:

输入:[7,1,5,3,6,4]
输出:5
解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
     注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。

示例 2:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 没有交易完成, 所以最大利润为 0。

提示:

  • 1 <= prices.length <= 105
  • 0 <= prices[i] <= 104
class Solution {
    public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) return 0;
        int[] dp = new int[2];
        //记录一次交易,0表示持有,1表示未持有
        dp[0] = -prices[0];
        dp[1] = 0;
        for (int i = 1; i < prices.length; i++) {
            //当天的状态,持有:前一天持有,当天买入
            dp[0] = Math.max(dp[0], -prices[i]);
            //当天的状态,未持有:前一天卖出,当天卖出
            dp[1] = Math.max(dp[1], dp[0] + prices[i]);
        }
        return dp[1];
    }
}

122.买卖股票的最佳时机II

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

示例 1:

输入:prices = [7,1,5,3,6,4]
输出:7
解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
     随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。
     总利润为 4 + 3 = 7 。

示例 2:

输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。
     总利润为 4 。

示例 3:

输入:prices = [7,6,4,3,1]
输出:0
解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。

提示:

  • 1 <= prices.length <= 3 * 104
  • 0 <= prices[i] <= 104
class Solution {
    public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) return 0;
        int[] dp = new int[2];
        //记录一次交易,0表示持有,1表示未持有
        dp[0] = -prices[0];
        dp[1] = 0;
        for (int i = 1; i < prices.length; i++) {
            //当天状态,持有:前一天持有:价值为前一天持有的收益,当天买入:价值为前一天未持有的收益减掉当天买入的成本
            dp[0] = Math.max(dp[0], dp[1] - prices[i]);
            //当天状态,未持有:前一天未持有:价值为前一天未持有的收益,当天卖出:价值为前一天持有的收益加上当天卖出的收益
            dp[1] = Math.max(dp[1], dp[0] + prices[i]);
        }
        return dp[1];
    }
}

123.买卖股票的最佳时机III

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入:prices = [3,3,5,0,0,3,1,4]
输出:6
解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。
     随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3 。

示例 2:

输入:prices = [1,2,3,4,5]
输出:4
解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。   
     注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。   
     因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。

示例 3:

输入:prices = [7,6,4,3,1] 
输出:0 
解释:在这个情况下, 没有交易完成, 所以最大利润为 0。

示例 4:

输入:prices = [1]
输出:0

提示:

  • 1 <= prices.length <= 105
  • 0 <= prices[i] <= 105
class Solution {
    public int maxProfit(int[] prices) {
        int[] dp = new int[4];
        //0:第一次持有
        dp[0] = -prices[0];
        //1:第一次未持有
        dp[1] = 0;
        //2:第二次持有
        dp[2] = -prices[0];
        //3:第二次未持有
        dp[3] = 0;
        for (int i = 1; i < prices.length; i++) {
            dp[0] = Math.max(dp[0], -prices[i]);
            dp[1] = Math.max(dp[1], dp[0] + prices[i]);
            dp[2] = Math.max(dp[2], dp[1] - prices[i]);
            dp[3] = Math.max(dp[3], dp[2] + prices[i]);
        }
        return dp[3];
    }
}

188.买卖股票的最佳时机IV

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格,和一个整型 k

设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。也就是说,你最多可以买 k 次,卖 k 次。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。

示例 2:

输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
     随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。

提示:

  • 0 <= k <= 100
  • 0 <= prices.length <= 1000
  • 0 <= prices[i] <= 1000
class Solution {
    public int maxProfit(int k, int[] prices) {
        if(prices.length == 0){
            return 0;
        }
        if(k == 0){
            return 0;
        }
        int[] dp = new int[2 * k];
        for(int i = 0; i < dp.length / 2; i++){
            dp[i * 2] = -prices[0];
        }
        for(int i = 1; i <prices.length; i++){
            dp[0] = Math.max(dp[0], -prices[i]);
            dp[1] = Math.max(dp[1], dp[0] + prices[i]);
            for(int j = 2; j < dp.length; j += 2){
                dp[j] = Math.max(dp[j], dp[j - 1] - prices[i]);
                dp[j + 1] = Math.max(dp[j + 1], dp[j] + prices[i]);
            }
        }
        // 返回最后一次交易卖出状态的结果就行了
        return dp[dp.length - 1];
    }
}

309.最佳买卖股票时机含冷冻期

给定一个整数数组 prices,其中第 prices[i] 表示第 *i* 天的股票价格 。

设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):

  • 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: prices = [1,2,3,0,2]
输出: 3 
解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出]

示例 2:

输入: prices = [1]
输出: 0

提示:

  • 1 <= prices.length <= 5000
  • 0 <= prices[i] <= 1000
class Solution {
    public int maxProfit(int[] prices) {
        if (prices == null || prices.length == 0) return 0;
        int[] dp = new int[4];
        //记录一次交易,0表示持有,1表示前面已卖出且今天未购入,2表示今天今天卖出,3表示今天是冷冻期
        dp[0] = -prices[0];
        dp[1] = 0;
        for (int i = 1; i < prices.length; i++) {
            int temp = dp[0];
            int temp1 = dp[2];
            //当天持有的状态值应该为持有状态值、冷冻状态值、买入状态值的最大值
            dp[0] = Math.max(dp[0], Math.max(dp[3] - prices[i], dp[1] - prices[i]));
            //前面已卖出且今天未购入下的未持有状态值应该为状态1与状态3的大值
            dp[1] = Math.max(dp[1], dp[3]);
            //今天卖出的状态值应该为持有未更新值加上物品卖出值
            dp[2] = temp + prices[i];
            //今天冷冻期状态值为未更新卖出值
            dp[3] = temp1;
        }
        return Math.max(dp[3], Math.max(dp[1], dp[2]));
    }
}
class Solution {
    public int maxProfit(int[] prices) {
        int[][] dp = new int[prices.length + 1][2];
        //dp[i][0] 第i天持有股票收益;
        //dp[i][1] 第i天不持有股票收益;
        dp[1][0] = -prices[0];
        for (int i = 2; i <= prices.length; i++) {
            //情况一:第i天是冷静期,不能以dp[i-1][1]购买股票,所以以dp[i - 2][1]买股票,没问题
            //情况二:第i天不是冷静期,理论上应该以dp[i-1][1]购买股票,但是第i天不是冷静期说明,第i-1天没有卖出股票,
            //则dp[i-1][1]=dp[i-2][1],所以可以用dp[i-2][1]买股票,没问题
            dp[i][0] = Math.max(dp[i - 1][0], dp[i - 2][1] - prices[i - 1]);
            dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i - 1]);
        }

        return dp[prices.length][1];
    }
}

714.买卖股票的最佳时机含手续费

给定一个整数数组 prices,其中 prices[i]表示第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。

返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

示例 1:

输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润:  
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8

示例 2:

输入:prices = [1,3,7,5,10,3], fee = 3
输出:6

提示:

  • 1 <= prices.length <= 5 * 104
  • 1 <= prices[i] < 5 * 104
  • 0 <= fee < 5 * 104
class Solution {
    public int maxProfit(int[] prices, int fee) {
        int[] dp = new int[2];
        //状态一:持有股票
        dp[0] = -prices[0];
        //状态二:未持有股票
        dp[1] = 0;
        for (int i = 1; i < prices.length; i++) {
            dp[0] = Math.max(dp[0], dp[1] - prices[i]);
            //在卖出时才支付手续费
            dp[1] = Math.max(dp[1], dp[0] + prices[i]-fee);
        }
        return dp[1];
    }
}

买卖股票总结

[买卖股票的最佳时机](#121. 买卖股票的最佳时机):只能交易一次

两种状态:

  • 持有股票:dp[0] = Math.max(dp[0], -prices[i]);
  • 未持有股票:dp[1] = Math.max(dp[1], dp[0] + prices[i]);

买卖股票的最佳时机III:只能交易两次

四种状态:

  • 第一次持有:dp[0] = Math.max(dp[0], -prices[i]);
  • 第一次未持有:dp[1] = Math.max(dp[1], dp[0] + prices[i]);
  • 第二次持有:dp[2] = Math.max(dp[2], dp[1] - prices[i]);
  • 第二次未持有:dp[3] = Math.max(dp[3], dp[2] + prices[i]);

买卖股票的最佳时机IV:只能交易\(k\)

\(2k\)种状态:\(k\)种持有状态,\(k\)种未持有状态

  • 第一次持有:dp[0] = Math.max(dp[0], -prices[i]);

  • 第一次未持有:dp[1] = Math.max(dp[1], dp[0] + prices[i]);

  • \(j\)次持有:dp[j] = Math.max(dp[j], dp[j - 1] - prices[i]);

  • \(j\)次未持有:dp[j + 1] = Math.max(dp[j + 1], dp[j] + prices[i]);

买卖股票的最佳时机II:不限制交易次数

两种状态:

  • 持有股票:dp[0] = Math.max(dp[0], dp[1]-prices[i]);
  • 未持有股票:dp[1] = Math.max(dp[1], dp[0] + prices[i]-fee);

买卖股票的最佳时机含手续费

两种状态:

  • 持有股票:dp[0] = Math.max(dp[0], dp[1]-prices[i]);
  • 未持有股票(在卖出时支付交易费):dp[1] = Math.max(dp[1], dp[0] + prices[i]);

最佳买卖股票时机含冷冻期

两种状态:

两种状态:

  • 持有股票:
    • 情况一:第\(i-1\)天是冷静期,说明\(i-2\)天卖出股票,所以以\(dp[i - 2][1]\)买股票
    • 情况二:第\(i-1\)天不是冷静期,以\(dp[i-1][1]\)购买股票,说明\(i-1\)天没有卖出股票,\(dp[i-1][1]=dp[i-2][1]\)
  • dp[i][0] = Math.max(dp[i - 1][0], dp[i - 2][1] - prices[i - 1]);
  • 未持有股票(在卖出时支付交易费):dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i - 1]);

300.最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

示例 1:

输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。

示例 2:

输入:nums = [0,1,0,3,2,3]
输出:4

示例 3:

输入:nums = [7,7,7,7,7,7,7]
输出:1

提示:

  • 1 <= nums.length <= 2500
  • -104 <= nums[i] <= 104
class Solution {
    public int lengthOfLIS(int[] nums) {
        //dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度
        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);
        for (int i = 0; i < dp.length; i++) {
            //取前i-1个最长递增子序列
            for (int j = 0; j < i; j++) {
                if (nums[i] > nums[j]) {
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
        }
        int res = 0;
        //取最长递增子序列
        for (int j : dp) {
            res = Math.max(res, j);
        }
        return res;
    }
}

674. 最长连续递增序列

给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。

连续递增的子序列 可以由两个下标 lrl < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。

示例 1:

输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。 

示例 2:

输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。

提示:

  • 1 <= nums.length <= 104
  • -109 <= nums[i] <= 109
class Solution {
    public int findLengthOfLCIS(int[] nums) {
        int[] dp = new int[nums.length];
        Arrays.fill(dp, 1);
        int res = 1;
        for (int i = 1; i < nums.length; i++) {
            if (nums[i] > nums[i - 1]) dp[i] = dp[i - 1] + 1;
            res = Math.max(res, dp[i]);
        }
        return res;
    }
}

718. 最长重复子数组

给两个整数数组 nums1nums2 ,返回 两个数组中 公共的 、长度最长的子数组的长度

示例 1:

输入:nums1 = [1,2,3,2,1], nums2 = [3,2,1,4,7]
输出:3
解释:长度最长的公共子数组是 [3,2,1] 。

示例 2:

输入:nums1 = [0,0,0,0,0], nums2 = [0,0,0,0,0]
输出:5

提示:

  • 1 <= nums1.length, nums2.length <= 1000
  • 0 <= nums1[i], nums2[i] <= 100
class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        //未避免过多的初始化,添加dp[0][0]作为两个数组的0位置重复长度用于动态规划
        //dp[i][j]数组1前i-1个数字和数组2前j-1个数字中最长公共部分长度
        int[][] dp = new int[nums1.length+1][nums2.length+1];
        int res = 0;

        for (int i = 1; i <= nums1.length; i++) {
            for (int j = 1; j <= nums2.length; j++) {
                if (nums1[i-1] == nums2[j-1]) dp[i][j] = dp[i - 1][j - 1] + 1;
                res = Math.max(res, dp[i][j]);
            }
        }
        return res;
    }
}
class Solution {
    //滚动数组
    public int findLength(int[] nums1, int[] nums2) {
        //dp[i]表示数组2中前i-1个数字中最长公共部分长度
        int[] dp = new int[nums2.length + 1];
        int res = 0;

        for (int i = 1; i <= nums1.length; i++) {
            //从后向前遍历,避免重复
            for (int j = nums2.length; j > 0; j--) {
                if (nums1[i - 1] == nums2[j - 1]) dp[j] = dp[j - 1] + 1;
                else dp[j] = 0;
                res = Math.max(res, dp[j]);
            }
        }
        return res;
    }
}

1143.最长公共子序列

给定两个字符串 text1text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

  • 例如,"ace""abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。

两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

示例 1:

输入:text1 = "abcde", text2 = "ace" 
输出:3  
解释:最长公共子序列是 "ace" ,它的长度为 3 。

示例 2:

输入:text1 = "abc", text2 = "abc"
输出:3
解释:最长公共子序列是 "abc" ,它的长度为 3 。

示例 3:

输入:text1 = "abc", text2 = "def"
输出:0
解释:两个字符串没有公共子序列,返回 0 。

提示:

  • 1 <= text1.length, text2.length <= 1000
  • text1text2 仅由小写英文字符组成。
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int[][] dp = new int[text1.length() + 1][text2.length() + 1];
        for (int i = 1; i <= text1.length(); i++) {
            char char1 = text1.charAt(i - 1);
            for (int j = 1; j <= text2.length(); j++) {
                char char2 = text2.charAt(j - 1);
                //与最长重复数组一样
                if (char1 == char2) dp[i][j] = dp[i - 1][j - 1] + 1;
                    //取dp[i-1][j]与dp[i][j-1]的大值
                else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
        return dp[text1.length()][text2.length()];
    }
}

1035.不相交的线

在两条独立的水平线上按给定的顺序写下 nums1nums2 中的整数。

现在,可以绘制一些连接两个数字 nums1[i]nums2[j] 的直线,这些直线需要同时满足满足:

  • nums1[i] == nums2[j]
  • 且绘制的直线不与任何其他连线(非水平线)相交。

请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。

以这种方法绘制线条,并返回可以绘制的最大连线数。

示例 1:

img
输入:nums1 = [1,4,2], nums2 = [1,2,4]
输出:2
解释:可以画出两条不交叉的线,如上图所示。 
但无法画出第三条不相交的直线,因为从 nums1[1]=4 到 nums2[2]=4 的直线将与从 nums1[2]=2 到 nums2[1]=2 的直线相交。

示例 2:

输入:nums1 = [2,5,1,2,5], nums2 = [10,5,2,1,5,2]
输出:3

示例 3:

输入:nums1 = [1,3,7,1,7,5], nums2 = [1,9,2,5,1]
输出:2

提示:

  • 1 <= nums1.length, nums2.length <= 500
  • 1 <= nums1[i], nums2[j] <= 2000
class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        int[][] dp = new int[nums1.length + 1][nums2.length + 1];
        for (int i = 1; i <= nums1.length; i++) {
            for (int j = 1; j <= nums2.length; j++) {
                if (nums1[i - 1] == nums2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
                else dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]);
            }
        }
        return dp[nums1.length][nums2.length];
    }
}

53. 最大子序和

给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

子数组 是数组中的一个连续部分。

示例 1:

输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。

示例 2:

输入:nums = [1]
输出:1

示例 3:

输入:nums = [5,4,-1,7,8]
输出:23

提示:

  • 1 <= nums.length <= 105
  • -104 <= nums[i] <= 104
class Solution {
    public int maxSubArray(int[] nums) {
        if (nums.length == 0) return 0;
        int res = nums[0];
        int pre=nums[0];
        for (int i = 1; i < nums.length; i++) {
            pre = Math.max(pre + nums[i], nums[i]);
            res = Math.max(res, pre);
        }
        return res;
    }
}

392.判断子序列

给定字符串 st ,判断 s 是否为 t 的子序列。

字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace""abcde"的一个子序列,而"aec"不是)。

示例 1:

输入:s = "abc", t = "ahbgdc"
输出:true

示例 2:

输入:s = "axc", t = "ahbgdc"
输出:false

提示:

  • 0 <= s.length <= 100
  • 0 <= t.length <= 10^4
  • 两个字符串都只由小写字符组成。
class Solution {
    public boolean isSubsequence(String s, String t) {
        //考虑空集的情况
        if (s.length() > t.length()) return false;
        if (s.length() == 0) return true;
        int[][] dp = new int[s.length() + 1][t.length() + 1];
        for (int i = 1; i <= s.length(); i++) {
            char char1 = s.charAt(i - 1);
            for (int j = 1; j <= t.length(); j++) {
                char char2 = t.charAt(j - 1);
                //与最长重复数组一样
                if (char1 == char2) dp[i][j] = dp[i - 1][j - 1] + 1;
                    //取dp[i-1][j]与dp[i][j-1]的大值
                else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
        return dp[s.length()][t.length()] == Math.min(t.length(), s.length());
    }
}

115.不同的子序列

给你两个字符串 st ,统计并返回在 s子序列t 出现的个数。

题目数据保证答案符合 32 位带符号整数范围。

示例 1:

输入:s = "rabbbit", t = "rabbit"
输出:3
解释:
如下所示, 有 3 种可以从 s 中得到 "rabbit" 的方案。
rabbbit
rabbbit
rabbbit

示例 2:

输入:s = "babgbag", t = "bag"
输出:5
解释:
如下所示, 有 5 种可以从 s 中得到 "bag" 的方案。 
babgbag
babgbag
babgbag
babgbag
babgbag

提示:

  • 1 <= s.length, t.length <= 1000
  • st 由英文字母组成
class Solution {
    public int numDistinct(String s, String t) {
        int[][] dp = new int[s.length() + 1][t.length() + 1];
        for (int i = 0; i <= s.length(); i++) {
            dp[i][0] = 1;
        }
        for (int i = 1; i <= s.length(); i++) {
            char char1 = s.charAt(i - 1);
            for (int j = 1; j <= t.length(); j++) {
                char char2 = t.charAt(j - 1);
                if (char1 == char2) dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
                else dp[i][j] = dp[i - 1][j];
            }
        }
        return dp[s.length()][t.length()];
    }
}

583. 两个字符串的删除操作

给定两个单词 word1word2 ,返回使得 word1word2 相同所需的最小步数

每步 可以删除任意一个字符串中的一个字符。

示例 1:

输入: word1 = "sea", word2 = "eat"
输出: 2
解释: 第一步将 "sea" 变为 "ea" ,第二步将 "eat "变为 "ea"

示例 2:

输入:word1 = "leetcode", word2 = "etco"
输出:4

提示:

  • 1 <= word1.length, word2.length <= 500
  • word1word2 只包含小写英文字母
class Solution {
    public int minDistance(String word1, String word2) {
        int[][] dp = new int[word1.length() + 1][word2.length() + 1];
        for (int i = 1; i <= word1.length(); i++) {
            for (int j = 1; j <= word2.length(); j++) {
                //与最长重复数组一样
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1] + 1;
                    //取dp[i-1][j]与dp[i][j-1]的大值
                else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
        return word1.length()+word2.length()-2*dp[word1.length()][word2.length()];
    }
}

72. 编辑距离

给你两个单词 word1word2请返回将 word1 转换成 word2 所使用的最少操作数

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

示例 1:

输入:word1 = "horse", word2 = "ros"
输出:3
解释:
horse -> rorse (将 'h' 替换为 'r')
rorse -> rose (删除 'r')
rose -> ros (删除 'e')

示例 2:

输入:word1 = "intention", word2 = "execution"
输出:5
解释:
intention -> inention (删除 't')
inention -> enention (将 'i' 替换为 'e')
enention -> exention (将 'n' 替换为 'x')
exention -> exection (将 'n' 替换为 'c')
exection -> execution (插入 'u')

提示:

  • 0 <= word1.length, word2.length <= 500
  • word1word2 由小写英文字母组成
class Solution {
    public int minDistance(String word1, String word2) {
        int m = word1.length();
        int n = word2.length();
        int[][] dp = new int[m + 1][n + 1];
        // 初始化
        for (int i = 1; i <= m; i++) {
            dp[i][0] =  i;
        }
        for (int j = 1; j <= n; j++) {
            dp[0][j] = j;
        }
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                // 因为dp数组有效位从1开始
                // 所以当前遍历到的字符串的位置为i-1 | j-1
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    //分三种情况:删除word1中的元素,删除word2中的元素,替换一个元素
                    dp[i][j] = Math.min(Math.min(dp[i - 1][j ], dp[i][j - 1]), dp[i - 1][j- 1]) + 1;
                }
            }
        }
        return dp[m][n];
    }
}

子序列总结

最长递增子序列dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度

遍历i之前的数字:如果i之前的数字nums[j]nums[i]小,更新:dp[i] = Math.max(dp[i], dp[j] + 1);

返回:最大的长度

[最长连续递增序列](#674. 最长连续递增序列):dp[i]表示i之前包括i的以nums[i]结尾的最长连续递增子序列的长度

只有当前数字大于其前一个数字时才更新:dp[i] = dp[i - 1] + 1;

返回:最大长度



对于两个数组,未避免过多的初始化,添加dp[0][0]作为两个数组的0位置重复长度用于动态规划


[最长重复子数组](#718. 最长重复子数组):dp[i][j]数组1i-1个数字和数组2j-1个数字中最长公共部分长度(连续)

遍历nums[i-1]之前的数字,如果和当前数字相等,更新:dp[i][j] = dp[i - 1][j - 1] + 1;

返回:最大长度

最长公共子序列dp[i][j]数组1i-1个数字和数组2j-1个数字中最长公共部分长度(不连续)

遍历nums[i-1]之前的字符:

  • 如果与当前字符相同,更新:dp[i][j] = dp[i - 1][j - 1] + 1;

  • 如果与当前字符不相同,取两个数组中分别去除一个数字的重复数组的大值:dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);

不相交的线:本质是最长公共子序列

判断子序列:求最长公共子序列的长度,如果等于子序列的长度满足题意

[两个字符串的删除操作](#583. 两个字符串的删除操作):求最长公共子序列的长度,总长度减掉两倍的公共长度为返回值


不同的子序列:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]

初始化:dp[i][0]=1表示s删除字符后等到空字符串的情况只有一种

遍历t序列:

  • 如果s[i-1]=t[j-1]dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
  • 反之, dp[i][j] = dp[i - 1][j];

[编辑距离](#72. 编辑距离):dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]

初始化:dp[i][0] = i;dp[0][j] = j;

遍历word2的字符:

  • 如果与当前字符相同,则不需要编辑,更新:dp[i][j] = dp[i - 1][j - 1];

  • 如果与当前字符不相同,取下面三种情况的最小值

    • 去除word1中的一个元素:dp[i][j] = dp[i - 1][j]+1;
    • 去除word2中的一个元素:dp[i][j] = dp[i][j-1]+1;
    • 替换一个元素,等价于在dp[i][j]的基础上编辑一次,dp[i][j] = dp[i-1][j-1]+1;

647. 回文子串

给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。

回文字符串 是正着读和倒过来读一样的字符串。

子字符串 是字符串中的由连续字符组成的一个序列。

具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。

示例 1:

输入:s = "abc"
输出:3
解释:三个回文子串: "a", "b", "c"

示例 2:

输入:s = "aaa"
输出:6
解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa"

提示:

  • 1 <= s.length <= 1000
  • s 由小写英文字母组成
class Solution {
    public int countSubstrings(String s) {
        int res = 0;
        int len = s.length();
        //dp[i][j]表示子字符串s[i,j]是否为回文字符串
        
        boolean[][] dp = new boolean[len][len];
        for (int i = 0; i < len; i++) {
            for (int j = 0; j <= i; j++) {
                //情况1:i和j相同。
                if (s.charAt(i) == s.charAt(j)) {
                    if (i - j < 3) dp[i][j] = true;
                    else dp[i][j] = dp[i - 1][j + 1];
                } else dp[i][j] = false;//情况2:i和j不同
            }
        }
        //遍历每个字符,统计回文串的个数
        for (int i = 0; i < len; i++) {
            for (int j = 0; j < len; j++) {
                if (dp[i][j]) res++;
            }
        }
        return res;
    }
}

516.最长回文子序列

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

输入:s = "bbbab"
输出:4
解释:一个可能的最长回文子序列为 "bbbb" 。

示例 2:

输入:s = "cbbd"
输出:2
解释:一个可能的最长回文子序列为 "bb" 。

提示:

  • 1 <= s.length <= 1000
  • s 仅由小写英文字母组成
class Solution {
    public int longestPalindromeSubseq(String s) {
        int len = s.length();
        //dp[i][j]字符串s在[i][j]范围内最长的回文字符串的长度
        int[][] dp = new int[len + 1][len + 1];
        for (int i = len - 1; i >= 0; i--) {//必须保证i作为起始字符后一个字符作为起始字符计算过,需要从后往前动态规划
            dp[i][i] = 1;
            for (int j = i + 1; j < len; j++) {
                if (s.charAt(i) == s.charAt(j)) dp[i][j] = dp[i + 1][j - 1] + 2;
                else dp[i][j] = Math.max(dp[i + 1][j], Math.max(dp[i][j], dp[i][j - 1]));
            }
        }
        return dp[0][len - 1];
    }
}

回文字符串总结

[回文子串](#647. 回文子串):dp[i][j]表示子字符串s[i,j]是否为回文字符串(连续)

如果s[i]!=s[j]s[i,j]是回文字符串

如果s[i]==s[j]

  • 如果s[i][j]的长度小于2,s[i,j]是回文字符串
  • 如果s[i][j]的长度大于2,dp[i][j] = dp[i - 1][j + 1]

最长回文子序列dp[i][j]字符串s[i][j]范围内最长的回文字符串的长度(不连续)

如果s[i]=s[j]dp[i][j] = dp[i + 1][j - 1] + 2;

反之,取dp[i + 1][j]dp[i][j]dp[i][j - 1]的最大值

单调栈

739. 每日温度

给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。

示例 1:

输入: temperatures = [73,74,75,71,69,72,76,73]
输出: [1,1,4,2,1,1,0,0]

示例 2:

输入: temperatures = [30,40,50,60]
输出: [1,1,1,0]

示例 3:

输入: temperatures = [30,60,90]
输出: [1,1,0]

提示:

  • 1 <= temperatures.length <= 105
  • 30 <= temperatures[i] <= 100
class Solution {
    public int[] dailyTemperatures(int[] temperatures) {
        int[] res = new int[temperatures.length];
        LinkedList<Integer> stack = new LinkedList<>();
        stack.push(0);
        for (int i = 1; i < temperatures.length; i++) {
            //保持单调栈
            if (temperatures[i] <= temperatures[stack.peek()]) {
                stack.push(i);
            } else {
                while (!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
                    //出现非单调值,计算中间凹陷部分的值,并弹出,保证栈的单调性
                    res[stack.peek()] = i - stack.peek();
                    stack.pop();
                }
                stack.push(i);
            }
        }
        return res;
    }
}

496.下一个更大元素 I

nums1 中数字 x下一个更大元素 是指 xnums2 中对应位置 右侧第一个x 大的元素。

给你两个 没有重复元素 的数组 nums1nums2 ,下标从 0 开始计数,其中nums1nums2 的子集。

对于每个 0 <= i < nums1.length ,找出满足 nums1[i] == nums2[j] 的下标 j ,并且在 nums2 确定 nums2[j]下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1

返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素

示例 1:

输入:nums1 = [4,1,2], nums2 = [1,3,4,2].
输出:[-1,3,-1]
解释:nums1 中每个值的下一个更大元素如下所述:
- 4 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。
- 1 ,用加粗斜体标识,nums2 = [1,3,4,2]。下一个更大元素是 3 。
- 2 ,用加粗斜体标识,nums2 = [1,3,4,2]。不存在下一个更大元素,所以答案是 -1 。

示例 2:

输入:nums1 = [2,4], nums2 = [1,2,3,4].
输出:[3,-1]
解释:nums1 中每个值的下一个更大元素如下所述:
- 2 ,用加粗斜体标识,nums2 = [1,2,3,4]。下一个更大元素是 3 。
- 4 ,用加粗斜体标识,nums2 = [1,2,3,4]。不存在下一个更大元素,所以答案是 -1 。

提示:

  • 1 <= nums1.length <= nums2.length <= 1000
  • 0 <= nums1[i], nums2[i] <= 104
  • nums1nums2中所有整数 互不相同
  • nums1 中的所有整数同样出现在 nums2
class Solution {
    public int[] nextGreaterElement(int[] nums1, int[] nums2) {
        LinkedList<Integer> stack = new LinkedList<>();
        int[] res = new int[nums1.length];
         
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums1.length; i++) {
            map.put(nums1[i], i);
        }
        stack.push(0);
        for (int i = 1; i < nums2.length; i++) {
            if (nums2[i] <= nums2[stack.peek()]) stack.push(i);
            else {
                while (!stack.isEmpty() && nums2[stack.peek()] < nums2[i]) {
                    if (map.containsKey(nums2[stack.peek()])) {
                        Integer index = map.get(nums2[stack.peek()]);
                        res[index] = nums2[i];
                    }
                    stack.pop();
                }
                stack.push(i);
            }
        }
        return res;
    }
}

503.下一个更大元素II

给定一个循环数组 numsnums[nums.length - 1] 的下一个元素是 nums[0] ),返回 nums 中每个元素的 下一个更大元素

数字 x下一个更大的元素 是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1

示例 1:

输入: nums = [1,2,1]
输出: [2,-1,2]
解释: 第一个 1 的下一个更大的数是 2;
数字 2 找不到下一个更大的数; 
第二个 1 的下一个最大的数需要循环搜索,结果也是 2。

示例 2:

输入: nums = [1,2,3,4,3]
输出: [2,3,4,-1,4]

提示:

  • 1 <= nums.length <= 104
  • -109 <= nums[i] <= 109
class Solution {
    public int[] nextGreaterElements(int[] nums) {
        int[] res = new int[nums.length];
        int[] tempNums = new int[2 * nums.length];
        System.arraycopy(nums, 0, tempNums, 0, nums.length);
        System.arraycopy(nums, 0, tempNums, nums.length, nums.length);
        int[] temp = new int[tempNums.length];
        Arrays.fill(temp, -1);

        LinkedList<Integer> stack = new LinkedList<>();
        stack.push(0);
        for (int i = 1; i < tempNums.length; i++) {
            //保持单调栈
            if (tempNums[i] <= tempNums[stack.peek()]) {
                stack.push(i);
            } else {
                while (!stack.isEmpty() && tempNums[i] > tempNums[stack.peek()]) {
                    //出现非单调值,计算中间凹陷部分的值,并弹出,保证栈的单调性
                    temp[stack.peek()] = tempNums[i];
                    stack.pop();
                }
                stack.push(i);
            }
        }
        System.arraycopy(temp, 0, res, 0, res.length);
        return res;
    }
}

42. 接雨水

给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。

示例 1:

img

输入:height = [0,1,0,2,1,0,1,3,2,1,2,1]
输出:6
解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 

示例 2:

输入:height = [4,2,0,3,2,5]
输出:9

提示:

  • n == height.length
  • 1 <= n <= 2 * 104
  • 0 <= height[i] <= 105
class Solution {
    public int trap(int[] height) {
        if (height.length <= 2) return 0;
        Stack<Integer> stack = new Stack<Integer>();
        stack.push(0);

        int sum = 0;
        for (int i = 1; i < height.length; i++) {
            if (height[i] < height[stack.peek()]) {
                stack.push(i);
            } else {
                while (!stack.isEmpty() && (height[i] > height[stack.peek()])) {
                    int mid = stack.pop();
                    if (!stack.isEmpty()) {
                        int left = stack.peek();
                        int h = Math.min(height[left], height[i]) - height[mid];
                        int w = i - left - 1;
                        sum += h * w;
                    }
                }
                stack.push(i);
            }
        }

        return sum;
    }
}

84.柱状图中最大的矩形

给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。

求在该柱状图中,能够勾勒出来的矩形的最大面积。

示例 1:

img

输入:heights = [2,1,5,6,2,3]
输出:10
解释:最大的矩形为图中红色区域,面积为 10

示例 2:

img

输入: heights = [2,4]
输出: 4

提示:

  • 1 <= heights.length <=105
  • 0 <= heights[i] <= 104
class Solution {
    public int largestRectangleArea(int[] heights) {
        LinkedList<Integer> stack = new LinkedList<>();
        int[] newHeights = new int[heights.length + 2];
        newHeights[heights.length + 1] = 0;
        newHeights[0] = 0;
        System.arraycopy(heights, 0, newHeights, 1, heights.length);
        heights = newHeights;
        stack.push(0);
        int res = 0;
        for (int i = 1; i < heights.length; i++) {
            if (heights[i] >= heights[stack.peek()]) stack.push(i);
            else {
                while (heights[i] < heights[stack.peek()]) {
                    int mid = stack.pop();
                    int left = stack.peek();
                    int w = i - left - 1;
                    int h = heights[mid];
                    res = Math.max(res, w * h);
                }
                stack.push(i);
            }
        }
        return res;
    }
}

常见排序算法

常见排序算法

冒泡排序(Bubble Sort)

冒泡排序是一种交换排序,核心是冒泡,把数组中最小的那个往上冒,冒的过程就是和他相邻的元素交换。

重复走访要排序的数列,通过两两比较相邻记录的排序码。排序过程中每次从后往前冒一个最小值,且每次能确定一个数在序列中的最终位置。若发生逆序,则交换;有俩种方式进行冒泡,一种是先把小的冒泡到前边去,另一种是把大的元素冒泡到后边。

void solution(int[] arr) {
    for (int i = 0; i < arr.length; i++) {
        for (int j = 1; j < arr.length - i; j++) {
            if (arr[j - 1] > arr[j]) {
                swap(arr, j - 1, j);
            }
        }
    }
}

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

插入排序(Insertion Sort)

插入排序的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

动图

void solution(int[] arr) {
    for (int i = 1; i < arr.length; i++) {
        int temp = arr[i];
        int j;
        for (j = i - 1; j >= 0; j--) {
            //将比arr[i]大的向后移一位
            if (arr[j] > temp) {
                arr[j + 1] = arr[j];
            } else {
                break;
            }
        }
        arr[j + 1] = temp;
    }
}

希尔排序(Shell Sort)

先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。

img

void solution(int[] arr) {
    for (int gap = arr.length / 2; gap > 0; gap /= 2) {
        for (int i = gap; i < arr.length; i++) {
            int j;
            int temp = arr[i];
            for (j = i - gap; j >= 0; j -= gap) {
                if (arr[j] > arr[j + gap]) {
                    arr[j + gap] = arr[j];
                } else {
                    break;
                }
            }
            arr[j + gap] = temp;
        }
    }
}

选择排序(Selection Sort)

首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

动图

 void solution(int[] arr) {
        for (int i = 0; i < arr.length; i++) {
            int min = i;
            for (int j = i + 1; j < arr.length; j++) {
                //找到后面最小的值
                if (arr[j] < arr[min]) {
                    min = j;
                }
            }
            swap(arr, i, min);
        }
    }

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

快速排序(Quick Sort)

实现逻辑:

快速排序使用分治法(Divide and conquer)策略来把一个序列(list)分为两个子序列(sub-lists)。

① 从数列中挑出一个元素,称为 “基准”(pivot),
② 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作。
③ 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

递归到最底部时,数列的大小是零或一,也就是已经排序好了。这个算法一定会结束,因为在每次的迭代(iteration)中,它至少会把一个元素摆到它最后的位置去。

图示:

动图

public static void quickMethod(int[] arr,int start,int end){
    if (start >= end) {
        return;
    }
    //将当前分区的最后一个作为基准
    int mid = arr[end];
    int left = start, right = end - 1;
    //left的左边都小于基准,right的右边都大于基准,且最后left和right指向同一个位置,且该位置大于等于基准
    while (left < right) {
        while (arr[left] <= mid && left < right) {
            left++;
        }
        while (arr[right] >= mid && left < right) {
            right--;
        }
        swap(arr,left, right);
    }
    if (arr[left] >= arr[end]) {//如果左侧还存在大于基准的值,将其与基准替换,保证left指向基准
        swap(arr,left, end);
    } else {//反之,left左侧的值均小于基准值,将left移向基准
        left++;
    }
    quickMethod(arr,start, left - 1);
    quickMethod(arr,left + 1, end);
}

public static void swap(int[] arr,int x, int y) {
    int temp = arr[x];
    arr[x] = arr[y];
    arr[y] = temp;
}

归并排序(Merge Sort)

动图演示:

动图

事例:

img

public static void  mergeMethod(int[] arr, int[] result, int start, int end) {
    if (start >= end) {
        return;
    }
    //将数组拆开直至全部分开
    int len = end - start, mid = (len >> 1) + start;
    int start1 = start;
    int start2 = mid + 1;
    mergeMethod(arr, result, start1, mid);
    mergeMethod(arr, result, start2, end);
    
    int k = start;
    while (start1 <= mid && start2 <= end) {
        //将拆开的两个有序数组比较大小后放入结果数组
        result[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];
    }
    //如果前一个数组没有放完,依次放入
    while (start1 <= mid) {
        result[k++] = arr[start1++];
    }
    //如果后一个数组没有放完,依次放入
    while (start2 <= end) {
        result[k++] = arr[start2++];
    }
    //将排好序的合并数组放入原来的数组
    for (k = start; k <= end; k++) {
        arr[k] = result[k];
    }
}

堆排序(Heap Sort)

堆的相关概念

堆一般指的是二叉堆,顾名思义,二叉堆是完全二叉树或者近似完全二叉树

1. 堆的性质

① 是一棵完全二叉树
② 每个节点的值都大于或等于其子节点的值,为最大堆;反之为最小堆。

img

2. 堆的存储

一般用数组来表示堆,下标为 i 的结点的父结点下标为(i-1)/2;其左右子结点分别为 (2i + 1)、(2i + 2)

img

3. 堆的操作

在堆的数据结构中,堆中的最大值总是位于根节点(在优先队列中使用堆的话堆中的最小值位于根节点)。堆中定义以下几种操作:

最大堆调整(Max_Heapify):将堆的末端子节点作调整,使得子节点永远小于父节点
创建最大堆(Build_Max_Heap):将堆所有数据重新排序
堆排序(HeapSort):移除位在第一个数据的根节点,并做最大堆调整的递归运算


堆排序(Heap Sort)

举例

给定一个待排序序列数组 arr = [ 0 , 2, 4, 1 , 5 ];
先构建成一个完全二叉树如下;

img

构建堆

「我们从最后一个非叶子节点开始,从左至右,从下到上,开始调整」
最后一个非叶子节点的索引即 arr.length / 2向下取整 - 1 ,对于此例就是 5 / 2向下取整 - 1 = 2 - 1 = 1;
即值为2的节点;

img

我们用左右孩子节点的最大值与该节点进行比较;
此时我们发现它的左右孩子节点的最大值为5,大于2,进行交换;

img

然后处理下一个非叶子节点,即刚才的索引减去1; 1 - 1 = 0;
即:

img

左右孩子节点为5和4,5最大,且大于该节点的值,发生交换;

img

这时我们发现了一个问题:
「值为0的节点的左右节点又比该节点大了,又不满足大顶堆的定义了」

继续进行调整:

img

对非叶子节点调整完毕,构建大顶堆完成。

交换

将堆顶元素与末尾元素进行交换,使得末尾元素最大。

img

当交换完毕后最大的元素已经到达数组末尾;

img

对数组中其他元素进行排序即可。

img

进行交换:

img

剩下的元素调整并交换后:

img

剩下的元素调整并交换后:

img

img

此时也意味着排序完成了。

动图演示

动图

void solution(int[] arr) {
    int beginIndex = (arr.length - 1) / 2;
    for (int i = 0; i < beginIndex; i--) {
        //下一个叶子节点为当前节点索引减一
        maxHeapify(arr, i, arr.length);
    }
    for (int i = arr.length; i > 0; i++) {
        swap(arr, 0, i);
        maxHeapify(arr, 0, i - 1);
    }

}

//构建大顶堆
void maxHeapify(int[] arr, int index, int len) {
    int left = index * 2 + 1;
    int right = left + 1;
    int max = left;
    if (left > len) {
        return;
    }
    if (right <= len && arr[right] > arr[len]) {
        max = right;
    }
    if (arr[max] > arr[index]) {
        swap(arr, max, index);
        //如果交换了节点,需要判断重新调整叶子节点的堆
        maxHeapify(arr, max, len);
    }
}

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

计数排序(Counting Sort)

计数排序使用一个额外的数组C,其中第i个元素是待排序数组A中值等于i的元素的个数。

计数排序的核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

动图

void solution(int[] arr) {
    //得到数组的最大最小值
    int max = arr[0];
    int min = max;
    for (int j : arr) {
        if (j > max) {
            max = j;
        }
        if (j < min) {
            min = j;
        }
    }
    int[] count = new int[max - min + 1];

    for (int i = 0; i <= arr.length; i++) {
        count[arr[i]]++;
    }
    int index = 0;
    for (int i = min; i <= max; i++) {
        for (int j = 0; j < count[i]; j++) {
            arr[index++] = i;
        }
    }
}

桶排序(Bucket Sort)

桶排序(Bucket sort)或所谓的箱排序,是一个排序算法,工作的原理是将数组分到有限数量的桶里。每个桶再个别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。桶排序是鸽巢排序的一种归纳结果。当要被排序的数组内的数值是均匀分配的时候,桶排序使用线性时间(Θ(n))。但桶排序并不是比较排序,他不受到O(n log n)下限的影响。

img

基数排序(Radix Sort)

原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。基数排序的方式可以采用LSD(Least significant digital)或MSD(Most significant digital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。

  • MSD:先从高位开始进行排序,在每个关键字上,可采用计数排序
  • LSD:先从低位开始进行排序,在每个关键字上,可采用桶排序
void solution(int[] arr) {
    int bit = maxBit(arr);
    int[] temp = Arrays.copyOf(arr, arr.length);
    for (int i = 0; i < bit; i++) {
        int[] pos = new int[arr.length];
        for (int j = 0; j < temp.length; j++) {//取数字的第i位
            pos[j] = temp[j] % 10;
            temp[j] /= 10;
        }
        //冒泡排序
        for (int j = 0; j < arr.length; j++) {
            for (int k = 1; k < arr.length - j; k++) {
                if (pos[k - 1] > pos[k]) {
                    swap(pos, k - 1, k);
                    swap(arr, k - 1, k);
                    swap(temp, k - 1, k);
                }
            }
        }
    }
}

int maxBit(int[] arr) { //辅助函数,求数据的最大位数
    int maxData = arr[0];      //最大数
    /// 先求出最大数,再求其位数
    for (int i = 1; i < arr.length; ++i) {
        if (maxData < arr[i]) {
            maxData = arr[i];
        }
    }
    int d = 1;
    int p = 10;
    while (maxData >= p) {
        maxData /= 10;
        d++;
    }
    return d;
}

void swap(int[] arr, int i, int j) {
    int temp;
    temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
}
posted @ 2023-07-04 15:40  久漫  阅读(20)  评论(0编辑  收藏  举报