【Coding】LeetCode刷题记录

常用数据结构

1. 集合

2. 排序

3. 二分

- 模板
/**
 * 原始/二分查找:有序数组中,查找元素target是否存在,存在返回索引,不存在返回-1
 */
public static int binarySearch(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            return mid;
        }
    }
    return -1;
}

/**
 * 二分查找:有序数组中,查找元素target,出现的最左边界,存在返回索引,不存在返回-1
 */
public static int binarySearchLeft(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            right = mid - 1;    // 继续判断最左边界
        }
    }
    if (left >= nums.length || nums[left] != target) {
        return -1;
    }
    return left;
}

/**
 * 二分查找:有序数组中,查找元素target,出现的最右边界,存在返回索引,不存在返回-1
 */
public static int binarySearchRight(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else if (nums[mid] == target) {
            left = mid + 1; // 继续判断最右边界
        }
    }
    if (right < 0 || nums[right] != target) {
        return -1;
    }
    return right;
}
33. 搜索旋转排序数组

整数数组 nums 按升序排列,数组中的值 互不相同 。

在传递给函数之前,nums 在预先未知的某个下标 k(0 <= k < nums.length)上进行了 旋转,使数组变为 [nums[k], nums[k+1], …, nums[n-1], nums[0], nums[1], …, nums[k-1]](下标 从 0 开始 计数)。例如, [0,1,2,4,5,6,7] 在下标 3 处经旋转后可能变为 [4,5,6,7,0,1,2] 。

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

示例 1:

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

示例 2:

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

示例 3:

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

示例:

[4,5,6,7,8,1,2,3] , target = 8 输出:4

/**
 * 方法二:二分查找
 * 方法二:直接二分法
 * 直接使用二分法,判断那个二分点,有几种可能性
 * 1. 直接等于target
 * 2. 在左半边的递增区域
 *      a. target 在 left 和 mid 之间
 *      b. 不在之间
 * 3. 在右半边的递增区域
 *      a. target 在 mid 和 right 之间
 *      b. 不在之间
 */
public static int search(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            return mid;
        } else if (nums[left] <= nums[mid]) {    // 左边是递增的
            if (target >= nums[left] && target < nums[mid]) {
                right = mid - 1;
            } else {
                left = mid + 1;
            }
        } else {
            if (target > nums[mid] && target <= nums[right]) {
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
    }
    if (left >= nums.length || nums[left] != target) {
        return -1;
    }
    return left;
}
34. 在排序数组中查找元素的第一和最后一个位置

给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]。

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8 输出:[3,4]

示例 2:

输入:nums = [5,7,7,8,8,10], target = 6 输出:[-1,-1]

示例 3:

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

/**
 * 二分查找:有序数组中,查找元素target,出现的最左边界,存在返回索引,不存在返回-1
 */
public static int searchLeft(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else {
            right = mid - 1;
        }
    }
    if (left >= nums.length || nums[left] != target) {
        return -1;
    }
    return left;
}

/**
 * 二分查找:有序数组中,查找元素target,出现的最右边界,存在返回索引,不存在返回-1
 */
public static int searchRight(int[] nums, int target) {
    int left = 0;
    int right = nums.length - 1;
    while (left <= right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid - 1;
        } else {
            left = mid + 1;
        }
    }
    if (right < 0 || nums[right] != target) {
        return -1;
    }
    return right;
}

public static int[] searchRange(int[] nums, int target) {
    int[] res = new int[2];
    res[0] = searchLeft(nums, target);
    res[1] = searchRight(nums, target);
    return res;
}
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

示例 4:

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

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

public static int searchInsert(int[] nums, int target) {
    if (target < nums[0]) {
        return 0;
    }
    if (target > nums[nums.length - 1]) {
        return nums.length;
    }
    int left = 0;
    int right = nums.length - 1;
    while (left < right) { // 注意这里不能取等于
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            left = mid + 1;
        } else if (nums[mid] > target) {
            right = mid;   // 注意这里差距(举例子模拟一下即可)
        }
    }
    return left;
}
[Offer 11. 旋转数组的最小数字](#Offer 11.旋转数组的最小数字)

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。

示例 1:

输入:[3,4,5,1,2]
输出:1
示例 2:

输入:[2,2,2,0,1]
输出:0

/**
 * 方法一:直接遍历
 */
public static int minArray(int[] nums) {
    for (int i = 0; i < nums.length - 1; i++) {
        if (nums[i] > nums[i + 1]) {
            return nums[i + 1];
        }
    }
    return nums[0];
}

/**
 * 方法二:二分法
 * 使用二分查找的思想:
 * mid = left + (right - left) / 2
 * 需要考虑三种情况:
 * (1)array[mid] > array[right]:
 * 出现这种情况的array类似[3,4,5,6,0,1,2],此时最小数字一定在mid的右边。
 * left = mid + 1
 * (2)array[mid] == array[right]:
 * 出现这种情况的array类似 [1,0,1,1,1] 或者[1,1,1,0,1],此时最小数字不好判断在mid左边,还是右边,这时只好一个一个试, right--,因为是升序的
 * right = right - 1
 * (3)array[mid] < array[right]:
 * 出现这种情况的array类似[2,2,3,4,5,6,6],此时最小数字一定就是array[mid]或者在mid的左边。因为右边必然都是递增的。
 * right = mid
 * 注意这里有个坑:如果待查询的范围最后只剩两个数,那么mid 一定会指向下标靠前的数字
 * 比如 array = [4,6]
 * array[left] = 4; array[mid] = 4; array[right] = 6;
 * 如果right = mid - 1,就会产生错误, 因此right = mid
 * 但情形(1)中left = mid + 1就不会错误
 */
public static int minArrayII(int[] nums) {
    int left = 0;
    int right = nums.length - 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > nums[right]) {
            if (mid + 1 < right && nums[mid] > nums[mid + 1]) {
                return nums[mid + 1];
            }
            left = mid + 1;
        } else if (nums[mid] < nums[right]) {
            right = mid;
        } else {
            right = right - 1;
        }
    }
    return nums[left];
}
153. 寻找旋转排序数组中的最小值

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,2,4,5,6,7] 在变化后可能得到: 若旋转 4 次,则可以得到 [4,5,6,7,0,1,2] 若旋转 7 次,则可以得到 [0,1,2,4,5,6,7] 注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。

给你一个元素值 互不相同 的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

示例 1:

输入:nums = [3,4,5,1,2] 输出:1 解释:原数组为 [1,2,3,4,5] ,旋转 3 次得到输入数组。

示例 2:

输入:nums = [4,5,6,7,0,1,2] 输出:0 解释:原数组为 [0,1,2,4,5,6,7] ,旋转 4 次得到输入数组。

示例 3:

输入:nums = [11,13,15,17] 输出:11 解释:原数组为 [11,13,15,17] ,旋转 4 次得到输入数组。

/**  实际就是剑指Offer11题:旋转数组的最小数字 **/
/**
 * 方法二:二分法
 * 剑指Offer11:搜索旋转数组的最小值
 */
public static int findMin(int[] nums) {
    int left = 0;
    int right = nums.length - 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > nums[right]) {
            if (mid + 1 < right && nums[mid] > nums[mid + 1]) { // 举例:3, 4, 5, 1, 2
                return nums[mid + 1];
            }
            left = mid + 1;
        } else if (nums[mid] < nums[right]) {
            right = mid;
        } else {
            right = right - 1;
        }
    }
    return nums[left];
}

/**
 * 方法一:直接遍历
 */
public static int findMin(int[] nums) {
    for (int i = 0; i < nums.length - 1; i++) {
        if (nums[i] > nums[i + 1]) {
            return nums[i + 1];
        }
    }
    return nums[0];
}
154. 寻找旋转排序数组中的最小值II

已知一个长度为 n 的数组,预先按照升序排列,经由 1 到 n 次 旋转 后,得到输入数组。例如,原数组 nums = [0,1,4,4,5,6,7] 在变化后可能得到: 若旋转 4 次,则可以得到 [4,5,6,7,0,1,4] 若旋转 7 次,则可以得到 [0,1,4,4,5,6,7] 注意,数组 [a[0], a[1], a[2], …, a[n-1]] 旋转一次 的结果为数组 [a[n-1], a[0], a[1], a[2], …, a[n-2]] 。

给你一个可能存在 重复 元素值的数组 nums ,它原来是一个升序排列的数组,并按上述情形进行了多次旋转。请你找出并返回数组中的 最小元素 。

示例 1:

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

示例 2:

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

/**  实际就是剑指Offer11题:旋转数组的最小数字(和上面的解题方法一模一样) **/
/**
 * 方法二:二分法
 * 剑指Offer11:搜索旋转数组的最小值
 */
public static int findMin(int[] nums) {
    int left = 0;
    int right = nums.length - 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > nums[right]) {
            if (mid + 1 < right && nums[mid] > nums[mid + 1]) { // 举例:3, 4, 5, 1, 2
                return nums[mid + 1];
            }
            left = mid + 1;
        } else if (nums[mid] < nums[right]) {
            right = mid;
        } else {
            right = right - 1;
        }
    }
    return nums[left];
}

/**
 * 方法一:直接遍历
 */
public static int findMin(int[] nums) {
    for (int i = 0; i < nums.length - 1; i++) {
        if (nums[i] > nums[i + 1]) {
            return nums[i + 1];
        }
    }
    return nums[0];
}
162. 寻找峰值

峰值元素是指其值严格大于左右相邻值的元素。

给你一个整数数组 nums,找到峰值元素并返回其索引。数组可能包含多个峰值,在这种情况下,返回 任何一个峰值 所在位置即可。

你可以假设 nums[-1] = nums[n] = -∞ 。

你必须实现时间复杂度为 O(log n) 的算法来解决此问题。

示例 1:

输入:nums = [1,2,3,1] 输出:2 解释:3 是峰值元素,你的函数应该返回其索引 2。

示例 2:

输入:nums = [1,2,1,3,5,6,4] 输出:1 或 5 解释:你的函数可以返回索引 1,其峰值元素为 2; 或者返回索引 5, 其峰值元素为 6。

/**
 * 方法二:二分法
 * 首先要注意题目条件,在题目描述中出现了 nums[-1] = nums[n] = -∞,
 * 这就代表着 只要数组中存在一个元素比相邻元素大,那么沿着它一定可以找到一个峰值
 * 根据上述结论,我们就可以使用二分查找找到峰值
 * 查找时,左指针 l,右指针 r,以其保持左右顺序为循环条件
 * 根据左右指针计算中间位置 m,并比较 m 与 m+1 的值,
 *      如果 m 较大,则左侧存在峰值,r = m,
 *      如果 m + 1 较大,则右侧存在峰值,l = m + 1
 * 参考:https://leetcode-cn.com/problems/find-peak-element/solution/hua-jie-suan-fa-162-xun-zhao-feng-zhi-by-guanpengc/
 */
public static int findPeakElementII(int[] nums) {
    int left = 0;
    int right = nums.length - 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > nums[mid + 1]) {
            right = mid;    // 注意这里区别
        } else {
            left = mid + 1;
        }
    }
    return left;
}

/**
 * 方法一:直接遍历
 */
public static int findPeakElement(int[] nums) {
    int len = nums.length;
    if (len == 1) {
        return 0;
    }
    if (len == 2) {
        return nums[0] > nums[1] ? 0 : 1;
    }
    for (int i = 1; i < len - 1; i++) {
        if (nums[i - 1] < nums[i] && nums[i] > nums[i + 1]) {
            return i;
        }
    }
    return nums[0] > nums[len - 1] ? 0 : len - 1;
}
215. 数组中的第K个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例 1:

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

示例 2:

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

/**
 * 快速排序中的Partition思想,降序排序
 */  
int Partition(vector<int> &a, int l, int r) {
    int i = l, j = r, key;
    if (l < r) {
        key = a[l];
        while (i < j) {
            while (i < j && a[j] < key) j--;
            if (i < j) 
                a[i++] = a[j];
            while (i < j && a[i] > key) i++;
            if (i < j)
                a[j--] = a[i];
        }
        a[i] = key;
    }
    return i;
}

/**
 * 找到第 k 大的元素
 */ 
int findKthLargest(vector<int>& nums, int k) {
    int len = nums.size();
    if (len <= 0) return 0;
    int l = 0, r = len - 1, target = -1;
    int pos = Partition(nums, l, r);
    while (pos != k-1) {
        if (pos > k-1) {
            r = pos - 1;
            pos = Partition(nums, l, r);
        } else {
            l = pos + 1;
            pos = Partition(nums, l, r);
        }
    }
    target = nums[pos];
    return target;
}
475. 供暖器

冬季已经来临。 你的任务是设计一个有固定加热半径的供暖器向所有房屋供暖。

在加热器的加热半径范围内的每个房屋都可以获得供暖。

现在,给出位于一条水平线上的房屋 houses 和供暖器 heaters 的位置,请你找出并返回可以覆盖所有房屋的最小加热半径。

说明:所有供暖器都遵循你的半径标准,加热的半径也一样。

示例 1:

输入: houses = [1,2,3], heaters = [2]
输出: 1
解释: 仅在位置2上有一个供暖器。如果我们将加热半径设为1,那么所有房屋就都能得到供暖。

示例 2:

输入: houses = [1,2,3,4], heaters = [1,4]
输出: 1
解释: 在位置1, 4上有两个供暖器。我们需要将加热半径设为1,这样所有房屋就都能得到供暖。

示例 3:

输入:houses = [1,5], heaters = [2]
输出:3
/**
 * 方法一:(暴力:直接遍历法) 超时!
 * 先求每个供暖器到每个房间的位置,每个供暖器对应一行,构造一个二维数组。
 * 然后求每一列的最小值,得到一个一维数组。
 * 然后再求这个一维数组的最大值,即为半径大小。
 * <p>
 * | 1  2  3  4  (house)
 * ---------------------
 * 1  | 0  1  2  3
 * 4  | 3  2  1  0
 * 3  | 2  1  0  1
 * ---------------------
 * 0  1  0  0
 * <p>
 * 转换一下:先求第i个house房子到所有供热器的距离,保留最小值 res[i]
 * 然后求所有最小值的最大值。
 */
public static int findRadius(int[] houses, int[] heaters) {
    int curDis = 0;
    int[] res = new int[houses.length];
    Arrays.fill(res, Integer.MAX_VALUE);
    for (int i = 0; i < houses.length; i++) {
        for (int j = 0; j < heaters.length; j++) {
            curDis = Math.abs(houses[i] - heaters[j]);
            res[i] = Math.min(res[i], curDis);
        }
    }
    Arrays.sort(res);
    return res[res.length - 1];
}

/**
 * 方法二:二分法
 * 1、找到每个房屋离加热器的最短距离(即找出离房屋最近的加热器)。
 * 2、在所有距离中选出最大的一个max(res)即为结果。
 */
public static int findRadiusII(int[] houses, int[] heaters) {
    // 先排序
    Arrays.sort(houses);
    Arrays.sort(heaters);
    int[] res = new int[houses.length];
    // 找到每个房屋离加热器的最短距离(即找出离房屋最近的加热器)
    for (int i = 0; i < houses.length; i++) {
        int house = houses[i];
        // 二分查找:从heaters中找第一个大于等于houses的下标
        int left = 0;
        int right = heaters.length - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (heaters[mid] < house) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        // 供热器与house位置相等,距离为0
        if (heaters[left] == house) {
            res[i] = 0;
        } else if (heaters[left] < house) { // 供热器坐标值小于house,说明该加热器的坐标与 house 之间没有别的加热器
            res[i] = house - heaters[left];
        } else if (left == 0) { // 若left == 0 即二分查找的结果指向第一个加热器的坐标,说明 heaters[left] 坐标的房屋之前的位置
                                // 未放置加热器,直接相减就是到房屋 house 最近加热器的距离
            res[i] = heaters[left] - house;
        } else { // 若left不等于 0 ,说明 house 介于left和left-1之间,房屋到加热器的最短距离就是left和left - 1处
                 // 加热器与 house 差值的最小值
            res[i] = Math.min(heaters[left] - house, house - heaters[left - 1]);
        }
    }
    Arrays.sort(res);
    return res[res.length - 1];
}

3. 快排 partition 思想

Offer 39. 数组中出现次数超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

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

/**
 * 方法一:使用map统计每个字符出现的次数
 */
public static int majorityElement(int[] nums) {
    Map<Integer, Integer> map = new HashMap<>();
    for (int num : nums) {
        map.put(num, map.getOrDefault(num, 0) + 1);
        if (map.get(num) > nums.length / 2) {
            return num;
        }
    }
    return 0;
}

/**
 * 方法二:先排序,如果存在,则取中间的数一定是超过一半的数字
 */
public static int majorityElementII(int[] nums) {
    Arrays.sort(nums);
    return nums[nums.length / 2];
}

/**
 * 方法三:基于快排中partition思想,从无序数组中寻找第 k = n/2 大的数字
 * 首先用partition将数组分为两部分,得到分界点下标pos,然后分三种情况:
 * (1)pos == k-1 则找到第 k 大的值,arr[pos]
 * (2)pos > k-1  则第 k 大的值在左边部分的数组
 * (3)pos < k-1  则第 k 大的值再右边部分数组
 */
public static int majorityElementIII(int[] nums) {
    int len = nums.length;
    if (len <= 0) {
        return 0;
    }
    int left = 0;
    int right = len - 1;
    int k = len / 2;
    int pos = partition(nums, left, right);
    while (k != pos) {
        if (pos > k) {
            right = pos - 1;
            pos = partition(nums, left, right);
        } else {
            left = pos + 1;
            pos = partition(nums, left, right);
        }
    }
    int target = nums[pos];
    return checkMoreThanHalf(nums, target);
}

/**
 * partition思想:常考!
 */
private static int partition(int[] nums, int left, int right) {
    int i = left;
    int j = right;
    int key;
    if (left < right) {
        key = nums[left];
        while (i < j) {
            while (i < j && nums[j] > key) {
                j--;
            }
            if (i < j) {
                nums[i++] = nums[j];
            }
            while (i < j && nums[i] < key) {
                i++;
            }
            if (i < j) {
                nums[j--] = nums[i];
            }
        }
        nums[i] = key;
    }
    return i;
}

private static int checkMoreThanHalf(int[] nums, int target) {
    int times = 0;
    int threshold = nums.length / 2;
    for (int i = 0; i < nums.length; i++) {
        if (nums[i] == target) {
            times++;
        }
        if (times > threshold) {
            return target;
        }
    }
    return -1;
}

4. 栈和队列

- 数据结构

栈和队列,主要使用 Deque 接口,LinkedList 实现类。

4.1 栈常用方法

Stack MethodDeque Method说明
push(e)addFirst(e)向栈顶插入元素,失败则抛出异常
offerFirst(e)向栈顶插入元素,失败则返回 false
pop()removeFirst()获取并删除栈顶元素,失败抛出异常
poolFirst()获取并删除栈顶元素,失败返回 null
peek()getFirst()获取但不删除栈顶元素,失败则抛出异常
peekFirst()获取但不删除栈顶元素,失败则返回 null

4.2 队列常用方法

补充 PriorityQueue


Offer 09. 用两个栈实现队列

题目描述
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

示例 1:

输入:
[“CQueue”,“appendTail”,“deleteHead”,“deleteHead”]
[[],[3],[],[]]
输出:[null,null,3,-1]
示例 2:

输入:
[“CQueue”,“deleteHead”,“appendTail”,“appendTail”,“deleteHead”,“deleteHead”]
[[],[],[5],[2],[],[]]
输出:[null,-1,null,null,5,2]

Deque<Integer> stackIn;     // stackIn 用于进栈

Deque<Integer> stackOut;    // stackOut 用于出栈

public Offer_09_CQueue() {
    stackIn = new LinkedList<>();
    stackOut = new LinkedList<>();
}

public void appendTail(int value) {
    stackIn.push(value);    // stackIn 用于进栈
}

public int deleteHead() {
    if (stackOut.isEmpty()) {   // stackOut 空,stackIn非空,将stackIn出栈压入stackOut
        while (!stackIn.isEmpty()) {
            stackOut.push(stackIn.poll());
        }
    }
    if (!stackOut.isEmpty()) {  // stackOut非空的时候出栈
        return stackOut.poll();
    } else {
        return -1;
    }
}
Offer 30. 包含min函数的栈

定义栈的数据结构, 请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
使用两个栈,一个栈用于压入数据,一个栈用于保存最小元素栈。

每次压栈操作时, 如果压栈元素比当前最小元素更小, 就把这个元素压入最小元素栈, 原本的最小元素就成了次小元素. 同理, 弹栈时, 如果弹出的元素和最小元素栈的栈顶元素相等, 就把最小元素的栈顶弹出.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yozecu2N-1635956518763)(E:\1study\大学\专业\博客\image\code\offer-30.png)]

Deque<Integer> stackData;

Deque<Integer> stackMin;

/** initialize your data structure here. */
public Offer_30_MinStack() {
    stackData = new LinkedList<>();
    stackMin = new LinkedList<>();
}

public void push(int x) {
    stackData.push(x); // 数据栈压入输入的数据
    if (stackMin.isEmpty()) {
        stackMin.push(x);
    } else {
        if (stackMin.peek() > x) { // 辅助栈压入此时最小的元素
            stackMin.push(x);
        } else {
            stackMin.push(stackMin.peek()); // 辅助栈压入此时最小的元素
        }
    }
}

public void pop() {
    stackData.pop();
    stackMin.pop();
}

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

public int min() {
    return stackMin.peek();
}
Offer 31. 栈的压入、弹出序列

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为
该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,
序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的
弹出序列。(注意:这两个序列的长度是相等的)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LWms5uXq-1635956518765)(E:\1study\大学\专业\博客\image\code\offer-31.png)]

public static boolean validateStackSequences(int[] pushed, int[] popped) {
    if (pushed.length <= 0) {
        return true;
    }
    Deque<Integer> stackPush = new LinkedList<>(); // 辅助栈,用来存放压入的元素
    int j = 0; // 弹出栈 当前弹出的元素下标
    for (int i = 0; i < pushed.length; i++) {
        stackPush.push(pushed[i]); // 压入元素到辅助栈
        while (!stackPush.isEmpty() && stackPush.peek() == popped[j]) { // 只有辅助栈的栈顶元素和当前弹出栈需要弹出元素相等,则弹出辅助栈栈顶元素
            stackPush.pop();
            j++;
        }
    }
    return stackPush.isEmpty(); // 辅助栈不为空,则表示弹出顺序合法
}
735. 行星碰撞

给定一个整数数组 asteroids,表示在同一行的行星。

对于数组中的每一个元素,其绝对值表示行星的大小,正负表示行星的移动方向(正表示向右移动,负表示向左移动)。每一颗行星以相同的速度移动。

找出碰撞后剩下的所有行星。碰撞规则:两个行星相互碰撞,较小的行星会爆炸。如果两颗行星大小相同,则两颗行星都会爆炸。两颗移动方向相同的行星,永远不会发生碰撞。

示例 1:

输入:asteroids = [5,10,-5]
输出:[5,10]
解释:10 和 -5 碰撞后只剩下 10 。 5 和 10 永远不会发生碰撞。

示例 2:

输入:asteroids = [8,-8]
输出:[]
解释:8 和 -8 碰撞后,两者都发生爆炸。

示例 3:

输入:asteroids = [10,2,-5]
输出:[10]
解释:2 和 -5 发生碰撞后剩下 -5 。10 和 -5 发生碰撞后剩下 10 。

示例 4:

输入:asteroids = [-2,-1,1,2]
输出:[-2,-1,1,2]
解释:-2 和 -1 向左移动,而 1 和 2 向右移动。 由于移动方向相同的行星不会发生碰撞,所以最终没有行星发生碰撞。

/**
 * 方法一:使用栈(一定要注意圈复杂度)
 */
public static int[] asteroidCollision(int[] nums) {
    Deque<Integer> stack = new LinkedList<>();
    stack.push(nums[0]);
    for (int i = 1; i < nums.length; i++) {
        if (nums[i] >= 0) { // 如果当前数为正数,则直接压入栈
            stack.push(nums[i]);
        } else { // 只有是负数,才需要进行碰撞
            while (!stack.isEmpty() && stack.peek() > 0 && stack.peek() < Math.abs(nums[i])) {
                // 栈不为空,并且栈顶元素是正数,并且小于当前负数的绝对值
                stack.pop();
            }
            if (!stack.isEmpty() && stack.peek() > 0 && stack.peek() == Math.abs(nums[i])) {
                // 栈不为空,并且栈顶元素是正数,并且等于当前负数的绝对值,需要结束本次循环
                stack.pop();
                continue;
            }
            // 栈为空;或者栈不为空,栈顶元素是负数,则需要将当前元素压入
            if (stack.isEmpty() || (!stack.isEmpty() && stack.peek() < 0)) {
                stack.push(nums[i]);
            }
        }
    }
    // 不断压出,保存结果。逆序保存
    int[] res = new int[stack.size()];
    for (int i = stack.size() - 1; i >= 0 ; i--) {
        res[i] = stack.peek();
        stack.pop();
    }
    return res;
}
// 5, 10, -15
// 2, 1, -1, -2
// 5, 10, -2, 8, -9, 20, -11
// 5, 10, -2, 8, -9, 20, -11, -21

5. 单调栈

739. 每日温度

请根据每日 气温 列表 temperatures ,请计算在每一天需要等几天才会有更高的温度。如果气温在这之后都不会升高,请在该位置用 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]

/**
 * 方法一:单调栈
 */
public static int[] dailyTemperatures(int[] nums) {
    int len = nums.length;
    if (len == 1) {
        return new int[1];
    }
    Deque<Integer> stack = new LinkedList<>(); // 维护了一个单调递减栈
    int[] res = new int[len];
    for (int i = 0; i < nums.length; i++) {
        int curVal = nums[i];
        // pre栈顶元素(前面元素) 小于 当前元素 (后面的这个数)
        while (!stack.isEmpty() && nums[stack.peek()] < curVal) {
            int preIdx = stack.pop(); // 出栈,移除栈顶元素
            res[preIdx] = i - preIdx; // 保存当前元素,温度上升距离结果
        }
        stack.push(i); // 当前元素的下标进栈
    }
    return res;
}
1475. 商品折扣后的最终价格

给你一个数组 prices ,其中 prices[i] 是商店里第 i 件商品的价格。

商店里正在进行促销活动,如果你要买第 i 件商品,那么你可以得到与 prices[j] 相等的折扣,其中 j 是满足 j > i 且 prices[j] <= prices[i] 的 最小下标 ,如果没有满足条件的 j ,你将没有任何折扣。

请你返回一个数组,数组中第 i 个元素是折扣后你购买商品 i 最终需要支付的价格。

示例 1:

输入:prices = [8,4,6,2,3]
输出:[4,2,4,2,3]
解释:
商品 0 的价格为 price[0]=8 ,你将得到 prices[1]=4 的折扣,所以最终价格为 8 - 4 = 4 。
商品 1 的价格为 price[1]=4 ,你将得到 prices[3]=2 的折扣,所以最终价格为 4 - 2 = 2 。
商品 2 的价格为 price[2]=6 ,你将得到 prices[3]=2 的折扣,所以最终价格为 6 - 2 = 4 。
商品 3 和 4 都没有折扣。

示例 2:

输入:prices = [1,2,3,4,5]
输出:[1,2,3,4,5]
解释:在这个例子中,所有商品都没有折扣。

示例 3:

输入:prices = [10,1,1,6]
输出:[9,0,1,6]

/**
 * 方法一:依题意,使用单调栈。找元素右边第一个比当前元素小的元素。
 */
public static int[] finalPrices(int[] nums) {
    int len = nums.length;
    if (len == 1) {
        return nums;
    }
    Deque<Integer> stack = new LinkedList<>();
    int[] res = new int[len];
    for (int i = 0; i < len; i++) {
        res[i] = nums[i];
    }
    for (int i = 0; i < len; i++) {
        int curVal = nums[i];
        while (!stack.isEmpty() && nums[stack.peek()] >= curVal) {
            int preIdx = stack.pop();
            res[preIdx] = nums[preIdx] - curVal;
        }
        stack.push(i);
    }
    return res;
}
496. 下一个更大元素 I

给你两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。

请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。

nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中 对应位置 的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。

示例 1:

输入: nums1 = [4,1,2], nums2 = [1,3,4,2].
输出: [-1,3,-1]
解释:
对于 num1 中的数字 4 ,你无法在第二个数组中找到下一个更大的数字,因此输出 -1 。
对于 num1 中的数字 1 ,第二个数组中数字1右边的下一个较大数字是 3 。
对于 num1 中的数字 2 ,第二个数组中没有下一个更大的数字,因此输出 -1 。
示例 2:

输入: nums1 = [2,4], nums2 = [1,2,3,4].
输出: [3,-1]
解释:
对于 num1 中的数字 2 ,第二个数组中的下一个较大数字是 3 。
对于 num1 中的数字 4 ,第二个数组中没有下一个更大的数字,因此输出 -1 。

/**
 * 求数组每一位,后面第一个大于它的元素
 */
public static int[] nextGreaterCore(int[] nums) {
    int len = nums.length;
    if (len <= 0) {
        return null;
    }
    if (len == 1) {
        return new int[]{-1};
    }
    // 5 4 3 2 1
    Deque<Integer> stack = new LinkedList<>();
    int[] res = new int[len];
    Arrays.fill(res, -1);
    for (int i = 0; i < len; i++) {
        int curVal = nums[i];
        while (!stack.isEmpty() && nums[stack.peek()] < curVal) {
            int preIdx = stack.pop();
            res[preIdx] = curVal;
        }
        stack.push(i);
    }
    return res;
}

/**
 * 方法一:单调栈,先求出后面那个数组中,每个元素比它大的第一个元素。然后就可以找子集num1中每个元素,在nums中的结果
 */
public static int[] nextGreaterElement(int[] nums1, int[] nums2) {
    List<Integer> nums2List = Arrays.stream(nums2).boxed().collect(Collectors.toList());
    int[] res = nextGreaterCore(nums2);
    int[] out = new int[nums1.length];
    for (int i = 0; i < nums1.length; i++) {
        out[i] = res[nums2List.indexOf(nums1[i])];
    }
    return out;
}
42. 接雨水
84. 柱状图中最大的矩形
85. 最大矩形

6. 指针法

15. 三数之和

给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有和为 0 且不重复的三元组。

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

示例 1:

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

示例 2:

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

示例 3:

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

/**
 * 方法二:使用三指针来处理 (要多看多巩固几遍)
 * 首先对数组进行排序,排序后固定一个数nums[i],再使用左右指针指向nums[i]后面的两端,
 * 数字分别为nums[L]和nums[R],计算三个数的和sum判断是否满足为 0,满足则添加进结果集
 * 如果nums[i]大于 0,则三数之和必然无法等于 0,结束循环
 * 如果nums[i] == nums[i−1],则说明该数字重复,会导致结果重复,所以应该跳过
 * 当sum == 0 时,nums[L] == nums[L+1]则会导致结果重复,应该跳过,L++
 * 当sum == 0 时,nums[R] == nums[R−1]则会导致结果重复,应该跳过,R--
 * 时间复杂度:O(n^2),n为数组长度
 * Reference:https://leetcode-cn.com/problems/3sum/solution/hua-jie-suan-fa-15-san-shu-zhi-he-by-guanpengchn/
 */
public List<List<Integer>> threeSum(int[] nums) {
    List<List<Integer>> res = new ArrayList<>();
    Arrays.sort(nums);
    int len = nums.length;
    for (int i = 0; i < nums.length; i++) {
        if (nums[i] > 0) {
            break; // 如果当前数字大于0,则三数之和一定大于0,循环结束
        }
        if (i > 0 && nums[i] == nums[i - 1]) {  // 去重
            continue;
        }
        int left = i + 1;
        int right = len - 1;
        while (left < right) {
            int sum = nums[i] + nums[left] + nums[right];
            if (sum == 0) {
                res.add(Arrays.asList(nums[i], nums[left], nums[right]));
                while (left + 1 < len && nums[left] == nums[left + 1]) { // 去重
                    left++;
                }
                while (right - 1 >= 0 && nums[right] == nums[right - 1]) { // 去重
                    right--;
                }
                left++;
                right--;
            } else if (sum < 0) {
                left++;
            } else {
                right--;
            }
        }
    }
    return res;
}
16. 最接近的三数之和

给定一个包括 n 个整数的数组 nums 和 一个目标值 target。找出 nums 中的三个整数,使得它们的和与 target 最接近。返回这三个数的和。假定每组输入只存在唯一答案。

示例:

输入:nums = [-1,2,1,-4], target = 1
输出:2
解释:与 target 最接近的和是 2 (-1 + 2 + 1 = 2) 。

/**
 * 方法一:先排序,然后不断缩小范围
 * 排序后固定一个数 nums[i],然后再使用左右指针指向nums[i]后面的两端
 * 数字分别为nums[L]和nums[R],计算三个数之和sum,以及与target之间的差dist
 * (1)如果 dis == 0 的话,直接返回sum
 * (2)如果 dis > 0 的话,R-- 即让 sum 变大,dis变小
 * (3)如果 dis < 0 的话,L-- 即让 sum 变小,dis变大
 * 如果找到更小的 abs(dis) 我们更新最终 min_sum = sum
 */
public static int threeSumClosest(int[] nums, int target) {
    int len = nums.length;
    int minDis = Integer.MAX_VALUE;
    int minSum = 0;
    Arrays.sort(nums);
    for (int i = 0; i < len; i++) {
        int left = i + 1;
        int right = len - 1;
        while (left < right) {
            int sum = nums[i] + nums[left] + nums[right];
            int curDis = target - sum;
            if (Math.abs(curDis) < minDis) {
                minDis = Math.abs(curDis);
                minSum = sum;
            }
            if (curDis == 0) {
                return target;
            } else if (curDis > 0) {
                left++;
            } else {
                right--;
            }
        }
    }
    return minSum;
}
18. 四数之和

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

0 <= a, b, c, d < n
a、b、c 和 d 互不相同
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]]

/**
 * 方法一:先排序,然后固定两个数,后面部分再使用两个指针,不断缩小指针范围
 * 注意其中去重的部分即可。
 * 方法二:不考虑复杂去重,使用Set
 */
public static List<List<Integer>> fourSum(int[] nums, int target) {
    Arrays.sort(nums);
    List<List<Integer>> res = new ArrayList<>();
    for (int i = 0; i < nums.length - 3; i++) {
        if (i > 0 && nums[i] == nums[i - 1]) {
            continue;  // 第一个去重
        }
        for (int j = i + 1; j < nums.length - 2; j++) {
            if (j - i > 1 && nums[j] == nums[j - 1]) {
                continue;  // 第二个去重
            }
            int left = j + 1;
            int right = nums.length - 1;
            while (left < right) {
                int sum = nums[i] + nums[j] + nums[left] + nums[right];
                if (sum == target) {
                    res.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right]));
                    while (left + 1 < right && nums[left] == nums[left + 1]) { // 去重
                        left++;
                    }
                    while (right - 1 > left && nums[right] == nums[right - 1]) { // 去重
                        right--;
                    }
                    left++;
                } else if (sum < target) {
                    left++;
                } else {
                    right--;
                }
            }
        }
    }
    return res;
}
454. 四数相加 II

给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,
使得 A[i] + B[j] + C[k] + D[l] = 0。为了使问题简单化,所有的 A, B, C, D
具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -228 到 228 - 1 之间,
最终结果不会超过 231 - 1 。


75. 颜色分类

给定一个包含红色、白色和蓝色,一共 n 个元素的数组,原地对它们进行排序,使得相同颜色的元素相邻,并按照红色、白色、蓝色顺序排列。

此题中,我们使用整数 0、 1 和 2 分别表示红色、白色和蓝色。

示例 1:

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

示例 2:

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

示例 3:

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

示例 4:

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

/**
 * 方法一:直接排序即可
 */
public static void sortColors(int[] nums) {
    Arrays.sort(nums);
}

/**
 * 方法二:使用指针
 * p0:0的最右边界
 * p2:2的最左边界
 * cur:当前考虑的元素  (手动Debug一遍下面的例子就明白了)
 * <p>
 * p0               p2
 * |                 |
 * nums:  [1   0   2    1    1    2]
 * |
 * cur
 * <p>
 * 沿着数组移动cur指针
 * 若 nums[cur] == 0, 则将其与nums[p0] 互换
 * 若 nums[cur] == 2,则与nums[p2] 互换
 * <p>
 * 【具体算法】
 * 初始化0的最右边界:p0 = 0。在整个算法执行过程中 nums[idx < p0] = 0.
 * 初始化2的最左边界 :p2 = n - 1。在整个算法执行过程中 nums[idx > p2] = 2.
 * 初始化当前考虑的元素序号 :curr = 0.
 * While curr <= p2 :
 * 若 nums[curr] = 0 :交换第 curr个 和 第p0个 元素,并将指针都向右移。
 * 若 nums[curr] = 2 :交换第 curr个和第 p2个元素,并将 p2指针左移 。
 * 若 nums[curr] = 1 :将指针curr右移。
 */
public static void sortColorsII(int[] nums) {
    // p0 指向0的最右边界,对于所有 cur < p0 : nums[cur] = 0
    int cur = 0;
    int p0 = 0;
    // p2 指向2的最左边界,对于所有 cur > p2 : nums[cur] = 2
    int p2 = nums.length - 1;
    while (cur <= p2) {
        if (nums[cur] == 0) {
            swap(nums, p0, cur);
            p0++;
            cur++;
        } else if (nums[cur] == 2) {
            swap(nums, p2, cur);
            p2--;
        } else {
            cur++;
        }
    }
}

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

7. 滑动窗口

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

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。

示例 2:

输入: s = “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。

示例 3:

输入: s = “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。

示例 4:

输入: s = “”
输出: 0

/**
 * 方法一:滑动窗口
 */
public static int lengthOfLongestSubstring(String s) {
    Map<Character, Integer> map = new HashMap<>();
    char[] str = s.toCharArray();
    int len = s.length();
    int left = 0;
    int right = 0;
    int count = 0; // 表示:出现次数超过两次以上的个数。 比如aaa,此时count=3; 比如aaabb,count=4。
    int maxLen = 0;
    while (right < len) {
        // 如果包含该字符,并且字符个数要大于0,则count++;因为下面左移left的时候,会将map值也对应相减
        if (map.containsKey(str[right]) && map.get(str[right]) > 0) {
            count++;
        }
        map.put(str[right], map.getOrDefault(str[right], 0) + 1);
        while (count > 0) {
            // 如果移除左指针的字符出现个数在2次及以上,则左移后需要将count--
            if (left <= right && map.get(str[left]) > 1) {
                count--;
            }
            map.put(str[left], map.get(str[left]) - 1); // 更新map
            left++;
        }
        maxLen = Math.max(maxLen, right - left + 1);
        right++;
    }
    return maxLen;
}
159. 至多包含两个不同字符的最长子串

给定一个字符串 s ,找出 至多 包含两个不同字符的最长子串 t 。

示例 1:
输入: “eceba”
输出: 3
解释: t 是 “ece”,长度为3。

示例 2:
输入: “ccaabbb”
输出: 5
解释: t 是 “aabbb”,长度为5。

/**
 * 方法一:滑动窗口
 */
public static int lengthOfLongestSubstringTwoDistinct(String s) {
    Map<Character, Integer> map = new HashMap<>();
    char[] str = s.toCharArray();
    int len = s.length();
    int left = 0;
    int right = 0;
    int count = 0; // 表示:不同key的个数。比如aaa,此时count=1; 比如aaabc,count=3。
    int maxLen = 0;
    while (right < len) {
        // 如果map不包含这个key;或者包含,但数量为0。则count++;
        if (!map.containsKey(str[right]) || map.get(str[right]) == 0) {
            count++;
        }
        map.put(str[right], map.getOrDefault(str[right], 0) + 1);
        while (count > 2) {
            // 如果移除左指针的字符出现个数等于1次,则左移后需要将count--
            if (left <= right && map.get(str[left]) == 1) {
                count--;
            }
            // 更新map
            map.put(str[left], map.get(str[left]) - 1);
            left++;
        }
        maxLen = Math.max(maxLen, right - left + 1);
        right++;
    }
    return maxLen;
}
340. 至多包含 K 个不同字符的最长子串

给定一个字符串 s ,找出 至多 包含 K 个不同字符的最长子串 t 。返回其长度。

/**
 * 方法一:滑动窗口
 */
public static int lengthOfLongestSubstringKDistinct(String s, int k) {
    Map<Character, Integer> map = new HashMap<>();
    char[] str = s.toCharArray();
    int len = s.length();
    int left = 0;
    int right = 0;
    int count = 0; // 表示:不同key的个数。比如aaa,此时count=1; 比如aaabc,count=3。
    int maxLen = 0;
    while (right < len) {
        // 如果map不包含这个key;或者包含,但数量为0。则count++;
        if (!map.containsKey(str[right]) || map.get(str[right]) == 0) {
            count++;
        }
        map.put(str[right], map.getOrDefault(str[right], 0) + 1);
        while (count > k) { // 次数大于 k 次
            // 如果移除左指针的字符出现个数等于1次,则左移后需要将count--
            if (left <= right && map.get(str[left]) == 1) {
                count--;
            }
            // 更新map
            map.put(str[left], map.get(str[left]) - 1);
            left++;
        }
        maxLen = Math.max(maxLen, right - left + 1);
        right++;
    }
    return maxLen;
}
76. 最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

注意:

对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。
如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:

输入:s = “ADOBECODEBANC”, t = “ABC”
输出:“BANC”

示例 2:

输入:s = “a”, t = “a”
输出:“a”

示例 3:

输入: s = “a”, t = “aa”
输出: “”
解释: t 中两个字符 ‘a’ 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

/**
     * 方法一:使用滑动窗口,左右指针,套用模板 (和python版本对应)
     * (1)使用双指针,初始化 left = right = 0, 把索引闭区间 [left, right] 称为窗口
     * (2)先不断的增加right指针,扩大窗口 [left, right],同时记录当前窗口中每个字符出现的次数,
     * 直到窗口中的字符串符合要求(包含了T中的所有字符)
     * (3)此时,我们停止增加right,转而不断增加left指针,缩小窗口[left, right],
     * 直到窗口中的字符串不再符合要求(不包含T中的所有字符了)。同时,每次增加left,我们都要更新一轮结果
     * (4)重负第2和第3步,直到right达到字符串S的末尾
     * 【第2步相当于在寻找一个可行解,然后第3步在优化这个可行解,最终找到最优解。指针轮流前进,窗口大小增增减减,窗口不断向右滑动】
     * ADOBECODEBANC
     * ABC
     */
    public static String minWindow(String s, String t) {
        int left = 0;
        int right = 0;
        int count = 0; // 记录当前窗口包含指定子串的元素个数
        int minLen = Integer.MAX_VALUE;
        String res = "";
        int[] map = new int[256];
        Arrays.fill(map, 0);
        for (int i = 0; i < t.length(); i++) {
            map[t.charAt(i)]++;
        }
        while (right < s.length()) {
            if (map[s.charAt(right)] > 0) { // 如果还有子串字符未加入,则count++
                count++;
            }
            map[s.charAt(right)]--; // 更新map
            while (count == t.length()) {
                if (right - left + 1 < minLen) {
                    minLen = right - left + 1;
                    res = s.substring(left, right + 1);
                }
                // 如果子串包含左指针元素,并且map该key的个数为0;则移除后,需要更新count数量
                if (t.contains(String.valueOf(s.charAt(left))) && map[s.charAt(left)] == 0) {
                    count--;
                }
                map[s.charAt(left)]++;
                left++;
            }
            right++;
        }
        return res;
    }
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]

示例 3:

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

示例 4:

输入:nums = [9,11], k = 2
输出:[11]

示例 5:

输入:nums = [4,-2], k = 2
输出:[4]

/**
 * 方法一:维护一个单调队列
 */
public static int[] maxSlidingWindow(int[] nums, int k) {
    // 双向队列,保存当前窗口最大值的数组位置,保证队列中数组位置的的数值按从大到小排序
    Deque<Integer> queue = new LinkedList<>();
    int[] res = new int[nums.length - k + 1];
    int idx = 0;
    for (int i = 0; i < nums.length; i++) {
        // 保证从大到小,如果前面数小,则需要依次弹出,直到满足要求
        // 维护一个单调递减队列,队列保存单调递减队列元素的下标。
        while (!queue.isEmpty() && nums[queue.peek()] < nums[i]) {
            queue.pop();
        }
        // 添加当前值对应的数组下标
        queue.push(i);
        // 判断当前队首的值是否有效
        if (queue.getLast() <= i - k) {
            queue.removeLast();
        }
        // 当窗口长度为k时(i遍历到到k - 1位置之后),保存当前窗口的最大值
        if (i + 1 >= k) {
            res[i + 1 - k] = nums[queue.getLast()];
        }
    }
    return res;
}

/**
 * 方法二:暴力穷举。超时
 */
public static int[] maxSlidingWindowII(int[] nums, int k) {
    int[] res = new int[nums.length - k + 1];
    int idx = 0;
    for (int i = k - 1; i < nums.length; i++) {
        res[idx++] = getMaxSlidingWindow(nums, i - k + 1, k);
    }
    return res;
}

private static int getMaxSlidingWindow(int[] nums, int left, int len) {
    int[] win = new int[len];
    int idx = 0;
    for (int i = left; i < left + len; i++) {
        win[idx++] = nums[i];
    }
    Arrays.sort(win);
    return win[len - 1];
}

8. 前缀和

五分钟学算法:前缀和系列

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cen5l3bk-1635956518767)(E:\1study\大学\专业\博客\image\code\前缀和.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YYd5qaAD-1635956518770)(E:\1study\大学\专业\博客\image\code\前缀和-1.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vf4Nlq18-1635956518771)(E:\1study\大学\专业\博客\image\code\前缀和-2.png)]

for (int i = 0; i < nums.length; i++) {
	preSum[i + 1] = nums[i] + preSum[i];
}
724. 寻找数组的中心下标

给你一个整数数组 nums ,请计算数组的 中心下标 。

数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。

如果中心下标位于数组最左端,那么左侧数之和视为 0 ,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。

如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1 。

示例 1:

输入:nums = [1, 7, 3, 6, 5, 6]
输出:3
解释:
中心下标是 3 。
左侧数之和 sum = nums[0] + nums[1] + nums[2] = 1 + 7 + 3 = 11 ,
右侧数之和 sum = nums[4] + nums[5] = 5 + 6 = 11 ,二者相等。

示例 2:

输入:nums = [1, 2, 3]
输出:-1
解释:
数组中不存在满足此条件的中心下标。

示例 3:

输入:nums = [2, 1, -1]
输出:0
解释:
中心下标是 0 。
左侧数之和 sum = 0 ,(下标 0 左侧不存在元素),
右侧数之和 sum = nums[1] + nums[2] = 1 + -1 = 0 。

/**
 * 先计算总和,然后遍历,判断当前位置是否左边等于右边
 */
public static int pivotIndex(int[] nums) {
    int totalSum = 0;
    for (int i = 0; i < nums.length; i++) {
        totalSum += nums[i];
    }
    int curSum = 0;
    for (int i = 0; i < nums.length; i++) {
        curSum += nums[i];
        if (curSum - nums[i] == (totalSum - curSum)) {  // 左边 = 右边
            return i;
        }
    }
    return -1;
}
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]

/**
 * 方法三:遍历一遍数组,使用HashMap
 * 事实证明,我们可以一次完成。在进行迭代并将元素插入到表中的同时,我们还会回过头来检查表中是否已
 * 经存在当前元素所对应的目标元素。如果它存在,那我们已经找到了对应解,并立即将其返回。
 *
 * @param nums
 * @param target
 * @return
 */
private static int[] twoSumIII(int[] nums, int target) {
    Map<Integer, Integer> map = new HashMap<>();
    for (int i = 0; i < nums.length; i++) {
        if (map.containsKey(target - nums[i])) {
            return new int[]{i, map.get(target - nums[i])};
        }
        map.put(nums[i], i);
    }
    throw new IllegalArgumentException("No two sum solution");
}
560. 和为 K 的子数组

给你一个整数数组 nums 和一个整数 k ,请你统计并返回该数组中和为 k 的连续子数组的个数。

示例 1:

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

示例 2:

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

/**
 * 方法一:暴力遍历。两重循环
 */
public static int subarraySum(int[] nums, int k) {
    int count = 0;
    for (int i = 0; i < nums.length; i++) {
        int curSum = 0;
        for (int j = i; j < nums.length; j++) {
            curSum += nums[j];
            if (curSum == k) {
                count++;
            }
        }
    }
    return count;
}

/**
 * 方法二:前缀和。先建立前缀和数组。
 * 时间复杂度:O(n^2)
 */
public static int subarraySumII(int[] nums, int k) {
    // 前缀和数组
    int[] preSum = new int[nums.length + 1];
    for (int i = 0; i < nums.length; i++) {
        // 注意:这里的前缀和数组是preSum[1]开始填充的。preSum[0] = 0
        preSum[i + 1] = preSum[i] + nums[i];
    }
    // 统计个数
    int count = 0;
    for (int i = 0; i < nums.length; i++) {
        for (int j = i; j < nums.length; j++) {
            // 注意下标偏移:nums[2] 到 nums[4] 等于 preSum[5] - preSum[2]
            // 如下可以获取区间 nums[i, j] 内的和
            if (preSum[j + 1] - preSum[i] == k) {
                count++;
            }
        }
    }
    return count;
}

方法三: 前缀和 + HashMap

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-N8AcBhXf-1635956518773)(E:\1study\大学\专业\博客\image\code\前缀和-3.png)]


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V0tHv0Gd-1635956518774)(E:\1study\大学\专业\博客\image\code\前缀和-4.png)]

/**
 * 方法三:前缀和 + HashMap
 */
public static int subarraySumIII(int[] nums, int k) {
    if (nums.length == 0) {
        return 0;
    }
    // map表示,前缀和为key的,有value个
    Map<Integer, Integer> map = new HashMap<>();
    map.put(0, 1);  // 前缀和为0的,初始值为1个。
    int count = 0;
    int preSum = 0;
    for (int i = 0; i < nums.length; i++) {
        preSum += nums[i];
        // 判断是否有 preSum - k 的前缀和,进而可知道是否有前缀和为 k 的。
        if (map.containsKey(preSum - k)) {
            count += map.get(preSum - k); // 获取次数
        }
        // 更新
        map.put(preSum, map.getOrDefault(preSum, 0) + 1);
    }

    return count;
}
1248. K个奇数的子数组

给你一个整数数组 nums 和一个整数 k。

如果某个 连续 子数组中恰好有 k 个奇数数字,我们就认为这个子数组是「优美子数组」。

请返回这个数组中「优美子数组」的数目。

示例 1:

输入:nums = [1,1,2,1,1], k = 3
输出:2
解释:包含 3 个奇数的子数组是 [1,1,2,1] 和 [1,2,1,1] 。

示例 2:

输入:nums = [2,4,6], k = 1
输出:0
解释:数列中不包含任何奇数,所以不存在优美子数组。

示例 3:

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oHZYZP7t-1635956518775)(E:\1study\大学\专业\博客\image\code\前缀和-5.png)]

/**
 * 方法一:前缀和 + HashMap
 * 只需将前缀区间的奇数个数保存到区间内即可,只不过将 sum += x 改成了判断奇偶的语句
 *
 */
public static int numberOfSubarrays(int[] nums, int k) {
    if (nums.length == 0) {
        return 0;
    }
    // map表示,前缀奇数个数为key的,有value个
    Map<Integer, Integer> map = new HashMap<>();
    map.put(0, 1);
    int count = 0;
    int preOdd = 0;
    for (int i = 0; i < nums.length; i++) {
        preOdd += (nums[i] % 2);
        if (map.containsKey(preOdd - k)) {
            count += map.get(preOdd - k);
        }
        // 更新
        map.put(preOdd, map.getOrDefault(preOdd, 0) + 1);
    }
    return count;
}
974. 和可被 K 整除的子数组

给定一个整数数组 A,返回其中元素之和可被 K 整除的(连续、非空)子数组的数目。

示例:

输入:A = [4,5,0,-2,-3,1], K = 5
输出:7
解释:
有 7 个子数组满足其元素之和可被 K = 5 整除:
[4, 5, 0, -2, -3, 1], [5], [5, 0], [5, 0, -2, -3], [0], [0, -2, -3], [-2, -3]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ge5kIipG-1635956518776)(E:\1study\大学\专业\博客\image\code\前缀和-6.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-whl0u8hM-1635956518777)(E:\1study\大学\专业\博客\image\code\前缀和-7.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u0o7PhQ2-1635956518778)(E:\1study\大学\专业\博客\image\code\前缀和-8.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-St8tOf40-1635956518779)(E:\1study\大学\专业\博客\image\code\前缀和-9.png)]

/**
 * 方法三:前缀和 + HashMap(查看具体题解)
 */
public static int subarraysDivByK(int[] nums, int k) {
    // map表示,前缀和为key的,有value个
    Map<Integer, Integer> map = new HashMap<>();
    map.put(0, 1);
    int count = 0;
    int preSum = 0;
    for (int i = 0; i < nums.length; i++) {
        preSum += nums[i];
        // 计算当前 preSum 与 k 关系,余数是几。
        // 当被除数为负数时,取模结果为负数,通过如下方式纠正
        int key = (preSum % k + k) % k;
        if (map.containsKey(key)) {
            count += map.get(key);
        }
        // 更新
        map.put(key, map.getOrDefault(key, 0) + 1);
    }
    return count;
}
// 注意:上面有这一行代码
int key = (preSum % k + k) % k;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8L8sirCx-1635956518780)(E:\1study\大学\专业\博客\image\code\前缀和-10.png)]

523. 连续的子数组和

给你一个整数数组 nums 和一个整数 k ,编写一个函数来判断该数组是否含有同时满足下述条件的连续子数组:

子数组大小 至少为 2 ,且
子数组元素总和为 k 的倍数。
如果存在,返回 true ;否则,返回 false 。

如果存在一个整数 n ,令整数 x 符合 x = n * k ,则称 x 是 k 的一个倍数。0 始终视为 k 的一个倍数。

示例 1:

输入:nums = [23,2,4,6,7], k = 6
输出:true
解释:[2,4] 是一个大小为 2 的子数组,并且和为 6 。

示例 2:

输入:nums = [23,2,6,4,7], k = 6
输出:true
解释:[23, 2, 6, 4, 7] 是大小为 5 的子数组,并且和为 42 。
42 是 6 的倍数,因为 42 = 7 * 6 且 7 是一个整数。

示例 3:

输入:nums = [23,2,6,4,7], k = 13
输出:false

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pbP9yIfV-1635956518781)(E:\1study\大学\专业\博客\image\code\前缀和-11.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E574TtZ0-1635956518782)(E:\1study\大学\专业\博客\image\code\前缀和-12.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aUb94QSV-1635956518783)(E:\1study\大学\专业\博客\image\code\前缀和-13.png)]

  • 视频解析

/**
 * 方法一:前缀和
 */
public static boolean checkSubarraySum(int[] nums, int k) {
    // map存放:key保存 前缀和%k取余的结果,value保存当前遍历到数据的下标。
    Map<Integer, Integer> map = new HashMap<>();
    map.put(0, -1);
    int preSum = 0;
    int count = 0;
    for (int i = 0; i < nums.length; i++) {
        preSum += nums[i];
        int key = (preSum % k + k) % k;
        if (map.containsKey(key)) {
            if (i - map.get(key) >= 2) { // 前缀和取k余存在,并且两者下标差要大于等于2; 如果不满足要求,则遍历下一次
                return true;
            } else {
                continue;
            }
        }
        // 更新前缀后取余的结果,<取余结果,当前下标>
        map.put(key, i);
    }
    return false;
}
930. 和相同的二元子数组

给你一个二元数组 nums ,和一个整数 goal ,请你统计并返回有多少个和为 goal 的 非空 子数组。

子数组 是数组的一段连续部分。

示例 1:

输入:nums = [1,0,1,0,1], goal = 2
输出:4
解释:
有 4 个满足题目要求的子数组:[1,0,1]、[1,0,1,0]、[0,1,0,1]、[1,0,1]

示例 2:

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

/**
 * 方法一:前缀和,和LeetCode 560:和为K的子数组一样
 */
public static int numSubarraysWithSum(int[] nums, int k) {
    // map表示,前缀和为key的,有value个
    Map<Integer, Integer> map = new HashMap<>();
    map.put(0, 1);  // 前缀和为0的,初始值为1个。
    int count = 0;
    int preSum = 0;
    for (int i = 0; i < nums.length; i++) {
        preSum += nums[i];
        // 判断是否有 preSum - k 的前缀和,进而可知道是否有前缀和为 k 的。
        if (map.containsKey(preSum - k)) {
            count += map.get(preSum - k); // 获取次数
        }
        // 更新
        map.put(preSum, map.getOrDefault(preSum, 0) + 1);
    }
    return count;
}

9. 并查集

概念

  • “并” - 合并集合(union),合并关联的多个元素为同一个根节点

  • “查” - 查找元素(find),查找根节点

基本操作

  • 初始化: init(), int[] parent = new int[size]

  • 合并元素:union(int x, int y, int[] parent)

  • 查找元素的根节点:find(int x, int[] parent)

参考链接:

  • https://segmentfault.com/a/1190000004023326
  • https://www.jianshu.com/p/8c74df1db116

无向连通图(求所有联通图的个数)

  • 323 无向图中联通分量的数目
  • 200 岛屿数量
  • 547 省份数量

最小生成树(包含权重、求包含所有节点的最值权重)

  • 1135 最低成本联通所有城市
  • 1102 得分最高的路径

10. 深搜+广搜+回溯

11. 树

12. 链表

13. 拓扑排序

14. 动态规划

15. 字符串

1041. 困于环中的机器人

在无限的平面上,机器人最初位于 (0, 0) 处,面朝北方。机器人可以接受下列三条指令之一:

“G”:直走 1 个单位
“L”:左转 90 度
“R”:右转 90 度
机器人按顺序执行指令 instructions,并一直重复它们。

只有在平面中存在环使得机器人永远无法离开时,返回 true。否则,返回 false。

示例 1:

输入:“GGLLGG”
输出:true
解释:
机器人从 (0,0) 移动到 (0,2),转 180 度,然后回到 (0,0)。
重复这些指令,机器人将保持在以原点为中心,2 为半径的环中进行移动。
示例 2:

输入:“GG”
输出:false
解释:
机器人无限向北移动。
示例 3:

输入:“GL”
输出:true
解释:
机器人按 (0, 0) -> (0, 1) -> (-1, 1) -> (-1, 0) -> (0, 0) -> … 进行移动。

注意:GLGLGGLGL

输出:false

/**
 * 方法一:画图一个坐标,模拟一下即可
 * 注意:题目意思是每次走完所有的,再判断是否走回原点。中间如果有走回0的,不算。
 */
public static boolean isRobotBounded(String instructions) {
    int[][] dir = {{0, 1}, {-1, 0}, {0, -1}, {1, 0}};
    int len = dir.length;
    int idx = 0;
    int[] curDir = {0, 1};
    int[] curPoint = {0, 0};
    String line = "";
    for (int i = 0; i < 4; i++) {   // 出现环,最多走4次即可
        for (char key : instructions.toCharArray()) {
            if (key == 'L') {
                idx = (idx + 1 + 4) % 4;
                curDir = dir[idx];
            } else if (key == 'R') {
                idx = (idx - 1 + 4) % 4;
                curDir = dir[idx];
            } else {
                curPoint[0] += curDir[0];
                curPoint[1] += curDir[1];
            }
        }
        if (curPoint[0] == 0 && curPoint[1] == 0) {
            return true;
        }
    }
    return false;
}
844 比较含退格的字符串

给定 st 两个字符串,当它们分别被输入到空白的文本编辑器后,请你判断二者是否相等。# 代表退格字符。

如果相等,返回 true ;否则,返回 false

**注意:**如果对空文本输入退格字符,文本继续为空。

示例 1:

输入:s = "ab#c", t = "ad#c"
输出:true
解释:S 和 T 都会变成 “ac”。

示例 2:

输入:s = "ab##", t = "c#d#"
输出:true
解释:s 和 t 都会变成 “”。

示例 3:

输入:s = "a##c", t = "#a#c"
输出:true
解释:s 和 t 都会变成 “c”。

示例 4:

输入:s = "a#c", t = "b"
输出:false
解释:s 会变成 “c”,但 t 仍然是 “b”。
/**
 * 方法一:从后往前遍历,统计#个数,即为需要往前遍历跳过的个数
 */
public static boolean backspaceCompare(String s, String t) {
    return getRes(s).equals(getRes(t));
}

private static String getRes(String s) {
    String sRes = "";
    int count = 0;
    for (int i = s.length() - 1; i >= 0; i--) {
        if (s.charAt(i) == '#') {
            count++;
        } else {
            if (count > 0) {
                count--;
            } else {
                sRes += s.charAt(i);
            }
        }
    }
    return sRes;
}
475. 供暖器

冬季已经来临。 你的任务是设计一个有固定加热半径的供暖器向所有房屋供暖。

在加热器的加热半径范围内的每个房屋都可以获得供暖。

现在,给出位于一条水平线上的房屋 houses 和供暖器 heaters 的位置,请你找出并返回可以覆盖所有房屋的最小加热半径。

说明:所有供暖器都遵循你的半径标准,加热的半径也一样。

示例 1:

输入: houses = [1,2,3], heaters = [2]
输出: 1
解释: 仅在位置2上有一个供暖器。如果我们将加热半径设为1,那么所有房屋就都能得到供暖。

示例 2:

输入: houses = [1,2,3,4], heaters = [1,4]
输出: 1
解释: 在位置1, 4上有两个供暖器。我们需要将加热半径设为1,这样所有房屋就都能得到供暖。

示例 3:

输入:houses = [1,5], heaters = [2]
输出:3
/**
 * 方法一:(暴力:直接遍历法) 超时!
 * 先求每个供暖器到每个房间的位置,每个供暖器对应一行,构造一个二维数组。
 * 然后求每一列的最小值,得到一个一维数组。
 * 然后再求这个一维数组的最大值,即为半径大小。
 * <p>
 * | 1  2  3  4  (house)
 * ---------------------
 * 1  | 0  1  2  3
 * 4  | 3  2  1  0
 * 3  | 2  1  0  1
 * ---------------------
 * 0  1  0  0
 * <p>
 * 转换一下:先求第i个house房子到所有供热器的距离,保留最小值 res[i]
 * 然后求所有最小值的最大值。
 */
public static int findRadius(int[] houses, int[] heaters) {
    int curDis = 0;
    int[] res = new int[houses.length];
    Arrays.fill(res, Integer.MAX_VALUE);
    for (int i = 0; i < houses.length; i++) {
        for (int j = 0; j < heaters.length; j++) {
            curDis = Math.abs(houses[i] - heaters[j]);
            res[i] = Math.min(res[i], curDis);
        }
    }
    Arrays.sort(res);
    return res[res.length - 1];
}

/**
 * 方法二:二分法
 * 1、找到每个房屋离加热器的最短距离(即找出离房屋最近的加热器)。
 * 2、在所有距离中选出最大的一个max(res)即为结果。
 */
public static int findRadiusII(int[] houses, int[] heaters) {
    // 先排序
    Arrays.sort(houses);
    Arrays.sort(heaters);
    int[] res = new int[houses.length];
    // 找到每个房屋离加热器的最短距离(即找出离房屋最近的加热器)
    for (int i = 0; i < houses.length; i++) {
        int house = houses[i];
        // 二分查找:从heaters中找第一个大于等于houses的下标
        int left = 0;
        int right = heaters.length - 1;
        while (left < right) {
            int mid = left + (right - left) / 2;
            if (heaters[mid] < house) {
                left = mid + 1;
            } else {
                right = mid;
            }
        }
        // 供热器与house位置相等,距离为0
        if (heaters[left] == house) {
            res[i] = 0;
        } else if (heaters[left] < house) { // 供热器坐标值小于house,说明该加热器的坐标与 house 之间没有别的加热器
            res[i] = house - heaters[left];
        } else if (left == 0) { // 若left == 0 即二分查找的结果指向第一个加热器的坐标,说明 heaters[left] 坐标的房屋之前的位置
                                // 未放置加热器,直接相减就是到房屋 house 最近加热器的距离
            res[i] = heaters[left] - house;
        } else { // 若left不等于 0 ,说明 house 介于left和left-1之间,房屋到加热器的最短距离就是left和left - 1处
                 // 加热器与 house 差值的最小值
            res[i] = Math.min(heaters[left] - house, house - heaters[left - 1]);
        }
    }
    Arrays.sort(res);
    return res[res.length - 1];
}

剑指Offer

03. 数组中重复的数字

题目描述
在一个长度为n的数组里的所有数字都在0到n-1的范围内。数组中某些数字是重复的,
但不知道有几个数字是重复的。也不知道每个数字重复几次。请找出数组中任意一个重复的数字。
例如,如果输入长度为7的数组{2,3,1,0,2,5,3},那么对应的输出是第一个重复的数字2。
思路:
(方法一):两重循环,每次将第i个,与后面的进行比较,直到找到重复的,或遍历完成。时间复杂度 O(n^2)

(方法二):使用一个Set集合,一次遍历,如果元素不在Set集合中,则加入Set集合中;如果存在,则找到重复字符,返回结果。

(方法三):因为是从0到n-1,如果不重复,那么每个下标和其数应该相等,如果不等,
我们就将当前数,交换到其应该在的下标位置。如果,此时要交换的两个数相等,则说明有重复数字。
否则遍历完整个序列都不会有重复数字。自己举个例子打一遍即可: [0 1 2 4 3 3] 和 [0 1 2 5 3 4]

要求时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。对于这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素调整到第 i 个位置上进行求解。以 (2, 3, 1, 0, 2, 5) 为例,遍历到位置 4 时,该位置上的数为 2,但是第 2 个位置上已经有一个 2 的值了,因此可以知道 2 重复:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5hayz31v-1635956518785)(image\code\49d2adc1-b28a-44bf-babb-d44993f4a2e3.gif)]

/**
 * 方法三:因为是从0到n-1,如果不重复,那么每个下标和其数应该相等,如果不等,
 * 我们就将当前数,交换到其应该在的下标位置。如果,此时要交换的两个数相等,则说明有重复数字。
 * 否则遍历完整个序列都不会有重复数字。自己举个例子打一遍即可: [0 1 2 4 3 3] 和 [0 1 2 5 3 4]
 */
private static int findRepeatNumberIII(int nums[]) {
    int len = nums.length;
    // 判断数组是否为空
    if (len <= 0) return -1;
    // 判断数组是否合法(每个数字都在0 ~ n-1 之间)
    for (int i = 0; i < len; i++) {
        if (nums[i] < 0 || nums[i] > len - 1) {
            return -1;
        }
    }

    int temp;
    for (int i = 0; i < len; i++) {
        while (nums[i] != i) {
            if (nums[i] == nums[nums[i]]) {
                return nums[i];
            }
            temp = nums[i];
            nums[i] = nums[nums[i]];
            nums[temp] = temp;
        }
    }
    return -1;
}

/**
 * 方法二:使用一个Set集合,一次遍历,如果元素不在Set集合中,则加入Set集合中;如果存在,则找到重复字符,返回结果。
 */
private static int findRepeatNumberII(int[] nums) {
    Set<Integer> set = new HashSet<>();
    for (int num : nums) {
        if (set.contains(num)) {
            return num;
        } else {
            set.add(num);
        }
    }
    return -1;
}

/**
 * 方法一:两重循环,每次将第i个,与后面的进行比较,直到找到重复的,或遍历完成。时间复杂度 O(n^2)
 * (注意判断数组的合法性!)
 */
private static int findRepeatNumber(int nums[]) {
    int len = nums.length;
    // 判断数组是否为空
    if (len <= 0) return -1;
    // 判断数组是否合法(每个数字都在0 ~ n-1 之间)
    for (int i = 0; i < len; i++) {
        if (nums[i] < 0 || nums[i] > len - 1) {
            return -1;
        }
    }

    for (int i = 0; i < len - 1; i++) {
        for (int j = i + 1; j < len; j++) {
            if (nums[i] == nums[j]) {
                return nums[i];
            }
        }
    }
    return -1;
}

public static void main(String[] args) {
    int[] nums = new int[]{2, 3, 1, 0, 2, 5, 3};
    int length = nums.length;
    int[] duplication = new int[1];
    System.out.println(findRepeatNumberIII(nums));
}
04. 二维数组中的查找

题目描述
在一个二维数组中(每个一维数组的长度相同),每一行都按照从左到右
递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个函数,
输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
例如:

1 2 8 9
2 4 9 12
4 7 10 13
6 8 11 15
如果查找数字 7,则返回true
如果查找数字 5,则放回false
Reference:

  • https://blog.csdn.net/a819825294/article/details/52088732
  • https://blog.csdn.net/duan19920101/article/details/50617190
/**
 * 方法一:直接两重循环进行遍历
 */
private static boolean findNumberIn2DArray(int[][] matrix, int target) {
    for (int i = 0; i < matrix.length; i++) {
        for (int j = 0; j < matrix[0].length; j++) {
            if (matrix[i][j] == target) {
                return true;
            }
        }
    }
    return false;
}

/**
 * 方法二: 矩阵是有序的,从左下角来看,向上数字递减,向右数字递增,
 * 因此从左下角开始查找,当要查找数字比左下角数字大时。右移要
 * 查找数字比左下角数字小时,上移
 * 时间复杂度: O(n^2)
 */
private static boolean findNumberIn2DArrayII(int[][] matrix, int target) {
    int rows = matrix.length;
    int cols = matrix[0].length;
    int i = rows - 1, j = 0;
    while (i >= 0 && j < cols) {
        if (target == matrix[i][j]) {
            return true;
        } else if (target > matrix[i][j]) {
            j++;
        } else {
            i--;
        }
    }
    return false;
}
05. 替换空格

题目描述
请实现一个函数,将一个字符串中的每个空格替换成“%20”。例如,当字符串为We Are Happy。 则经过替换之后的字符串为We%20Are%20Happy。

/**
 * 方法一:直接使用replace函数
 */
private static String replaceSpace(String s) {
    return s.replace(" ", "%20");
}

/**
 * 方法二:for循环遍历替换
 */
private static String replaceSpaceII(String s) {
    String res = "";
    for (int i = 0; i < s.length(); i++) {
        if (s.charAt(i) == ' ') {
            res += "%20";
        } else {
            res += String.valueOf(s.charAt(i));
        }
    }
    return res;
}
09. 用两个栈实现队列

题目描述
用两个栈实现一个队列。队列的声明如下,请实现它的两个函数 appendTail 和 deleteHead ,分别完成在队列尾部插入整数和在队列头部删除整数的功能。(若队列中没有元素,deleteHead 操作返回 -1 )

示例 1:

输入:
[“CQueue”,“appendTail”,“deleteHead”,“deleteHead”]
[[],[3],[],[]]
输出:[null,null,3,-1]
示例 2:

输入:
[“CQueue”,“deleteHead”,“appendTail”,“appendTail”,“deleteHead”,“deleteHead”]
[[],[],[5],[2],[],[]]
输出:[null,-1,null,null,5,2]

Deque<Integer> stackIn;     // stackIn 用于进栈

Deque<Integer> stackOut;    // stackOut 用于出栈

public Offer_09_CQueue() {
    stackIn = new LinkedList<>();
    stackOut = new LinkedList<>();
}

public void appendTail(int value) {
    stackIn.push(value);    // stackIn 用于进栈
}

public int deleteHead() {
    if (stackOut.isEmpty()) {   // stackOut 空,stackIn非空,将stackIn出栈压入stackOut
        while (!stackIn.isEmpty()) {
            stackOut.push(stackIn.poll());
        }
    }
    if (!stackOut.isEmpty()) {  // stackOut非空的时候出栈
        return stackOut.poll();
    } else {
        return -1;
    }
}
10- I. 斐波那契数列

写一个函数,输入 n ,求斐波那契(Fibonacci)数列的第 n 项(即 F(N))。斐波那契数列的定义如下:

F(0) = 0, F(1) = 1
F(N) = F(N - 1) + F(N - 2), 其中 N > 1.
斐波那契数列由 0 和 1 开始,之后的斐波那契数就是由之前的两数相加而得出。

答案需要取模 1e9+7(1000000007),如计算初始结果为:1000000008,请返回 1。

示例 1:

输入:n = 2
输出:1
示例 2:

输入:n = 5
输出:5

public static int fib(int num) {
    if (num == 0 || num == 1) {
        return num;
    }
    int a = 0;
    int b = 1;
    int c = 1;
    for (int i = 2; i <= num; i++) {
        c = (a + b) % 1000000007;
        a = b;
        b = c;
    }
    return b;
}
// 0 1 1 2 3 5 8
10- II. 青蛙跳台阶问题

题目描述
一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)

思路:

当 n = 1 时,只有一种跳法:

当 n = 2 时,有两种跳法:

跳 n 阶台阶,可以先跳 1 阶台阶,再跳 n-1 阶台阶;或者先跳 2 阶台阶,再跳 n-2 阶台阶。而 n-1 和 n-2 阶台阶的跳法可以看成子问题,该问题的递推公式为:

public static int numWays(int num) {
    num++;	// 注意这里++了
    if (num == 0 || num == 1) {
        return num;
    }
    int a = 1;
    int b = 1;
    int c;
    for (int i = 2; i < num; i++) {
        c = (a + b) % 1000000007;
        a = b;
        b = c;
    }
    return b;
}

// 1 1 2 3 5 8 13 21
10.3-变态跳台阶

题目描述
一只青蛙一次可以跳上 1 级台阶,也可以跳上 2 级… 它也可以跳上 n 级。求该青蛙跳上一个 n 级的台阶总共有多少种跳法。

/**
 * 方法一:数学归纳出公式
 *    用数学归纳法可知:f(n) = 2^{n-1}
 * 可以直接使用快速幂来计算。
 */
public static int jumpFloorII(int num) {
    num--;
    int ans = 1;
    int base = 2;
    while (num != 0) {
        if (num % 2 == 1) {
            ans *= base;
        }
        base *= base;
        num /= 2;
    }
    return ans;
}
10.4-矩形覆盖

题目描述
我们可以用2x1的小矩形横着或者竖着去覆盖更大的矩形。请问用n个2x1的小矩形无重叠地覆盖一个2xn的大矩形,总共有多少种方法?

思路:

当 n 为 1 时,只有一种覆盖方法:

当 n 为 2 时,有两种覆盖方法:

要覆盖 2*n 的大矩形,可以先覆盖 2*1 的矩形,再覆盖 2*(n-1) 的矩形;或者先覆盖 2*2 的矩形,再覆盖 2*(n-2) 的矩形。而覆盖 2*(n-1) 和 2*(n-2) 的矩形可以看成子问题。该问题的递推公式如下:

#include <cstdio>

/**
 * 思路:依旧是斐波拉契数列思想
 */ 
public static int RectCover(int num) {
    num++;	// 注意这里++了
    if (num == 0 || num == 1) {
        return num;
    }
    int a = 1;
    int b = 1;
    int c;
    for (int i = 2; i < num; i++) {
        c = (a + b) % 1000000007;
        a = b;
        b = c;
    }
    return b;
}

// 1 1 2 3 5 8 13 21
11. 旋转数组的最小数字

把一个数组最开始的若干个元素搬到数组的末尾,我们称之为数组的旋转。输入一个递增排序的数组的一个旋转,输出旋转数组的最小元素。例如,数组 [3,4,5,1,2] 为 [1,2,3,4,5] 的一个旋转,该数组的最小值为1。

示例 1:

输入:[3,4,5,1,2]
输出:1
示例 2:

输入:[2,2,2,0,1]
输出:0

/**
 * 方法一:直接遍历
 */
public static int minArray(int[] nums) {
    for (int i = 0; i < nums.length - 1; i++) {
        if (nums[i] > nums[i + 1]) {
            return nums[i + 1];
        }
    }
    return nums[0];
}

/**
 * 方法二:二分法
 * 使用二分查找的思想:
 * mid = left + (right - left) / 2
 * 需要考虑三种情况:
 * (1)array[mid] > array[right]:
 * 出现这种情况的array类似[3,4,5,6,0,1,2],此时最小数字一定在mid的右边。
 * left = mid + 1
 * (2)array[mid] == array[right]:
 * 出现这种情况的array类似 [1,0,1,1,1] 或者[1,1,1,0,1],此时最小数字不好判断在mid左边,还是右边,这时只好一个一个试, right--,因为是升序的
 * right = right - 1
 * (3)array[mid] < array[right]:
 * 出现这种情况的array类似[2,2,3,4,5,6,6],此时最小数字一定就是array[mid]或者在mid的左边。因为右边必然都是递增的。
 * right = mid
 * 注意这里有个坑:如果待查询的范围最后只剩两个数,那么mid 一定会指向下标靠前的数字
 * 比如 array = [4,6]
 * array[left] = 4; array[mid] = 4; array[right] = 6;
 * 如果right = mid - 1,就会产生错误, 因此right = mid
 * 但情形(1)中left = mid + 1就不会错误
 */
public static int minArrayII(int[] nums) {
    int left = 0;
    int right = nums.length - 1;
    while (left < right) {
        int mid = left + (right - left) / 2;
        if (nums[mid] > nums[right]) {
            if (mid + 1 < right && nums[mid] > nums[mid + 1]) {
                return nums[mid + 1];
            }
            left = mid + 1;
        } else if (nums[mid] < nums[right]) {
            right = mid;
        } else {
            right = right - 1;
        }
    }
    return nums[left];
}
30. 包含min函数的栈

定义栈的数据结构, 请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。
使用两个栈,一个栈用于压入数据,一个栈用于保存最小元素栈。

每次压栈操作时, 如果压栈元素比当前最小元素更小, 就把这个元素压入最小元素栈, 原本的最小元素就成了次小元素. 同理, 弹栈时, 如果弹出的元素和最小元素栈的栈顶元素相等, 就把最小元素的栈顶弹出.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RXaYnJBd-1635956518786)(E:\1study\大学\专业\博客\image\code\offer-30.png)]

Deque<Integer> stackData;

Deque<Integer> stackMin;

/** initialize your data structure here. */
public Offer_30_MinStack() {
    stackData = new LinkedList<>();
    stackMin = new LinkedList<>();
}

public void push(int x) {
    stackData.push(x); // 数据栈压入输入的数据
    if (stackMin.isEmpty()) {
        stackMin.push(x);
    } else {
        if (stackMin.peek() > x) { // 辅助栈压入此时最小的元素
            stackMin.push(x);
        } else {
            stackMin.push(stackMin.peek()); // 辅助栈压入此时最小的元素
        }
    }
}

public void pop() {
    stackData.pop();
    stackMin.pop();
}

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

public int min() {
    return stackMin.peek();
}
31. 栈的压入、弹出序列

输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否可能为
该栈的弹出顺序。假设压入栈的所有数字均不相等。例如序列1,2,3,4,5是某栈的压入顺序,
序列4,5,3,2,1是该压栈序列对应的一个弹出序列,但4,3,5,1,2就不可能是该压栈序列的
弹出序列。(注意:这两个序列的长度是相等的)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4N3eta3B-1635956518787)(E:\1study\大学\专业\博客\image\code\offer-31.png)]

public static boolean validateStackSequences(int[] pushed, int[] popped) {
    if (pushed.length <= 0) {
        return true;
    }
    Deque<Integer> stackPush = new LinkedList<>(); // 辅助栈,用来存放压入的元素
    int j = 0; // 弹出栈 当前弹出的元素下标
    for (int i = 0; i < pushed.length; i++) {
        stackPush.push(pushed[i]); // 压入元素到辅助栈
        while (!stackPush.isEmpty() && stackPush.peek() == popped[j]) { // 只有辅助栈的栈顶元素和当前弹出栈需要弹出元素相等,则弹出辅助栈栈顶元素
            stackPush.pop();
            j++;
        }
    }
    return stackPush.isEmpty(); // 辅助栈不为空,则表示弹出顺序合法
}
39. 数组中出现次数超过一半的数字

数组中有一个数字出现的次数超过数组长度的一半,请找出这个数字。

你可以假设数组是非空的,并且给定的数组总是存在多数元素。

示例 1:

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

/**
 * 方法一:使用map统计每个字符出现的次数
 */
public static int majorityElement(int[] nums) {
    Map<Integer, Integer> map = new HashMap<>();
    for (int num : nums) {
        map.put(num, map.getOrDefault(num, 0) + 1);
        if (map.get(num) > nums.length / 2) {
            return num;
        }
    }
    return 0;
}

/**
 * 方法二:先排序,如果存在,则取中间的数一定是超过一半的数字
 */
public static int majorityElementII(int[] nums) {
    Arrays.sort(nums);
    return nums[nums.length / 2];
}

/**
 * 方法三:基于快排中partition思想,从无序数组中寻找第 k = n/2 大的数字
 * 首先用partition将数组分为两部分,得到分界点下标pos,然后分三种情况:
 * (1)pos == k-1 则找到第 k 大的值,arr[pos]
 * (2)pos > k-1  则第 k 大的值在左边部分的数组
 * (3)pos < k-1  则第 k 大的值再右边部分数组
 */
public static int majorityElementIII(int[] nums) {
    int len = nums.length;
    if (len <= 0) {
        return 0;
    }
    int left = 0;
    int right = len - 1;
    int k = len / 2;
    int pos = partition(nums, left, right);
    while (k != pos) {
        if (pos > k) {
            right = pos - 1;
            pos = partition(nums, left, right);
        } else {
            left = pos + 1;
            pos = partition(nums, left, right);
        }
    }
    int target = nums[pos];
    return checkMoreThanHalf(nums, target);
}

/**
 * partition思想:常考!
 */
private static int partition(int[] nums, int left, int right) {
    int i = left;
    int j = right;
    int key;
    if (left < right) {
        key = nums[left];
        while (i < j) {
            while (i < j && nums[j] > key) {
                j--;
            }
            if (i < j) {
                nums[i++] = nums[j];
            }
            while (i < j && nums[i] < key) {
                i++;
            }
            if (i < j) {
                nums[j--] = nums[i];
            }
        }
        nums[i] = key;
    }
    return i;
}

private static int checkMoreThanHalf(int[] nums, int target) {
    int times = 0;
    int threshold = nums.length / 2;
    for (int i = 0; i < nums.length; i++) {
        if (nums[i] == target) {
            times++;
        }
        if (times > threshold) {
            return target;
        }
    }
    return -1;
}
40. 最小的k个数

输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。

示例 1:

输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]

示例 2:

输入:arr = [0,1,2,1], k = 1
输出:[0]
/**
 * 方法一:直接升序排序,然后取前k个数字
 */
public static int[] getLeastNumbers(int[] nums, int k) {
    Arrays.sort(nums);
    int[] res = new int[k];
    for (int i = 0; i < k; i++) {
        res[i] = nums[i];
    }
    return res;
}

/**
 * 方法二:大根堆方法(大根堆,最小的元素在堆顶)
 * 取前k个数,构建大根堆res,然后将后面的数,依次与堆顶比较,如果比堆顶小,则将其与堆顶交换
 * (具体的交换方法是,先将堆顶出堆,然后将小的数加入,重新构造大根堆)
 * 时间复杂度:O(nlogk)
 */
public static int[] getLeastNumbersII(int[] nums, int k) {
    int[] res = new int[k];
    if (k == 0) {
        return res;
    }
    PriorityQueue<Integer> queue = new PriorityQueue<>(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o2 - o1;
        }
    });
    // 取前k个元素,构造大根堆
    for (int i = 0; i < k; i++) {
        queue.offer(nums[i]);
    }
    // 然后将后面的数,依次与堆顶比较,如果比堆顶小,则将其与堆顶交换
    // (具体的交换方法是,先将堆顶出堆,然后将小的数加入,重新构造大根堆)
    for (int i = k; i < nums.length; i++) {
        if (queue.peek() > nums[i]) {
            queue.poll();
            queue.offer(nums[i]);
        }
    }
    for (int i = 0; i < k; i++) {
        res[i] = queue.poll();
    }
    return res;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值