【问题标题】:Comparison function for DFS ordering of an arbitrary tree任意树的 DFS 排序的比较函数
【发布时间】:2025-11-24 13:30:02
【问题描述】:

想象一个完整的二叉树,其中每个深度级别的节点从左到右编号。

  • 节点 1 有子节点 2 和 3。
  • 节点 2 有子节点 4 和 5。
  • 节点 3 有孩子 6 和 7。

等等

在深度 D 将有 2^D 个节点,编号为 2^D ... 2^(D+1)-1

任何深度的完整树的深度优先搜索遍历都是确定性的。

例如,将始终遍历深度为 4 的树: 1,2,4,8,9,5,10,11,3,6,12,13,7,14,15。

我正在寻找一种方法来对数字列表进行排序,根据它们在任何树的 DFS 遍历中的位置来排序。

特别是,我想要一个比较函数,它可以接受两个数字并确定在 DFS 遍历中哪个先出现。

有什么想法吗?


为某个最大树大小预先计算 DFS 遍历是一种方法,但我更喜欢不需要计算和存储该信息的数学解决方案。

【问题讨论】:

  • 您可以使用一些术语来代替您所写的内容:“理想树”->“完全二叉树”、“遍历..是常数”->“遍历..是确定性的”
  • 谢谢,我已经更新了问题。

标签: algorithm tree


【解决方案1】:

性能最好的算法将是 FUD 建议的算法,因为您只需要遍历树一次,然后比较将只是 O(1)

但是,如果您不想遍历整个树,而只想要一个比较器,则有一个O(log n) 比较器(可以优化为O(log log n),或者实际上是O(1))。

想法是:

观察1:如果两个节点的深度相同,则编号较大的节点将被稍后遍历。

观察 2:如果两个节点不在同一深度上,通过注意总是先访问父节点,然后再访问后代,我们将较深节点的祖先与较浅节点的深度相同。然后使用观察 1 进行比较。

使用完整二叉树中的数字系统,您可以通过获取n/2 来获得节点n 的父节点。所以,在得到每个节点的深度之后(可以在O(log n) 中完成,或者预先计算),比如说d1 < d2,我们用2^(d2-d1) 划分更深的节点(幂可以在O(log p) 中完成,在这种情况下@ 987654331@ 是O(log n),所以它是O(log log n))。然后看看哪个更大=)

在 C++ 中:

// This method can be modified to be faster
// See: http://*.com/questions/671815/what-is-the-fastest-most-efficient-way-to-find-the-highest-set-bit-msb-in-an-i
int depth(int n){
    int result=-1;
    for(;n>0; result++, n/=2);
    return result;
}

bool n1_before_n2(int n1, int n2){
    int d1 = depth(n1);
    int d2 = depth(n2);
    if(d1>d2) n1 >>= (d1-d2);
    if(d2>d1) n2 >>= (d2-d1);
    return n1<n2;
}

【讨论】:

  • 编号为 n 的节点的深度,您可以使用 floor(log(2, n)) 在 O(1) 中获得。不过,我不确定我是否遵循您的回答-在我们发现哪个更深之后,我们除以 2 以得到深度差异的幂?那将如何比较相同深度的两个节点,例如2 & 3?
  • log(2,n) 理论上不是O(1),但是好吧,你可以假设。如果您有相同深度的节点,我们使用第一个观察,稍后访问编号较高的节点。进行划分以获取更深节点的祖先。祖先将与另一个节点处于相同的深度。
【解决方案2】:

这是@Abhishek 答案的 C 实现:

//returns -1 if a before b; 0 if same; else 1
int treesort(unsigned int a,unsigned int b)
{
  int diff, swap=1, side=0;
  unsigned int ra, rb;
  if (a==b) return 0;  
  //ensure deeper node is always in b.
  if (b<a) { int t=a;a=b;b=t;swap=-1;}
  //treat 0 as before everything else
  if (a==0) return -1*swap;
  //clear all but the msb
  ra=base(a); rb=base(b);
  //move up to same level, tracking child side
  while (rb!=ra)  { side=b&1;rb/=2;b/=2; }
  //compare parents at same level
  diff = (b-rb)-(a-ra);
  //if same parent, answer depends on side.
  if (diff==0) diff = side;
  //restrict to [-1,1], be sure to handle swap
  return (diff>0?-1:1)*swap;
}

base 是一个清除除最高有效位之外的所有内容的函数。我使用来自this questionSir Slick's msb32() 测试了
int base(unsigned int x){return 1&lt;&lt;(msb32(x)-1);}

【讨论】:

    【解决方案3】:

    您可以预先计算完整树的最大深度。并为每个节点分配一组递增的值,例如在你的深度 4 树中

    v[1]=1, v[2]=3, v[4]=3 ...
    

    那么比较函数就是

    int cmp(i,j):
        return v[i]<v[j]
    

    【讨论】:

    • 当然。我想要一个数学解决方案,它不涉及对 DFS 算法进行编码,也不涉及随着树的大小而增加的存储。我将更新问题以指定。
    • DFS 对于具有结构的树来说效率很低,例如,如果在 2^10 - 1 个节点的完整二叉树中找到 3 的索引,它需要 2^9 次遍历,而我们知道完整BT 左子树的节点数是非根节点总数的一半,因此我们可以很容易地推导出它的公式。
    【解决方案4】:

    如果您注意到这种模式,您可以尝试以下方法。我不确定,它可能需要一些改进,但它为您提供了一些进一步探索的想法。

    1. 对于每个节点,找出小于该值的 2 的最大幂。例如,如果我们比较 7 和 13,那么对于 7,它将是 4,对于 13,它将是 8。

    2. 现在计算这些值和它们各自 2 的幂之间的差值。对于 7,它将是 3,对于 13,它将是 5。

    3. 如果两者的幂相同,则差值较小的先于差值较大的。

    4. 对于我们的例子,计算 13 与 7 在同一行中的父级。这将是 13 / 2^(7 和 13 之间的级别差)。

    因此,13 的父母 = 13 / 2^1 = 6。
    由于 6

    编辑:按照建议,删除了不必要的步骤 3。

    【讨论】:

    • 您不需要第 2 步和第 3 步。第 4 步和第 5 步将涵盖所有情况。
    【解决方案5】:

    这是问题的数学解决方案,但我无法得到方程的封闭形式:-

    在总节点 N 的树中找到节点 k 的 DFS 索引: -

    DFS-order(k) = DFS-order((k-1)/2) + 2^(log(N+1) - log((k-1)/2) - 1)       if k is odd
    
    DFS-order(k) = DFS-order(k/2) + 1             if k is even 
    
    Base Case: DFS-order(1) = 0
    

    你可以为上面的方程找到一个封闭的形式,我认为这是更高层次的数学。

    解释:-

    对于奇数节点,我们首先遍历父节点的所有左子树节点,并为新索引加 1。我们知道左子树有一半的节点都以节点的父节点为根,不包括父节点。完整 BT 中的节点总数为2^d - 2,不包括根。左子树有一半2^(d-1) - 1。 d 是父级树根的深度。以节点 k 为根的树的深度为Total depth - log(k)。总深度为log(N+1)。因此,左子树中的节点数为2^(log(N+1) - log((k-1)/2) - 1) - 1。我们将 1 用于从父节点到当前节点的另一次遍历,因此 final sum = 2^(log(N+1) - log((k-1)/2) - 1)。我们将它添加到父索引以获取当前节点索引,因此 DFS-order(k) = DFS-order((k-1)/2) + 2^(log(N+1) - log((k-1)/2) - 1)

    对于偶数节点,它的索引是父索引+1是微不足道的

    注意:方程可以找到节点k在O(log(k))时间的索引&log函数使用地板算子得到离散值。

    【讨论】: