使用搜索找到树中最深的节点,然后移动它

Using search to find the deepest node in a tree and then moving it

我有一棵树,我想找到到最深节点的最长路径,然后我想以某种方式更改它以使其更加平衡。在这个非常简单的示例中,我想移动 4,因为它是最深的路径,而不是将它放在 1 中,这样高度差异就不会那么大。我想在 lisp 中执行此操作,但我不太确定如何,我知道我想使用搜索但它有它,所以它实际上确实找到了最长的。我知道如何获得从根到给定节点的路径,但我不确定如何真正获得最深的节点。我想我有一个关于如何将最深的节点放在更好的部分上的想法,但任何建议都会很棒。

到目前为止,我只有一些代码可以 return 树的深度,但是我可以通过任何方式得到它 return 最深的节点是什么)?

(defun maxdepth (l)
  (cond ((null l) 0)
    ((atom l) 0)
    (t (+ 1 (max (maxdepth (cadr l)) (maxdepth (caddr l)))))))

您可以使用深度优先搜索,每次(每个节点)扩展一个节点时增加一个计数器,保留所有已探索节点的列表,然后解析它以找到最高的计数器,这应该是最深的路径.

根据您对 maxdepth 的实施,我假设如果节点有分支,则树的格式为 (value left-branch &optional right-branch),如果是叶节点,则为 value

因为不这样做会导致每次迭代都必须重建整个树的病态情况,所以 (value) 也是叶节点,(value nil)(value nil nil) 也是.

我还假设您想移动现有树中的分支,而不是在不同的地方创建一个分支的新树。

如果是这样,您的目标是保留在计算树的 maxdepth 时遇到的最后一个非叶节点。

新函数的工作方式与 maxdepth 完全相同,只是它将 return 三个值而不是一个。对于您的图表,表示为 (1 (2 (3 4))),它将 return:

3
(3 4)
LEFT

...其中3是深度,(3 4)是最深的非叶节点,LEFT 意味着 4 在该节点的左分支 (CADR) 而不是 右 (CADDR) 分支。

这意味着每次它使用自己的 return 值时,它必须捕获所有三个 return 值以便它可以重新 return 它们,并且需要特殊处理 确保我们不会将“4”作为 return 值,您不会 能够移动。

因为我们要折腾 3 个值,所以我们必须将我们自己的 max1+ 版本定义为宏(lbetterp 是自由变量下面。它们的值分别是 maxdepthl 参数和要使用的比较运算符:

(defmacro my-max (value-form1 value-form2)
  (let ((n1 (gensym))
    (v1 (gensym))
    (n2 (gensym))
    (v2 (gensym))
    (pos1 (gensym))
    (pos2 (gensym)))
    `(multiple-value-bind (,n1 ,v1 ,pos1) ,value-form1
       (multiple-value-bind (,n2 ,v2 ,pos2) ,value-form2
     (if (funcall betterp ,n1 ,n2)
         (apply #'values (cons ,n1 (if (consp ,v1)
                                       (list ,v1 ,pos1)
                                       (list l 'left))))
         (apply #'values (cons ,n2 (if (consp ,v2)
                                       (list ,v2 ,pos2)
                                       (list l 'right)))))))))

(defmacro my-1+ (value-form)
  (let ((number (gensym))
    (value (gensym))
    (position (gensym)))
    `(multiple-value-bind (,number ,value ,position) ,value-form
       (values (1+ ,number) ,value ,position))))

因为叶节点可以是一个裸值,也可以是一个列表 非空成员,我们需要一些函数来帮助消除这些差异 出去。第一个函数识别叶子,第二个函数将任何叶子转换为裸值:

(defun leafp (val)
  (or (not (listp val))
      (= (length (remove nil val)) 1)))

(defun leaf-value (leaf)
  (if (listp leaf)
      (car leaf)
      leaf))

然后是 maxdepth 函数本身,我只对它做了一些小改动:

(defun maxdepth (l &optional (betterp #'>=))
  (cond ((null l) (values -1 l nil))
        ((leafp l) (values 0 (leaf-value l) nil))
        (t (my-1+ (my-max (maxdepth (cadr l) betterp) (maxdepth (caddr l) betterp))))))

最大的变化是,通过将 #'<= 作为 betterp 参数传递,可以将 maxdepth 变成 mindepth。这是因为我们必须在树上找到我们找到的深度值要去的地方。

此外,l = NIL 被视为具有 -1 的深度。这样一来,如果您有分支 (8 NIL 10),最深的值将是 RIGHT 上的 10 而不是 `NIL,它可能根本不是一个节点,只是一个存根节点已删除或尚未添加。

移动节点,一旦找到,可以使用(setf (cdr x))(setf (cddr x))来完成。但首先,您需要编写一个 mindepth 函数:

(defun mindepth (tree)
    (maxdepth tree #'<=))

我想出了这个函数来实际将值从最深的节点移动到最浅的节点:

(defun balance-tree (l)
  (multiple-value-bind (deepest-depth deepest-node deepest-side)
      (maxdepth l)
    (multiple-value-bind (shallowest-depth shallowest-node shallowest-side)
         (mindepth l)
      (let ((max-value (case deepest-side
                        ((left) (prog1
                                    (cadr deepest-node)
                                 (setf (cadr deepest-node) nil)))
                        ((right) (prog1
                                    (caddr deepest-node)
                                   (setf (cddr deepest-node) nil))))))
         (case shallowest-side
          ((left) (cond ((= (length shallowest-node) 1)
                         (setf (cdr shallowest-node) (list max-value)))
                        ((leafp (cadr shallowest-node))
                         (setf (cadr shallowest-node)
                         (list (leaf-value (cadr shallowest-node)) max-value)))
                        (t (setf (cadr shallowest-node) max-value))))
          ((right) (case (length shallowest-node)
                      ((1) (setf (cdr shallowest-node)
                                 (list nil max-value)))
                      ((2) (setf (cddr shallowest-node)
                                 (list max-value)))
                      ((3) (cond ((null (caddr shallowest-node))
                                  (setf (caddr shallowest-node)
                                        max-value))
                                 ((leafp (caddr shallowest-node))
                                  (setf (caddr shallowest-node)
                                        (list (leaf-value (caddr shallowest-node))
                                              max-value)))
                                 (t (error "Didn't think this was reachable!"))))))))))
         ;; Return value: the modified tree.
         l)

如果在我测试用的树(1 (2 (3 4)) (5 (6 (7 (8 nil 10)))))上反复调用这个函数,最终 树在以下两种状态之间交替:

(1 (2 (3 4) 10) (5 (6 (7 NIL)) (8 NIL)))
(1 (2 (3 NIL 4) 10) (5 (6 (7 NIL)) (8 NIL)))

...两者的 maxdepth 均为 3.

在您的原始示例中使用时,它会产生正确的结果:

CL-USER> (balance-tree '(1 (2 (3 4))))
(1 (2 (3 NIL)) 4)

...相当于:

(1 (2 3) 4)

如果你想将 (3 NIL) 之类的实例转换为普通的 3,有一个 功能,但它会遍历整棵树并重建它,所以请谨慎使用:

(defun clean-tree (tree)
  (cond
    ((null tree) nil)
    ((atom tree) tree)
    ((= (length (remove nil tree)) 1)
     (car tree))
    ((and (= (length tree) 3)
          (null (caddr tree)))
     (setf (cddr tree) nil)
     (clean-tree tree))
    (t
     (unless (null (cadr tree))
       (setf (cadr tree)
             (clean-tree (cadr tree))))
     (unless (null (caddr tree))
       (setf (caddr tree)
             (clean-tree (caddr tree))))
       tree)))

由于这两种情况,它只是 return 其参数或其参数的函数,clean-tree 应始终以 setf 形式使用:

(setf tree (clean-tree tree))

这是上面示例输出之一的 clean-tree 的输出:

CL-USER> (clean-tree '(1 (2 (3 4) 10) (5 (6 (7 NIL)) (8 NIL))))
(1 (2 (3 4) 10) (5 (6 7) 8))