是否可以在内存开销不变的情况下重新排列数组?

时间:2019-07-12 06:24:44

标签: arrays algorithm in-place

我在阅读一些StackOverflow问题时偶然发现了这个问题,但是找不到令人满意的答案。

假设我们有一个长度为array A的{​​{1}},其中以随机方式包含从n0的索引。是否可以重新排列数组,使其结果为n-1且具有恒定的A[ A[i] ]内存开销?

示例:

O(1)

如果可能,该算法的概述将是不错的选择。否则,解释为什么这是不可能的。由于就地排序是一件很重要的事情,因此可能与此类似。

说明: 如果没有内存限制,该算法将很简单:

A = [ 3, 2, 0, 1 ]
rearrange( A )
A = [ 1, 0, 3, 2 ]

结果A = [ 3, 2, 0, 1 ] A_r = Array_of_size( A ) for i = 0 to Arraysize - 1 A_r[ i ] = A[ A[i] ]

3 个答案:

答案 0 :(得分:7)

有可能。

输入数组由作为同一数组中索引的值组成,因此它是一个或多个循环的集合。每个循环可以包含1个或多个元素。

阵列的变异最好逐周期执行,因为一个周期的更改只需要在该周期中临时存储一个值,而将“下一个”值复制到“上一个”插槽中,直到已经访问了整个周期,并且可以将临时值放在最后一个时隙中。

要考虑的事情之一是,在一个周期这样突变之后,它可能不会导致相同长度的周期。例如,如果一个循环的长度为4,则该突变将导致2个循环的2个值。更一般地,一个长度相等的循环将分为两个循环。奇数循环保持其长度,只是顺序改变了。

一旦一个周期发生突变,该算法就永远不要“偶然”重新将其任何值应用于该周期。

确保不发生这种情况的一种方法是,仅在从左到右的迭代中访问其最右元素时,才将突变应用于循环。这是所提出算法的关键。这样一来,可以确保该循环中的元素永远不会再次访问,也不会再次突变。

这是JavaScript的实现。您可以在输入字段中输入数组值。该算法在每次输入更改时执行:

function arrange(a) {
    function isRightmostOfCycle(i) {
        let j = a[i]
        while (j < i) j = a[j]
        return j == i
    }

    function updateCycle(i) {
        let saved = a[i]
        let k = i
        for (let j = a[i]; j < i; j = a[j]) {
            a[k] = a[j]
            k = j
        }
        a[k] = saved
    }

    for (let i = 0; i < a.length; i++) {
        if (isRightmostOfCycle(i)) updateCycle(i)
    }
    return a
}

// I/O handling
let input = document.querySelector("input")
let output = document.querySelector("pre");
(input.oninput = function () {
    let a = (input.value.match(/\d+/g) || []).map(Number)
    // Check whether input is valid
    let i = a.findIndex((_,i) => !a.includes(i))
    output.textContent = i < 0 ? arrange(a) : "Missing " + i
})();
input { width: 100% }
Array values: <input value="2,0,5,7,6,4,1,8,3"><p>
Result: 
<pre></pre>

检查索引是否代表循环的最右元素具有时间复杂度 O(n),因此总时间复杂度为 O(n²)。但是,额外的空间复杂度是恒定的。

答案 1 :(得分:2)

如果结合使用基于孔的链交换,并且确实可以通过排列中数组的最低或最高位置来重新排列排列中的循环,则确实是可能的。如果我们正在查看的职位在该周期中最高,那么我们仅对每个职位进行循环。基于孔的方法确保我们仅使用恒定空间来执行循环的旋转。缺点是时间复杂度变为O(n^2)。这是一个简单的python实现:

def is_start(array, index):
    start = index
    index = array[index]
    while start > index:
        index = array[index]
    return start == index


def rotate(array, index):
    start = index
    next = array[index]
    hole = next
    while start > next:
        array[index] = array[next]
        index = next
        next = array[index]
    array[index] = hole


def rearrange(array):
    for index in range(len(array)):
        if is_start(array, index):
            rotate(array, index)
  

错误修复

     

(@ trincot)指出,使用循环的最后一个条目而不是第一个条目可以确保我们从不调查可能损坏的循环。   效果:将不等式的方向从start < index更改为start > index

答案 2 :(得分:-1)

for i in range(0, n):
    a[i] += (a[a[i]] % n) * n

for i in range(0, n):
    a[i] //= n

解释这有点困难。如您所见,我们通过向其添加a[a[i]]乘以n的值来增加数组条目的值。现在,可能会发生a[a[i]]本身在增加条目期间以相同方式更改的情况。但是,只有两种可能的情况:

a[i]包含(a[i]的原始值)+(a[a[i]]的原始值)* n

a[i]包含(原始值a[i]

我们可以通过将原始语句更改为以下两种情况来规范这两种情况: a[i] = a[i] + (a[a[i]] % n) * n,如果尚未更改,则为a[i] % n = a[i],或者a[i] % n = a[i]的原始值,为((original a[i]) + k * n) % n = (original a[i])。请注意,a[i]的元素小于n

因此,在第一个循环结束时,这是每个a[i]的场景:

new a[i] = old a[i] + (old a[a[i]]) * n

我们要做的只是将每个新的a[i]除以n,对于(r + q * a) // a = q范围内的所有整数r[0, a)来划分。

此后,new a[i] = old a[a[i]]

相关问题