单调栈
如果要频繁对栈中元素取最值,可考虑在原来的栈之外再开一个单调栈(栈中元素单调,栈顶元素一定是最值)。单调栈的操作与普通栈同步。假设需要频繁取栈中的最小值,则每次有新元素入原来的栈时,让该元素与单调栈的栈顶比较,若小于单调栈的栈顶,则新元素同时入单调栈。原有栈的栈顶元素出栈时,若该元素与单调栈栈顶元素相等,则两栈同时pop。
LeetCode 155
题目链接:https://leetcode.com/problems/min-stack/
设计一个栈,可分别在O(1)O(1)O(1)时间内完成入栈、出栈、获得栈顶元素以及获得栈中最小值的操作。
typedef struct {
int s[30000], min[30000];
int n, m;
} MinStack;
MinStack* minStackCreate() {
MinStack* obj = (MinStack*)malloc(sizeof(MinStack));
obj->n = obj->m = 0;
return obj;
}
void minStackPush(MinStack* obj, int val) {
obj->s[obj->n++] = val;
if (!obj->m || val <= obj->min[obj->m-1])
obj->min[obj->m++] = val;
}
void minStackPop(MinStack* obj) {
if (!obj->n)
return;
obj->n--;
if (obj->s[obj->n] == obj->min[obj->m-1])
obj->m--;
}
int minStackTop(MinStack* obj) {
return obj->s[obj->n-1];
}
int minStackGetMin(MinStack* obj) {
return obj->min[obj->m-1];
}
void minStackFree(MinStack* obj) {
free(obj);
}
LeetCode 334
题目链接:https://leetcode.com/problems/increasing-triplet-subsequence/
给出一个数组,问数组中是否存在长度为3的严格递增的子序列。
只开一个单调栈就行了,当单调栈中元素数量达到3时返回True。另外,为了防止某些较小值干扰单调栈(例如:数组4 5 1 6,单调栈会被1打断),同时用变量last记录单调栈中第二项曾出现过的最小值,并在遍历数组时持续更新。若遍历时遇到大于last的数组元素则直接返回True。
bool increasingTriplet(int* a, int n){
int s[3], top = 1, num = 1, last = a[0];
s[0] = a[0];
for (int i=1; i<n; i++) {
if (a[i] > last && num == 2)
return 1;
if (a[i] == s[top-1])
continue;
while (top>0 && a[i]<=s[top-1])
top--;
s[top++] = a[i];
if (top>num) {
num = top;
last = a[i];
}
else if (top==num && a[i] < last)
last = a[i];
if (top == 3)
return 1;
}
return 0;
}
LeetCode 901
题目链接:https://leetcode.com/problems/online-stock-span/
简单来说,就是给出一个数组,对于数组中的每一个元素ai,(0≤i<n)a_i, (0 \leq i < n)ai,(0≤i<n),在0−i0 - i0−i中找到第一个大于aia_iai的数ata_tat,返回i−ti-ti−t,若找不到对应的ata_tat,返回1。
举个例子,输入数组[100,80,60,70,60,75,85],输出[1,1,1,2,1,4,6]。
使用类似单调栈的数据结构,若当前元素不小于栈顶元素,则栈顶元素出栈,直到当前元素小于栈顶元素,然后将当前元素入栈,如此维护下,从栈底至栈顶单调递减,若栈顶元素大于当前元素,则栈顶元素必为目标元素。
typedef struct {
int n, top;
int *a, *f;
} StockSpanner;
StockSpanner* stockSpannerCreate() {
StockSpanner* obj = (StockSpanner*)malloc(sizeof(StockSpanner));
obj->n = 0; //记录当前元素的坐标
obj->top = -1; //记录当前栈的深度
obj->a = (int*)malloc(sizeof(int)*10005); //存储入栈元素的数值
obj->f = (int*)malloc(sizeof(int)*10005); //存储入栈元素的坐标
return obj;
}
int stockSpannerNext(StockSpanner* obj, int price) {
while (obj->top>=0 && obj->a[obj->top] <= price)
obj->top--;
obj->a[++obj->top] = price;
obj->f[obj->top] = obj->n++;
return obj->top ? obj->f[obj->top] - obj->f[obj->top-1] : obj->n;
}
void stockSpannerFree(StockSpanner* obj) {
free(obj->a);
free(obj->f);
free(obj);
}
利用栈实现简易计算器
思路一:栈
输入数学表达式,输出结果。其中符号支持加减乘除及括号,数字支持整数,允许存在前导负号。
开两个数组,一个数组为符号栈,一个数组为数字栈。字符串按顺序逐个读入token(数字或符号),如果是数字就截取下来存入数字栈,符号就截下来存入符号栈,若发现当前符号优先级小于等于前一个符号的优先级,就要把前面那部分的值计算出来,例如3*2+2,乘号的优先级高于加号,所以先算3*2再算6+2。
如果遇到左括号直接存入符号栈,遇到了右括号就把右括号前的所有表达式的值计算出来,直到遇见左括号。
LeetCode 224
链接: https://leetcode.com/problems/basic-calculator/
int GetPriority(char c){
if (c == '+' || c == '-')
return 0;
if (c == '*' || c == '/')
return 1;
return 2;
}
bool Calc(int* num, int n, char* oper, int operNum) {
int n1 = num[n-1];
int n2 = num[n];
char expr = oper[operNum];
if (expr == '+') num[n-1] = n1 + n2;
if (expr == '-') num[n-1] = n1 - n2;
if (expr == '*') num[n-1] = n1 * n2;
if (expr == '/') num[n-1] = n1 / n2;
return 1;
}
int calculate(char * s){
char oper[200000];
int num[200000];
int p = 0, operNum = 0, n = 0;
bool lastIsNum = 0; //记录字符串中上一个token是否为数字
while (p < strlen(s)){
if (s[p] >= '0' && s[p] <= '9') { //获取数字
num[n] = 0;
while (s[p] >= '0' && s[p] <= '9')
num[n] = num[n]*10 - '0' + s[p++];
n++;
lastIsNum = 1;
}
else {
if (s[p] <'0' && s[p] >= '*') { //获取加减乘除符号
//若负号前一个token是左括号,或者负号前无任何token,说明这是前导负号,需要在负号前补个0
if (s[p]=='-' && !lastIsNum && (operNum==0 || oper[operNum-1] == '('))
num[n++] = 0;
lastIsNum = 0;
while (operNum && oper[operNum-1] != '(' && GetPriority(s[p]) <= GetPriority(oper[operNum-1]))
Calc(num, --n, oper, --operNum);
//每次运算都会令符号栈和数字栈容量减1
oper[operNum++] = s[p];
}
if (s[p] == '(') {
oper[operNum++] = s[p];
lastIsNum = 0;
}
if (s[p] == ')') {
while (operNum && oper[operNum-1] != '(')
lastIsNum = Calc(num, --n, oper, --operNum);
//括号内的token被运算完,只会剩一个数字结果,因此lastIsNum要置为1
operNum--; //删掉左括号
}
p++;
}
}
while (operNum)
Calc(num, --n, oper, --operNum);
return num[0];
}
思路二:后缀表达式
对于表达式1+2×(4-3)-1,我们可以将其以二叉树的形式进行表达,如下图:
叶节点为数字,非叶节点为操作符,优先级越低的操作符深度越低,同优先级后执行的操作会放在更靠近根的地方。(例如1+2-3,-后执行,为根节点)
通过上图可知,1+2×4-3-1其实是该二叉树的中序遍历,其实就是我们所习惯的表达式去掉括号了而已。我们所习惯的这种带括号的表达式称为中缀表达式。虽然中序遍历没有括号,但括号内的内容是会被连续访问的。如上图,4-3作为优先级最高的操作,处于树的最深处,遍历时是会作为一个整体进行遍历的,不会访问到其他节点。
若对这样的二叉树进行先序遍历及后序遍历,可分别得到前缀表达式与后缀表达式(又称逆波兰表达式):-+12-431以及1243-+1-。这两类就没有括号。不同于人类,计算机对后缀或前缀表达式的逻辑会有更为便捷的处理,以后缀表达式为例:从左至右扫描后缀表达式,遇到数字直接进栈,遇到操作符,将操作符前的两个数字出栈,并运算,结果入栈,直到栈空,以上图举个实际的例子:
- 首先,1、2、4、3依次入栈。
- 其次,读到-,弹出栈中最外的两个数字4和3,与-运算得到1,1入栈,此时栈中为1、2、1。
- 再次,读到*,弹出栈最外的两个数2和1,运算得到2并压入栈,此时栈中为1、2。
- 复次,读到+,弹出1和2,运算后压入3,此时栈中只有3一个数字。
- 最后,遇到1,进栈,再读到-,执行3-1得到2入栈,结束,最后结果为2。
若要将中缀表达式转换为树,可通过对表达式进行检索,找到优先级最低且后执行的一个操作符,然后将该操作符左侧表达式作为左子树,右侧表达式作为右子树,并对两子树递归进行上述处理,从而构建出表达式树,然后后缀遍历即可。
若要将中缀表达式直接转换为后缀表达式,思路其实和本文开头程序思路差不多,这里就不再赘述。
LeetCode 150
链接: https://leetcode.com/problems/evaluate-reverse-polish-notation/
题目内容与上述类似,给出后缀表达式s,计算出表达式的结果。
int evalRPN(char ** s, int n){
int num[5000];
int size = 0;
for (int i=0; i<n; i++)
if ((s[i][0]>='0' && s[i][0]<='9') || strlen(s[i])>1)
sscanf(s[i], "%d", &num[size++]);
else {
int x = num[size-2];
int y = num[size-1];
if (s[i][0] == '+')
x = x+y;
if (s[i][0] == '-')
x = x-y;
if (s[i][0] == '*')
x = x*y;
if (s[i][0] == '/')
x = x/y;
size--;
num[size-1] = x;
}
return num[0];
}