数据结构与算法之栈思想解问题

717 阅读16分钟

前言

做算法题的方式方法

  • 充分阅读题目.了解题目背后的关键意思

  • 分析题目,涉及到哪些数据结构,对问题进行分类. 到底属于链表问题, 栈思想问题, 字符串问题,二叉树问题,图相关问题,排序问题等与之前所接触过的算法题是否类似,找到问题的解题思路

  • 算法的实现的过程,并不是一蹴而就, 一定是需要不断的调试,修改的

  • 验证算法正确性

  • 找到题源, 看其他的同行对齐的解决思路.

  • 找到题解建议之后, 对于其他优秀思路,分析它的优势,并且学习它的思路.并且写成其他解法的代码

  • 算法题的解题能力来自于2点: 对于数据结构与算法核心问题是否夯实 + 是否有足够多且足够耐心的积累;

栈的思想应用

指的是利用栈的特性(先进后出)去解决问题,那么什么问题适合用栈思想解决了?

  • 数据是线性的

  • 问题中常常涉及到数据的来回比较,匹配问题;例如,每日温度,括号匹配,字符串解码,去掉重复字母等问题.

  • 问题中涉及到数据的转置,例如进制问题.链表倒序打印问题等

  • 利用栈思想解决问题时,首先需要透彻的解析问题之后,找到问题解决的规律.才能使用它解决;思想只有指导作用,遇到不同的题目,需要个例分析.在基本思想上去找到解决问题之道;


括号匹配检验问题

/*
1. 初始化一个空栈S 
2. 当十进制N非零时,循环执行以下操作   把N与8求余得到的八进制数压入栈S; 
   N更新为N与8的商; 
3. 当栈S非空时,循环执行以下操作
   弹出栈顶元素e;    
   输出e; 
*/
void conversion(int N){  
    SqStack S;   
    SElemType e;
 
    //初始化一个空栈S   
    InitStack(&S);
   
    // 入栈   
    while (N) {  
        PushData(&S, N%8);   
        N = N/8;//多少进制/多少 
     }    

    //出栈   
    while (!StackEmpty(S)) {     
        Pop(&S, &e);       
        printf("%d\n",e);
    }
}

int  main{
  conversion(1348);
}

打印结果:


杨辉三角问题

思路:需要利用两个循环

  • 第一层循环控制行数i,默认[i][0] = 1,[i][i] = 1
  • 第二层循环控制列数j,tringle[i][j] = tringle[i - 1][j - 1] + tringle[i - 1][j ]
  • 结果输出一个二维数组,需要定义一个指针的指针 **

int **generate(int numRows,int returnSize){
    *returnSize = numRows;
    int **res = (int **)malloc(sizeof(int *))//动态生成一个二维数组=
     
    for(int i = 0; i < numRows; i ++){
        res[i] = (int *)(malloc(sizeof(int) * (i + 1))
        res[i][0] = 1;     
        res[i][i] = 1;       
       
        for (int j = 1; j < i; j++) {       
            res[i][j] = res[i-1][j] + res[i-1][j-1];   
        }
       
        return res;}

main函数
int main(int argc, const char * argv[]) {  
    int numRows = 9;  
    int returnSize;    
    int **returnResult;
    
   //得到结果通过两个循环输出
    returnResult =  generate(numRows, &returnSize);  
    for (int i = 0; i < returnSize; i++) {    
        printf("~");      
        for (int j = 0;  j<=i; j++) {      
            printf(" %d ",returnResult[i][j]);     
        }     
        printf("~\n");  
     }   
  return 0;
}

打印结果:


爬楼梯问题

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的⽅法可以爬到楼顶呢? 注意:给定 n 是一个正整数 

示例1:

输入: 2 则输出: 2
解释: 有2种⽅方法可以爬到楼顶 1:1阶+1阶;2.:2阶 

示例2:

输⼊: 3,输出: 3
解释: 有3种⽅方法可以爬到楼顶 

1. 1阶 + 1阶 +1阶
2. 1阶 +2阶
3. 2阶 +1阶 

  • 方法一:递归

int ClimbstairsWithRecursion(int n){
    //三个结果位置  
    if (n<1)  return 0;//不用爬了  
    if (n == 1) return 1;//就一个台阶,一步到达   
    if (n == 2) return 2;//两个台阶,一步一个台阶,两部到达     
    return ClimbstairsWithRecursion(n-1) + ClimbstairsWithRecursion(n-2);
}

  • 动态规划解法

       动态规划(英语:Dynamic programming,简称 DP)是一种在数学、管理科学、计算机科学、经济学和⽣物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的⽅法。
       
动态规划常常适⽤于有重叠子问题和最优子结构性质的问题,动态规划方法所耗时间往远少于朴素解法。 

       动态规划背后的基本思想非常简单。大致上,若要解一个给定问题,我们需要解其不同部分(即子问题),再根据子 问题的解以得出原问题的解。动态规划往往用于优化递归问题,例如斐波那契数列,如果运⽤用递归的方式来求解会重 复计算很多相同的子问题,利用动态规划的思想可以减少计算量量。 

       通常许多子问题⾮常相似,为此动态规划法试图仅仅解决每个子问题一次,具有天然剪枝的功能,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便便下次需要同一个子问题解之时直接查表。这种做法在重复⼦问题的数目关于输入的规模呈指数增长时特别有⽤。       

假设爬 n 个台阶有f(n)个可能:
1. 假设先爬1阶, 剩下n-1阶有f(n-1)种可能 

2.假设先爬2阶,剩下n-2阶有f(n-2)种可能 

因此爬n阶可以转化成2种爬n-1问题的和:f(n) = f(n-1) + f(n-2)

int ClimbstairsDynamicProgramming(int n){ 
    if(n==1) return 1;   
    int temp = n + 1;  
    int *sum = (int *)malloc(sizeof(int) * (temp));   
    sum[0] = 0;    
    sum[1] = 1;   
    sum[2] = 2;  
  
    for (int i = 3; i <= n; i++) {    
         sum[i] = sum[i-1] + sum[i-2];  
    }    
    
    return sum[n];
}

打印结果:



每日温度问题

根据每日气温列表,请重新生成一个列表,对应位置的输入是你需要再等待多久温度才会升高超过该日的天数。如果之后都不会升高,请在该位置0来代替。例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。这里是华氏温度。

提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。

解题关键: 实际上就是找当前元素 从[i,TSize] 找到大于该元素时. 数了几次. 首先最后一个元素默认是0,因为它后面已经没有元素了.

  • 解法一:暴力破解法 
  1.   左到右进行遍历,一个数~最后一个数;最后一个元素由于没有后继,默认为0
  2.  [i + 1,TSize]这个范围内找到大于当前的元素值;索引相减之间对应值;

    思路:

    1.创建一个result 结果数组.

    2.默认reslut[TSize-1] = 0;

    3.从0个元素遍历到最后一个元素[0,TSize-1];

    A.如果当前i >0 并且当前的元素和上一个元素相等,则没有必要继续循环. 则判断一下result[i-1]是否等于0,如果等于则直接将result[i] = 0,否则将result[i] = result[i-1]-1;

    B.遍历元素[i+1,TSize]

    如果当前T[j]>T[i],则result[i] = j-i;

    如果当前T[j]已经是最后一个元素,则默认result[i] = 0;

 int *daliyTemperatures_one(int* T, int TSize, int* returnSize){
     
    //创建一个result数组   
     int *result = (int)malloc(sizeof(int) *TSize); 
     *returnSize = TSize;   
     result[TSize-1] = 0;

     for(int i = 0;i < TSize - 1;i++){      
         if(i>0 && T[i] == T[i-1]){            
            result[i] = result[i-1] == 0 ? 0 : result[i-1]-1;    
         }else{         
             for (int j = i+1; j < TSize; j++) { 
                  if(T[j] > T[i]){               
                  result[i] = j-i;                 
                  break;               
             }             
             if (j == TSize-1) {    
                 result[i] = 0;            
             }        
          }      
       }  
    }  
    return result;
}
  • 解法二: 跳跃对比
  1.   跳跃对比法能够利用已知相邻的元素来减少比较次数
  2.  从右到左遍历. 由于最后一天的气温不会再升高,默认等于0;

  3.  循环 i 在[TSize-2,0]范围遍历; 从倒数第二天开始遍历比较. 每次减一;

  4. j 循环 j 在[i+1,TSize]范围遍历,j += result[j],可以利用已经有结果的位置进行跳跃,从而减少遍历次数

    -若T[i]<T[j],那么Result = j - i;

    -若reuslt[j] == 0,则表示后面不会有更大的值,那么当前值就应该也是0;

int  *dailyTemperatures_two(int* T, int TSize, int* returnSize){   
    //创建一个result 结果数组.    
    int *result = (int *)malloc(sizeof(int) * TSize);   
    *returnSize = TSize;  
    result[TSize-1] = 0;

    //从TSize-2个元素遍历到第一个元素[TSize-2,0];   
    for (int i = TSize-2; i >= 0; i--) {     
    for (int j = i+1; j < TSize; j += result[j]) {
         //从[i+1,TSize]遍历,j+=result[j];          
         if (T[i] < T[j]) {//若T[i]<T[j],那么Result = j - i;           
             result[i] = j - i;               
             break;           
         }else{               
         //若reuslt[j] == 0,则表示后面不会有更大的值,那么当前值就应该也是0;
              if (result[j] == 0) {  
                    result[i] = 0;       
                    break;        
              }      
           }       
        }   
    }   
    return result;
}

  • 解法三:利用栈思想

    思路:

  1.  初始化一个栈(用来存储索引),value数组
  2.  栈中存储的是元素的索引值index; 
  3.  遍历整个温度数组从[0,TSize];

      1). 如果栈顶元素<当前元素,则将当前元素索引index-栈顶元素index,计算完毕则将当前栈顶元素移除,将当前元素索引index 存储到栈中; 出栈后,只要栈不为空.继续比较,直到栈顶元素不能满足T[i] > T[stack_index[top-1]]

     2). 如果当前的栈为空,则直接入栈;

     3). 如果当前的元素小于栈顶元素,则入栈

     4). while循环结束后,当前元素也需要入栈;

int *dailyTemperatures_three(int* T, int TSize, int* returnSize) {  
    int* result = (int*)malloc(sizeof(int)*TSize);    
   
    // 用栈记录T的下标。   
    int *stack_index = malloc(sizeof(int)*TSize);   
    *returnSize = TSize;
    
    // 栈顶指针。 
    int top = 0;   
    int tIndex;//中间变量存储索引   
    for (int i = 0; i < TSize; i++)     
    result[i] = 0;    
    for (int i = 0; i < TSize; i++) {     
         printf("\n循环第%d次,i = %d\n",i,i);      
        // 若当前元素大于栈顶元素,栈顶元素出栈。即温度升高了,所求天数为两者下标的差值。  
         while (top > 0 && T[i] > T[stack_index[top-1]]) {
            //首先top-1取栈顶;拿到索引之后再去找T对应此索引的值         
             tIndex = stack_index[top-1];          
             result[tIndex] = i - tIndex;        
             top--;          
             printf("tIndex = %d; result[%d] = %d, top = %d \n",tIndex,tIndex,result[tIndex],top);      
          }
         
         // 当前元素入栈。        
         stack_index[top] = i;    
         printf("i= %d;  StackIndex[%d] = %d ",i,top,stack_index[top]);       
         top++;       
         printf(" top = %d \n",top); 
     }   
  return result;
}

字符串编码问题 

给定一个经过编码的字符串串,返回它解码后的字符串。
编码规则为: 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". 

  • 算法思路

这里以12[a]为例

  1. 遍历字符串 S
  2. 如果当前字符不为方括号"]" 则入栈stack中;
  3. 如果当前字符遇到了方括号"]" 则:

        1) 首先找到要复制的字符,例如stack="12[a",那么我要首先获取字符a;将这个a保存在另外一个栈去tempStack;

        2) 接下来,要找到需要备份的数量,例如stack="12[a",因为出栈过字符"a",则当前的top指向了"[",也就是等于2;

        3) 而12对于字符串是2个字符, 我们要通过遍历找到数字12的top上限/下限的位置索引, 此时上限curTop = 2, 下限通过出栈,top = -1;

       4) 根据范围[-1,2],读取出12保存到strOfInt 字符串中来, 并且将字符"12\0",转化成数字12;

       5) 当前top=-1,将tempStack中的字符a,复制12份入栈到stack中来;

       6) 为当前的stack扩容, 在stack字符的末尾添加字符结束符合'\0';

代码实现:

char  *decodeString(char * s){    
    int len = (int)strlen(s); //1.获取字符串长度   

    int stackSize = 50; //设置默认栈长度50   

    char* stack = (char*)malloc(stackSize * sizeof(char)); //开辟字符串栈(空间为50)  

    int top = -1; //设置栈头指针top = -1;

     //遍历字符串,在没有遇到"]" 之前全部入栈  
    for (int i = 0; i < len; ++i) {     
         if (s[i] != ']') {          

            //如果top到达了栈的上限,则为栈扩容;      
            if (top == stackSize - 1) {         
                stack = realloc(stack, (stackSize += 50) * sizeof(char));         
            }

            //将字符入栈stack      
            stack[++top] = s[i];      
            printf("#① 没有遇到']'之前# top = %d\n",top);    
         }else {     
           int tempSize = 10;         
           char *temp = (char*)malloc(tempSize * sizeof(char));         
           int topOfTemp = -1;  
         
           printf("#② 开始获取要复制的字符信息之前 # top = %d\n",top);
     
           //从栈顶位置开始遍历stack,直到"["结束;         
           //把[a]这个字母a 赋值到temp栈中来;        
           //简单说,就是将stack中方括号里的字符出栈,复制到temp栈中来;       
           while (stack[top] != '[') {                
                //优化:如果topOfTemp到达了栈的上限,则为栈扩容;          
                if (topOfTemp == tempSize - 1) {       
                    temp = realloc(temp, (tempSize += 10) * sizeof(char));      
                }               
         
                //temp栈的栈顶指针自增;      
                ++topOfTemp;              
                //将stack栈顶字符复制到temp栈中来;          
                temp[topOfTemp] = stack[top];              
                //stack出栈,则top栈顶指针递减;                
                top--;         
          }          
          
          printf("#开始获取要复制的字符信息之后 # top = %d\n",top);            
        
          //找到倍数数字.strOfInt字符串;          
          //注意:如果是大于1位的情况就处理          
          char strOfInt[11];           
          //p记录当前的top;        
          int curTop = top;          
          printf("#③ 开始获取数字,数字位置上限 # curTop = %d\n",curTop);        
         
          //top--的目的是把"["剔除,才能找到数字;           
          top--;            
          
         //遍历stack得出数字            
         //例如39[a] 就要找到这个数字39.           
         //p指向当前的top,我就知道上限了; 那么接下来通过循环来找它的数字下限;           
         //结束条件:栈指针指向为空! stack[top] 不等于数字          
         while (top != -1 && stack[top] >= '0' && stack[top] <= '9') {           
               top--;           
         }           
 
         printf("#③ 开始获取数字,数字位置下限 # top = %d\n",top);       
         //从top-1遍历到p之间, 把stack[top-1,p]之间的数字复制到strOfInt中来;         
         //39中3和9都是字符. 我们要获取到这2个数字,存储到strOfInt数组           
         for (int j = top + 1; j < curTop; ++j) {              
              strOfInt[j - (top + 1)] = stack[j];     
         }          

         //为字符串strOfInt数组加一个字符结束后缀'\0'         
         strOfInt[curTop - (top + 1)] = '\0';

          //把strOfInt字符串转换成整数 atoi函数;     
          //把字母复制strOfInt份到stack中去;          
          //例如39[a],就需要把复制39份a进去;        
          int curNum = atoi(strOfInt);          
          for (int k = 0; k < curNum ; ++k) {         
               
             //从-1到topOfTemp 范围内,复制curNum份到stackTop中去;             
               int kk = topOfTemp;             
               while (kk != -1) {                 
                
                   //如果stack到达了栈的上限,则为栈扩容;                   
                   if (top == stackSize - 1) {                  
                       stack = realloc(stack, (stackSize += 50) * sizeof(char));                 
                    }

                   //将temp栈的字符复制到stack中;               
                   //stack[++top] = temp[kk--];             
                    ++top;                 
                    stack[top] = temp[kk];              
                    kk--;            
                }         
             }         
             free(temp);           
             temp = NULL;      
         }    
      }

     //realloc 动态内存调整;    
     //void *realloc(void *mem_address, unsigned int newsize);
     //构成字符串stack后, 在stack的空间扩容.  
     char* ans = realloc(stack, (top + 1) * sizeof(char));   
     ans[++top] = '\0';

     //stack 栈不用,则释放; 
     free(stack);   
     return ans;
}

main 函数

int main(int argc, const char * argv[]) {   
   char *s ;   
   s = decodeString("12[a]");   
   printf("字符编码后的结果: %s\n\n\n\n",s);   

   s = decodeString("3[a]2[bc]");   
   printf("字符编码后的结果: %s\n\n\n\n",s);

   s = decodeString("3[a2[c]]");   
   printf("字符编码后的结果: %s\n\n\n\n",s);   
 
   s = decodeString("2[abc]3[cd]ef");    
   printf("字符编码后的结果: %s\n\n\n\n",s); 
   printf("\n"); 

   return 0;
}

打印结果:


括号匹配检验问题

假设表达式中允许包含两种括号:圆括号与方括号,其嵌套顺序随意,即([]()) 或者[([][])]都是正确 的.⽽这[(]或者(()])或者([()) 都是不正确的格式. 检验括号是否匹配的方法可用”期待的急迫程 度"这个概念来描述.例如,考虑以下括号的判断: [ ( [ ] [ ] ) ] 


  • 前期代码辅助

#include<stdlib.h>
#include<string.h>

#define Stack_Init_Size 100
#define Stack_Increment 10

//栈的定义
typedef struct {   
 char* base;     //栈底指针   
 char* top;      //栈顶指针   
 int stacksize;  //栈MaxSize
}SqStack;

//初始化栈
/* 
思路:  
      1. 如果栈底为空 
      2. 分配一个最大容量Stack_Init_Size的数组,栈底/栈顶都指向它 
      3. 初始化栈的最大容易Stack_Init_Size */
int Init(SqStack *stack){   
     stack->base=(char*)malloc(Stack_Init_Size*sizeof(char));     
     stack->top=stack->base;
     if(stack->top) return -1; //表示无法初始化已出始化栈      
   
     stack->stacksize = Stack_Init_Size;       
     printf("初始化成功\n");        
     return 0; //初始化成功
}

//获取栈顶数据
/* 
思路: 
   1.判断栈是否为空
   2.非空,则栈定指针-1,返回栈顶元素;
*/

char GetTop(SqStack stack){   
    if(stack.base==stack.top){    
    printf("栈中没有数据\n");        
    return '#';   
 }   

 //printf("获取栈顶数据成功\n");  
  return *(stack.top-1);
}

//往栈中插入元素
/* 
思路: 
  1.判断栈是否已满,若满则返回ERROR #问题:如何判断栈是否已满? 
  2.栈满,则续容空间 #问题:如何给已满栈续容空间? 
  3.将元素element压栈 
  4.栈顶指针加"1" 
*/
int Push(SqStack *stack,char element){   
    if(stack->top-stack->base==stack->stacksize){    
       stack->base=(char*)realloc(stack->base,Stack_Increment*sizeof(char));    
       stack->top=stack->base+stack->stacksize;       
       stack->stacksize+=Stack_Increment;   
     }   

    *stack->top=element;   
    stack->top+=1;  
    return 0;
}

//删除栈顶元素
/* 
思路:
 1.判断栈是否已空 
 2.非空,则获取栈顶元素,并将栈顶减"1";
*/
char Pop(SqStack *stack){
    if(stack->top==stack->base){  
       printf("栈为空\n");      
       return '#';  
 }    

   //printf("删除数据成功");  
   return *--stack->top;
}

//释放栈空间
int Destroy(SqStack *stack){  
    free(stack->base);  
    stack->stacksize=0;    
    return 0;
}

  • 回归主题

     算法思路

  1.  将第0个元素压栈
  2.  循环遍历在[1,strlen(data)]范围

       2.1取栈顶字符

       2.2  检查该字符是左括号("(","[")

          2.2.1: 是左"(",则判断紧接其后的data[i]是为右")",YES->压栈,NO->出栈

          2.2.2: 是左"[",则判断紧跟其后的data[i]是为右"]",YES->压栈,NO->出栈

          2.2.3: 表示式如果以"#"结尾,则判断紧跟其后的data是为左"(""]",YES->压栈,NO->-1;

  3.  遍历结束,则判断栈是否为空,为空则表示匹配成功;否则匹配失败;

int ExecuteData(SqStack stack,char* data){   
    Push(&stack,data[0]);   
    for(int i=1;i<strlen(data);i++){  
        char top = GetTop(stack);     
        switch(top){          
            case '(':         
                 if(data[i]==')') Pop(&stack);             
                 else Push(&stack,data[i]);            
                 break;          
            case '[':              
                 if(data[i]==']')Pop(&stack);           
                 else Push(&stack,data[i]);             
                 break;            
            case '#':               
                 if(data[i]=='('||data[i]=='['){      
                    Push(&stack,data[i]);            
                    break;              
                 } else            
             default:return -1;
             break;     
         }     
    }

    //如果栈为空,则返回"0"->匹配成功 否则返回"-1"匹配失败   
    if(stack.top==stack.base){       
       Destroy(&stack);     
       return 0;   
     } else{     
         Destroy(&stack); 
         return -1; 
     }
}

int main(){   
    SqStack stack;    
    Init(&stack);   
    char data[180];  
    scanf("%s",data);  
    int result = ExecuteData(stack,data);   
    if(result==0)printf("括号是正确匹配的\n");   
    else printf("括号匹配不正确\n");    
    
    return 0;
}