文章目录
动态规划之正则表达 -> 总结了leetcode的刷题套路
面试题 17.22. 单词转换 -> 还未通过
6. Z 字形变换 -> 找规律
top k 问题
面试题 17.14. 最小K个数 -> 优先队列,构建规模为k的大顶堆
最大的K个数 -> 构建规模为k的小顶堆
LRU
146. LRU缓存机制
双向链表存放数据(key和value),哈希表通过key映射到双向链表上的节点。
实现get()和put()函数。
注:下面代码,中最近最少使用的节点为双向链表的尾部。
struct DLinkedNode{
DLinkedNode* pre, *next;
int key, val;
DLinkedNode(int _key, int _val): key(_key), val(_val), pre(nullptr), next(nullptr){
}
};
class LRUCache {
public:
LRUCache(int capacity) {
head = new DLinkedNode(0,0);
tail = new DLinkedNode(0,0);
head -> next = tail;
tail -> pre = head;
cap = capacity;
}
int get(int key) {
if(mp.find(key)==mp.end()){
return -1;
}
DLinkedNode* p = mp[key];
deleteNode(p);
insertInHead(p);
return p->val;
}
void put(int key, int value) {
if(mp.find(key)!=mp.end()){
DLinkedNode* p = mp[key];
p->val = value;
deleteNode(p);
insertInHead(p);
return;
}
if(mp.size() == cap){
DLinkedNode* tmp = tail->pre;
mp.erase(tail->pre->key); // map删除元素
deleteNode(tail->pre);
// delete tail->pre; //deleteNode后tail->pre的指向节点已经改变
delete tmp;
}
DLinkedNode* p = new DLinkedNode(key, value);
insertInHead(p);
mp[key] = p;
}
private:
void deleteNode(DLinkedNode* p){
DLinkedNode* pre = p->pre;
DLinkedNode* next = p->next;
pre->next = next;
next->pre = pre;
// delete p; // 节点P后续还需要被插入,因此此处内存不需要释放
}
void insertInHead(DLinkedNode* p){
p->next = head->next;
head->next->pre = p;
head->next = p;
p->pre = head;
}
// 辅助的头节点和尾节点
DLinkedNode* head;
DLinkedNode* tail;
unordered_map<int, DLinkedNode*> mp;
int cap;
};
剑指 Offer 42. 连续子数组的最大和 -> 贪心算法
int maxSubArray(vector<int>& nums) {
if(nums.size()==0){
cout << "error!" << endl;
return -1;
}
// 贪心算法
int sum = nums[0], re = nums[0];
for(int i = 1; i < nums.size(); i++){
// 如果s连续子数组的和小于0了,则从当前重新开始计算连续子数组的和
if(sum < 0){
sum = nums[i];
}else{
sum += nums[i];
}
re = max(sum,re);
}
return re;
上面的题目扩展到二维数组:面试题 17.24. 最大子矩阵
动态规划
// dp[i]表示前i个元素中以nums[i]作为最大元素的最长上升子序列长度
int lengthOfLIS(vector<int>& nums) {
if(nums.empty()) return 0;
int n = nums.size();
vector<int> dp(n+1,1); // 初始化均为1
for(int i = 2; i <= n; i++ ){
for(int k = 1; k < i; k++){
if(nums[i-1] > nums[k-1]){
dp[i] = max(dp[i],dp[k]+1);
}
}
}
int m = 0;
for(int num:dp){
m = max(m,num);
}
return m;
}
string longestPalindrome(string s) {
int n = s.size();
vector<vector<bool>> dp(n,vector<bool>(n,false));
int m = 1, start = 0;
// 长度为1和2的情况
for(int i = 0; i < n; i++){
dp[i][i] = true;
if(i+1 < n){
dp[i][i+1] = s[i]==s[i+1];
if(2 > m && dp[i][i+1]){
start = i;
m = 2;
}
}
}
for(int len=3; len <= n ; len++){
for(int j = 0; j+len-1 < n; j++){
dp[j][j+len-1] = dp[j+1][j+len-2] && s[j]==s[j+len-1];
if(len > m && dp[j][j+len-1]){
start = j;
m = len;
}
}
}
return s.substr(start,m);
}
二叉树
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode *father;
* TreeNode(int x) : val(x), left(NULL), right(NULL), father(NULL) {}
* };
*/
class Solution {
public:
TreeNode* inorderSuccessor(TreeNode* p) {
// 分两种情况:第一种是该节点有右子树,则右子树的最左边的节点为中序遍历下一个节点
if(p->right!=nullptr){
TreeNode* q = p->right;
while(q->left!=nullptr){
q = q->left;
}
return q;
}
// 第二中情况是该节点没有右子树,则递归寻找父节点,
// 若节点为父节点的左孩子,则此父节点为中序遍历的下一个节点
// 如果节点的父节点为空,则说明节点p为中序遍历输出的最后一个节点
TreeNode* q = p;
while(q->father!=nullptr && q->father->right==q){
q = q->father;
}
return q->father;
}
};
根据条件构建二叉树
105. 从前序与中序遍历序列构造二叉树
106. 从中序与后序遍历序列构造二叉树
面试题 04.02. 最小高度树 -> 高度最小,取已排序的数组中间的数为根节点
二叉树后续遍历
根据"左右根"的遍历方式,可以利用左右子树返回的结果,自底向上计算。
124. 二叉树中的最大路径和
二叉树的最近公共祖先
求一个二叉树中两个结点的最大距离
思路:将所有的结点的左右子树的高度和计算一下,然后取出最大值,就是最远的距离。
中序遍历二叉搜索树
解析文章:中序遍历团灭系列二叉搜索树问题
530. 二叉搜索树的最小绝对差 -> 中序遍历二叉搜索树获得的就是升序数组。
二分查找
基于排除法的二分查找,查找位置的左边界和右边界:
67. 数字在排序数组中出现的次数
// 当只剩[1,1]两个元素,mid = l + (r-l)/2 = l = 0,所以必须 l = mid + 1 才能避免死循环
// mid向下取整,取的是重复元素位置的左边界,l = mid + 1
int lowBound(vector<int>& nums, int k){
int l = 0, r = nums.size()-1;
while(l < r){
int mid = l + (r-l)/2;
if(nums[mid] < k){
l = mid + 1;
}else{
r = mid;
}
}
return nums[l]==k ? l : -1;
}
int upBound(vector<int>& nums, int k){
int l = 0, r = nums.size()-1;
while(l < r){
int mid = l + (r-l+1)/2;
if(nums[mid] > k){
r = mid - 1;
}else{
l = mid;
}
}
return nums[l]==k ? l : -1;
}
int getNumberOfK(vector<int>& nums , int k) {
if(nums.empty()){
return 0;
}
int l = lowBound(nums,k);
int r = upBound(nums,k);
return l==-1 ? 0 : r-l+1;
}
题目:给出一个字符串,每个字母都连续出现两次,且这两次是相邻的,只有一个字母出现了一次。返回出现一次的那个字母。 -> 二分查找(奔着目标元素的二分查找,循环条件为l<=r,然后找到目标元素就直接返回,出了循环表示找不到)
例子: aax, xaabb, aaxbb 返回都是x
char findChar(string s){
int l = 0, r = s.size()-1;
while(l <= r){
int mid = l + (r-l+1)/2;
// 与两边都不相等,前面两种情况分别是出现一次的字母在头部和尾部的情况
if( (mid-1<0 && s[mid]!=s[mid+1]) || (mid+1 == s.size() && s[mid]!=s[mid-1]) || (s[mid]!=s[mid-1] && s[mid]!=s[mid+1])){
return s[mid];
}else if(s[mid]==s[mid+1]){
// 与右边相等
if((mid+1-1)%2==0){
//判断左边0~mid-1子串长度是否为偶数
l = mid;
}else{
r = mid-1;
}
}else{
// 与左边相等
if((mid+1)%2==0){
//判断左边0~mid子串长度是否为偶数
l = mid+1;
}else{
r = mid;
}
}
}
return '0'; // 找不到返回字符‘0’
}
int main(){
string s = "aax";
cout << findChar(s) << endl;
}
面试题 10.03. 搜索旋转数组 -> 两次二分查找过不了特殊例子(先找到最小元素的下标,然后确定目标元素所在区间,再进行第二次二分查找);一次二分查找理解起来有点麻烦,详情见提交的代码。
面试题 10.05. 稀疏数组搜索-> 遇到空字符串退化为线性查找,注意用基于排除法的二分查找时需要避免出现死循环的情况!并且在出了循环的时候还要判断一次nums[l]是否等于目标元素!(当不考虑元素重复时,奔着目标元素的二分查找即可,即循环条件为l<=r,然后找到目标元素就直接返回,出了循环表示找不到)
int findString(vector<string>& words, string s) {
int l = 0, r = words.size()-1;
while(l < r){
int mid = l + (r-l+1)/2;
// 当words[mid]为空字符串的时候,退化为线性搜索
// 注意边界是l和r,e而不是0和 words.size()-1!!
while(mid<=r && words[mid]==""){
mid++;
}
if(mid>r){
// 说明[l,r]右半部分都为空字符串
mid = l + (r-l+1)/2; // mid需要重新计算
r = mid - 1;
continue;
}
if(words[mid] <= s){
l = mid;
}else if(words[mid] > s){
// 由于碰到空字符串mid会向右移动,可能会出现mid=r,
// 因此r=mid会出现死循环情况,所以必须r=mid-1,此时mid向上取整,即mid = l + (r-l+1)/2
r = mid-1;
}
}
return words[l]==s?l:-1;
}
2020.06.15 -> 手写了冒泡排序,选择排序,插入排序,堆排序,快速排序,归并排序(堆排序还是不太熟练,快速排序遇到点小错误)
2020.06.27 -> 手写了堆排序、快速排序,
哈希
1. 两数之和 -> 暴力求解O(n^2);排序O(nlogn)+双指针O(n);哈希O(n),元素值哈希映射对应的下标
抽屉原理+原地哈希
题目:数组大小为n,数组中元素范围为[1,n],元素有重复,找出缺失(重复)的元素。
如果允许O(n)的空间复杂度,则可以用额外的哈希表记录元素出现的次数。
若只允许O(1)空间复杂度。
思路:在原来的数组中进行哈希。
哈希函数为:f(nums[i]) = nums[i] - 1
448. 找到所有数组中消失的数字
442. 数组中重复的数据
vector<int> findDisappearedNumbers(vector<int>& nums) {
int n = nums.size();
for(int i = 0; i < n; i++){
// 需要把nums[i]放到下标为nums[i]-1处,
// 如果下标nums[i]-1处元素的值已经为nums[i],则不用交换
while(nums[i]!=nums[nums[i]-1]){
swap(nums[i],nums[nums[i]-1]);
}
}
vector<int> v;
// 如果所有元素只出现1次,那么交换后最终满足nums[i]=i+1
for(int i = 0; i < n; i++){
if(nums[i]!=i+1){
v.push_back(i+1); // 没有出现过的数字
// v.push_back(nums[i]); // 出现过两次的数字
}
}
return v;
}
41. 缺失的第一个正数 -> 与[448. 找到所有数组中消失的数字]类似
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
for(int i = 0; i < n; i++){
while(nums[i]>=1 && nums[i]<=n && nums[i]!=nums[nums[i]-1]){
swap(nums[i],nums[nums[i]-1]);
}
}
int re = n+1; // 如果数组置换后,为元素1~n,则缺失的是n+1
for(int i = 0; i < n; i++){
if(nums[i]!=i+1){
re = i+1;
break;
}
}
return re;
}
int missingNumber(vector<int>& nums) {
int n = nums.size();
for(int i = 0; i < n; i++){
while(nums[i] != n && nums[i]!=nums[nums[i]]){
swap(nums[i],nums[nums[i]]);
}
}
int re = n;
for(int i = 0; i < n; i++){
// if(nums[i]!=i){
// re = i;
// break;
// }
if(nums[i]==n){
// 数字n会到缺失那个数为下标的位置中
re = i;
break;
}
}
return re;
}
由于是连续数字中只缺失一个数字,因此有更好的办法,可以不改变传入的数组。
int missingNumber(vector<int>& nums) {
int n = nums.size();
int sum = (0+n)*(n+1)/2;
for(int num:nums){
sum-=num;
}
return sum;
}
双指针
15. 三数之和 -> 排序,固定一个数,剩下两个数用双指针,O(n^2)。(如何去除重复是难点)
18. 四数之和 -> 排序,固定两个数,剩下两个数用双指针,O(n^3)。(如何去除重复是难点)
链表
23. 合并K个排序链表 -> 优先队列O(knlog(k));分治合并O(knlog(k));两两合并O(k^2*n)
// 仿函数:类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了。
struct cmp{
bool operator() (ListNode* l1, ListNode* l2){
return l1->val > l2->val; // 小顶堆
}
};
class Solution {
public:
// lists存放k个指向链表的头节点的指针
// 维护堆O(logk),k个链表最多k*n个节点,所以时间复杂度O(k*n*logk)
ListNode* mergeKLists(vector<ListNode*>& lists) {
priority_queue<ListNode*,vector<ListNode*>,cmp> q;
for(auto head:lists){
if(head == nullptr){
//空链表不加入优先队列
continue;
}
q.push(head);
}
ListNode* head = new ListNode(0); // 辅助节点
ListNode* p = head;
while(!q.empty()){
ListNode* t = q.top();
q.pop();
p->next = t;
p = p->next;
if(t->next != nullptr){
q.push(t->next);
}
}
return head->next;
}
};
19. 删除链表的倒数第N个节点 -> 辅助节点+双指针
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* fast = head, *slow = head;
ListNode* pre = new ListNode(0); // 辅助节点,在slow的前一步
pre->next = head;
head = pre;
while(n > 0){
fast = fast->next;
n--;
}
// fast 比 slow 快 n步
while(fast!=nullptr){
fast = fast->next;
pre = pre->next;
slow = slow->next;
}
// 删除倒数第n个节点
pre -> next = slow -> next;
return head->next;
}
206. 反转链表 -> 双指针pre和now,pre指向now的前一个节点,初始为空指针,now->next = pre。
// 双指针版本
ListNode* reverseList(ListNode* head) {
if(head==nullptr){
// 空链表情况
return head;
}
ListNode* pre = nullptr;
ListNode* now = head;
ListNode* tmp = now->next;
while(tmp!=nullptr){
now->next = pre;
pre = now;
now = tmp;
tmp = tmp->next;
}
// 出循环时tmp为为空指针的时候,now还需要指向前一个节点一次!
now->next = pre;
return now;
}
// 递归版本
ListNode* reverseList(ListNode* head) {
if(head==nullptr || head->next == nullptr){
// head==nullptr是空链表的情况
return head;
}
ListNode* t = reverseList(head->next);
head->next->next = head;
head->next = nullptr;
return t;
}
695. 岛屿的最大面积 -> dfs
int maxAreaOfIsland(vector<vector<int>>& grid) {
int r = grid.size();
if(r == 0){
return 0;
}
int c = grid[0].size(), re = 0;
vector<vector<int>> g = grid; // 为了不修改传入的矩阵
for(int i = 0; i < r; i++){
for(int j = 0; j < c; j++){
if(g[i][j]==1){
re = max(dfs(g,i,j),re);
}
}
}
return re;
}
int dfs(vector<vector<int>>& g, int x, int y){
// 边界条件判断
if(x < 0 || x >= g.size() || y < 0 || y >=g[0].size() || g[x][y]==0){
return 0;
}else{
g[x][y]=0;
}
int down = dfs(g,x+1,y);
int up = dfs(g,x-1,y);
int right = dfs(g,x,y+1);
int left = dfs(g,x,y-1);
return up+down+right+left+1;
}
- 动态规划,O(n^2),当n很大的时候只能使用贪心法。
// 假设 n 不小于 2 且不大于 58。
int integerBreak(int n) {
vector<int> dp(n+1,0);
// 初始化条件(除了2和3,其余拆分成两个正整数后的积要大于等于i)
for(int i = 1; i <= n; i++){
dp[i] = i;
}
// 题目要求至少拆分两段,只有n等于2或3的时候拆分后积小于它本身
if(n==2 || n==3){
return n-1;
}
for(int i = 1; i <= n; i++){
for(int j = 1; j < i ; j++){
dp[i] = max(dp[i],dp[j]*dp[i-j]);
}
}
return dp[n];
}
- 贪心法,最后的结果一定是由若干个3和1或2个2组成,先拆成若干个3,最后拆成1个或者两个2。(理论证明略)
int cuttingRope(int n) {
long long re = 1;
// 由于至少拆成两段,故n<=3时算特殊情况
if(n==2 || n == 3){
return n-1;
}
while(n > 4){
re = (re*3)%1000000007;
n-=3;
}
re = (re*n)%1000000007;
return re;
}
面试题 08.04. 幂集 -> 输出集合的所有不重复子集合,前提是集合中不包含重复的元素。dfs+回溯,每个集合元素都有包含或者不包含两种可能,因此求二叉树节点的路径,二叉树的高度等于集合的元素个数;另一种方法是,以空集开始,遍历集合中的每一个元素,每次在集合中的每一个子集末尾加添加新元素,并将添加新元素后的所有子集加入到原来的集合中。(如果是计算子集合的个数,dp[i] = dp[i-1] +dp[i-1],dp[0]=1,包括空集)
940. 不同的子序列 II -> 集合中包含重复元素,求非空子序列的个数。动态规划:新加入元素与集合元素不同时: d p [ i ] = d p [ i − 1 ] + d p [ i − 1 ] + 1 , d p [ 0 ] = 0 , dp[i] = dp[i-1] +dp[i-1] + 1,dp[0]=0, dp[i]=dp[i−1]+dp[i−1]+1,dp[0]=0,不包括空集;新加入元素与集合中元素相同时, d p [ i ] = d p [ i − 1 ] + d p [ i − 1 ] − d p [ j − 1 ] dp[i] = dp[i-1] +dp[i-1] - dp[j-1] dp[i]=dp[i−1]+dp[i−1]−dp[j−1],j为集合中与新加入元素相同的元素的最大位置从1开始)。 解析
115. 不同的子序列 -> 动态规划,还未做
注意:下面相减小于0时需要特殊处理,即dp[i]+=mod;
int distinctSubseqII(string S) {
vector<int> dp(S.size()+1,0); // 求非空子集个数,dp[0]=0
vector<int> hash(128,-1); // 存储字符最近一次出现的下标
int mod = pow(10,9)+7;
for(int i = 1; i <= S.size(); i++){
if(hash[S[i-1]] == -1){
dp[i] = (dp[i-1] + dp[i-1] + 1)%mod;
}else{
int p = hash[S[i-1]]+1; // 与新加入字符相同字符的位置(1开始)
dp[i] = (dp[i-1] + dp[i-1] - dp[p-1])%mod; // 此处dp[i]可能会小于0
if(dp[i] < 0){
dp[i]+=mod;
}
}
hash[S[i-1]] = i-1; // 更新字符的位置,使其始终为最近一次出现的下标
}
return dp[S.size()];
}
下面三题有通用的解法,即n循环除以2或3或4,最后判断n是否为1。(n小于等于0肯定不是,首先排除)
231. 2的幂
// 解法1:位运算
bool isPowerOfTwo(int n) {
if(n <= 0){
//0和负数不可能是2的幂次方
return false;
}
int count = 0;
// 思路:2的幂,二进制中除了符号位,其余位有且仅有有一个位为1
// 对于int类型,最后一位是符号位,故循环退出条件为 i < 31
for(int i = 0; i < 31 && count <= 1; i++){
if(((n>>i) & 1 ) == 1){
count++;
}
}
return count==1;
}
// 解法2:数学原理
bool isPowerOfTwo(int n) {
// 当n大于0,(n & (n-1)) == 0 是n为2的幂次方的充要条件
return n > 0 && (n & (n-1)) == 0; //注意运算优先级== > & > &&
}
326. 3的幂 -> 通用解法
bool isPowerOfThree(int n) {
if(n <= 0){
// n可能为负数
return false;
}
if(n == 1){
// 0次幂
return true;
}
while(n > 1){
if(n%3==0){
n/=3;
}else{
return false;
}
}
return true;
}
bool isPowerOfFour(int num) {
// 第二个条件是判断是否为2的幂次,第三个条件是在第二个条件的基础上判断是否为4的幂次
return num > 0 && (num&(num-1))==0 && num%3 ==1;
}
// 解法1:递归,状态转移方程f[n]= (f[n-1]+m)%n,f[n]表示有n个数时,最后剩下的那个数的下标
int lastRemaining(int n, int m) {
return f(n,m);
}
int f(int n, int m){
if(n==1){
// 当只剩一个数,最后剩下那个数下标必为0
return 0;
}
return (f(n-1,m)+m)%n;
}
// 解法2:倒推,f[n]= (f[n-1]+m)%n
int lastRemaining(int n, int m) {
// 取模的时候会出现0,因此计算的时候假设位置从0开始(如果数字从1开始,最后pos需要加1)
int pos = 0;
for(int i = 2; i <= n; i++){
pos = (pos + m)%i;
}
return pos;
}
面试题 16.16. 部分排序 -> 左右两次遍历,从左向右遍历确定右边界n,从右向左遍历确定左区间m,返回[m,n]
// 默认array为升序
vector<int> subSort(vector<int>& array) {
int len = array.size();
int m = -1, n = -1;
// 索引区间[m,n]右边的数应该满足大于等于其左侧最大的值
int MAX = INT_MIN;
for(int i = 0; i < len; i++){
if(array[i] >= MAX){
MAX = max(array[i], MAX);
}else{
// 不满足array[i] >= MAX条件说明在区间[m,n]内,最后一个不满足的元素下标为n
n = i;
}
}
// 索引区间[m,n]左边的数应该满足小于等于其右侧最小的值
int MIN = INT_MAX;
for(int i = len-1; i >= 0; i--){
if(array[i] <= MIN){
MIN = min(array[i], MIN);
}else{
m = i;
}
}
return vector<int>{
m,n};
}
dfs+回溯(枚举所有的可能,或者剪枝)
模式匹配
C语言strstr()函数:返回字符串中首次出现子串的地址。
28. 实现 strStr()
方法一:
// sunday算法,最坏O(n*m),平均O(n)
int strStr(string haystack, string needle) {
int m = needle.size();
// 空字符串返回0
if(m == 0){
return 0;
}
// 记录当前字符下标(有重复字符,取下标大的值)
unordered_map<char,int> table;
for(int i = 0; i <= m; i++){
table[needle[i]] = i;
}
for(int i = 0; i + m <= haystack.size();){
// needle与haystack中[i,i+m-1]子串进行比较
if(needle == haystack.substr(i,m)){
return i;
}
if(needle.find(haystack[i+m]) != string::npos){
// needle中找到haystack中第i+m个字符,进行偏移
i += m - table[haystack[i+m]];
}else{
// needle中找不到haystack第i+m个字符,needle头部直接跳到i+m+1位置进行比较
i += m + 1;
}
}
return -1;
}
方法二:字符串匹配的KMP算法 -> O(m+n)
10. 正则表达式匹配
动态规划:12ms
// dp[i][j]表示s的前i个字符和p的前j个字符能否匹配上
bool isMatch(string s, string p) {
vector<vector<bool>> dp(s.size()+1,vector<bool>(p.size()+1,false));
dp[0][0] = true; // 初始化状态,其余为false
for(int i = 0; i <= s.size(); i++){
for(int j = 1; j <= p.size(); j++){
// 注意:s和p的下标是从0开始的
if(p[j-1] != '*'){
dp[i][j] = i>0 && dp[i-1][j-1] && (s[i-1]==p[j-1] || p[j-1]=='.');
}else{
if(j>=2){
if(i==0){
// 对于空字符串,只能不匹配
dp[0][j] = dp[0][j-2];
}else{
// dp[i][j-2]表示不匹配的情况,( (s[i-1]==p[j-2] || p[j-2]=='.') && dp[i-1][j])表示匹配的情况
dp[i][j] = dp[i][j-2] || ( (s[i-1]==p[j-2] || p[j-2]=='.') && dp[i-1][j]);
}
}
}
}
}
return dp[s.size()][p.size()];
}
递归:780ms
bool isMatch(string s, string p) {
if(p.empty()){
return s.empty();
}
bool match = false;
if(!s.empty() && (p[0]==s[0] || p[0]=='.')){
match = true;
}
if(p.size() > 1 && p[1]=='*'){
if(match){
// 当第一个字符匹配上,可以匹配多次(递归中一次一次匹配),也可以不匹配
return isMatch(s.substr(1),p) || isMatch(s,p.substr(2));
}else{
// 当第一个字符匹配不上,只能不匹配
return isMatch(s,p.substr(2));
}
}else{
return match && isMatch(s.substr(1),p.substr(1));
}
}
单调队列 -> 滑动窗口
求最大值:单调非严格递减队列(栈);
求最小值:单调非严格递增队列(栈);
239. 滑动窗口最大值:
「单调队列」的核心思路和「单调栈」类似。单调队列的 push 方法依然在队尾添加元素,但是要把前面比新元素小的元素都删掉。
需要关注两点:
- 为什么保证队列中的元素非严格递减?非严格递增可不可以?
!q.empty() && nums[i-1]==q.front()
为什么可以判断队列头部元素可以出列?
举例:[1,2,3,8,5,5,7,3],滑动窗口大小为3。
那么初始时候队列应该为[3],因为1和2早于3出滑动窗口,3又大于1和2,因此在1和2出滑动窗口时,滑动窗口内最大元素仍是3。这就是为什么要把前面比新元素小的元素都删掉的原因。
队列中元素的变化:
[8]
[8,5]
[8,5,5]
[7] -> 8出窗口,7进入,5,5从队列中删除。
[7,3]
根据条件!q.empty() && nums[i-1]==q.front()
能安全地pop_front是因为队列是非严格递减,窗口中的最大值有重复时保留重复。
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
deque<int> q;
vector<int> mval;
if(k > nums.size()){
return mval;
}
// 单调队列,保证队列中的元素非严格递减
for(int i = 0; i < k; i++){
while(!q.empty() && nums[i] > q.back()){
q.pop_back();
}
q.push_back(nums[i]);
}
mval.push_back(q.front());
for(int i = 1; i <= nums.size()-k; i++){
if(!q.empty() && nums[i-1]==q.front()){
q.pop_front();
}
while(!q.empty() && nums[i+k-1] > q.back()){
q.pop_back();
}
q.push_back(nums[i+k-1]);
mval.push_back(q.front());
}
return mval;
}
单调栈
题目描述:有一个字符串s,仅有数字组成(第一位数字不为0),现在将字符串的删除k个字符,使得剩下的字符组成的数字最小,不能打乱字符的顺序。
/*
71245323308
4
1223308
1221
2
11
324682385
3
242385
*/
// 思路:数字最小,必定是一个非严格递增的序列,可以使用单调栈
// 从字符串的左边开始,保证放入栈的数字非严格递增(后一个数大于等于前一个数),
// 栈顶大于将要放入的数字,则将栈顶弹出,直到栈顶弹出k次
int main() {
string s;
int k;
cin >> s;
cin >> k;
stack<char> st; // 单调栈
st.push(s[0]);
int count = 0;
for(int i = 1; i < s.size(); i++){
if(count < k){
while(!st.empty() && s[i] < st.top()){
st.pop();
count++;
if(count == k){
break;
}
}
}
st.push(s[i]);
}
// 字符串全部数字入栈后弹出次数小于k,则继续弹出栈顶,直到弹出k次
while(count < k){
st.pop();
count++;
}
string out;
while(!st.empty()){
out += st.top();
st.pop();
}
reverse(out.begin(),out.end());
cout << stoi(out) << endl; // stoi()是为了去掉头部的0
return 0;
}