子串(3) --hot 100 in leetcode
和为K的子数组
算法概述
原题
题目要求为给定数组中有多少个子数组的加和为k。使用前缀和和哈希表,实际上也是把问题从“串”的维度降为“数”的维度,从“和为k的子数组”变为了两数之和,但是中间加了一层抽象,可以参考一下两数之和 。
- 时间复杂度为O(n):只遍历一次
- 空间复杂度为O(n):需要另外创建哈希表,最坏情况需要n的空间大小
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Solution { public int subarraySum(int[] nums, int k) { int count=0,pre=0; HashMap<Integer,Integer> mp=new HashMap<>(); mp.put(0,1); for(int i=0;i<nums.length;i++){ pre+=nums[i]; if(mp.containsKey(pre-k)){ count+=mp.get(pre-k); } mp.put(pre,mp.getOrDefault(pre,0)+1); } return count; } }
|
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Solution { public: int subarraySum(vector<int>& nums, int k) { unordered_map<int, int> mp; mp[0] = 1; int count = 0, pre = 0; for (auto& x:nums) { pre += x; if (mp.find(pre - k) != mp.end()) { count += mp[pre - k]; } mp[pre]++; } return count; } };
|
重要实例方法及属性(C++)
主要是用了[]
索引和 for-each 遍历更加方便,主要还是要记住之前记过的find()
找不到返回end()
。
注意
这道题需要考虑如何把子串转换成能够计算的单位,这里用到的是“前缀和”的思想,通过前缀和将不同长度的符合条件的子串变为了字典类型,而键作为匹配条件,值用于计数,要善于思考将复杂问题“放缩”。
滑动窗口最大值
算法概述
原题
这道题目要求在实现基本的滑动窗口的基础上找到滑动窗口内的最大值。在求最大值这类窗口内部的某个特殊的值,而非整个窗口的整体性质时,要考虑的就是窗口移动时的特性,即 每次移动 都共用了 k-1个元素 ,只有 一个元素 在变化,由此找到优化的方法,也就是把在滑动窗口整体中寻找最大值降为 窗口首尾 的变化。
使用双向队列来解决,通过只存储一系列单调排列的值,在滑动窗口移动的时候,只会读取新值和抛弃可能超出边界的队首,关键点在于不仅 队列内部的值是单调排列的,而且索引也是从小到大 ,这就是为什么用两个while,而不是if。
- 时间复杂度为O(n):遍历一次
- 空间复杂度为O(k):滑动窗口长度
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| class Solution { public int[] maxSlidingWindow(int[] nums, int k) { int n=nums.length; Deque<Integer> deque=new LinkedList<Integer>(); for(int i=0;i<k;++i){ while(!deque.isEmpty()&&nums[i]>=nums[deque.peekLast()]){ deque.pollLast(); } deque.offerLast(i); } int[] ans=new int[n-k+1]; ans[0]=nums[deque.peekFirst()]; for(int i=k;i<n;++i){ while(!deque.isEmpty()&&nums[i]>=nums[deque.peekLast()]){ deque.pollLast(); } deque.offerLast(i); while(deque.peekFirst()<=i-k){ deque.pollFirst(); } ans[i-k+1]=nums[deque.peekFirst()]; } return ans;
} }
|
重要实例方法及实现(JAVA)
Deque<Integer> deque=new LinkedList<Integer>()
:双向队列(double-ended queue)接口及实现实例化
deque.peekLast()
:查看队尾元素
deque.peekFirst()
:查看队首元素
deque.pollFirst()
:移除并返回队首元素
deque.pollLast()
:移除并返回队尾元素
deque.offerLast()
:添加元素入队尾
deque.isEmpty()
:检查队列是否为空
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| class Solution { public: vector<int> maxSlidingWindow(vector<int>& nums, int k) { int n = nums.size(); deque<int> q; for (int i = 0; i < k; ++i) { while (!q.empty() && nums[i] >= nums[q.back()]) { q.pop_back(); } q.push_back(i); } vector<int> ans = {nums[q.front()]}; for (int i = k; i < n; ++i) { while (!q.empty() && nums[i] >= nums[q.back()]) { q.pop_back(); } q.push_back(i); while (q.front() <= i - k) { q.pop_front(); } ans.push_back(nums[q.front()]); } return ans; } };
|
重要实例方法及属性(C++)
deque.pop_back()
:删除队尾元素(deque特有)
deque.pop_front()
:删除队首元素(deque特有)
deque.empty()
:检查是否为空
deque.front()
:返回队首元素引用
deque.back()
:返回队尾元素引用
deque.at()
:(带边界检查)索引
[]
:(不带边界检查)索引
注意
这里的基础结构还是滑动窗口,但其实只表现为 i 和 i+k 这两个索引罢了。真正的内核就是在滑动窗口之上用单调队列来管理一些真正有用的属性,一定要学会像这样分离然后分层的思想。除此之外,还有通过 预处理+分块 的方法能够更便捷的解决这个问题。
最小覆盖子串
算法概述
原题
这道题要求在目标字符串中找到包含给定字符串所有字符的最小子字符串。还是用滑动窗口实现,难点与之前不同,之前需要构造新的数据结构来辅助与答案的交互,也就是说需要搭建抽象的桥梁,但这个的难点主要在实现上,因为滑动窗口的大小是动态的,如何创建行之有效的辅助函数帮助滑动窗口移动是关键点(实际上这里又使用了中间数组)。这里给出的JAVA代码是确定了测试集中不含英文字母外字符,且选择 双指针放在同一循环条件内,左指针跟着右指针移动 。
- 时间复杂度为O(n+m+∑):两个字符串遍历加上动态窗口移动
- 空间复杂度为O(n+m) (C++):要存字符出现频次
JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| class Solution { public String minWindow(String s, String t) { char[] chs = s.toCharArray(); char[] cht = t.toCharArray(); int[] pats = new int[52]; int[] patt = new int[52]; int tot = 0; for (char ct : cht) { if (++patt[index(ct)]==1) { tot++; } } String res = ""; for (int i=0,l=0;i<s.length();++i) { int ids = index(chs[i]); if(++pats[ids]==patt[ids]){ --tot; } while (l<i) { int li = index(chs[l]); if (pats[li]>patt[li]) { pats[li]--; ++l; continue; } break; } if(tot==0 &&(res == "" || (i-l+1)<res.length())){ res = s.substring(l,i+1); } } return res; }
private int index(char c){ return c>='a'?c-'a':c-'A'+26; } }
|
重要实例方法及属性(JAVA)
String.toCharArray()
:字符串转为字符数组
return c>='a'?c-'a':c-'A'+26;
:小写字母直接映射到0-25,大写字母映射到26-51
String.substring(begin,end)
:两个参数应该为起始的索引
C++
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| class Solution { public: string minWindow(string s, string t) { vector<int> pats(56, 0); vector<int> patt(56, 0); int tot = 0; for (char ct : t) { if (++patt[index(ct)] == 1) { tot++; } } string res{""}; for (int i = 0, l = 0; i < s.size(); ++i) { int ids = index(s[i]); if (++pats[ids] == patt[ids]) { --tot; } while (l < i) { int li = index(s[l]); if (pats[li] > patt[li]) { pats[li]--; l++; continue; } break; } if (tot == 0 && (res.empty() || (i - l + 1) < res.size())) { res = s.substr(l, i-l + 1); } } return res; } private: int index(char c) { return c >= 'a' ? c - 'a' : c - 'A' + 26; } };
|
重要实例方法及属性(C++)
string.substr(pos,len)
:取起始位置和截取长度为参数,返回子字符串
注意
这其中也包含了一个相对定式的结构,也就是以滑动窗口为单位来看,在求某个特殊的滑动窗口时,将滑动窗口的整体特征提取压缩,此题中压缩到了tot一个值(e.g., 之前的differ),然后只通过这个一个值检查是否匹配。