首页 > 其他分享 >栈和队列labuladong

栈和队列labuladong

时间:2024-02-15 10:44:05浏览次数:27  
标签:队列 元素 括号 int 数组 need labuladong stack

平衡括号串(⼀)

先来个简单的,⼒扣第 921 题「使括号有效的最少添加」:

给你输⼊⼀个字符串 s,你可以在其中的任意位置插⼊左括号 ( 或者右括号 ),请问你最少需要⼏次插⼊才
能使得 s 变成⼀个有效的括号串?

⽐如说输⼊ s = "())(",算法应该返回 2,因为我们⾄少需要插⼊两次把 s 变成 "(())()",这样每个左
括号都有⼀个右括号匹配,s 是⼀个有效的括号串。
这其实和前⽂的判断括号有效性⾮常类似,我们直接看代码:

#include <stdio.h>

int minAddToMakeValid(char* s) {
int res = 0;  // 记录插入次数
int need = 0;  // 右括号的需求量

// 遍历字符串中的每个字符
for (int i = 0; s[i] != '\0'; i++) {
if (s[i] == '(') {
need++;  // 对右括号的需求 + 1
}
if (s[i] == ')') {
need--;  // 对右括号的需求 - 1
if (need == -1) {  // 当需求量为-1时,表示需要插入一个左括号
need = 0;
res++;  // 需要插入一个左括号,插入次数加1
}
}
}

return res + need;  // 返回插入次数加上剩余的右括号需求量
}

int main() {
char inputString[] = "((()))";  // 输入要处理的字符串
int result = minAddToMakeValid(inputString);
printf("Minimum number of additions to make the string valid: %d\n", result);
return 0;
}

这段代码就是最终解法,核⼼思路是以左括号为基准,通过维护对右括号的需求数 need,来计算最⼩的插⼊
次数。需要注意两个地⽅:

1、当 need == -1 的时候意味着什么?
因为只有遇到右括号 ) 的时候才会 need--,need == -1 意味着右括号太多了,所以需要插⼊左括号。
⽐如说 s = "))" 这种情况,需要插⼊ 2 个左括号,使得 s 变成 "()()",才是⼀个有效括号串。

2、算法为什么返回 res + need?
因为 res 记录的左括号的插⼊次数,need 记录了右括号的需求,当 for 循环结束后,若 need 不为 0,那么
就意味着右括号还不够,需要插⼊。
⽐如说 s = "))(" 这种情况,插⼊ 2 个左括号之后,还要再插⼊ 1 个右括号,使得 s 变成 "()()()",才
是⼀个有效括号串。
以上就是这道题的思路,接下来我们看⼀道进阶题⽬,如果左右括号不是 1:1匹配,会出现什么问题呢?

平衡字符串(二)

这是⼒扣第 1541 题「平衡括号字符串的最少插⼊次数」:
现在假设 1 个左括号需要匹配 2 个右括号才叫做有效的括号组合,那么给你输⼊⼀个括号串 s,请问你如何
计算使得 s 有效的最⼩插⼊次数呢?

核⼼思路还是和刚才⼀样,通过⼀个 need 变量记录对右括号的需求数,根据 need 的变化来判断是否需要
插⼊。

第⼀步,我们按照刚才的思路正确维护 need 变量:

int minInsertions(string s) {
 // need 记录需右括号的需求量
 int res = 0, need = 0;
 
 for (int i = 0; i < s.size(); i++) {
 // ⼀个左括号对应两个右括号
 if (s[i] == '(') {
 need += 2;
 }
 
 if (s[i] == ')') {
 need--;
 }
 }
 
 return res + need;
}

现在想⼀想,当 need 为什么值的时候,我们可以确定需要进⾏插⼊?

⾸先,类似第⼀题,当 need == -1 时,意味着我们遇到⼀个多余的右括号,显然需要插⼊⼀个左括号。

⽐如说当 s = ")",我们肯定需要插⼊⼀个左括号让 s = "()",但是由于⼀个左括号需要两个右括号,所
以对右括号的需求量变为 1:

if (s[i] == ')') {
 need--;
 // 说明右括号太多了
 if (need == -1) {
 // 需要插⼊⼀个左括号
 res++;
 // 同时,对右括号的需求变为 1
 need = 1;
 }
}

另外,当遇到左括号时,若对右括号的需求量为奇数,需要插⼊ 1 个右括号。因为⼀个左括号需要两个右括
号嘛,右括号的需求必须是偶数,这⼀点也是本题的难点。

所以遇到左括号时要做如下判断:

if (s[i] == '(') {
 need += 2;
 if (need % 2 == 1) {
 // 插⼊⼀个右括号
 res++;
 // 对右括号的需求减⼀
 need--;
 }
}

综上,我们可以写出正确的代码:

int minInsertions(char* s) {
int res = 0, need = 0;
int len = strlen(s);
for (int i = 0; i < len; i++) {
if (s[i] == '(') {
need += 2;
if (need % 2 == 1) {
res++;
need--;
}
}
if (s[i] == ')') {
need--;
if (need == -1) {
res++;
need = 1;
}
}
}
return res + need;
}

单调栈结构解决三道算法题

栈(stack)是很简单的一种数据结构,先进后出的逻辑顺序,符合某些问题的特点,比如说函数调用栈。单调栈实际上就是栈,只是利用了一些巧妙的逻辑,使得每次新元素入栈后,栈内的元素都保持有序(单调递增或单调递减)。

听起来有点像堆(heap)?不是的,单调栈用途不太广泛,只处理一类典型的问题,比如「下一个更大元素」,「上一个更小元素」等。本文用讲解单调队列的算法模版解决「下一个更大元素」相关问题,并且探讨处理「循环数组」的策略。至于其他的变体和经典例题,我会在 数据结构精品课 中讲解。

单调栈模板

现在给你出这么一道题:输入一个数组 nums,请你返回一个等长的结果数组,结果数组中对应索引存储着下一个更大元素,如果没有更大的元素,就存 -1。

比如说,输入一个数组 nums = [2,1,2,4,3],你返回数组 [4,2,4,-1,-1]。因为第一个 2 后面比 2 大的数是 4; 1 后面比 1 大的数是 2;第二个 2 后面比 2 大的数是 4; 4 后面没有比 4 大的数,填 -1;3 后面没有比 3 大的数,填 -1。

这道题的暴力解法很好想到,就是对每个元素后面都进行扫描,找到第一个更大的元素就行了。但是暴力解法的时间复杂度是 O(n^2)。

这个问题可以这样抽象思考:把数组的元素想象成并列站立的人,元素大小想象成人的身高。这些人面对你站成一列,如何求元素「2」的下一个更大元素呢?很简单,如果能够看到元素「2」,那么他后面可见的第一个人就是「2」的下一个更大元素,因为比「2」小的元素身高不够,都被「2」挡住了,第一个露出来的就是答案。

#include <stdio.h>
#include <stdlib.h>

// 定义栈的结构体
struct Stack {
int top;// 栈顶指针
unsigned capacity;  // 栈的容量
int* array; // 存放元素的数组
};

// 创建一个具有指定容量的栈
struct Stack* createStack(unsigned capacity) {
struct Stack* stack = (struct Stack*)malloc(sizeof(struct Stack));
stack->capacity = capacity;
stack->top = -1;  // 初始化栈顶指针为-1,表示空栈
stack->array = (int*)malloc(stack->capacity * sizeof(int));  // 为栈数组分配内存
return stack;
}

// 判断栈是否为空
int isStackEmpty(struct Stack* stack) {
return stack->top == -1;  // 栈顶指针为-1表示空栈
}

// 判断栈是否已满
int isStackFull(struct Stack* stack) {
return stack->top == stack->capacity - 1;  // 栈顶指针等于容量减一表示栈已满
}

// 元素入栈
void push(struct Stack* stack, int item) {
if (isStackFull(stack)) {
return;  // 栈满时无法入栈
}
stack->array[++stack->top] = item;  // 栈顶指针加一,将元素放入栈顶
}

// 元素出栈
int pop(struct Stack* stack) {
if (isStackEmpty(stack)) {
return -1;  // 空栈无法出栈,返回-1表示失败
}
return stack->array[stack->top--];  // 返回栈顶元素并将栈顶指针减一
}

// 寻找数组中每个元素的下一个更大元素,并返回结果数组
int* nextGreaterElement(int* nums, int numsSize, int* returnSize) {
int* res = (int*)malloc(numsSize * sizeof(int));  // 分配存放结果的数组内存
struct Stack* stack = createStack(numsSize);  // 创建一个栈
for (int i = numsSize - 1; i >= 0; i--) {
while (!isStackEmpty(stack) && stack->array[stack->top] <= nums[i]) {
pop(stack);  // 弹出栈中较小的元素,直到栈为空或者栈顶元素大于当前元素
}
res[i] = isStackEmpty(stack) ? -1 : stack->array[stack->top];  // 将下一个更大元素存入结果数组
push(stack, nums[i]);  // 当前元素入栈
}
*returnSize = numsSize;  // 设置返回结果的大小
free(stack->array);  // 释放栈数组的内存
free(stack);  // 释放栈的内存
return res;  // 返回结果数组指针
}

int main() {
int nums[] = { 1, 2, 3, 4}; // 示例输入数组,
int numsSize = sizeof(nums) / sizeof(nums[0]);
int returnSize;
int* result = nextGreaterElement(nums, numsSize, &returnSize);  // 调用函数得到结果数组
printf("Next Greater Elements: ");
for (int i = 0; i < returnSize; i++) {
printf("%d ", result[i]);  // 打印结果数组
}
free(result);  // 释放结果数组内存
return 0;
}

这就是单调队列解决问题的模板。for 循环要从后往前扫描元素,因为我们借助的是栈的结构,倒着入栈,其实是正着出栈。while 循环是把两个「个子高」元素之间的元素排除,因为他们的存在没有意义,前面挡着个「更高」的元素,所以他们不可能被作为后续进来的元素的下一个更大元素了。

这个算法的时间复杂度不是那么直观,如果你看到 for 循环嵌套 while 循环,可能认为这个算法的复杂度也是 O(n^2),但是实际上这个算法的复杂度只有 O(n)。

分析它的时间复杂度,要从整体来看:总共有 n 个元素,每个元素都被 push 入栈了一次,而最多会被 pop 一次,没有任何冗余操作。所以总的计算规模是和元素规模 n 成正比的,也就是 O(n) 的复杂度。

单调队列结构解决滑动窗口问题

cal笔记里有

⼀道数组去重的算法题把我整不会了

关于去重算法,应该没什么难度,往哈希集合里面塞不就行了么?

最多给你加点限制,问你怎么给有序数组原地去重,这个我们前文 双指针技巧秒杀七道数组题目 讲过。

本文讲的问题应该是去重相关算法中难度最大的了,把这个问题搞懂,就再也不用怕数组去重问题了。

给你一个字符串 s ,请你去除字符串中重复的字母,使得每个字母只出现一次。需保证 返回结果的字典序最小(要求不能打乱其他字符的相对位置)。

示例 1:

输入:s = "bcabc"
输出:"abc"
示例 2:

输入:s = "cbacdcbc"
输出:"acdb"
提示:

1 <= s.length <= 104

s 由小写英文字母组成

题目的要求总结出来有三点:

要求一、要去重。

要求二、去重字符串中的字符顺序不能打乱 s 中字符出现的相对顺序。

要求三、在所有符合上一条要求的去重字符串中,字典序最小的作为最终结果。

上述三条要求中,要求三可能有点难理解,举个例子。

比如说输入字符串 s = "babc",去重且符合相对位置的字符串有两个,分别是 "bac" 和 "abc",但是我们的算法得返回 "abc",因为它的字典序更小。

按理说,如果我们想要有序的结果,那就得对原字符串排序对吧,但是排序后就不能保证符合 s 中字符出现顺序了,这似乎是矛盾的。

其实这里会借鉴后文 单调栈解题框架 中讲到的「单调栈」的思路,没看过也无妨,等会你就明白了。

#include <stdio.h>
#include <stdbool.h>
#include <string.h>

char* removeDuplicateLetters(char* s) {
int freq[26] = {0}; // 记录每个字符出现的频率
int visited[26] = {false}; // 记录每个字符是否已经访问过
int len = strlen(s);
for (int i = 0; i < len; i++) {
freq[s[i] - 'a']++; // 统计每个字符出现的频率
}
char result[26]; // 存储结果的数组
int index = 0; // 结果数组的下标
for (int i = 0; i < len; i++) {
freq[s[i] - 'a']--; // 每遇到一个字符,将其频率减一
if (visited[s[i] - 'a']) {
continue; // 如果字符已经访问过,则跳过当前循环
}
while (index > 0 && s[i] < result[index - 1] && freq[result[index - 1] - 'a'] > 0) {
visited[result[index - 1] - 'a'] = false; // 将之前的字符标记为未访问过
index--; // 移除之前的字符
}
result[index++] = s[i]; // 将当前字符加入结果数组中
visited[s[i] - 'a'] = true; // 标记当前字符为已访问过
}
result[index] = '\0'; // 添加字符串结束的标记
char* res = strdup(result); // 将结果数组复制到动态分配的内存中
return res;
}

int main() {
char s[] = "cbacdcbc";
char* result = removeDuplicateLetters(s);
printf("Result: %s\n", result);
free(result);
return 0;
}

标签:队列,元素,括号,int,数组,need,labuladong,stack
From: https://www.cnblogs.com/lulixiu1999/p/18016017

相关文章

  • 彻底搞定栈和队列
    栈和队列都是C++中的一种线性数据结构,这一篇博客,我们就来学习学习关于这两个数据结构的知识栈什么是栈栈(Stack)是一种后进先出(LastInFirstOut)的线性数据结构,这种数据结构简称为LIFO表,由于其特殊性,这种数据结构经常被用于关于括号匹配以及字符串解码等问题,当然这种题目可以出......
  • P1886-单调队列【黄】
    一道普普通通的模版题,让我想起了此前做过的绿题P1725,于是运用相同的知识轻松切掉本题Code#include<iostream>#include<cstring>#include<string>#include<cstdio>#include<cstdlib>#include<algorithm>#include<stack>#include<queue>#include......
  • 力扣 递归 迭代 栈 广度 队列 之 226. 翻转二叉树
    给你一棵二叉树的根节点root,翻转这棵二叉树,并返回其根节点。 示例1:输入:root=[4,2,7,1,3,6,9]输出:[4,7,2,9,6,3,1]示例2:输入:root=[2,1,3]输出:[2,3,1]示例3:输入:root=[]输出:[]栈/** *Definitionforabinarytreenode. *publicclassTreeNode......
  • 单调队列
    单调队列239.滑动窗口最大值int*maxSlidingWindow(int*nums,intnumsSize,intk,int*returnSize){*returnSize=numsSize-k+1;int*res=(int*)malloc(sizeof(int)*(*returnSize));//双端队列,从大到小排,记录在nums中的下标intdequeue[1......
  • 【Java 并发】【队列应用】【二】Tomcat的NioEndPoint中ConcurrentLinkedQueue 的使用
    1 前言这一节我们讲解Tomcat的NioEndPoint中ConcurrentLinkedQueue的使用。2  Tomcat的容器结构本节讲解apache-tomcat-7.0.32-src源码中ConcurrentLinkedQueue的使用。首先介绍Tomcat的容器结构以及NioEndPoint的作用,以便后面能够更加平滑地切入话题,如图11-4所示......
  • 【Java 并发】【队列应用】【一】ArrayBlockingQueue 的使用-Logback异步日志打印
    1 前言看了那么多Java提供的队列工具,那么我们这节开始看看哪些地方用到了这些队列哈。这一节我们讲解logback异步日志打印中ArrayBlockingQueue的使用。2  异步日志打印模型概述在高并发、高流量并且响应时间要求比较小的系统中同步打印日志已经满足不了需求了,这是因为......
  • 栈和队列
    栈如同叠猫猫,而队列就像猫猫排队。两者分别代表着先入后出和先入先出的逻辑关系。「栈stack」是一种遵循先入后出的逻辑的线性数据结构。我们可以将栈类比为桌面上的一摞盘子,如果需要拿出底部的盘子,则需要先将上面的盘子依次取出。我们将盘子替换为各种类型的元素(如整数、字......
  • 详解golang实现一个带时效的环形队列
    1.需求mysql执行时间超过100ms以上打warn日志,但是一分钟以内这种warn日志超过10条就需要告警。所以需求就是获得一分钟以内mysql的warn的个数。2.分析为什么使用环形队列而不使用slice?因为队列长度固定,所以可以一开始就分配好空间,不用自动扩容,环形的目的就是不用改变数组的值,只用移......
  • [数据结构] 队列
    队列的基本概念队列(Queue),是一种操作受限的线性表,只允许在表的一端进行插入,而在表的另一端进行删除。向队列中插入元素称为入队,删除元素称为出队。其操作特性是先进先出队列的常见操作:函数名功能InitQueue(*Q)初始化队列,构造一个空队列QQueueEmpty(Q)判断队列空......
  • 单调队列优化DP&斜率优化&四边形不等式
    在本文中,我们将通过一道题来浅谈DP优化三大妈。P3195[HNOI2008]玩具装箱-洛谷|计算机科学教育新生态(luogu.com.cn)对于这种类型的题目,我们一般可以转化为如下形式:那么,$val(i,j)$又通常分为两种情况:其值仅与$i,j$中的一个有关。其值与$i,j$两者都有关。单调队列......