从零学算法

394.给定一个经过编码的字符串,返回它解码后的字符串。
编码规则为: k[encoded_string],表示其中方括号内部的 encoded_string 正好重复 k 次。注意 k 保证为正整数。
你可以认为输入字符串总是有效的;输入字符串中没有额外的空格,且输入的方括号总是符合格式要求的。
此外,你可以认为原始数据不包含数字,所有的数字只表示重复的次数 k ,例如不会出现像 3a 或 2[4] 的输入。
示例 1:
输入:s = “3[a]2[bc]”
输出:“aaabcbc”
示例 2:
输入:s = “3[a2[c]]”
输出:“accaccacc”
示例 3:
输入:s = “2[abc]3[cd]ef”
输出:“abcabccdcdcdef”
示例 4:
输入:s = “abc3[cd]xyz”
输出:“abccdcdcdxyz”

  • 我的原始人解法又登场了:首先我的第一反应肯定是用栈,不断 push,直到发现有字符为 ] 则说明有一组可以解码的字符串了,以示例 2 来看好了。当遍历到字符 c 后面的 ] 时,开始解码。首先不断 pop 直到发现 [ 就说明我们要重复 n 次的字符串被得到了,然后继续 pop 直到下一个要 pop 的不为数字则得到了需要重复的次数,解码这一部分继续 push 回栈然后遍历。最终栈里面存储的就是逆序的结果,我们 pop 到另一个栈再依次读取就得到了顺序的结果。
  •   public String decodeString(String s) {
          char[] cs = s.toCharArray();
          Stack<Character> stack = new Stack<>();
          for(char c:cs){
              if(c != ']'){
                  stack.push(c);
              }else{
              	// 记录找到要重复的字符串没
                  boolean findLetter = false;
                  // 当前组的解码结果
                  String curAns = "";
                  // 当前要重复的字符串
                  String curString = "";
                  // 当前字符串要重复的次数
                  String curNumber = "";
                  while(true){
                  	// 比如 "2[ab]" 这个字符串找到需要重复的字符串后
                  	// 找数字时找到头了(stack 被找空了)也没发现不是数字的字符
                  	// 那就该直接解码当前字符串了;
                  	// 或者比如 "b2[ab]",我已经找到了重复字符串为 "ab" 了
                  	// 继续找直到找到第一个字符 'b' 发现不为数字了
                  	// 也就是说后面的 '2' 和 "ab" 可以用来解码一次了
                      if(stack.isEmpty() || 
                      (findLetter && (stack.peek()<'0' || stack.peek()>'9')) ){
                          int number = Integer.valueOf(curNumber);
                          while(number-- > 0){
                              curAns += curString;
                          }
                          // 解码完这一次 push 回 stack 等下次计算
                          for(char ans:curAns.toCharArray())stack.push(ans);
                          break;
                      }
                      char curChar = stack.pop();
                      // 找到这个说明要重复的字符串找完整了
                      if(curChar == '['){
                          findLetter = true;
                          continue;
                      }
                      // 先找要重复的字符串,找到了再找重复次数
                      if(!findLetter){
                      	// 为什么不直接 curString += curChar 是因为栈是后进先出的
                      	// 我们 pop 出来的字符只能每次都放在首位才能保证我们的结果的顺序的
                          curString = curChar + curString;
                      }else{
                          curNumber = curChar + curNumber;
                      }
                  }
              }
          }
          Stack<Character> temp = new Stack<>();
          String ans = "";
          // 倒腾一遍把顺序搞正
          while(!stack.isEmpty()){
              temp.push(stack.pop());
          }
          while(!temp.isEmpty()){
              ans+=temp.pop();
          }
          return ans;
      }  
    
  • 他人精简版:首先只有字符四种情况,数字,字母,或者左右括号。使用两个栈,当为数字时我们就把数字入数字栈,当为字母时,我们把他组成字符串入字符串栈,当遇到右括号就从栈中取出数字和字符串组合即可。遍历字符串,当遇到数字先计算着;遇到字母也先把字母组合成字符串;因为数字完了后面必定是左括号,所以在遇到左括号时才把计算完的数字入数字栈,同时由于可能字符串为为 "ab3[cd]" ,我们同时也要把当前组合的字符串 res 也入字符串栈(就算总的字符串为 "3[ab]" 也没事,我们在遇到左括号时入栈的那个空字符串之后也用不上),别忘了重置组合字符串和计算的数字。当遇到右括号时我们就从两个栈中分别取出当前所需重复次数和之前的字符串计算结果(或者没计算过的初始字符串),我们在遇到 [] 之间组合的字符串 res,其实就是当前需要重复的字符串。有之前的结果,现在所需重复的次数和字符串,直接遍历累加然后把结果赋给 res。(这个 res 是设计得最为巧妙的,它有组合当前所需重复字符串的作用,也有保存解码结果的作用,因为嵌套的括号中,内部的解码结果其实对外部来说就是他要重复的字符串,比如 2[2[b]],2[b] 解码后的 "bb" 其实就是 2[bb] 需要重复的字符串)
  •   public String decodeString(String s) {
          int num = 0;
          StringBuilder res = new StringBuilder();
          Stack<Integer> numStack = new Stack<>();
          Stack<StringBuilder> resStack = new Stack<>();
          for(char c:s.toCharArray()){
              if(Character.isDigit(c)){
                  num = num*10 + (c-'0');
              }else if(c=='['){
                  resStack.push(res);
                  numStack.push(num);
                  res = new StringBuilder();
                  num = 0;
              }else if(c==']'){
                  int repeatTimes = numStack.pop();
                  StringBuilder preRes = resStack.pop();
                  while(repeatTimes-- > 0){
                      preRes.append(res);
                  }
                  res = preRes;
              }else{
                  res.append(c);
              }
          }
          return res.toString();
      }  
    
  • 递归实际上就是用栈实现的,那么既然我们能用栈解决,不妨改成递归版。入栈就是递归进入下一层,出栈就是递归结束上一层。对应到上面的代码,其实需要改动的就是遍历到字符为左右括号的部分。我们知道递归是把一件事分成了一件件重复的小事,对应到本题,那么很明显,我们分成的小事也就是递归的最小基本单元,就是一次解码。伪代码大致如下:
  •   public 未知 dfs(未知){
      	StringBuilder res = new StringBuilder();
      	int num = 0;
          for(char c:s.toCharArray()){
              if(Character.isDigit(c)){
                  num = num*10 + (c-'0');
              }else if(c=='['){
                  调用 dfs
              }else if(c==']'){
                  结束 dfs
              }else{
                  res.append(c);
              }
          }
          return res.toString();
      }
    
  • 接着就是具体分析我们在遇到左右括号时到底做了什么,首先遇到左括号是把一次解码结果入栈了,然后重置变量,你会发现这就是 dfs 的一次调用,调用后变量不就等于重置了。遇到右括号时又做了什么,他进行了一次解码。那么左括号要操作的解码结果从哪来,从右括号的解码结果返回而来,也就是说 dfs 是有返回值的,根据我们对递归最小单元的定义,他返回的就是一次解码的结果。我们知道递归是处理到最深的一层才返回结果。对应到本题,从左括号的递归调用到右括号的递归结果的处理,我们最内层的其实是最内层的左右括号中间的内容,比如 "2[3[4[ab]]]" 我们最先返回的就是最里面的 "ab",也就是说 "ab" 其实就是最内层的解码结果,左括号拿到 ab 以后做什么?继续解码呗,重复 4 次。然后它又被返回给左括号,然后重复 3 次…,此时伪代码可以丰富一下了
  •   public String dfs(未知){
      	StringBuilder res = new StringBuilder();
      	int num = 0;
          for(char c:s.toCharArray()){
              if(Character.isDigit(c)){
                  num = num*10 + (c-'0');
              }else if(c=='['){
                  String repeatRes =  dfs(未知);
                  while(num-- > 0) {
                      res.append(repeatRes);
                  }
              }else if(c==']'){
              	  // 返回解码结果,暂时为返回 String,是否足够还需要继续分析
                  return res.toString()
              }else{
                  res.append(c);
              }
          }
          return res.toString();
      }
    
  • 继续分析一下入参,我们调用 dfs ,总得给他点参数限定一个范围,否则空参的话,因为每个 dfs 都遍历完整的 s,就会无限循环遍历了。能限制范围的条件很容易想到的就是当前遍历的下标。那我们就用下标形式遍历 s,然后 dfs 入参为当前遍历下标的下一位(为什么下一位?不然岂不是无限等于左括号,我们要取的是左括号后面的字符串),但是这里还有个问题,我们在解码一次后,最外层的遍历可不知道你里面是解码了多少层,他会继续解码已经解码过的内容,也就是说我们解码完后需要更新下标,跳过已经解码的部分,所以返回值需要传一个右括号的下标 j,让外层循环下标更新为 j+1。(为什么会想到这个,因为测试用例没通过啊,比如 "3[a]2[bc]" ,我直接返回了 "aaaa",先是遍历到第一个左括号,解码了 3[a],然后继续遍历到了 a,然后拼成了 aaaa,然后遇到右括号直接给我返回了)
  •   String S;
      public String decodeString(String s) {
          S = s;
          return dfs(0)[0];
      }  
      public String[] dfs(int cur){
      	StringBuilder res = new StringBuilder();
      	int num = 0;
          for(int i=cur; i < S.length(); i++){
              char c = S.charAt(i);
              if(Character.isDigit(c)){
                  num = num*10 + (c-'0');
              }else if(c=='['){
                  String[] result =  dfs(i+1);
                  i = Integer.parseInt(result[0]);
                  String repeatRes =  result[1];
                  // 我始终无法理解的就是为什么这里我不能用 
                  // while(num-- > 0) {
                  //    res.append(repeatRes);
                  // }
                  while(num > 0) {
                      res.append(repeatRes);
                      num--;
                  }
              }else if(c==']'){
                  return new String[]{String.valueOf(i),res.toString()};
              }else{
                  res.append(c);
              }
          }
          return new String[]{res.toString()};
      }
    

你可能感兴趣的:(算法学习,#,栈,算法,java)