【问题标题】:Double Linked List in Common LispCommon Lisp 中的双链表
【发布时间】:2021-04-06 15:05:58
【问题描述】:

我想在SBCL中实现一个简单的双链表,键结构如下

(defstruct element
  (value 0 :type fixnum)
  (next nil :type element)
  (prev nil :type element))

问题是,实例化第一个元素是不可能的,因为 next 和 prev 都不可能是 nil。它们的类型是元素,所以它们必须是实例。

如果我删除 type 属性,这个问题基本上很容易解决。但事实是,这部分程序需要非常快,所以我想给优化器一个充分利用它的机会。

还有其他方法可以指定成员的类型并使其为零吗?或者更好:有没有办法通过 next 和 prev 引用实例本身来创建启动实例?

【问题讨论】:

    标签: data-structures initialization lisp common-lisp sbcl


    【解决方案1】:

    也可以设置层次结构:

    (defstruct element)
    
    (defstruct (null-element (:include element)))
    
    (defstruct (link-element (:include element))
      (value 0                   :type fixnum)
      (next  (make-null-element) :type element)
      (prev  (make-null-element) :type element))
    

    使用它:

    * (compile-file "linked-list.lisp")
    ; compiling file "/Users/joswig/Desktop/linked-list.lisp"
    ;   (written 08 APR 2021 12:58:56 PM):
    ; processing (DEFSTRUCT ELEMENT)
    ; processing (DEFSTRUCT (NULL-ELEMENT #))
    ; processing (DEFSTRUCT (LINK-ELEMENT #) ...)
    
    ; wrote /Users/joswig/Desktop/linked-list.fasl
    ; compilation finished in 0:00:00.032
    #P"/Users/joswig/Desktop/linked-list.fasl"
    NIL
    NIL
    * (load *)
    T
    * (make-link-element)
    #S(LINK-ELEMENT :VALUE 0
                    :NEXT #S(NULL-ELEMENT)
                    :PREV #S(NULL-ELEMENT))
    

    【讨论】:

      【解决方案2】:
      (defstruct element
        (value 0 :type fixnum)
        (next nil :type (or element null))
        (prev nil :type (or element null)))
      

      【讨论】:

        【解决方案3】:

        用 NIL 表示 null 元素并没有错,但如果你真的想强制类型,你可以按如下方式进行。

        ALLOCATE-INSTANCE 泛型函数(但显然不是INITIALIZE-INSTANCE)被指定用于STRUCTURE-CLASS 的实例。

        函数空元素的前向声明

        此函数返回类型 element 的空值。 使用 SBCL,如果您一次编译所有定义(即使用 compile-file),则不是绝对必要的。

        (declaim (ftype function null-element))
        

        定义结构

        values 的 initform 是对null-element 的调用,以便在形成新结构时该值满足类型。没有为此结构定义任何构造函数,以使手动构建它变得更加困难(并避免命名问题)。

        (defstruct (element (:constructor nil))
          (value 0 :type fixnum)
          (next (null-element) :type element)
          (prev (null-element) :type element))
        

        实例化一个元素

        这里的代码分配结构,但不初始化它。这避免了 sbcl 可能遇到的问题,它会自动检查其插槽的类型。这里,槽的内容在分配后是未定义的,但随后它们被设置为适当类型的值(元素本身)。

        (defun make-element (value)
          (let ((element (allocate-instance (find-class 'element))))
            (prog1 element
              (setf (element-value element) value
                    (element-next  element) element
                    (element-prev  element) element))))
        

        使用一个类,我会调用initialize-instance,但我认为这并不适用于所有实现。使用setf直接设置槽位即可。

        在这里,我同意您的问题,并让元素具有指向自身的链接。

        或者更好:有没有办法通过 next 和 prev 引用实例本身来创建起始实例?

        您也可以将字段设为(null-element),但这只是设计选择的问题。

        单例空元素值

        调用(null-element) 返回一个代表空元素的标记值。在这里,我在加载时分配一个element 实例,并在每次调用时返回相同的实例。存储在节点中的实际值并不重要。正如 RainerJoswig 所指出的,null 元素可能是没有此类值的结构的实例。

        (defun null-element ()
          (load-time-value (make-element 0)))
        
        (defun null-element-p (element)
          (eq element (null-element)))
        

        【讨论】:

          【解决方案4】:

          我想给优化器一个充分利用它的机会。

          如果您希望双向链表代码尽可能快,请不要使用nil 终止指针。

          使用哨兵节点终止链表,有效地使其循环。

          也就是说,空列表是一个指向自身的节点:

          (defun make-dl-list()
            (let ((do (make-element)))
               (setf (element-next sentinel) dl
                     (element-prev sentinel) dl)
                dl))
          

          列表的头部是(element-next list)。如果该值是列表本身,则列表为空:

           (defun dl-list-empty (dl)
             (eq (element-next dl) dl))
          

          在前面插入:

           (defun dl-insert-front (list node)
             (let ((head (element-next list)))
               (setf (elem-next node) head
                     (elem-prev node) list
                     (elem-prev head) node
                     (elem-next list) node)))
          

          删除节点:不需要指向列表的指针!

           (defun dl-delete-node (node)
             (let ((next (elem-next node))
                   (prev (elem-prev node))
               (setf (elem-next prev) next
                     (elem-prev next) prev)))
          

          注意这些操作都不执行任何测试;它们只是基本的代码块更新指针。

          【讨论】:

          • 在 SBCL (make-element) 中发出 TYPE-ERROR 的信号,因为 NIL 不是正确的类型,这就是问题所在。我尝试在 defstruct 周围使用 locally 块将安全级别降低到 0;如果插槽没有立即设置为正确的值,它确实会破坏环境,但如果设置了 next 和 prev,那么它也可以工作
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2013-05-16
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2011-03-31
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多