栈及其变体

单调栈

如果要频繁对栈中元素取最值,可考虑在原来的栈之外再开一个单调栈(栈中元素单调,栈顶元素一定是最值)。单调栈的操作与普通栈同步。假设需要频繁取栈中的最小值,则每次有新元素入原来的栈时,让该元素与单调栈的栈顶比较,若小于单调栈的栈顶,则新元素同时入单调栈。原有栈的栈顶元素出栈时,若该元素与单调栈栈顶元素相等,则两栈同时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,(0i<n),在0−i0 - i0i中找到第一个大于aia_iai的数ata_tat,返回i−ti-tit,若找不到对应的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];
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值