【问题标题】:Algorithm for BASH/CSH/ZSH style brace expansionBASH/CSH/ZSH 样式大括号扩展的算法
【发布时间】:2011-06-03 17:39:23
【问题描述】:

如果我有一个像

这样的字符串
a/{b,c,d}/e

那么我希望能够产生这个输出:

a/b/e
a/c/e
a/d/e

你明白了。我需要在 C 中实现这一点。我编写了一种蛮力类型的代码,我能够解析一对大括号(例如:/a/{b,c,d}/e/,但如果有多对大括号,例如 /a/{b,c}/{d,e}/f 在这种情况下我的方法会失败。我想采取更好的方法。

我不是直接要求代码,只是对有效算法的提示就足够了。我认为解析大括号的任务是重复的,我们可以遵循递归算法吗?

【问题讨论】:

  • 看起来像一个简单的正则表达式解析。看看有限状态机(FSM 又名确定性有限自动机,DFA)。
  • 是的,这也是我最初的想法,我对自动机理论的概念似乎很粗略。也许我应该重新拿起这本书并修改概念:)
  • 你当然可以看看zsh/bash/csh源代码(xpandbraces in glob.c for zsh, brace_expand in @ 987654332@ 为bash 等)

标签: c algorithm string-parsing brace-expansion


【解决方案1】:

如果您在任何类型的 Unix、Linux 或 OS X 系统上,都有一个内置的库函数可以执行此操作。 man 3 glob 将告诉您如何从 C 中调用它。或者您可以访问 http://linux.die.net/man/3/glob 查找在线文档。

如果你想自己滚动,一个简单的方法是首先扫描字符串并构建一个中间数据结构,然后递归地遍历该数据结构,打印字符串。该数据结构可以由具有以下字段的结构构建:

  • 文本:指向一段字符串的指针
  • next_node:指向打印时该文本之后的内容的指针
  • sibling_node:指向下一个可以代替这个选择的选择的指针

【讨论】:

  • +1 用于讲述 glob(3)。它似乎解决了我的问题,虽然大括号扩展功能是非标准添加,但它在 Linux 和 BSD 上可用,这几乎是我想要的。虽然在这个问题上花了这么多时间,但自己解决如何做会很棒。 :)
  • @Abhinav Upadhyay:我添加了一个方向,您可以自行选择。
  • 哇,您的解决方案看起来如此简单。 +1
【解决方案2】:

您在此处显示的内容并不是真正的递归。如果你可以嵌套括号,那将是递归的。

基本上你所拥有的是一个简单的语法:

thing ::= element { "/" element }*
element ::= symbol || list
list ::= "{" symbol { "," symbol }* "}"
symbol ::= [a-z]+

这是一种即兴的语法语言。 * 表示“零个或多个”,+ 表示“1 个或多个”。相当普遍。

因此,您需要一个简单的标记器,它可以将符号分组并主要分离标点符号。

然后是一个简单的解析器

parseThing() {
    Element e = parseElement();
    while (nextToken != null) {
        Slash s = parseSlash();
        e = parseElement():
    }
}

Slash parseSlash() {
    Token t = peekNextToken();
    if (t.getText().equals("/")) {
        return new Slash();
    }
    throw "expected a '/' but got a " + t;
}

Element parseElement() {
    Token t = peekNextToken();
    if (t.isSymbol()) {
        return parseSymbol();
    }
    if (t.isOpenCurly()) {
        return parseList());
    }
    thrown "Syntax error, wanted a symbol or { and got " + t;
}

List parseList() {
    List l = new List();
    Token t = peekNextToken();
    if (t.isOpenCurly()) {
        consumeNextToken();
        Symbol s = parseSymbol();
        l.add(s);
        t = peekNextToken();
        while (t.isComma()) {
            consumeNextToken();
            s = parseSymbol();
            l.add(s);
            t = peekNextToken();
        }
        if (!t.closeCurly()) {
            throw "expected close of list, but got " + t;
        }
        consumeNextToken();
     } else {
         throw "expected start of list but got " + t;
     }
     return l;
}

Symbol parseSymbol() {
    Token t = peekNextToken();

    if(!t.isSymbol()) {
        throw "expected symbol, got " + t;
    }
    consumeNextToken();
    return new Symbol(t);
}

这是不完整的,而且是高级别的,但可以让您了解如何去做。

【讨论】:

  • 其实可以嵌套括号。试试echo {foo,b{a,i}r},你会得到3行。
  • @btilly:但我认为 Will Hartung 的观点是提问者没有描述递归,只是重复。 Shell 提供了递归解析的可能性这一事实很酷,但递归并不是问题所要求的。
  • @iconoclast:提问者要求“BASH/CSH/ZSH 样式的大括号扩展”,并描述了他的代码遇到的第一个问题。如果这是他想要的,嵌套大括号将是他遇到的下一个问题。
【解决方案3】:

我最近一直在做这样的事情,我花了很多时间来解决这个问题,所以这就是我的做法。不过可能有一个更简单的算法。

您可以编写递归下降解析器将文本转换为树。使包含该字符串的字符串叶节点和匹配的大括号对成为内部节点。每个叶子节点可以包含多个字符串。

例如,这个:

/a/{b,c}/{d,e{f,g,h}}/i

可以变成:

(
   ["/a/"]
   {
      ( ["b"] )
      ( ["c"] )
   }
   ["/"]
   {
      ( ["d"] )
      (
         ["e"]
         {
            ( ["f"] )
            ( ["g"] )
            ( ["h"] )
         }
      )
   }
   ["i"]
)

尝试将其视为一棵树,其中["stringA", "stringB"] 表示叶节点,匹配的大括号对表示内部节点。内部节点有两种类型,一种可以在其中一种选择之间进行选择(我在这个例子中使用{}),另一种可以组合所有组合(我在这里使用())。

所以,上面的树应该是这样的:

(
   ["/a/"]
   {
      ["b"]
      ["c"]
   }
   ["/"]
   {
      ["d"]
      (
         ["e"]
         {
            ["f"]
            ["g"]
            ["h"]
         }
      )
   }
   ["i"]
)

然后

(
   ["/a/"]
   ["b", "c"]
   ["/"]
   {
      ["d"]
      (
         ["e"]
         ["f", "g", "h"]
      )
   }
   ["i"]
)

然后

(
   ["/a/"]
   ["b", "c"]
   ["/"]
   {
      ["d"]
      ["ef", "eg", "eh"]
   }
   ["i"]
)

然后

(
   ["/a/"]
   ["b", "c"]
   ["/"]
   ["d", "ef", "eg", "eh"]
   ["i"]
)

最后,你会得到一个叶子节点,这是所有的组合:

["/a/b/di", "/a/b/efi", "/a/b/egi", "/a/b/ehi",
 "/a/c/di", "/a/c/efi", "/a/c/egi", "/a/c/ehi"]

然后你就可以打印出来了。

【讨论】:

  • 在这种情况下是一个有用的技巧。如果从字符串的末尾到开头工作,则更容易构建数据结构。那是因为这样你就有了父节点和/或兄弟节点,你永远不需要回溯。
【解决方案4】:

不知道效率如何,但一种直观的方法是使用某种形式的递归。该函数应该能够找到第一个大括号。假设第一个大括号包含 N 个选项。因此该函数产生 N 次扩展,并在每次扩展时递归调用自身。每个“叉子”都不断地叉子,直到用尽每个大括号。

这有帮助吗?

【讨论】:

  • 是的,我也想过类似的方法,但我没有尝试实现它,因为它需要在每个级别的递归中跟踪大量字符串,并且需要连续的内存分配。也就是说,这似乎是一种可行的方法。也许可以使用链表来存储字符串。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-01-10
  • 1970-01-01
  • 2012-02-07
  • 1970-01-01
  • 1970-01-01
  • 2021-04-12
  • 1970-01-01
相关资源
最近更新 更多