数据结构与算法5 -- 栈练习题

1,311 阅读14分钟

前言

上一篇文章中讲述了栈和队列的概念,并且也用代码演示了如何分别使用顺序存储和链式存储定义栈结构和队列结构。

对于队列而言,相信大家应该都不陌生,队列一般配合着多线程使用,在iOS开发中也经常能用到,这里就不再多说了。

本篇文章重点要写的是如何利用栈的思想来解决一些问题?

栈思想练习题

括号匹配

哈哈,提起这个题都感觉自己有点尴尬😅,因为这个题笔者之前面试的时候遇到过(没做出来,凉凉了😅)。现在再做肯定是没问题了,这就是学习吧。

题目很简单,就是让输入一串字符串,其中包括(){}[]这几种括号,判断这些括号是否是匹配的。
如:(()[]{}){[()]} 这种就是匹配的,打印出括号匹配。
如:([){]} 这种就是不匹配的,需要打印出括号不匹配。

题目分析:
这个题很明显就是要利用栈的思想来解(笔者当时面试的时候也想到了,但是奈何当时不会定义栈,尴了个尬的)。解题思想:

  1. 当输入了左括号{ [ (时,就把这些符号压栈(也叫入栈)。
  2. 当输入了右括号} ] )时,要和栈顶元素进行对比:
    • 如果是{} [] ()这样匹配的,就把栈顶元素出栈,然后继续;
    • 如果不匹配,如{], [}, [), {), (], (}, 栈空 ), ], } 这些,就说明了输入的括号是不匹配的,直接打印括号不匹配结束就可以了。
  3. 如果一直没出现问题,直到输入完成了,还需要再判断一次栈中是否还有元素(即栈是否为空)。
    • 如果栈为空,说明刚刚好,括号匹配。
    • 如果不为空,很抱歉,做括号多了,也是不匹配。

代码如下:

#import <Foundation/Foundation.h>

#define MaxCount 100

#define l1 '{'
#define r1 '}'
#define l2 '['
#define r2 ']'
#define l3 '('
#define r3 ')'

typedef struct _Stack{
    char s[MaxCount];
    int top;
} Stack;

// 创建一个栈
Stack createStack() {
    Stack st;
    st.top = -1;     // -1表示栈中没有东西
    for (int i = 0; i < MaxCount; i++) {
        st.s[i] = ' ';
    }
    return st;
}
// 入栈
void inStack(Stack *st, char c) {
    if (st->top < -1 || st->top >= MaxCount - 1) {
        printf("栈已满,无法入栈\n");
        return ;
    }
    st->top++;
    st->s[st->top] = c;
}
// 出栈
int outStack(Stack *st) {
    if (st->top < -1) {
        printf("栈中没有元素了,无法再出栈\n");
        return 0;
    }
    return st->s[st->top--];
}
// 遍历打印
void foreachStack(Stack st) {
    for (int i = 0; i < st.top + 1; i++) {
        printf("%c", st.s[i]);
    }
    printf("\n");
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Stack st = createStack();
        char c;
        scanf("%c", &c);
        while (c != '\n') {
            // 其他字符不算
            if (c == l1 || c == l2 || c == l3 || c == r1 || c == r2 || c == r3) {
                printf("%c", c);
                // 如果是左括号
                if (c == l1 || c == l2 || c == l3) {
                    inStack(&st, c);
                }
                else {
                    // 如果是右括号,判断栈顶元素是否和输入的括号匹配
                    if ((c == r1 && st.s[st.top] == l1) || (c == r2 && st.s[st.top] == l2) || (c == r3 && st.s[st.top] == l3)) {
                        outStack(&st);
                    }
                    else {
                        printf("括号不匹配---->%c\n", c);
                        return 0;
                    }
                }
            }
            // 输入下一个字符
            scanf("%c", &c);
        }
        if (st.top == -1) {
            printf("括号匹配\n");
        }
        else {
            printf("括号不匹配,剩余括号:");
            foreachStack(st);
        }
    }
    return 0;
}

进制转换

这种问题其实还是数学问题,想想在数学上如何进行进制转换?
对,就是使用短除法

下面就直接粘代码了,不会原理可以看看上面那个链接。

#import <Foundation/Foundation.h>

// 定义可扩展栈(纯属一时兴起)
typedef struct _Stack {
    int *data;      // 数据
    int top;        // 栈顶的index
    int size;       // 大小
} Stack;

Stack initStack(void) {
    Stack s;
    s.data = malloc(sizeof(int) * 20);
    s.top  = -1;
    s.size = 20;
    return s;
}
void extensionStack(Stack *s) {
    s->data = realloc(s->data, sizeof(int) * (s->size + 20));
    if (s->data) {
        s->size += 20;
    }
    else {
        NSLog(@"扩展空间失败");
    }
}
void curtailStack(Stack *s) {
    s->data = realloc(s->data, sizeof(int) * (s->size) - 20);
    if (s->data) {
        s->size -= 20;
    }
    else {
        NSLog(@"缩减空间失败");
    }
}
void checkStackSize(Stack *s) {
    if (s->top > s->size - 5) {
        extensionStack(s);
    }
    if (s->top < s->size - 25) {
        curtailStack(s);
    }
}
void inStack(Stack *s, int inData) {
    checkStackSize(s);
    s->data[s->top + 1] = inData;
    s->top++;
}
void outStack(Stack *s, int *outData) {
    checkStackSize(s);
    *outData = s->data[s->top];
    s->top--;
}

// 逻辑区
// 十进制转十进制以下其他进制
void decConversionOther(int data, int targetSystem) {
    Stack s = initStack();
    while (data > 0) {
        inStack(&s, data % targetSystem);
        data = data / targetSystem;
    }
    int i = s.top;
    printf("%d进制:", targetSystem);
    while (i >= 0) {
        printf("%d", s.data[i]);
        i--;
    }
    printf("\n");
}
// 十进制转16进制
void decConversionHex(int data) {
    Stack s = initStack();
    while (data > 0) {
        inStack(&s, data % 16);
        data = data / 16;
    }
    int i = s.top;
    printf("16进制:");
    while (i >= 0) {
        if (s.data[i] < 10) {
            printf("%d", s.data[i]);
        }
        else {
            switch (s.data[i]) {
                case 10:
                    printf("A");
                    break;
                case 11:
                    printf("B");
                    break;
                case 12:
                    printf("C");
                    break;
                case 13:
                    printf("D");
                    break;
                case 14:
                    printf("E");
                    break;
                case 15:
                    printf("F");
                    break;
                    
                default:
                    break;
            }
        }
        i--;
    }
    printf("\n");
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        decConversionOther(7, 2);
        decConversionOther(24, 2);
        
        decConversionHex(0xF2CD24);
    }
    return 0;
}

字符串编码

编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 重复 k 次。注意 k 保证为正整数。你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。

例如:
s = "3[a]2[bc]", 返回"aaabcbc"。
s = "3[a2[c]]", 返回 "accaccacc"。
s = "2[abc]3[cd]ef", 返回 "abcabccdcdcdef"。

题目分析:
对于这样的题目其实和括号匹配是类似的,只不过它更麻烦的地方在于,需要找到[ ]中间的字符以及[前面的数字(注意:如上面举例中的2,[前面不一定只有数字,还有可能有字母)。

解题思路:

  1. 遍历格式字符串,不是]的全部压入栈s中。
  2. 遇到]时,就循环出栈栈s中的元素并判断栈是否为空以及出栈元素是否是数字?
  3. 出栈的过程中在遇到[之前,出栈的都是要拷贝n份的字符串;遇到[之后出栈的所有数字字符,就是要拷贝的次数n。
  4. 找出这两个之后,将字符串拷贝n份,再压入到栈s中,继续往后循环步骤2。

代码如下:

#import <Foundation/Foundation.h>
#import <stdlib.h>

// 定义可扩展栈
typedef struct _Stack {
    char *data;     // 数据
    int top;        // 栈顶的index
    int size;       // 大小
} Stack;

Stack initStack(void) {
    Stack s;
    s.data = malloc(sizeof(char) * 20);
    s.top  = -1;
    s.size = 20;
    return s;
}
void extensionStack(Stack *s) {
    s->data = realloc(s->data, sizeof(char) * (s->size + 20));
    if (s->data) {
        s->size += 20;
    }
    else {
        NSLog(@"扩展空间失败");
    }
}
void curtailStack(Stack *s) {
    s->data = realloc(s->data, sizeof(char) * (s->size) - 20);
    if (s->data) {
        s->size -= 20;
    }
    else {
        NSLog(@"缩减空间失败");
    }
}
void checkStackSize(Stack *s) {
    if (s->top > s->size - 5) {
        extensionStack(s);
    }
    if (s->top < s->size - 25) {
        curtailStack(s);
    }
}
void inStack(Stack *s, char inData) {
    checkStackSize(s);
    s->data[s->top + 1] = inData;
    s->top++;
}
void outStack(Stack *s, char *outData) {
    checkStackSize(s);
    if (outData) {
        *outData = s->data[s->top];
    }
    s->top--;
}

char* encodeStr(char *forStr) {
    int fl = (int)strlen(forStr);            // 格式化字符串的长度
    Stack s = initStack();
    Stack c = initStack();
    Stack n = initStack();
    for (int i = 0; i < fl; i++) {
        if (forStr[i] != ']') {
            inStack(&s, forStr[i]);
        }
        else {
            BOOL isNum = NO;
            char topC;
            // 在isNum变成YES之前,都可以执行循环;变成YES之后,那么栈顶元素就必须是数字
            while (s.top >= 0 && (!isNum || (s.data[s.top] >= '0' && s.data[s.top] <= '9'))) {
                outStack(&s, &topC);
                if (topC == '[') {
                    isNum = YES;
                    continue;
                }
                // 入栈到对应的字符栈里面
                inStack(isNum ? &n : &c, topC);
            }
            // 将 栈n 里面的字符转化为数字
            char *numStr  = malloc(sizeof(char) * strlen(n.data));
            char top;
            for (int j = 0; j < strlen(n.data); j++) {
                outStack(&n, &top);
                numStr[j] = top;
            }
            // 根据字符串获取数字
            int num = atoi(strlen(numStr) > 0 ? numStr : "1");
            // 将栈c中的元素转化为字符数组
            char *cArr = malloc(sizeof(char) * strlen(c.data));
            for (int j = 0; j < strlen(c.data); j++) {
                outStack(&c, &top);
                cArr[j] = top;
            }
            for (int k = 0; k < num; k++) {
                for (int x = 0; x < strlen(cArr); x++) {
                    inStack(&s, cArr[x]);
                }
            }
            // 本轮结束,虽然n 和 c 都已经全部出栈,但为了保险起见将栈置空
            n.top = -1;
            c.top = -1;
            free(cArr);
            s.data[s.top + 1] = '\0';       // 作为结束符
        }
    }
    free(n.data);
    free(c.data);
    
    return s.data;
}

// 逻辑区
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        char *formatStr = "2[abc][cd]ef";        // 格式字符串
        char *resultStr = encodeStr(formatStr);
        
        printf("%s\n", resultStr);
    }
    return 0;
}

删除重复字母

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

题目分析:
这个题,其实删除重复字母就非常简单了,难的是最后一句话,保证字典序最小,并且不能打乱其他字符的相对位置。

解题思路:
由于只考虑小写字母,因此我们可以定义两个大小为26的int数组,一个用来保存每个字母出现的次数,另一个用来保存下标对应的字母是否已经入栈。

代码如下:

#import <Foundation/Foundation.h>

// 删除重复字母
char* removeDuplicateLetters(char *s)
{
    if (!s || strlen(s) < 2) {
        return s;
    }
    int top = -1;           // 作为栈顶index
    char *resStack = malloc(sizeof(char) * 26);         // 保存结果的栈
    int  isInStack[26] = {0};                           // 对应字母是否已经入栈
    int  countStack[26] = {0};                          // 对应字母剩余出现的次数
    // 先遍历一遍,找到每个字母出现的次数
    char *p = s;
    while (*p) {
        countStack[*p - 'a']++;
        p++;        // 找下一个字符
    }
    
    p = s;
    int i = 0;
    while (*p) {
        i = *p - 'a';
        // 判断当前的字母是否已经在栈里面了?
        if (!isInStack[i]) {
            // 循环栈顶字母,判断是否需要出栈
            while (top != -1 &&                         // 不为空栈
                   *p < resStack[top] &&                // 当前字母小于栈顶元素
                   countStack[resStack[top] - 'a'] > 0) // 后面还有栈顶元素的字母,可以先把这个删了,反正后面还会继续添加进来
            {
                isInStack[resStack[top] - 'a'] = 0;     // 设置对应字母为未入栈
                top--;      // 出栈
            }
            // 将当前字母入栈
            resStack[++top] = *p;
            // 设置当前字母为已入栈
            isInStack[i] = 1;
        }
        countStack[i]--;
        p++;
    }
    
    return resStack;
}


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        char *s = removeDuplicateLetters("xyzbcabcxyz");
        printf("%s\n", s);
    }
    return 0;
}

其他练习题

每日温度

根据每日气温列表,生成一个列表,列表表示需要再等待多久时间温度才能超过该日的天数。如果不会升高,则使用0表示。

解题思路:

  1. 暴力法,直接双重循环,一一对比,使用下标相减,就可算出下一次温度超过需要的天数。
  2. 跳跃法,从最后一天开始向前循环。
    • 如果[i + 1] > [i],那么第i天就是1。
    • 如果[i + 1] == [i],要分两种情况,一个是两个都为0,则[i] = 0;另一个是不为0,则[i] = [i + 1] + 1;
    • 如果[i + 1] < [i],那就回到了暴力法那样。

总体来说,使用跳跃法,能够根据已经计算出的结果来计算本次循环的结果,减少了对比次数,有点动态规划的感jio。

代码如下:

#import <Foundation/Foundation.h>

int randTem(void) {
    return 60 + arc4random() % 20;
}

// 暴力法
void bfFunc(int *tem, int tSize) {
    // 打印下一个超过的温度
    for (int i = 0; i < tSize; i++) {
        int next = 0;
        for (int j = i + 1; j < tSize; j++) {
            if (tem[j] > tem[i]) {
                next = j - i;
                break;
            }
        }
        printf("%-5d", next);
    }
    printf("\n");
}
// 跳跃对比法
void skipFunc(int *tem, int tSize) {
    int a[tSize];
    a[tSize - 1] = 0;
    // 倒着遍历,对比跳跃,减少循环次数
    for (int i = tSize - 2; i >= 0; i--) {
        if (tem[i] < tem[i + 1]) {
            a[i] = 1;
        }
        else if (tem[i] == tem[i + 1]) {
            if (a[i + 1] == 0) {
                a[i] = 0;
            }
            else {
                a[i] = a[i + 1] + 1;
            }
        }
        else {
            a[i] = 0;
            for (int j = i + 2; j < tSize; j++) {
                if (tem[j] > tem[i]) {
                    a[i] = j - i;
                    break;
                }
            }
        }
    }
    
    for (int i = 0; i < tSize; i++) {
        printf("%-5d", a[i]);
    }
    printf("\n");
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int *tem = malloc(sizeof(int) * 50);
        int tSize = 50;
        for (int i = 0; i < tSize; i++) {
            tem[i] = randTem();
            printf("%-5d", tem[i]);
        }
        printf("\n\n");
        printf("暴力法:\n");
        bfFunc(tem, tSize);
        
        printf("跳跃法:\n");
        skipFunc(tem, tSize);
        
    }
    return 0;
}

杨辉三角

原本的杨辉三角是第一个,在计算机中一般都存储为第二种结构。

    1                           1
   1 1                          1 1
  1 2 1         ----->          1 2 1
 1 3 3 1                        1 3 3 1
1 4 6 4 1                       1 4 6 3 1

这个题目其实就需要注意3点:

  1. 使用二维数组存储。
  2. arr[i][0] = arr[i][i] = 0。
  3. arr[i][j] = arr[i - 1][j - 1] + arr[i - 1][j]。

代码如下:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a[10][10];
        for (int i = 0; i < 10; i++) {
            a[i][0] = 1;
            a[i][i] = 1;
            for (int j = 1; j < i; j++) {
                a[i][j] = a[i - 1][j - 1] + a[i - 1][j];
            }
        }
        
        for (int i = 0; i < 10; i++) {
            for (int j = 0; j <= i; j++) {
                printf("%-5d", a[i][j]);
            }
            printf("\n");
        }
    }
    return 0;
}

爬楼梯

假如有n层楼梯,一次只能走一层或两层,问有多少种走法?

第一次遇见这个题,可能会很懵,不知道从哪下手,好在笔者不是第一次遇到这个题了(已经n次了😂)

题目分析:有两种解法,正向和逆向。

逆向思考,将爬楼梯问题改为下楼梯问题,我现在已经在第n层了,那么我是如何到第n层的呢?有且只有下面这两种方法:

  1. 从第n - 1层一次走一层上来的。
  2. 从第n - 2层一次走两层上来的。

那么到这里,是不是就说明了n层楼梯的走法 = n - 1层楼梯的走法 + n - 2层楼梯的走法。仔细思考🤔,好像是那么回事。

从上面推导出来的那个公式,别跟我说看不出来那是个递归?? f(n) = f(n - 1) + f(n - 2)

接着,代码来了,递归解法

// 递归法
int climbStairs(int stairs) {
    if (stairs == 1) {
        return 1;           // 如果只有一层,那么就只有一种走法
    }
    else if (stairs == 2) {
        return 2;           // 如果有两层,那么有两种走法
    }
    else {
        return climbStairs(stairs - 1) + climbStairs(stairs - 2);
    }
}

正向思考,你说逆向思考有点难,不一定能想到,没关系,就下来就说说如何用正向思考解决。

楼梯层数n 有多少种走法
1 1
2 2
3 3
4 5(不是4)
5 8
6 13
7 21

所谓正向思考,就是找规律呗,多列举几组数据,找到他们之间的规律,再另外找几组数组验证一下,没问题的话差不多就算是找到了这个规律。

从上面,我们列举了7组数据,发现了一个规律,前两组的走法数量加起来就是当前组的走法,是不是也间接的证实了刚刚逆向分析时得到的那个规律n层楼梯的走法 = n - 1层楼梯的走法 + n - 2层楼梯的走法??

那么规律就是它了,没错了。哎等等?这个规律咋看着那么熟悉呢?好像斐波那契数列啊??

没错,其实爬楼梯问题的本质就是斐波那契数列。
那么我们就使用非递归的方式来解决吧。

// 非递归
int climbStairs2(int stairs) {
    if (stairs == 1) {
        return 1;
    }
    else if (stairs == 2) {
        return 2;
    }
    else {
        int f1 = 1;      // 一阶
        int f2 = 2;      // 两阶
        
        for (int i = 3; i <= stairs; i++) {
            f2 = f1 + f2;
            f1 = f2 - f1;
        }
        return f2;
    }
}

这里说明一下,斐波那契数列是从兔子生娃那个问题演变出来的,只是这个爬楼梯问题刚好和这个规律相同罢了。

我觉得,算法太多了,把所有算法的解题代码都背下来那不现实,我们需要做的就是记住算法要考察的本质是什么?

  • 爬楼梯--->本质是斐波那契数列
  • 斐波那契数列的特点--->前面两项相加等于后面这一项
  • 解决方法--->循环和递归。

这样通过本质一步步推导,没有解决不了的问题,反而死记代码,记得了一时,记不了一世,到用的时候还不会推导,注定凉凉。。。(跑题了,有感而发)。

总结

本篇文章主要将栈思想带入到了算法中,更深刻的理解栈的特点及用法。

另外就是记录了一些经典的算法题,题目的本质,解题思路,以及解题的代码。

本文地址https://juejin.cn/post/6844903632081518600

(直接点是没用的,选中才可复制呦)