算法题的解题思路以及例题

509 阅读13分钟
  • 采用递归算法解题

    • 定义

    什么是递归? 若在⼀个函数,过程或数据结构定义的内部⼜直接(或间接)出现定义本身的应⽤; 则称为他们是 递归的. 或者是递归定义.

    在实际项目开发中不建议使用递归实现某种功能,应为递归比较浪费空间应为每一次递归的实现中,系统都会重新为变量分配空间而不是覆盖原来的空间

    • 使用递归的场景
      • 定义就是递归的
        例如阶乘,

      1 阶乘Fact(n)
      (1) 若n = 0,则返回1;
      (2) 若n > 1,则返回 n*Fact(n-1);
      2 ⼆阶斐波拉契数列Fib(n)
      (1) 若n = 1或者n = 2, 则返回1;
      (2) 若n > 2,则Fib(n-1) + Fib(n-2);

      对于类似这种复杂问题,若能够分解成⼏个简单且解法相同或类似的⼦问题,来求解,便称为递归求解. 这种求解方式叫做”分治法“ 使用分治法的三个条件:

      1. 能将⼀个问题转换变成⼀个⼩问题,⽽新问题和原问题解法相同或类同. 不同的仅仅是处理的对象, 并且 这些处理更⼩且变化有规律的.
      2. 可以通过上述转换⽽使得问题简化
      3. 必须有⼀个明确的递归出⼝, 或称为递归边界.
      • 数据结构是递归的
        其数据结构本身具有递归的特性.
        例如,对于链表,其结点LNode的定义由数据域data和指针域next 组成,⽽指针域next是⼀种指向LNode类型的指针,即LNode的定义中⼜⽤到了其⾃身. 所以链表是⼀种递归的数据结构;
      • 问题的解法是递归的
        有⼀类问题,虽然问题本身并没有明显的递归结构,但是采样递归求解⽐迭代求解更简单, 如Hanoi塔问题,⼋ 皇后问题,迷宫问题.
    • 例题
      /**
       爬楼梯问题
       假设你正在爬楼梯。需要n 阶你才能到达楼顶。每次你可以爬1 或2 个台阶。你有多少种不 同的⽅法可以爬到楼顶呢?注意:给定n 是⼀个正整数å
       
       解题思路:
       可先将大问题化成小问题(可以先想爬一层的方法数2层的方法数),例如以下层数和方法映射
        层数     方法
         1          1
         2          2
         3          3
         4          5
         5          8
       可发现其实爬上n层的楼梯需要的方法数就等于f(n-1)+f(n-2)
       就可以现冲1层2层算出3层的方法、2层3层算出4层的方法直到n层
       */
      int stairs(int n){
          if(n == 1 || n == 2){
              return n;
          }else{
              return stairs(n-1) + stairs(n - 2);
          }
      }
      
      
  • 采用栈思想解决问题

    • 使用场景

      指的是利⽤栈的特性(先进后出)去解决问题

      1. 数据是线性的
      2. 问题中常常涉及到数据的来回⽐较,匹配问题;例如,每⽇温度,括号匹配,字符串解码,去掉重复字⺟等问题.
      3. 问题中涉及到数据的转置,例如进制问题.链表倒序打印问题等
      4. 注意并不是说栈思想只是⼀个解决的的参考思想.并不是万能的.它适⽤于以上这样的情况下去解决问题; 利⽤栈思想解决问题时,⾸先需要透彻的解析问题之后,找到问题解决的规律.才能使⽤它解决;
    • 例题:
      1. 括号匹配检验:
      /*
       假设表达式中允许包含两种括号:圆括号与⽅括号,其嵌套
       顺序随意,即([]()) 或者[([][])]都是正 确的.⽽这[(]或者(()])或者([()) 都是不正确的格式.
       检验括号是否匹配的⽅法可⽤"期待的急迫程度"这个概念
       来描述.例如,考虑以下括号的判断: [ ( [ ] [ ] ) ]
       解题思路:
       用利用栈思想,第一个符号先入栈,第二个符号入栈的时候先判断是否和栈顶元素的符号正好匹配,
       如果匹配则出栈,否则继续入栈,最后只需判断栈是否为空即可(为空则说明匹配成功)
       */
      Status matchingBrackets(char *data){
          Stack s;
          if(!creatStack(&s)) return ERROR;
          if(strlen(data) <= 0) return ERROR;
          pushData(&s, data[0]);
          for(int i = 1; i < strlen(data); i ++){
              if(s.length == 0){
                  pushData(&s, data[i]);
              }else{
                  switch (getTop(s)) {
                      case '(':
                          if(data[i] == ')'){
                              popData(&s);
                          }else{
                              pushData(&s, data[i]);
                          }
                          break;
                      case '[':
                          if(data[i] == ']'){
                              popData(&s);
                          }else{
                              pushData(&s, data[i]);
                          }
                          break;
                      default:
                          break;
                  }
              }
          }
          if(s.length == 0){
              printf("匹配成功\n");
              return TRUE;
          }else{
              printf("匹配不成功\n");
              return FALSE;
          }
      }
      
      1. 每日气温问题(这里可以用栈思想解决问题,单并不是最优解,这里还给出了暴力法解决,跳跃法解决这两个方法不是利用的栈思想)
      /**
       题⽬: 根据每⽇⽓温列表,请重新⽣成⼀个列表,对应位
       置的输⼊是你需要再等待多久温度才会
       升⾼超过该⽇的天数。如果之后都不会升⾼,请在该位置0
       来代替。例如,给定⼀个列表  temperatures = [73, 74,
       75, 71, 69, 72, 76, 73],你的输出应该是  [1, 1, 4, 2,
       1, 1, 0, 0]。
       */
      /**
       (1)暴力法解决
       解题思路:
       两层遍历,第一层冲0开始遍历整个数组,第二层冲i+1开始遍历只要遇到比自己大的元素(或者大于
       等于数组的长度)第二层循环截止,如果找到比自己大的元素则索引相减得出天数
       */
      Status temperature(int *t, int len, int **result){
      
          *result = (int *)calloc(len, sizeof(int));
          if(!result) return ERROR;
          for(int i = 0; i < len; i ++){
              for (int j = i+1; j < len; j++) {
                  if(t[i] < t[j]){
                      (*result)[i] = j-i;
                      break;
                  }
              }
          }
          return OK;
      }
      
      //(2)跳跃法解决
      /**
       思路提示:从右到左的计算,那么如果遇到计算过的位置则不需要重复计算.
       例如,当计算75时, 遍历到71时,则直接使⽤计算好的71上对应的值2. 那么我就直接 跳2步再进⾏⽐较. 利⽤已有的结果,减少遍历次数.
       */
      Status temperature2(int *t, int len, int **result){
      
          *result = (int *)calloc(len, sizeof(int));
          if(!result) return ERROR;
          for(int i = len-2; i >= 0; i --){
              int j = i+1;
              while (1) {
                  if(t[i] < t[j]){
                      (*result)[i] = j - i;
                      break;
                  }
                  if((*result)[j] == 0) break;
                  j += (*result)[j];
              }
          }
          return OK;
      }
      
      //(3)栈思想解决问题
      /**
        解题思路:
       1. 栈中存储的是元素的索引值index;
       2.将当前元素和栈顶元素⽐较;
       如果栈为空,那么直接将当前元素索引index 存储到栈中;
       如果栈顶元素>当前元素,则将当前元素索引index 存储到栈中;
       如果栈顶元素<当前元素,则将当前元素索引index-栈顶元素index,计算完毕则将当前栈顶元素移除,将当前元素索引index 存储到栈中
       */
      Status temperature3(int *t, int len, int **result){
          *result = (int *)calloc(len, sizeof(int));
          Stack s;
          if(!creatStack(&s)) return ERROR;
          if(!result) return ERROR;
          pushData(&s, 0);
          for(int i = 1; i < len; i ++){
              while (1) {
                  if(s.length > 0 && t[getTop(s)] < t[i]){
                      (*result)[getTop(s)] = i - getTop(s);
                      popData(&s);
                  }else{
                      pushData(&s, i);
                      break;
                  }
              }
          }
          return OK;
      }
      
      1. 字符串编码问题
      /**
       编码规则为: k[encoded_string],表示其中⽅括号内部的encoded_string
       正好重复k 次。 注意k
       保证为正整数。你可以认为输⼊字符串总是有效的;输⼊
       字符串中没有额外的空格,
       且输⼊的⽅括号总是符合格式要求的。此外,你可以认为
       原始数据不包含数字,所有的数字只 表示重复的次数k
       ,例如不会出现像3a 或2[4] 的输⼊。
       例如:
       s = "12[a]2[bc]", 返回"aaabcbc".
       s = "3[a2[c]]", 返回"accaccacc".
       s = "2[abc]3[cd]ef", 返回"abcabccdcdcdef".
       
       解题思路:
       栈思想解决问题,先入栈元素如果元素不是 】则一直入栈当遇见】
       停止入栈,现在开始出栈(这里的出栈只是将栈顶的索引
       建议并不是将对应的内容也置空)出栈的同时将对应的
       元素保存在一个新的栈里面 一直到 【
       此时出栈的区域此时就找到了【】里面的字母同事栈顶元
       素是【,再出栈一次
       将现在的栈顶索引保存下来,开始判断如果栈顶元素不是
       数字则出栈,最后找到的索引区域就是数字的区域
       然后拿到数字循环将【】中的字母入栈,最后得出结果
       */
      char * decodeString(char *t){
          int len = (int)strlen(t);
          int stackSize = 50;
          char * stack = (char *)malloc(sizeof(char) * stackSize);
          int top = -1;
          char strInt[11];
          for(int i = 0; i < len; i ++){
              if(t[i] != ']'){
                  if(top == stackSize -1){
                      stack = realloc(&stack, (stackSize += 50)*sizeof(char));
                  }
                  stack[top + 1]=t[i];
                  top ++;
              }else{
                  int tempSize = 10;
                  char* temp = (char*)malloc(tempSize * sizeof(char));
                  int tempOfTop = -1;
                  int curTop = top;
                  while (1) {
                      if(strlen(stack) == 0) break;
                      if(stack[top] == '['){
                          break;
                      }else{
                          if(tempOfTop == tempSize + 1){
                              temp = realloc(&temp, (tempSize += 10) * sizeof(char));
                          }
                          temp[tempOfTop + 1] = stack[top];
                          tempOfTop ++;
                          top --;
                      }
                  }
                  top = top - 1;
                  curTop = top;
                  while (1) {
                      if(top < 0) break;
                      if(stack[top] >= '0' && stack[top] <= '9'){
                          top --;
                      }else{
                          break;
                      }
                  }
                  
                  for(int i = top+1; i <= curTop; i ++){
                      strInt[i - (top+1)] = stack[i];
                  }
                  strInt[curTop - top] = '\0';
                  if(strlen(strInt) == 0){
                      strInt[0] = '1';
                  }
                  int charNum = atoi(strInt);
                  for(int i = 0; i < charNum; i ++){
                      
                      for(int t = 0; t <= tempOfTop; t ++){
                          if(top == stackSize + 1){
                              stack = realloc(&stack, (stackSize += 50)*sizeof(char));
                          }
                          stack[top + 1] = temp[tempOfTop-t];
                          top ++;
                      }
                  }
                  free(temp);
                  temp = NULL;
                  
              }
          }
          char * ans = realloc(stack, (top + 1)*sizeof(char));
          ans[top+1] = '\0';
          return ans;
      }
      
      1. 去重字母问题
      /**
       去除重复字⺟问题
       给你⼀个仅包含⼩写字⺟的字符串,请你去除字符串中重
       复的字⺟,使得每个字⺟只出现⼀次。需保
       证返回结果的字典序最⼩(要求不能打乱其他字符的相对
       位置)
       
       解题思路:
       首先一层遍历计算出所有字母出现的次数(这里可以创建
       一个有26个存储单元的数组,对应的字母次数就存储在对
       应的位置
       例如a 0   b  1    c 2   d
       3)创建一个新栈,判断栈为空入栈,不为空比较两个字母
       的ASCII码,当前元素小于栈顶元素,找到栈顶元素的重复
       次数
       如果大于0出栈当前元素入栈,否则判断栈内是否有该元素
       如果没有直接入栈
       */
      char *removeDuplicate(char * str){
          if(strlen(str) <= 1) return str;
          int indexNum[26] = {0};
          int i;
          for(i = 0; i < strlen(str); i ++){
              indexNum[str[i] - 'a'] ++;
          }
          char *result = (char *)malloc(sizeof(char) * strlen(str) + 1);
          int top = -1;
          
          for(i = 0; i < strlen(str); i ++){
              if(top == -1){
                  //当前为空栈
                  result[++top] = str[i];
              }else{
                  if(str[i] > result[top]){
                      //当前元素大于栈顶元素
                      result[++top] = str[i];
                  }else{
                      //当前元素小于栈顶元素
                      while (1) {
                          int flag = 0;
                          for(int j = 0; j <= top; j ++){
                              if(result[j] == str[i]){
                                  flag = 1;
                                  break;
                              }
                          }
                          if(indexNum[result[top] - 'a'] > 1){
                              if(flag){
                                  //栈内存在该元素直接对应计数减一
                                  indexNum[result[top] - 'a'] --;
                              }else{
                                  //后面有重复元素且目前栈内无该元素 则该元素计数减一
                                  indexNum[result[top] - 'a'] --;
                                  top --;
                              }
                          }else{
                              //后面没有重复元素并且栈内没有改元素直接入栈
                              if(!flag){
                                  result[++top] = str[i];
                              }
                              break;
                          }
                          if(top == -1){
                              result[++top] = str[i];
                              break;
                          }
                      }
                      
                  }
              }
          }
          result[++top] = '\0';
          return result;
      }
      
  • BF算法

    • 定义

    BF算法即暴风算法,是普通的模式匹配算法。BF算法的思想:将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果

    • 例题
     /**
      字符串匹配问题
      题目: 有一个主串S = {a, b, c, a, c, a, b, d, c},
      模式串T  = { a, b, d } ; 请找到模
      式串在主串中第一次出现的位置;
      提示: 不需要考虑字符串大小写问题, 字符均为小写字母;
      解题思路:
      BF算法;
      BF算法即暴风算法,是普通的模式匹配算法。BF算法的思
      想:将目标串S的第一个字符与模式串T的第一个字符进行
      匹配,若相等,则继续比较S的第二个字符和
      T的第二个字符;若不相等,则比较S的第二个字符和T的第
      一个字符,依次比较下去,直到得出最后的匹配结果
      
      步骤:
      两层遍历第一层遍历主串遍历范围[0,主串长度-模式串长度]
      【i】
      第二层遍历遍历模式串遍历范围[0,模式串长度)i  【j】
      在第二程遍历中对比主串中的第i+j个字符和模式串中的第
      j个字符  如果全部相等则返回i
      */
     //BF算法
     int matchStr(char * str,char *model){
         if(strlen(model) > strlen(str)) return -1;
         int flag = 0;
         for(int i = 0; i <= strlen(str) - strlen(model); i ++){
             flag = 0;
             for(int j = 0; j < strlen(model); j ++){
                 if(str[i+j] != model[j]){
                     flag = 1;
                     break;
                 }
             }
             if(!flag){
                 return i;
             }
         }
         return -1;
     }
     
    
  • RK算法

    • 定义

    基本思想: 如果两个字符串hash后的值不相同,则它们肯定不相同;如果它们hash后的值相同,它们不一定相同。 RK算法的基本思想就是:将模式串P的hash值跟主串S中的每一个长度为|P|的子串的hash值比较。如果不同,则它们肯定不相等;如果相同,则再诸位比较之。

    • 例题
    /**
     字符串匹配问题(RK算法解题)
     解题思路:
     RK算法:
     精髓在于:1、将字符串转成数字进行比较,省去遍历模式串逐个对比字符的步骤
             2、边遍历边比较,计算出主串中的一段字符串的数值之后就和模式串
             的数值进行比较,如果相等直接返回。这样省去计算主串后面的数值
             3、计算数值时可借用上一个数值的后面(模式串长度-1)位进行计算
             例如 234 345 456   345 = (234 -200)*10+5  456=(345-300)*10+6
             4、最后防止哈希冲突可以遍历字符串在进行字符比较确认
     */
    int matchStr2(char * str,char *model){
        //1.计算模式串的哈希值
        unsigned int modelHash = 0;
        int i = 0;
        for(i = 0;i < strlen(model); i ++){
            modelHash += (model[i] - 'a' + 1) * pow(26, strlen(model)-i-1);
        }
        unsigned int strHash = 0;
        for(i = 0;i < strlen(str) - strlen(model) + 1; i++){
            if(i == 0){
                //第一次需要算出具体哈希值
                for(int j = i;j < strlen(model); j ++){
                    strHash += (str[j] - 'a' + 1) * pow(26, strlen(model)-i-j-1);
                }
            }else{
                //可借用上一个哈希值算出现在的哈希值
                //例如 234 345 456   345 = (234 -200)*10+5  456=(345-300)*10+6
                strHash = (strHash - (str[i-1] - 'a' + 1)*pow(26, strlen(model) - 1))*26 +(str[i+strlen(model)-1] - 'a' + 1);
            }
            if(strHash == modelHash){
                //防止哈希冲突
                int flag = 0;
                for(int n = i;n < strlen(model); n ++){
                    if(str[n] != model[n-i]){
                        flag = 1;
                    }
                }
                if(!flag){
                    return i+1;
                }
            }
            
        }
        return -1;
    }