从数组创建最小堆 - 2 种方法

Creating min heap from array - 2 methods

我正在研究有关从数组构建最小堆的问题。我有两种方法——第一种是递归,第二种是使用 while 循环。递归方法通过了在线评分器的测试,但 while 循环版本似乎不起作用。我在下面的代码中生成了一些随机压力测试,发现这两种方法也给出了不同的答案。

请问我第二种方法有什么问题吗?题目如下:

Input Format. The first line of the input contains single integer . The next line contains space-separated integers .

Constraints. 1 ≤ ≤ 100 000; 0 ≤ , ≤ − 1; 0 ≤ 0, 1,..., −1 ≤ 109. All are distinct.

Output Format. The first line of the output should contain single integer — the total number of swaps.

must satisfy conditions 0 ≤ ≤ 4. The next lines should contain the swap operations used to convert the array into a heap. Each swap is described by a pair of integers , — the 0-based indices of the elements to be swapped. After applying all the swaps in the specified order the array must become a heap, that is, for each where 0 ≤ ≤ − 1 the following conditions must be true:

  1. If 2 + 1 ≤ − 1, then < 2+1.
  2. If 2 + 2 ≤ − 1, then < 2+2.

Note that all the elements of the input array are distinct. Note that any sequence of swaps that has length at most 4 and after which your initial array becomes a correct heap will be graded as correct.

我的代码:

# python3

from random import randint

swaps = []

def sift_down(i, n, data):
    min_index = i
    left_child = 2*i + 1
    right_child = 2*i + 2
    if left_child < n and data[left_child] < data[min_index]:
        min_index = left_child
    if right_child < n and data[right_child] < data[min_index]:
        min_index = right_child
    if i != min_index:
        swaps.append([i, min_index])
        data[i], data[min_index] = data[min_index], data[i]
        sift_down(min_index, n, data)

def build_heap(data):
    n = len(data)
    for i in range(n//2, -1, -1):
        sift_down(i, n, data)

    return swaps

# wrong answer using while loop instead of recursion
def build_heap2(data):
    swap = []
    for i in range(len(data)-1, 0, -1):
        current_node = i
        prev_node = i // 2 if i % 2 != 0 else i // 2 - 1

        while data[prev_node] > data[current_node] and current_node != 0:
            swap.append((prev_node, current_node))
            data[prev_node], data[current_node] = data[current_node], data[prev_node]
            current_node = prev_node
            prev_node = current_node // 2 if current_node % 2 != 0 else current_node // 2 - 1

    return swap


def main():
    # n = int(input())
    # data = list(map(int, input().split()))
    # assert len(data) == n
    
    while True:
        n = randint(1, 100000)
        data = []
        data2 = []
        for i in range(n):
            data.append(randint(0, 10^9))
        data2 = data.copy()
        
        swaps = build_heap(data)
        swaps2 = build_heap2(data2)
        
        
        if swaps != swaps2:
            print("recursion")
            print(data[0], len(data), len(swaps))
            print("loop:")
            print(data2[0], len(data2), len(swaps2))
            break
        
        else:
            print("success")
    
    swaps = build_heap(data)

    print(len(swaps))
    for i, j in swaps:
        print(i, j)

if __name__ == "__main__":
    main()

您的build_heap2实现了一个不正确的想法。它从树的底部开始(正确),然后在尚未堆化的树的上部冒泡值 up 树(错误)。这个不好。它不仅会报告错误的交换次数,而且不会始终提供有效的堆。例如,对于 [3, 1, 2, 4, 0],交换后的结果仍然不是堆,因为值 1 最终是 child of 3.

目的是在树的底部建立小堆,当parent节点的children变成堆后,parent中的值节点被筛选 down 到这些 child-heaps 中的任何一个。这是正确的,因为现在移动值正在 内移动一个已经堆化的子树。结果是这两个小堆的 parent 现在是有效堆本身的根。因此在算法结束时,根将成为有效堆的根。

因此,您需要向下交换(选择值最小的 child),而不是在树中向上交换值。

这是更正后的版本:

def build_heap(data):
    swap = []
    # We can start at the deepest parent:
    for i in range(len(data) // 2 - 1, -1, -1):
        current_node = i
        
        while True:
            child_node = current_node * 2 + 1
            if child_node >= len(data):
                break
            if child_node + 1 < len(data) and data[child_node + 1] < data[child_node]:
                child_node += 1
            if data[current_node] < data[child_node]:
                break
            # swap the current value DOWN, with the least of both child values
            swap.append((child_node, current_node))
            data[child_node], data[current_node] = data[current_node], data[child_node]
            current_node = child_node
    return swap

有(至少)两种构建堆的方法。

O(N) 解决方案从数据集的中间向开始反向工作,确保每个连续的元素在该点都是子树的正确根:

def build_heap_down(data):
    n = len(data)
    for subtree in range(n // 2 - 1, -1, -1):
        sift_down(subtree, n, data)

另一个解决方案,即 O(N log N),只是依次将每个元素添加到一个连续更大的堆中:

def build_heap_up(data):
    for new_element in range(1, n):
        sift_up(new_element, data)

因为在最坏的情况下 build_heap_up() 是 log-linear(我相信是 reverse-sorted 输入),它可能不满足你的任务的需要,它强加了线性受交换次数的约束。尽管如此,一些实验还是值得做的。也许这就是这项作业的意义所在。

def sift_up(elt, data):
    while elt > 0:
        parent = (elt - 1) // 2
        if data[parent] <= data[elt]: return
        swap(parent, elt, data)
        elt = parent

def sift_down(elt, limit, data):
    while True:
        kid = 2 * elt + 1
        if kid >= limit: return
        if kid + 1 < limit and data[kid + 1] < data[kid]: kid += 1
        if data[elt] <= data[kid]: return
        swap(elt, kid, data)
        elt = kid

这里的关键见解是 sift_upsift_down 都要求他们正在使用的数组是一个堆,除了被筛选的元素。 sift_down 适用于从筛选元素到末尾的数组,因此在整个数组上正确执行它需要向后工作。 sift_up 从开始到筛选元素处理数组,因此迭代必须向前进行。

据我所知,您的 build_heap 确实 build_heap_down。尽管它使用递归,但它与我上面的循环(以及来自 ); recursion at the very end of a function can always be turned into a simple loop using tail call elimination 的版本)做同样的事情。(一些语言自动执行此程序转换,但 Python 不是其中之一。)

您的 build_heap2build_heap_up 的错误版本,因为它向后工作而不是向前工作。这很容易修复。但是不要指望它会产生相同的堆,更不用说相同的交换列表了。可以从给定的数字列表构建许多可能的堆,这就是为什么可以找到 build_heap 而不是 sort.

的 O(N) 算法的原因