Scheme中Binary Heap与Binary Search Tree插入对比及堆插入位置困惑
嘿,我太懂你这种从BST转堆时的困惑了——毕竟这俩数据结构的核心逻辑完全不在一个频道上!BST靠谓词比较导航左右,堆却要严格遵循完全二叉树的填充规则:新元素必须插在最后一层的最左空位,要是最后一层满了,就去下一层的最左端。下面给你两种在Scheme里实现的思路,从简单到复杂,你可以按需选:
方法一:用数组模拟堆(最省心高效)
堆的本质是完全二叉树,用数组存储的话,父子节点的索引有天然的数学关系,完全不用费劲找插入位置——直接把新元素追加到数组末尾就行!之后只需要做「上浮(heapify up)」操作,把新元素调整到符合堆性质的位置。
举个小例子(以小顶堆为例,谓词用<):
; 堆插入:先把元素加在末尾,再上浮调整 (define (heap-insert heap comp-func val) (let ((new-heap (append heap (list val)))) (heapify-up new-heap comp-func (- (length new-heap) 1)))) ; 上浮操作:不断和父节点比较,不符合堆性质就交换 (define (heapify-up heap comp-func idx) (if (= idx 0) ; 已经到根节点了,直接返回 heap (let ((parent-idx (quotient (- idx 1) 2))) ; 父节点索引计算:(i-1)//2 (if (comp-func (list-ref heap idx) (list-ref heap parent-idx)) ; 子节点比父节点更符合堆性质,交换后继续上浮 (heapify-up (swap heap idx parent-idx) comp-func parent-idx) heap)))) ; 辅助函数:交换数组中两个位置的元素 (define (swap lst i j) (let ((temp (list-ref lst i))) (list-set! (list-set! lst i (list-ref lst j)) j temp) lst))
这种方式的好处是完全不用纠结「找位置」,数组的末尾就是天然的下一个可用位置,后续的上浮操作也很容易实现。
方法二:用树形结构实现(适合理解原理)
如果你坚持想用类似BST的节点结构(比如每个节点存值、左孩子、右孩子),那得先找到正确的插入路径。这里有个小技巧:
- 先统计当前堆的节点总数
n - 把
n+1转成二进制,去掉最高位的1,剩下的每一位对应路径:0代表左孩子,1代表右孩子 - 沿着这个路径走到最后,就是新元素的插入位置
举个例子:如果当前堆有5个节点(二进制101),n+1=6是110,去掉最高位的1剩下10,那路径就是「右→左」,意思是从根节点先往右走,再往左走,那个空位就是插入点。
对应的Scheme代码大概是这样(假设你已经定义了make-node、node-val、node-left、node-right这些节点操作函数):
; 树形堆插入 (define (tree-heap-insert root comp-func val) (if (null? root) (make-node val '() '()) (let ((insert-path (get-insert-path (count-nodes root)))) (insert-along-path root insert-path comp-func val)))) ; 统计节点总数 (define (count-nodes root) (if (null? root) 0 (+ 1 (count-nodes (node-left root)) (count-nodes (node-right root))))) ; 生成插入路径:把n+1转二进制,去掉最高位后转成左右方向 (define (get-insert-path n) (let ((binary-str (number->string (+ n 1) 2))) (map (lambda (c) (if (char=? c #\0) 'left 'right)) (cdr (string->list binary-str))))) ; 沿着路径插入新节点,插入后还要做上浮调整(这里省略了,你可以参考数组版的heapify逻辑扩展) (define (insert-along-path node path comp-func val) (if (null? path) (make-node val '() '()) (let ((dir (car path))) (if (eq? dir 'left) (make-node (node-val node) (insert-along-path (node-left node) (cdr path) comp-func val) (node-right node)) (make-node (node-val node) (node-left node) (insert-along-path (node-right node) (cdr path) comp-func val))))))
注意:插入节点后别忘了做堆调整(不管是上浮还是下沉),不然堆的性质就被破坏了哦!
最后提个小建议
如果是实际写代码,优先选数组模拟的方式——不仅代码更简洁,运行效率也更高,毕竟Scheme里列表的末尾追加虽然方便,但用可变数组(比如vector)会更高效,你也可以把上面的数组换成vector来优化。
内容的提问来源于stack exchange,提问作者nwelch

