# 将二叉树写到磁盘上

## 出来吧，二叉搜索树

``````(defclass <node> ()
((data
:accessor node-data
:initarg :data
:documentation "节点中的数据")
(left
:accessor node-left
:initarg :left
:documentation "左子树")
(right
:accessor node-right
:initarg :right
:documentation "右子树"))
(:documentation "二叉搜索树的节点"))``````

``(deftype <bst> () '(or <node> null))``

``````(defun make-node (data left right)
"创建一个二叉搜索树的节点"
(check-type data integer)
(check-type left <bst>)
(check-type right <bst>)
(make-instance '<node>
:data data
:left left
:right right))

(defun make-empty-bst ()
"创建一颗空树"
nil)``````

``````(defun empty-bst-p (bst)
"检查BST是否为一个空的二叉搜索树"
(null bst))``````

``````(defun insert-node (bst data)
"往一颗现有的二叉搜索树BST中加入一个数据，并返回这颗新的二叉搜索树"
(check-type bst <bst>)
(check-type data integer)
(when (empty-bst-p bst)
(return-from insert-node
(make-node data
(make-empty-bst)
(make-empty-bst))))

(cond ((< data (node-data bst))
(setf (node-left bst)
(insert-node (node-left bst) data))
bst)
(t
(setf (node-right bst)
(insert-node (node-right bst) data))
bst)))``````

``````(defun create-bst (numbers)
"根据NUMBERS中的数值构造一棵二叉搜索树。相当于NUMBERS中的数字从左往右地插入到一棵空的二叉搜索树中"
(check-type numbers list)
(reduce #'(lambda (bst data)
(insert-node bst data))
numbers
:initial-value (make-empty-bst)))``````

``(defvar *bst* (create-bst '(2 1 3)))``

``````(defun print-spaces (n)
"打印N个空格"
(dotimes (i n)
(declare (ignorable i))
(format t " ")))

(defun print-bst (bst)
"打印二叉树BST到标准输出"
(check-type bst <bst>)
(labels ((aux (bst depth)
(cond ((empty-bst-p bst)
(format t "^~%"))
(t
(format t "~D~%" (node-data bst))
(print-spaces (* 2 depth))
(format t "|-")
(aux (node-left bst) (1+ depth))
(print-spaces (* 2 depth))
(format t "`-")
(aux (node-right bst) (1+ depth))))))
(aux bst 0)))``````

``````2
|-1
|-^
`-^
`-3
|-^
`-^``````

## 接下来终于要写盘了

1. 下标为0到3的4个字节，存储的是节点中的数据；
2. 下标为4到7的4个字节，存储的是节点的左子树在文件中的偏移；
3. 下标为8到11的4个字节，存储的是节点的右子树在文件中的偏移

1. 写入的总字节数`bytes`
2. 根节点所占据的字节数`root-bytes`

`bytes`便是右子树开始写入时的文件偏移，必须依靠这个信息确定右子树的每一个节点在文件内的偏移；使用`bytes`减去`root-bytes`，再加上左子树开始写入时的偏移量，便可以得知左子树的根节点在文件内的位置。最终实现写盘功能的代码如下

``````;;; 定义序列化二叉树的函数
(defun write-fixnum/32 (n stream)
"将定长数字N输出为32位的比特流"
(check-type n fixnum)
(check-type stream stream)
(let ((octets (bit-smasher:octets<- n)))
(setf octets (coerce octets 'list))
(dotimes (i (- 4 (length octets)))
(declare (ignorable i))
(push 0 octets))
(dolist (n octets)
(write-byte n stream))))

;;; 这是一个递归的函数，写入一棵二叉树的逻辑，就是先写入左子树，再写入右子树，最后写入根节点，也就是后序遍历
;;; 由于要序列化为字节流，因此需要用字节流中的偏移的形式代替内存中的指针，实现从根节点指向左右子树
;;; offset是开始序列化bst的时候，在字节流中所处的偏移，同时也是这颗树第一个被写入的节点在字节流中的偏移
;;; 每次调用write-bst-bytes后的返回值有两个，分别为二叉树一共写入的字节数，以及根节点所占的字节数
(defun write-bst-bytes (bst stream offset)
"将二叉树BST序列化为字节写入到流STREAM中。OFFSET表示BST的第一个字节距离文件头的偏移"
(check-type bst <bst>)
(check-type stream stream)
(check-type offset integer)
(when (empty-bst-p bst)
(return-from write-bst-bytes
(values 0 0)))

;; 以后序遍历的方式处理整棵二叉树
(multiple-value-bind (left-bytes left-root-bytes)
(write-bst-bytes (node-left bst) stream offset)

(multiple-value-bind (right-bytes right-root-bytes)
(write-bst-bytes (node-right bst) stream (+ offset left-bytes))

(write-fixnum/32 (node-data bst) stream)
(if (zerop left-bytes)
(write-fixnum/32 0 stream)
(write-fixnum/32 (- (+ offset left-bytes) left-root-bytes) stream))
(if (zerop right-bytes)
(write-fixnum/32 0 stream)
(write-fixnum/32 (- (+ offset left-bytes right-bytes) right-root-bytes) stream))
;; 之所以要加上12个字节，是因为在写完了左右子树之后，就紧邻着写根节点了。因此，根节点就是在从right-node-offset的位置，接着写完右子树的根节点后的位置，而右子树的根节点占12个字节
(let ((root-bytes (* 3 4)))
(values (+ left-bytes right-bytes root-bytes)
root-bytes)))))

(defun write-bst-to-file (bst filespec)
"将二叉树BST序列化为字节流并写入到文件中"
(check-type bst <bst>)
(with-open-file (stream filespec
:direction :output
:element-type '(unsigned-byte 8)
:if-exists :supersede)
(write-fixnum/32 (char-code #\m) stream)
(write-fixnum/32 (char-code #\y) stream)
(write-fixnum/32 (char-code #\b) stream)
(write-fixnum/32 (char-code #\s) stream)
(write-fixnum/32 (char-code #\t) stream)
(write-bst-bytes bst stream (* 5 4))))``````

``(write-bst-to-file *bst* "/tmp/bst.dat")``

169 声望
3.7k 粉丝
0 条评论