【问题标题】:How to add index numbers to the nodes of a tree如何将索引号添加到树的节点
【发布时间】:2026-01-30 18:15:01
【问题描述】:

假设我有这种数据类型来表示一棵树(rose tree):

type tree =
    | Function of string * tree list
    | Terminal of int

例如:

Function ("+", [Function ("*", [Terminal 5; Terminal 6]);
                Function ("sqrt", [Terminal 3])])

表示如下树((5 * 6) + sqrt(3)):

我想将这棵树转换为另一个树数据结构,称为“索引树”,其中包含每个节点的深度优先(或呼吸优先)索引。在上图中,我用深度优先索引标记了所有节点。

这是索引树的数据类型:

type index = int
type indexed_tree =
    | IFunction of index * string * indexed_tree list
    | ITerminal of index * int

这表示上图的索引树(深度优先):

IFunction (0, "+", [IFunction (1, "*", [ITerminal (2, 5); ITerminal (3, 6)]);
                    IFunction (4, "sqrt", [ITerminal (5, 3)])])

这表示上图的索引树(广度优先):

IFunction (0, "+", [IFunction (1, "*", [ITerminal (3, 5); ITerminal 4, 6)]);
                    IFunction (2, "sqrt", [ITerminal (5, 3)])])

现在的问题是:如何定义函数tree -> indexed_tree

我尝试采用 DFS 和 BFS 技术来保持堆栈,但我很快意识到这个问题是完全不同的。 DFS 和 BFS 只搜索一项,它们可以忽略树的其余部分。在这里,我试图用它们的索引号标记树的节点。我该怎么做?


编辑

下面是我在指定索引处获取子树的实现(深度优先索引和广度优先索引都已实现)。我无法看到如何调整此实现以将给定树转换为索引树。我尝试使用counter(参见下面的实现),但复杂的是深度优先遍历必须回溯,并且我不知道在回溯时如何传递计数器。

(* Helper function for subtree_index_dfs and subtree_index_bfs.
 * join_func should either be prepend (for depth-first), or postpend
 * (for breadth-first). *)
let subtree_index tree index join_func =
    let node_children = function
        | Terminal _ -> []
        | Function (_, children) -> children in
    let rec loop counter stack =
        match stack with
        | [] -> failwith "Index out of bounds"
        | (hd::_) when counter = index -> hd
        | (hd::tl) -> loop (counter + 1) (join_func (node_children hd) tl)
    in
    loop 0 [tree]

(* Get the subtree rooted at the specified index.
 * Index starts at 0 at the root of the tree and is ordered depth-first. *)
let subtree_index_dfs tree index =
    let prepend a b =
        a@b
    in
    subtree_index tree index prepend

(* Get the subtree rooted at the specified index.
 * Index starts at 0 at the root of the tree and is ordered breadth-first. *)
let subtree_index_bfs tree index =
    let append a b =
        b@a
    in
    subtree_index tree index append

(* Misc. *)
let rec string_of_tree t =
    match t with
    | Terminal i -> string_of_int i
    | Function (sym, children) ->
        let children_str = List.map (fun child -> string_of_tree child) children
        in
        "(" ^ sym ^ " " ^ String.concat " " children_str ^ ")"

let print_tree t =
    print_endline (string_of_tree t)

示例用法:

let () =
    let t1 = Function ("+", [Function ("*", [Terminal 5; Terminal 6]);
                             Function ("sqrt", [Terminal 3])])
    in
    print_tree (subtree_index_dfs t1 0);  (* (+ ( * 5 6) (sqrt 3)) *)
    print_tree (subtree_index_dfs t1 1);  (* ( * 5 6) *)
    print_tree (subtree_index_dfs t1 2);  (* 5 *)
    print_tree (subtree_index_dfs t1 3);  (* 6 *)
    print_tree (subtree_index_dfs t1 4);  (* (sqrt 3) *)
    print_tree (subtree_index_dfs t1 5);  (* 3 *)
    print_tree (subtree_index_dfs t1 6);  (* Exception: Failure "Index out of bounds". *)

【问题讨论】:

  • 这并没有什么不同。 DFS 和 BFS 可以重新表述为遍历算法,用于转换树而不是搜索任何内容。
  • 对于家庭作业问题,您应该包含一些您自己的代码,以证明您实际上已经自己进行了尝试,并指出您到底在努力解决什么问题。否则很难给出不只是提供解决方案的答案。正如代词先生所指出的,调整 DFS 和 BFS 算法来进行转换而不是搜索应该非常简单。获得正确的索引比较棘手,但有一个有效的转换函数是必要的第一步,所以我建议你从那开始。
  • @glennsl 这不是作业问题;我提出了问题并自己绘制了图表。问题是我完全被卡住了。我什至不知道如何绘制函数的骨架。我之前已经解决了一些相关的问题。例如,我使用 continuation-passing 样式解决了How do I get a subtree by index?
  • 即使不是家庭作业,有一些代码作为答案的基础仍然非常有帮助,可以稍微缩小问题范围,也许可以说明是什么让你卡住了。即使只是可以调整的工作 DFS 和/或 BFS 功能。我当然可以只写一个解决方案,但我认为这不是一个非常令人满意的答案,写这样的答案也不是特别令人满意。
  • CPS 似乎是正确的选择。我曾经向very similar problem 发布了一个方案(和伪代码)answer,其中还有两个答案,一个在 Scala 中,另一个在 JS 中。

标签: algorithm tree ocaml graph-algorithm


【解决方案1】:

这不是一个完整的答案,我认为同时要求深度优先和广度优先有点过于宽泛,因为我认为没有太多可以概括的内容,但我希望它至少能让你明白更进一步,也许会激发一些想法。

我认为您遇到的问题源于您当前的代码过于笼统。您基本上是将树转换为一个列表,效率相当低,然后索引到该列表中。然后您无法将列表转换回树,因为您已经丢弃了该信息。

深度优先遍历确实非常简单,并且很自然地适合递归实现。如果我们暂时忽略索引,tree -> indexed_tree 函数就是这样的:

let indexed tree =
  let rec loop = function
    | Terminal value -> ITerminal (0, value)
    | Function (name, children) -> IFunction (0, name, loop_children children)
  and loop_children = function
    | [] -> []
    | child :: rest -> loop child :: loop_children rest
  in loop tree

然后我们只需要填写索引。这是通过在递归时向上传递一个递增的索引来实现的,并在向下的过程中返回节点计数以及构造的子树,以便我们知道索引要增加多少:

let indexed_dfs tree =
  let rec loop i = function
    | Function (name, children) ->
      let (child_count, children') = loop_children (i + 1) children in
      (child_count + 1, IFunction (i, name, children'))
    | Terminal value -> (1, ITerminal (i, value))
  and loop_children i = function
    | [] -> (0, [])
    | child :: rest ->
      let (count, child') = loop i child in
      let (rest_count, rest') = loop_children (i + count) rest in
      (count + rest_count, child' :: rest')
in snd (loop 0 tree)

每个调用也可以只返回最后一个索引而不是计数,但我认为遵循两个独立的概念而不是重载一个概念更简单。

不幸的是,广度优先转换要复杂得多,因为我们不能轻易地构造一个新的树广度优先。但我们也不应该这样做。我认为不是跟踪总计数,而是通过传递迄今为止看到的级别的堆栈偏移量来跟踪每个级别的累积计数或偏移量,然后使用它来计算当前正在构建的节点的索引。

【讨论】:

  • 构造树广度优先lazily相对容易。严格语言中的相应方法是通过自上而下构建它,在递归调用中填充节点的子字段(是的,破坏性地,通过突变),拉尾递归模缺点。它在 C 中是最简单的,使用数组支持的树,它是无操作的。 (所有这些都是指二叉树)。