【问题标题】:How can I implement a splay tree that performs the zig operation last, not first?如何实现最后执行 zig 操作的展开树,而不是首先执行?
【发布时间】:2010-05-18 23:14:45
【问题描述】:

对于我的算法和数据结构课程,我的任务是在 Haskell 中实现一个展开树。我的splay操作算法如下:

  1. 如果要展开的节点是根,则返回未更改的树。
  2. 如果要展开的节点距根节点一层,则执行 zig 操作并返回结果树。
  3. 如果要展开的节点距根节点有两层或更多层,则对从该节点开始展开子树的结果执行 zig-zig 或 zig-zag 操作,并返回结果树。李>

我的老师说这是有效的。然而,the Wikipedia description of a splay tree 表示 zig 步骤“将仅作为展开操作的最后一步完成”,而在我的算法中,它是展开操作的第一步。

我想实现一个展开树,它最后而不是第一个执行 zig 操作,但我不确定如何最好地完成。在我看来,这样的算法会变得更加复杂,因为需要先找到要展开的节点,然后才能确定是否应该执行 zig 操作。

如何在 Haskell(或其他一些函数式语言)中实现这一点?

示例

在此示例中,我们搜索值 4,提示我们将其展开到树的顶部。

我的算法(zig 作为第一步)

1 1 4 \ \ / 2 之字形 2 之字形 2 \ --> \ ------> / \ 3 4 1 3 \ / 4 3

维基百科算法(zig 作为最后一步)

1 1 4 \ \ / 2 之字形 4 之字形 1 \ ------> / --> \ 3 3 3 \ / / 4 2 2

两棵树都是有效的,但它们的结构不同。我想用函数式语言实现第二个,最好是 Haskell。

【问题讨论】:

  • 这在技术上是家庭作业,但是由于算法已经被接受并且我已经提交了作业,所以我只是出于好奇而问。这就是为什么我没有将这个问题标记为作业。
  • 这里有一个很好的 Haskell Splay Trees 实现 haskell.org/pipermail/haskell-cafe/2010-October/085377.html

标签: haskell functional-programming splay-tree


【解决方案1】:

关键是为要展开的值构建路径,然后从底部重建树,如果可能的话一次两层(这样就可以做出 zig-zip 与 zig-zag 的决定):

data Tree a = Empty | Node a (Tree a) (Tree a)
    deriving (Eq, Show)

data Direction = LH | RH
    deriving (Eq, Show)


splay :: (Ord a) => a -> Tree a -> Tree a
splay a t = rebuild $ path a t [(undefined,t)]
    where path a Empty ps = ps
          path a n@(Node b l r) ps =
              case compare a b of
                  EQ -> ps
                  LT -> path a l $ (LH, l) : ps
                  GT -> path a r $ (RH, r) : ps

          rebuild :: (Ord a) => [(Direction,Tree a)] -> Tree a
          rebuild ((_,n):[]) = n
          rebuild ((LH,x):(_,p):[]) = zigL x p
          rebuild ((RH,x):(_,p):[]) = zigR x p
          rebuild ((LH,x):(LH,p):(z,g):ps) = rebuild $ (z, zigzigL x p g):ps
          rebuild ((RH,x):(RH,p):(z,g):ps) = rebuild $ (z, zigzigR x p g):ps
          rebuild ((RH,x):(LH,p):(z,g):ps) = rebuild $ (z, zigzagL x p g):ps
          rebuild ((LH,x):(RH,p):(z,g):ps) = rebuild $ (z, zigzagR x p g):ps

          zigL (Node x a b) (Node p _ c) = Node x a (Node p b c)
          zigR (Node x a b) (Node p c _) = Node x (Node p c a) b

          zigzigL (Node x a b) (Node p _ c) (Node g _ d) =
              Node x a (Node p b (Node g c d))

          zigzigR (Node x a b) (Node p c _) (Node g d _) =
              Node x (Node p (Node g d c) a) b

          zigzagL (Node x b c) (Node p a _) (Node g _ d) =
              Node x (Node p a b) (Node g c d)

          zigzagR (Node x b c) (Node p _ a) (Node g d _) =
              Node x (Node g d b) (Node p c a)

您可以在我的repo 中找到此代码以及可运行的单元测试和快速检查。

【讨论】:

  • reverse 是一个很小的牺牲,可以通过复杂的弃牌来避免。此代码不处理尝试展开不在树中的值。而且我能想到几个小清理……但已经晚了,我想告诉你基本的想法。我明天看看能不能再清理一下。
  • 重构了代码以使其清晰,并添加了可运行版本的链接和测试。享受吧!
  • 移除了反面,不需要奇数折叠。 (令人惊奇的是在淋浴时思考可以做什么!)
  • 这似乎效果很好!顺便说一句,我仍然不太了解计算算法复杂度。这是否与原始算法具有相同的复杂性?
  • 复杂性是一样的:path 向下扫描到要展开的元素。这当然是 O(log n),并且每一步都在做持续的工作(建立一个列表)。 rebuild 然后扫描该列表,每一步将其减少一到两个元素。它所做的工作是每步 O(1)(没有步骤涉及循环或遍历),因此 rebuild 是 O(log n)。最后,splay 只是两个 O(log n) 的进程串在一起,所以整体是 O(log n)。
【解决方案2】:

您确定您正确阅读了维基百科的描述吗?有“zig”、“zig-zig”和“zig-zag”三种步骤。 “zig”步骤定义为仅当x 是根的子级时才会发生。尽管有名称,但“zig-zig”和“zig-zag”步骤没有“zig”步骤作为第一个组件。

在我看来,您的实现在这方面遵循了 Wikipedia 的描述。

【讨论】:

  • 就像你说的,zig 被定义为只有当x 是根的孩子时才会发生的事情。然而,在我的算法中,它发生在树中的任何地方,作为整个展开操作的最后一步。我的问题与 zig-zig 和 zig-zag 步骤的实施无关。
  • 等等,这不符合维基百科的描述吗?在什么意义上它也是“第一步”?如果您可以发布一些代码,也许会更清楚?
  • 我认为我可以发布的任何代码都不会让我的问题更清楚,因为它只会遵循我已经发布的算法。不过,我可以汇总一个以两种不同方式展开示例树的示例。
  • 查看我的更新问题,了解两种算法之间的差异。
  • 原始论文在第 4 节中讨论了“自上而下”展开的方法,尽管仅在特定树和指针表示方案的上下文中,这使得抽象地看不清。该论文没有证明这些方法的效率引理也适用。因此,Jakob,您的原始方法是有效的展开树实现。尽管如此,尝试匹配“自下而上”的方法是一项有趣的练习。我很想知道这两种方法的 Haskell 代码比较谁。
【解决方案3】:

您可以参考this course,它有一个非常好的讲义,其中包含 OCaml 中用于 Splay 树的代码。

【讨论】:

  • 然而,该代码使用相同的算法。讲义指出“简单旋转的情况发生在树的底部而不是顶部。”
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-12-26
  • 1970-01-01
  • 1970-01-01
  • 2012-07-28
相关资源
最近更新 更多