参考:
滑动窗口
滑动窗口的题目,通常需要维护两个变量,一个是窗口,一个是结果值。比如求平均值,需要另外维护一个sum字段来计算总和,计算异位词,需要另外维护一个队列,来判断是否是异位词。
| 序号 | 题目 | 完成 |
|---|---|---|
| 窗口大小固定 | 346. 数据流中的移动平均值 | ✅ |
| 剑指 Offer II 041. 滑动窗口的平均值 | ✅ | |
| 3. 无重复字符的最长子串 | ✅ | |
| 剑指 Offer 48. 最长不含重复字符的子字符串 | ✅ | |
| 438. 找到字符串中所有字母异位词 | ✅ | |
| 567. 字符串的排列 | ✅ | |
| 窗口大小不固定 | 76. 最小覆盖子串 | ✅ |
数据流中的移动平均值
解法
其实很简单的。但是一开始有一个思维误区,只用一个窗口计算平均值,没想到可以再用一个专门的字段来记录sum。
class MovingAverage {
LinkedList<Integer> window;
double total = 0d;
int size = 0;
public MovingAverage(int size) {
window = new LinkedList<>();
this.size = size;
}
public double next(int val) {
if (window.size() == size) {
total -= window.pollFirst();
}
window.addLast(val);
total += val;
return total / window.size();
}
}
无重复字符的最长子串
给定一个字符串,找出其中不含有重复字符的最长子串。
解法
- 用滑动窗口,右边加,左边减。
// 我自己写了一个:效率不是很高
// 执行耗时:6 ms,击败了42.52% 的Java用户
// 内存消耗:42.2 MB,击败了9.19% 的Java用户
class Solution {
public int lengthOfLongestSubstring(String s) {
LinkedList<Character> win = new LinkedList<>();
Set<Character> all = new HashSet<>();
int len = s.length();
char[] chars = s.toCharArray();
int max = 0;
for (int i = 0; i < len; i++) {
char cur = chars[i];
// 满足条件,可以一直往窗口里放
if (!all.contains(cur)) {
all.add(cur);
win.addLast(cur);
// 在窗口扩容的时候记录最大值
max = Math.max(max, win.size());
continue;
}
// 不满足条件,从窗口踢出元素
while (!win.isEmpty()) {
if (win.getFirst() != cur) {
all.remove(win.getFirst());
win.removeFirst();
} else {
all.remove(win.getFirst());
win.removeFirst();
break;
}
}
// 踢完了,把当前的元素加到队尾
win.addLast(cur);
all.add(cur);
}
return max;
}
}
// 参考了labuladong的题解,不过好像差距不大
// 执行耗时:6 ms,击败了34.72% 的Java用户
// 内存消耗:41.7 MB,击败了26.91% 的Java用户
class Solution {
public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> window = new HashMap<>();
int left = 0, right = 0;
int res = 0; // 记录结果
while (right < s.length()) {
char c = s.charAt(right);
right++;
// 进行窗口内数据的一系列更新
window.put(c, window.getOrDefault(c, 0) + 1);
// 判断左侧窗口是否要收缩
while (window.get(c) > 1) {
char d = s.charAt(left);
left++;
// 进行窗口内数据的一系列更新
window.put(d, window.get(d) - 1);
}
// 在这里更新答案
res = Math.max(res, right - left);
}
return res;
}
}
找到字符串中所有字母异位词
解法
这个题目,拿到手第一想法就是用一个窗口一直移动,然后每次都判断一下窗口里的字符是否是异位词,如果是就放入结果集。
如何判断异位词:用字符数组或者Map。
不过这种暴力解法效率不高。
class Solution {
public List<Integer> findAnagrams(String s, String p) {
int left = 0;
// 窗口的大小就是p的大小
int right = p.length() - 1;
List<Integer> result = new ArrayList<>();
while (right < s.length()) {
if (isAnagrams(s, left, right, p)) {
result.add(left);
}
left++;
right++;
}
return result;
}
public boolean isAnagrams(String s, int left, int right, String p) {
int[] chars = new int[26];
for (int i = left; i <= right; i++) {
chars[s.charAt(i) - 'a']++;
}
for (char pp : p.toCharArray()) {
if (chars[pp - 'a'] == 0) {
return false;
}
chars[pp - 'a']--;
}
for (int c : chars) {
if (c < 0) {
return false;
}
}
return true;
}
}
// 参考了labuladong的题解
class Solution {
public List<Integer> findAnagrams(String s, String t) {
int valid = 0;
int left = 0, right = 0;
Map<Character, Integer> need = new HashMap<>();
Map<Character, Integer> win = new HashMap<>();
for (char c : t.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
int len = s.length();
List<Integer> res = new ArrayList<>();
while (right < len) {
// 先不停的加,直到超出了t的长度
char cur = s.charAt(right);
right++;
// 加这一步的意义是,当字符没有出现在need中的时候,直接进行下一个窗口的检查
if (need.containsKey(cur)) {
win.put(cur, win.getOrDefault(cur, 0) + 1);
if (win.get(cur).equals(need.get(cur))) {
valid++;
}
}
// 踢出
while (right - left >= t.length()) {
// 这里需要注意,必须是need的size,因为t中可能有重复字符
if (valid == need.size()) {
res.add(left);
}
char out = s.charAt(left);
if (need.containsKey(out)) {
if (need.get(out).equals(win.get(out))) {
valid--;
}
// 从窗口中删除这个元素
win.put(out, win.get(out) - 1);
}
left++;
}
}
return res;
}
}
字符串的排列
和438基本一样。
class Solution {
public boolean checkInclusion(String s1, String s2) {
Map<Character, Integer> win = new HashMap<>();
Map<Character, Integer> need = new HashMap<>();
for (char c : s1.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
int left = 0, right = 0;
int valid = 0;
boolean rs = false;
while (right < s2.length() && !rs) {
char cur = s2.charAt(right);
right++;
if (need.containsKey(cur)) {
win.put(cur, win.getOrDefault(cur, 0) + 1);
if (win.get(cur).equals(need.get(cur))) {
valid++;
}
}
while (!rs && right - left >= s1.length()) {
// 找到一个匹配的就退出
if (valid == need.size()) {
rs = true;
break;
}
char out = s2.charAt(left);
left++;
if (need.containsKey(out)) {
if (need.get(out).equals(win.get(out))) {
valid--;
}
win.put(out, win.getOrDefault(out, 0) - 1);
}
}
}
return rs;
}
}
最小覆盖子串
解法
应该先找到把所有字段包含的窗口,然后尝试剔除元素,找到最小窗口。
复杂度
时间:O(n)
空间:O(n)
class Solution {
public String minWindow(String s, String t) {
if (s.length() < t.length()) {
return "";
}
int left = 0;
int right = 0;
int min = Integer.MAX_VALUE;
int minLeft = 0;
int minRight = 0;
Map<Character, Integer> need = new HashMap<>();
Map<Character, Integer> win = new HashMap<>();
int valid = 0;
for (char c : t.toCharArray()) {
need.put(c, need.getOrDefault(c, 0) + 1);
}
int len = s.length();
while (right < len) {
char cur = s.charAt(right);
right++;
if (need.containsKey(cur)) {
win.put(cur, win.getOrDefault(cur, 0) + 1);
if (win.get(cur).equals(need.get(cur))) {
valid++;
}
}
while (valid >= need.size()) {
if (right - left < min) {
min = right - left;
minLeft = left;
minRight = right;
}
char toDel = s.charAt(left);
if (need.containsKey(toDel)) {
if (win.get(toDel).equals(need.get(toDel))) {
valid--;
}
win.put(toDel, win.get(toDel) - 1);
}
left++;
}
}
return s.substring(minLeft, minRight);
}
}
双指针数组
如何原地删除数组里的元素?
- 把要删除的元素和最后一个元素交换。
- 用两个指针,第一个指针蹲结果的位置,第二个指针找结果,从后往前覆盖。
如何判断数组里的重复元素?
- 常规思路,可以用map
- 如果规定了范围,比如都是小写字母,可以直接创建一个char[26]数组
- 如果重复元素都是相邻的,可以用快慢指针,快指针找到不相同的的即可
| 序号 | 题目 | 完成 |
|---|---|---|
| 26. 删除有序数组中的重复项 | ✅ | |
| 27. 移除元素 | ✅ | |
| 283. 移动零 | ✅ | |
| 344. 反转字符串 | ||
| 5. 最长回文子串 | ||
| 167. 两数之和 II - 输入有序数组 | ||
| 剑指 Offer 57. 和为s的两个数字 | ||
| 剑指 Offer II 006. 排序数组中两个数字之和 | ||
| 392. 判断子序列 |
删除有序数组中的重复项
要求
- 没有排过序
- 原地删除
- 删除后保持原顺序
class Solution {
int removeDuplicates(int[] nums) {
if (nums.length == 0) {
return 0;
}
int slow = 0, fast = 0;
while (fast < nums.length) {
if (nums[fast] != nums[slow]) {
slow++;
// 维护 nums[0..slow] 无重复
nums[slow] = nums[fast];
}
fast++;
}
// 数组长度为索引 + 1
return slow + 1;
}
}
移除元素
//leetcode submit region begin(Prohibit modification and deletion)
class Solution {
public int removeElement(int[] nums, int val) {
int left = 0;
int right = 0;
int n = nums.length;
while (right < n) {
if (nums[right] != val) {
nums[left++] = nums[right];
}
right++;
}
return left;
}
}
//leetcode submit region end(Prohibit modification and deletion)
移动零
class Solution {
public void moveZeroes(int[] nums) {
int left = 0;
int right = 0;
int n = nums.length;
while (right < n) {
if (nums[right] != 0) {
nums[left++] = nums[right];
}
right++;
}
for (int i = left; i < n; i++) {
nums[i] = 0;
}
}
}
以下参考:nSum问题
1. 两数之和 II - 输入有序数组
2. 剑指 Offer 57. 和为s的两个数字
3. 剑指 Offer II 006. 排序数组中两个数字之和
字符串处理
反转字符串
很简单,不用写了。
class Solution {
public void reverseString(char[] s) {
int left = 0;
int right = s.length - 1;
while (left < right) {
char tmp = s[left];
s[left] = s[right];
s[right] = tmp;
left++;
right--;
}
}
}
最长回文子串
2024.2.21 终于自己写出来了。
解法
提到回文,首先想到的是找到字符串的中心,然后从中间往两边扩散。但是最长回文不一定包含中轴,可能出现在任何位置。
顺着这个思路继续思考,那么应该以每一个元素为轴,分别做扩散。
如何扩散呢?
应该有三种方式,假设当前轴的索引为i
(1)i作为中轴,...i-1, i, i+1...
(2)i作为右边,...i-1, i...
(3)i作为左边,...i, i+1...
针对这三种情况,分别计算,就可以得出结果了。
有没有优化空间?
看到这种多次的计算,自然会想到能否优化。这边确实可以优化。因为我们遍历字符串是从左往右走,i作为右边的情况,其实和i-1作为左边的是重复计算的。
举个例子就清楚了:
i=3,i作为右边,这时候去算(2,3)
i=2,i作为左边,这时候去算的是(2,3)
复杂度
时间复杂度:O(n)
空间复杂度:O(1)
class Solution {
int max = 0;
int maxLeft = 0;
int maxRight = 0;
int len;
public String longestPalindrome(String s) {
len = s.length();
for (int i = 0; i < len; i++) {
findMax(i - 1, i + 1, s);
findMax(i, i + 1, s);
}
return s.substring(maxLeft, maxRight + 1);
}
public void findMax(int left, int right, String s) {
while (left >= 0 && right < len && s.charAt(left) == s.charAt(right)) {
if (right - left > max) {
maxRight = right;
maxLeft = left;
max = maxRight - maxLeft;
}
left--;
right++;
}
}
}
判断子序列
s和t,判断s是否为t的子序列,比较简单。
class Solution {
public boolean isSubsequence(String s, String t) {
int i = 0, j = 0;
while (i < s.length() && j < t.length()) {
if (s.charAt(i) == t.charAt(j)) {
i++;
}
j++;
}
return i == s.length();
}
}
双指针链表
| 序号 | 题目 | 完成 |
|---|---|---|
| 141. 环形链表 | ||
| 142. 环形链表 II | ||
| 160. 相交链表 | ||
| 19. 删除链表的倒数第 N 个结点 | ||
| 83. 删除排序链表中的重复元素 | ||
| 21. 合并两个有序链表 | ||
| 23. 合并K个升序链表 | ||
| 86. 分隔链表 | ||
| 328. 奇偶链表 | ||
| 876. 链表的中间结点 |
环形链表
判断一个链表中是否有环,这个题目非常经典,也很简单。
public class Solution {
public boolean hasCycle(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
// 相遇的时候,node应该直接判断相等,不应该用val比较
if (fast == slow) {
return true;
}
}
return false;
}
}
环形链表 II
找到环的入口,用公式推导出 x = (n - 1) * (a + b) + b
说明在相遇之后,重新从头节点和相遇节点出发,这两个节点肯定会在入口的地方相遇。
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) {
break;
}
}
// 如果fast走到最后一个节点都没发现环
if (fast == null || fast.next == null) {
return null;
}
ListNode nxt = head;
// 找到环的入口,因为肯定有环,所以这里没有考虑NPE
while (nxt != fast) {
fast = fast.next;
nxt = nxt.next;
}
return nxt;
}
}
相交链表
相交链表的解法是:A再去遍历B,B遍历完再去遍历A,这两次遍历的长度是相等的,肯定会在相交点相遇。
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode x1 = headA;
ListNode x2 = headB;
while (x1 != x2) {
x1 = x1 == null ? headB : x1.next;
x2 = x2 == null ? headA : x2.next;
}
// 如果没有相交此时x1和x2都是null
return x1;
}
}
链表的中间结点
快慢指针,fast的速度是slow的两倍。
- 如果是奇数长度的链表:fast到最后一个的时候,slow正好走到中间位置,返回slow
- 如果是偶数长度的链表:fast走到最后一个的后一个(相当于给长度+1),因为题目条件是返回中间位置的后一个,此时slow正好走到后一个的位置。
class Solution {
public ListNode middleNode(ListNode head) {
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
}
删除链表的倒数第 N 个结点
用快慢指针,快指针先走N步,然后快慢指针一起走,最后的目的是让慢指针停留在要删除的节点前一个位置。
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
// 只有一个节点,需要删除自身
if (head.next == null) {
return null;
}
ListNode slow = head;
ListNode fast = head;
ListNode result = head;
ListNode pre = null;
int k = n;
while (k > 0 && fast != null) {
fast = fast.next;
k--;
}
// 说明是需要删除头节点
if (fast == null) {
return head.next;
}
while (fast != null) {
pre = slow;
fast = fast.next;
slow = slow.next;
}
// slow就是倒数第N个节点,pre是前面一个节点
if (slow != null) {
pre.next = slow.next;
}
return head;
}
}
删除排序链表中的重复元素
其实还是比较简单的,不过看了官方题解写的更好些。
class Solution {
public ListNode deleteDuplicates(ListNode head) {
// 队列为空
if (head == null) {
return head;
}
ListNode left = head;
ListNode right = head;
while (right != null) {
if (right.val != left.val) {
left.next = right;
left = left.next;
}
right = right.next;
}
// 最后一串元素都相等的时候,left的next无法设置
left.next = null;
return head;
}
}
class Solution {
public ListNode deleteDuplicates(ListNode head) {
// 队列为空
if (head == null) {
return head;
}
ListNode cur = head;
while (cur.next != null) {
if (cur.val == cur.next.val) {
cur.next = cur.next.next;
} else {
cur = cur.next;
}
}
return head;
}
}
合并两个有序链表
这个题目每刷一遍都会遇到,每次都能写出来,刻在脑子里了。
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode pre = new ListNode(-1);
ListNode ans = pre;
while (list1 != null || list2 != null) {
// 注意给的node的val的范围
int v1 = list1 == null ? 101 : list1.val;
int v2 = list2 == null ? 101 : list2.val;
if (v1 < v2) {
pre.next = new ListNode(v1);
list1 = list1.next;
} else {
pre.next = new ListNode(v2);
list2 = list2.next;
}
pre = pre.next;
}
if (list1 != null) {
pre.next = list1;
} else {
pre.next = list2;
}
return ans.next;
}
}
合并K个升序链表
22的升级版,用一个优先队列实现比较就可以。当然如果刷过TopK问题,也可以手写比较器,将优先队列替换掉。
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
if (lists == null || lists.length == 0) {
return null;
}
ListNode pre = new ListNode(-1);
ListNode ans = pre;
PriorityQueue<ListNode> queue = new PriorityQueue<>((o1, o2) -> {
return o1.val - o2.val;
});
for (ListNode l : lists) {
if (l != null) {
//先把所有链表的头结点加入队列
queue.offer(l);
l = l.next;
}
}
// 所有节点都需要遍历到
while (!queue.isEmpty()) {
ListNode cur = queue.poll();
pre.next = new ListNode(cur.val);
pre = pre.next;
cur = cur.next;
// 这个链表的头结点被选中了,需要从这个链表下一个节点补位上来
if (cur != null) {
queue.offer(cur);
}
}
return ans.next;
}
}
分隔链表
合并链表的对立问题,要把链表拆开。
class Solution {
public ListNode partition(ListNode head, int x) {
ListNode dummy1 = new ListNode(-1);
ListNode dummy2 = new ListNode(-1);
ListNode ans = dummy1;
ListNode head2 = dummy2;
ListNode p = head;
while (p != null) {
if (p.val < x) {
dummy1.next = p;
dummy1 = dummy1.next;
} else {
dummy2.next = p;
dummy2 = dummy2.next;
}
// 把当前节点从链表里断开,避免循环依赖的问题
ListNode tmp = p.next;
p.next = null;
p = tmp;
}
// 将两个链表连起来
dummy1.next = head2.next;
return ans.next;
}
}
奇偶链表
和分隔链表的区别无非就是判断条件不一样。
class Solution {
public ListNode oddEvenList(ListNode head) {
ListNode dummy1 = new ListNode(-1);
ListNode dummy2 = new ListNode(-1);
ListNode ans = dummy1;
ListNode head2 = dummy2;
ListNode p = head;
int step = 1;
while (p != null) {
if (step % 2 != 0) {
// 奇数
dummy1.next = p;
dummy1 = dummy1.next;
} else {
// 偶数
dummy2.next = p;
dummy2 = dummy2.next;
}
// 把当前节点从链表里断开,避免循环依赖的问题
ListNode tmp = p.next;
p.next = null;
p = tmp;
step++;
}
// 将两个链表连起来
dummy1.next = head2.next;
return ans.next;
}
}