【问题标题】:Generate balanced parentheses in java在java中生成平衡括号
【发布时间】:2020-06-13 23:56:11
【问题描述】:

问题是: 给定 n 对括号,编写一个函数来生成格式正确的括号的所有组合。

例如,给定 n = 3,解集是:

“((()))”、“(()())”、“(())()”、“()(())”、“()()()”

我曾经使用字符串来解决这个问题,如下代码:

public class Solution {
public List<String> generateParenthesis(int n) {

    ArrayList<String> result = new ArrayList<String>();
    //StringBuilder s = new StringBuilder();
    generate(n, 0, 0, "", result);
    return result;
}
public void generate(int n, int left, int right, String s, ArrayList<String> result){

    //left is num of '(' and right is num of ')'
    if(left < right){
        return;
    }
    if(left == n && right == n){
        result.add(s);
        return;
    }
    if(left == n){
        //add ')' only.
        generate(n, left, right + 1, s + ")", result);
        return;
    }
    generate(n, left + 1, right, s + "(", result);
    generate(n, left, right + 1, s + ")", result);
    }
}

现在我想用StringBuilder解决这个问题,代码是这样的:

import java.util.ArrayList;

public class generateParentheses {
public static ArrayList<String> generateParenthesis(int n) {

    ArrayList<String> result = new ArrayList<String>();
    StringBuilder sb = new StringBuilder();

    generate(n, 0, 0, result, sb);
    return result;
}

public static void generate(int n, int left, int right, ArrayList<String> result,
        StringBuilder sb) {

    if (left < right) {
        return;
    }
    if (left == n && right == n) {
        result.add(sb.toString());
        sb = new StringBuilder();
        return;
    }
    if (left == n) {
        generate(n, left, right + 1, result, sb.append(')'));
        return;
    }
    generate(n, left + 1, right, result, sb.append('('));
    //sb.delete(sb.length(), sb.length() + 1);
    generate(n, left, right + 1, result, sb.append(')'));
    //sb.delete(sb.length(), sb.length() + 1);
}

public static void main(String[] args) {
    // TODO Auto-generated method stub
    System.out.println(generateParenthesis(4));
    }
}

结果不是我想要的: (((()))), (((()))))())), (((()))))())))()), (((())))) ())))()))(), (((()))))())))()))()))(())), (((()))))( ))))()))()))(())))()).........

有人告诉我这是什么问题吗?非常感谢。

【问题讨论】:

  • 请更具体。不要只经过一行括号并期望我们计算它们并将其与您的递归算法进行比较。 IOW,“结果不是我想要的”到底是什么意思?
  • 您是否尝试过调试您的代码并查看为什么没有创建您期望用来产生某些输出的路径?我想你很快就会发现你有什么错误的逻辑或缺少的逻辑。如果您已经进行了调试,也许您有一个更具体的问题,例如,鉴于这一系列递归调用等,为什么这个 if 语句无助于创建这种输出模式。

标签: java algorithm


【解决方案1】:

你小心点。你的错误是:

  • 尝试重置sb,而不是仅删除其最后一个字符
  • 你想重置sb的方式:

    • 通过使用sb = new StringBuilder();,您正在重新分配sb,它是current 方法的局部变量,而不是调用它的方法的变量(Java 不是通过引用传递,而是通过传递-值)。
    • 您几乎正确的尝试件评论了sb.delete(sb.length(), sb.length() + 1);,但在这里您实际上是在尝试删除从位置sb.length()开始的字符,但就像StringBuilder中字符的数组索引从0sb.length() <strong>- 1</strong>一样在最后一个字符之后删除一个字符,这实际上无法删除任何内容。

      你需要的是

      sb.delete(sb.length() - 1, sb.length());
      

      或更具可读性

      sb.deleteCharAt(sb.length() - 1);
      

      但在性能方面可能是最佳方法setLength(在答案底部描述)

      sb.setLength(sb.length() - 1); 
      
  • when 从 StringBuilder 中删除字符的逻辑也存在缺陷

    • 您只在一个结束(回溯)递归调用的地方执行此操作:在找到正确的结果之后。但是像if (left &lt; right)最重要的是,如果方法将正常结束

      这样的其他情况呢?
      generate(3, 1, 1, ')');
      generate(3, 1, 2, ')');//here we stop and backtrack
      

      这里generate(3, 1, 2, ')'); 结束并从sb 中删除最后一个字符,但以前的方法generate(3, 1, 1, ')') 不应该也删除它自己添加到StringBuilder 的) 吗?

    换句话说,您不应该只在递归调用的成功条件结束时删除最后一个字符,而是在每次递归调用之后,以确保该方法也将删除它添加的字符。

所以把你的代码改成类似

public static void generate(int n, int left, int right, ArrayList<String> result,
        StringBuilder sb) {

    if (left < right) {
        return;
    }
    if (left == n && right == n) {
        result.add(sb.toString());
        return;
    }
    if (left == n) {
        generate(n, left, right + 1, result, sb.append(')'));
        sb.deleteCharAt(sb.length() - 1);// <--
        return;
    }
    generate(n, left + 1, right, result, sb.append('('));
    sb.deleteCharAt(sb.length() - 1);// <--
    generate(n, left, right + 1, result, sb.append(')'));
    sb.deleteCharAt(sb.length() - 1);// <--
}

或者尝试写一些更易读的东西,比如

public static void generate(int maxLength, int left, int right,
        ArrayList<String> result, StringBuilder sb) {
    if (left + right == maxLength) {
        if (left == right)
            result.add(sb.toString());
    } else if (left >= right) {
        generate(maxLength, left + 1, right, result, sb.append('('));
        sb.deleteCharAt(sb.length() - 1);

        generate(maxLength, left, right + 1, result, sb.append(')'));
        sb.deleteCharAt(sb.length() - 1);
    }
}

但在调用时您需要将maxLength 设置为2*n,因为它是StringBuilder 应包含的最大长度,因此您还必须将generateParenthesis(int n) 更改为:

public static ArrayList<String> generateParenthesis(int n) {

    ArrayList<String> result = new ArrayList<String>();
    StringBuilder sb = new StringBuilder(2 * n);

    generate(2 * n, 0, 0, result, sb);
    //       ^^^^^
    return result;
}

进一步改进:

如果您的目标是性能,那么您可能不想使用deletedeleteCharAt,因为每次它都会创建新数组并用您不想要的值的副本填充它。

相反,您可以使用setLength 方法。如果您传递的值小于当前存储的字符数,它将设置count 为在此方法中传递的值,这将有效地使它们后面的字符无关紧要。换句话说,这些字符将不再用于例如toString(),即使它们仍在 StringBuilder 缓冲区数组中。

例子:

StringBuilder sb = new StringBuilder("abc");  // 1
sb.setLength(2);                              // 2
System.out.println(sb);                       // 3
sb.append('d');                               // 4
System.out.println(sb);                       // 5
  • 在第 1 行中,StringBuilder 将为至少 3 字符分配数组(默认情况下,它使用str.length() + 16 来确定它现在将存储的缓冲字符数组的大小)并将从传递的字符串中放置字符,所以它将包含

    ['a', 'b', 'c', '\0', '\0', ... , '\0']
                     ^^^ - it will put next character here 
    

    下一个字符应该放置的位置索引存储在count字段中,现在它等于3

  • 在第 2 行中,count 的值将设置为 2,但我们的数组不会改变,所以它仍然看起来像

    ['a', 'b', 'c', '\0', '\0', ... , '\0']
               ^^^ - it will put next character here 
    
  • 在第 3 行中,将创建并打印新字符串,但它只会填充位于存储在 count 中的索引之前的字符,这意味着它将仅包含 ab(数组仍将保持不变)。

  • 在第 4 行中,您将向缓冲区添加新字符,并将其放置在“重要”字符之后。由于重要字符的数量存储在count字段中(并且它们被放置在数组的开头),因此下一个不相关字符必须在count指向的位置,这意味着d将被放置在索引为@987654366的位置@ 这意味着现在数组看起来像

    ['a', 'b', 'd', '\0', '\0', ... , '\0']
                     ^^^ - it will put next character here 
    

    count 的值将递增(我们只添加了一个字符,因此 count 现在将变为 3)。

  • 在第 5 行,我们将创建并打印包含 StringBuilder 使用的数组中第一个 3 字符的字符串,因此我们将看到 abd

【讨论】:

  • 哇,非常感谢,Pshemo!你的回答对我帮助很大!
  • @tonymiao 不客气,如果你有兴趣,我会添加改进。
【解决方案2】:

在仔细推出这个程序后,我发现了问题所在。正确的代码如下:

import java.util.ArrayList;

public class generateParentheses {
    public static ArrayList<String> generateParenthesis(int n) {

    ArrayList<String> result = new ArrayList<String>();
    StringBuilder sb = new StringBuilder();

    generate(n, 0, 0, result, sb);
    return result;
}

public static void generate(int n, int left, int right, ArrayList<String> result,
        StringBuilder sb) {

    if (left < right) {
        return;
    }
    if (left == n && right == n) {
        result.add(sb.toString());
        //sb.delete(0,sb.length());
        return;
    }
    if (left == n) {
        generate(n, left, right + 1, result, sb.append(')'));
        //delete current ')'.
        sb.delete(sb.length() - 1, sb.length());
        return;
    }

    generate(n, left + 1, right, result, sb.append('('));
    //delete current '(' after you finish using it for next recursion.
    sb.delete(sb.length() - 1, sb.length());
    generate(n, left, right + 1, result, sb.append(')'));
    //same as above here.
    sb.delete(sb.length() - 1, sb.length());
}

public static void main(String[] args) {
    // TODO Auto-generated method stub
    System.out.println(generateParenthesis(4));


    }

}

结果是: (((()))), ((()())), ((())()), ((()))(), (()(())), (()() ()), (()())(), (())(()), (())()(), ()((())), ()(()()), ( )(())(), ()()(()), ()()()()

【讨论】:

    【解决方案3】:

    参数、赋值和返回的组合:

    ..., StringBuilder sb,...
        //...
        sb = new StringBuilder();
        return sb;
    

    没有意义,因为参数是按值传入的,即sb是一个局部变量,改变它对调用环境没有影响。

    如果你想清除 StringBuilder,有一个方法 sb.delete(start, end) 会真正影响通过 sb 引用的对象。

    【讨论】:

      【解决方案4】:

      另一种更简单的方法。 在这里,我正在尝试一个递归解决方案,其中递归函数以 3 种方式添加平衡括号:函数返回的 HashSet 中的每个项目的“()”+result 和 result+“()”和“(”+result+")”它调用然后将它们放入 HashSet 以删除重复项。

           import java.util.HashSet;
           import java.util.Set;
      
           public class BalancedParanthesis {
             public static void main(String args[]){
               int noOfBrackets = 3;
               HashSet<String> hs=new HashSet(generate(noOfBrackets));
               System.out.println(hs);
               }
               public static HashSet<String> generate(int in)
               {
                HashSet<String> hs= new HashSet<String>();
                if(in ==1)
               {
              hs.add("()");
              return hs;
               }
              else{
                 Set<String> ab=generate(in-1);
                 for(String each:ab)
                  {
                  hs.add("("+each+")");
                  hs.add("()"+each);
                  hs.add(each+"()");
                  }
              return hs;
               }
              }
             }
      

      【讨论】:

      • 请为您在这里所做的事情添加更多上下文。
      • 我已经编辑了答案来解释我做了什么。
      【解决方案5】:

      您的代码效率低下。这是一个更好的方法。我们对 2^N 可能的括号排列进行了有效的强力修剪(如果当前参数没有可能的有效解决方案,我们立即退出递归)。 这是 C++ 中的代码:

      #include <iostream>
      #include <vector>
      #include <string>
      using namespace std;
      string result;
      vector<string> solutions;
      int N = 10;
      void generateBrackets(int pos, int balance)
      {
          if(balance > N-pos) return;
          if(pos == N)
          {
              //we have a valid solution
              //generate substring from 0 to N-1
              //and push it to the vector
              string currentSolution;
              for(int i = 0; i < N; ++i)
              {
                  currentSolution.push_back(result[i]);
              }
              solutions.push_back(currentSolution);
              return;
          }
          result[pos] = '(';
          generateBrackets(pos+1, balance+1);
          if(balance > 0)
          {
              result[pos] = ')';
              generateBrackets(pos+1, balance-1);
          }
      }
      int main()
      {
          result.assign(N, 'a');
          generateBrackets(0, 0);
          cout<<"Printing solutions:\n";
          for(int i = 0; i < solutions.size(); ++i)
          {
              cout<<solutions[i]<<endl;
          }
          return 0;
      }
      

      一些澄清: pos 标记我们正在生成的括号解决方案中的当前位置。如果我们到达位置 N(这意味着解决方案是有效的),我们在 string result 变量中有一个有效的解决方案,我们只需将它推入具有有效解决方案的向量中。现在对于我们使用的余额,您可能会问。我们观察到,为了使括号置换有效,在任何给定位置的任何时间,'(' 从一开始的计数必须大于')' 的计数,并且平衡我们测量它们的差异,所以只有当余额 > 0 时,我才会在给定位置放置一个 ')'。

      【讨论】:

      • 但他并没有传递“完整列表”。他正在传递对 List 的引用,这是一个可以忽略不计的成本。并且发布 C++ 解决方案并不能真正帮助修复 Java 程序。
      • 请删除“将整个列表作为参数传递”效率低下的说法。不复制列表(这可能会导致一些开销) - 仅传递一个引用。
      • 另外,即使大型数据结构确实被复制,也不会不会导致stackoverflow;它可能导致 OutOfMemory。堆栈溢出通常是由于递归调用下降了太多级别。
      • 并且 OP 想在 Java 中使用它,所以请遵守或弃权。谢谢。
      • 提出了一些有效的观点。好吧,我相信他把它翻译成Java不会有问题,算法在这里很重要。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-06-05
      • 1970-01-01
      • 2015-05-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多