排列的堆算法

时间:2015-07-15 08:42:44

标签: algorithm language-agnostic permutation pseudocode

我正在准备面试,而且我正在努力记住Heap的算法:

procedure generate(n : integer, A : array of any):
    if n = 1 then
          output(A)
    else
        for i := 0; i < n; i += 1 do
            generate(n - 1, A)
            if n is even then
                swap(A[i], A[n-1])
            else
                swap(A[0], A[n-1])
            end if
        end for
    end if

这个算法非常有名,可以生成排列。它简洁而快速,与代码一起生成组合。

问题是:我不想记住事情,我总是试图将概念保持在&#34;演绎&#34;算法稍后。

这个算法真的不直观,我无法找到解释它对我自己有用的方法。

在生成排列时,有人可以告诉我为什么 这个算法如何正常工作?

4 个答案:

答案 0 :(得分:5)

Heap的算法可能不是任何合理的面试问题的答案。有一种更直观的算法可以按字典顺序产生排列;虽然它是分摊O(1)(每个排列)而不是O(1),但它在实践中并没有明显变慢,并且在运行中更容易推导出来。

词典顺序算法非常容易描述。给定一些排列,找到下一个:

  1. 找到最右边的元素,该元素小于右边的元素。

  2. 将具有最小元素的元素交换为大于它的元素。

  3. 将排列的部分反转到该元素所在的右侧。

  4. 步骤(1)和(3)都是最坏情况的O(n),但很容易证明这些步骤的平均时间是O(1)。

    表明Heap的算法是多么棘手(在细节中)是你的表达略有错误,因为它做了一次额外的交换;如果n是偶数,则额外交换是无操作,但当n为奇数时会产生问题。请参阅https://en.wikipedia.org/wiki/Heap%27s_algorithm了解正确的算法(至少今天是正确的)或参见Heap's algorithm permutation generator

    的讨论

    要了解Heap的算法是如何工作的,您需要查看循环的完整迭代对偶数和奇数情况的影响。给定偶数长度的向量,Heap算法的完整迭代将使向量向左旋转一个位置。如果向量具有奇数长度,则在算法结束时,向量将被反转。您可以使用归纳证明这两个事实都是正确的,尽管这并不能说明为什么它是真的。查看维基百科页面上的图表可能会有所帮助。

答案 1 :(得分:0)

Heap算法构造所有排列的原因是它将每个元素与其余元素的每个排列相邻。当您执行Heap算法时,偶数长度输入上的递归调用将元素n, (n-1), 2, 3, 4, ..., (n-2), 1放在最后位置,奇数长度输入上的递归调用将元素n, (n-3), (n-4), (n-5), ..., 2, (n-2), (n-1), 1放在最后位置。因此,在任何一种情况下,所有元素都与n - 1元素的所有排列相邻。

如果您想了解更详细的图解说明,请查看this article

答案 2 :(得分:0)

我发现了一篇试图在此解释它的文章:Why does Heap's algorithm work?

但是,我认为很难理解它,所以提出了一个希望更容易理解的解释:

请假设这些陈述暂时成立(我稍后会再说明):

每次调用&#34;生成&#34;功能

(I)其中n为奇数,在完成时将元素保留为完全相同的顺序。

(II)其中n为偶数,将元素向右旋转,例如ABCD变为DABC。

所以在&#34;对于i&#34; -loop

  • n甚至是

    递归调用&#34;生成(n-1,A)&#34;不会改变顺序。

    因此for循环可以迭代地将i = 0 ..(n-1)处的元素与(n-1)处的元素交换,并且将调用&#34; generate(n-1,A)& #34;每次都缺少另一个元素。

  • n是奇数

    递归调用&#34;生成(n-1,A)&#34;已经正确地旋转了元素。

    因此索引0处的元素将始终是一个不同的元素。

    只需在每次迭代中交换0和(n-1)处的元素,以生成一组唯一的元素。

最后,让我们看看为什么初始陈述是正确的:

旋转右

(III)这一系列掉期导致向右旋转一个位置:

A[0] <-> A[n - 1]
A[1] <-> A[n - 1]
A[2] <-> A[n - 1]
...
A[n - 2] <-> A[n - 1]

例如,尝试使用序列ABCD:

A[0] <-> A[3]: DBCA
A[1] <-> A[3]: DACB
A[2] <-> A[3]: DABC

空操作

(IV)这一系列步骤使序列保持与以前完全相同的顺序:

Repeat n times:
    Rotate the sub-sequence a[0...(n-2)] to the right
    Swap: a[0] <-> a[n - 1]

直观地说,这是真的:

如果序列长度为5,则将其旋转5次,最后不变。

在旋转之前将元素取为0,然后在旋转之后将新元素交换为0不会改变结果(如果旋转n次)。

感应

现在我们可以看到为什么(I)和(II)是真的:

如果n为1:     平凡的是,在调用函数后,顺序没有改变。

如果n为2:     递归调用&#34;生成(n-1,A)&#34;保持顺序不变(因为它调用第一个参数为1的生成)。     所以我们可以忽略这些调用。     在此调用中执行的交换导致右旋转,请参阅(III)。

如果n为3:     递归调用&#34;生成(n-1,A)&#34;导致右旋。     因此,此调用的总步数相等(IV)=&gt;序列没有变化。

重复n = 4,5,6,......

答案 3 :(得分:-2)

我知道这个问题是要求语言不可知,但Ruby在许多情况下读取比伪代码更好,并且它更容易记忆,因为它非常紧凑:

def gen_perms (list, n=list.size - 1 )

  return p list if n.zero? 

  for i in 0..n do
    gen_perms list, n - 1
    list[1], list[n] = list[n], list[1] if n.even?
    list[i], list[n] = list[n], list[i] if n.odd?
  end
end

list = ['a','b','c']

gen_perms list