在处理滑动窗口问题时,双端队列(Deque)是一种极其高效的数据结构。让我们深入分析这个经典问题的解法。
双端队列在这里的作用是维护一个递减序列。每次新元素进入时,我们会从队尾开始比较,移除所有小于当前值的元素,确保队列始终保持递减顺序。这种处理方式有以下几个关键点:
注意:在Java中,ArrayDeque的peek/poll操作都是O(1)时间复杂度,这使得整个算法的时间复杂度可以控制在O(n)。
java复制class Solution {
public int[] maxSlidingWindow(int[] nums, int k) {
int[] res = new int[nums.length - k + 1];
int index = 0;
Deque<Integer> dp = new ArrayDeque<>();
for (int i = 0; i < nums.length; i++) {
// 维护递减序列
while (!dp.isEmpty() && nums[dp.peekLast()] <= nums[i]) {
dp.pollLast();
}
dp.offerLast(i);
// 当窗口形成后开始记录结果
if (i >= k - 1) {
// 移除不在窗口内的最大值
if (dp.peekFirst() <= i - k) {
dp.pollFirst();
}
res[index++] = nums[dp.peekFirst()];
}
}
return res;
}
}
这个算法的时间复杂度是O(n),其中n是数组长度。虽然有一个内层while循环,但每个元素最多被加入和移除队列各一次,所以总体操作次数是线性的。
最小覆盖子串问题要求我们在字符串s中找到包含字符串t所有字符的最短子串。滑动窗口是解决这类子串问题的利器,其基本框架如下:
java复制class Solution {
public String minWindow(String s, String t) {
Map<Character, Integer> map = new HashMap<>();
Set<Character> set = new HashSet<>();
int tlen = t.length(), slen = s.length();
// 初始化字符频率表
for (int i = 0; i < tlen; i++) {
map.put(t.charAt(i), map.getOrDefault(t.charAt(i), 0) + 1);
set.add(t.charAt(i));
}
int l = 0, r = 0, start = 0, end = slen, fit = 0;
while (r < slen) {
char rc = s.charAt(r);
if (set.contains(rc)) {
map.put(rc, map.get(rc) - 1);
if (map.get(rc) == 0) fit++;
}
// 当窗口满足条件时尝试收缩左边界
while (fit == set.size()) {
// 更新最小窗口
if (end - start > r - l) {
end = r;
start = l;
}
char lc = s.charAt(l);
if (set.contains(lc)) {
map.put(lc, map.get(lc) + 1);
if (map.get(lc) == 1) fit--;
}
l++;
}
r++;
}
return end == slen ? "" : s.substring(start, end + 1);
}
}
最大子数组和问题可以通过Kadane算法高效解决,其核心思想是:
java复制class Solution {
public int maxSubArray(int[] nums) {
int res = nums[0], sum = nums[0];
for (int i = 1; i < nums.length; i++) {
sum = Math.max(nums[i], sum + nums[i]);
res = Math.max(res, sum);
}
return res;
}
}
虽然Kadane算法更高效,但最大子数组和也可以用分治法解决,时间复杂度为O(nlogn)。分治法的基本思路是:
区间合并问题的关键在于先排序后合并:
java复制class Solution {
public int[][] merge(int[][] intervals) {
// 按起始点排序
Arrays.sort(intervals, (a, b) -> a[0] - b[0]);
List<int[]> list = new ArrayList<>();
list.add(intervals[0]);
for (int i = 1; i < intervals.length; i++) {
int[] last = list.get(list.size() - 1);
int[] now = intervals[i];
if (last[1] >= now[0]) {
// 合并区间
last[1] = Math.max(last[1], now[1]);
} else {
list.add(now);
}
}
return list.toArray(new int[list.size()][]);
}
}
轮转数组可以通过三次反转实现:
这种方法的时间复杂度是O(n),空间复杂度是O(1),是最优解法。
java复制class Solution {
public void rotate(int[] nums, int k) {
k = k % nums.length; // 处理k大于数组长度的情况
reverse(nums, 0, nums.length - 1 - k);
reverse(nums, nums.length - k, nums.length - 1);
reverse(nums, 0, nums.length - 1);
}
private void reverse(int[] nums, int l, int r) {
while (r > l) {
int temp = nums[l];
nums[l] = nums[r];
nums[r] = temp;
l++;
r--;
}
}
}
题目要求不能用除法且希望空间复杂度为O(1)(输出数组不算),可以通过以下方式实现:
java复制class Solution {
public int[] productExceptSelf(int[] nums) {
int[] res = new int[nums.length];
res[0] = 1;
// 计算前缀积
for (int i = 1; i < nums.length; i++) {
res[i] = res[i - 1] * nums[i - 1];
}
// 计算后缀积并合并结果
int suffix = 1;
for (int i = nums.length - 1; i >= 0; i--) {
res[i] *= suffix;
suffix *= nums[i];
}
return res;
}
}
要在O(n)时间且常数空间内找到缺失的最小正整数,可以使用原地哈希:
java复制class Solution {
public int firstMissingPositive(int[] nums) {
for (int i = 0; i < nums.length; i++) {
while (nums[i] >= 1 && nums[i] <= nums.length
&& nums[i] != nums[nums[i] - 1]) {
swap(nums, i, nums[i] - 1);
}
}
for (int i = 0; i < nums.length; i++) {
if (nums[i] != i + 1) {
return i + 1;
}
}
return nums.length + 1;
}
private void swap(int[] nums, int i, int j) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
}
为了不使用额外空间,可以利用矩阵的第一行和第一列作为标记:
java复制class Solution {
public void setZeroes(int[][] matrix) {
boolean firstRowZero = false;
boolean firstColZero = false;
// 检查第一行和第一列
for (int i = 0; i < matrix[0].length; i++) {
if (matrix[0][i] == 0) {
firstRowZero = true;
break;
}
}
for (int i = 0; i < matrix.length; i++) {
if (matrix[i][0] == 0) {
firstColZero = true;
break;
}
}
// 使用第一行和第一列作为标记
for (int i = 1; i < matrix.length; i++) {
for (int j = 1; j < matrix[0].length; j++) {
if (matrix[i][j] == 0) {
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
// 根据标记置零
for (int i = 1; i < matrix.length; i++) {
for (int j = 1; j < matrix[0].length; j++) {
if (matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
}
// 处理第一行和第一列
if (firstRowZero) {
for (int j = 0; j < matrix[0].length; j++) {
matrix[0][j] = 0;
}
}
if (firstColZero) {
for (int i = 0; i < matrix.length; i++) {
matrix[i][0] = 0;
}
}
}
}
螺旋矩阵可以通过控制四个边界来逐层遍历:
java复制class Solution {
public List<Integer> spiralOrder(int[][] matrix) {
List<Integer> res = new ArrayList<>();
int m = matrix.length, n = matrix[0].length;
int up = 0, down = m - 1, left = 0, right = n - 1;
while (up <= down && left <= right) {
// 从左到右
for (int i = left; i <= right; i++) {
res.add(matrix[up][i]);
}
up++;
// 从上到下
for (int i = up; i <= down; i++) {
res.add(matrix[i][right]);
}
right--;
if (up > down) break;
// 从右到左
for (int i = right; i >= left; i--) {
res.add(matrix[down][i]);
}
down--;
if (left > right) break;
// 从下到上
for (int i = down; i >= up; i--) {
res.add(matrix[i][left]);
}
left++;
}
return res;
}
}
旋转图像90度可以通过两次翻转实现:
java复制class Solution {
public void rotate(int[][] matrix) {
int n = matrix.length;
// 上下翻转
for (int i = 0; i < n / 2; i++) {
for (int j = 0; j < n; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[n - 1 - i][j];
matrix[n - 1 - i][j] = temp;
}
}
// 对角线翻转
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int temp = matrix[i][j];
matrix[i][j] = matrix[j][i];
matrix[j][i] = temp;
}
}
}
}