如何通过索引数组重新排列数组?

时间:2016-05-26 12:12:31

标签: javascript arrays algorithm sorting data-structures

给定一个数组arr和一系列索引ind,我想重新排列arr 就地以满足给定的索引。例如:

var arr = ["A", "B", "C", "D", "E", "F"];
var ind = [4, 0, 5, 2, 1, 3];

rearrange(arr, ind);

console.log(arr); // => ["B", "E", "D", "F", "A", "C"]

这是一种可能的解决方案,使用O(n)时间和O(1)空间,但变异ind

function swap(arr, i, k) {
  var temp = arr[i];
  arr[i] = arr[k];
  arr[k] = temp;
}

function rearrange(arr, ind) {
  for (var i = 0, len = arr.length; i < len; i++) {
    if (ind[i] !== i) {
      swap(arr, i, ind[i]);
      swap(ind, i, ind[i]);
    }
  }
}

如果我们仅限于O(1)空间并且不允许变异ind,那么最佳解决方案会是什么?

编辑:上述算法错误。请参阅this question

9 个答案:

答案 0 :(得分:9)

这是&#34;符号位&#34;解。

鉴于这是一个JavaScript问题,因此 ind 数组中指定的数字文字存储为有符号浮点数,输入使用的空格中有一个符号位。

该算法根据 ind 数组循环遍历元素,并将元素移动到位,直到它返回到该循环的第一个元素。然后它找到下一个循环并重复相同的机制。

ind 数组在执行期间被修改,但在算法完成时将恢复为原始数组。在你提到的其中一条评论中,这是可以接受的。

ind 数组由有符号浮点数组成,即使它们都是非负数(整数)。符号位用作指示值是否已经处理。通常,这可以被视为额外存储( n 位,即 O(n)),但由于存储已经被输入占用,因此不会额外获取空间。表示循环最左边成员的 ind 值的符号位不会改变。

编辑:我替换了~运算符的使用,因为它不会产生等于或大于 2 31 ,而JavaScript应该支持用作至少 2 32 - 1 的数组索引的数字。所以我现在使用 k = -k-1 ,它是相同的,但适用于整个范围的浮点数,可以安全地用作整数。请注意,作为替代方案,可以使用浮点数的小数部分(+/- 0.5)。

以下是代码:

&#13;
&#13;
var arr = ["A", "B", "C", "D", "E", "F"];
var ind = [4, 0, 5, 2, 1, 3];

rearrange(arr, ind);

console.log('arr: ' + arr);
console.log('ind: ' + ind);

function rearrange(arr, ind) {
    var i, j, buf, temp;
    
    for (j = 0; j < ind.length; j++) {
        if (ind[j] >= 0) { // Found a cycle to resolve
            i = ind[j];
            buf = arr[j];
            while (i !== j) { // Not yet back at start of cycle
                // Swap buffer with element content
                temp = buf;
                buf = arr[i];
                arr[i] = temp;
                // Invert bits, making it negative, to mark as visited
                ind[i] = -ind[i]-1; 
                // Visit next element in cycle
                i = -ind[i]-1;
            }
            // dump buffer into final (=first) element of cycle
            arr[j] = buf;
        } else {
            ind[j] = -ind[j]-1; // restore
        }
    }
}
&#13;
&#13;
&#13;

虽然算法有一个嵌套循环,但它仍然在 O(n)时间内运行:每个元素只发生一次交换,外部循环也只访问每个元素一次。

变量声明表明内存使用量是常量,但是注意到 ind 数组元素的符号位 - 在已经由输入分配的空间中 - 也被使用。

答案 1 :(得分:4)

索引数组定义了一个排列。每个排列都包含循环。我们可以通过跟随每个循环并在整个过程中替换数组元素来重新排列给定的数组。

这里唯一的问题是每个周期完全遵循一次。一种可能的方法是按顺序处理数组元素,并为每个元素检查通过该元素的循环。如果这样的循环触及至少一个具有较小索引的元素,则沿着该循环的元素已经被置换。否则,我们会遵循此循环并重新排序元素。

function rearrange(values, indexes) {
    main_loop:
    for (var start = 0, len = indexes.length; start < len; start++) {
        var next = indexes[start];
        for (; next != start; next = indexes[next])
            if (next < start) continue main_loop;

        next = start;
        var tmp = values[start];
        do {
            next = indexes[next];
            tmp = [values[next], values[next] = tmp][0]; // swap
        } while (next != start);
    }
    return values;
}

该算法只覆盖给定数组的每个元素一次,不会改变索引数组(甚至是暂时的)。其最坏情况的复杂性是O(n 2 )。但对于随机排列,其预期复杂度为O(n log n)(如related answer的注释中所述)。

该算法可以稍微优化一下。最明显的优化是使用短位集来保持当前位置之前的几个索引的信息(无论它们是否已经处理)。使用单个32位或64位字来实现此位集不应违反O(1)空间要求。这种优化可以提供小但明显的速度提升。虽然它不会改变最坏情况和预期的渐近复杂性。

为了优化更多,我们可以暂时使用索引数组。如果该阵列的元素至少有一个备用位,我们可以使用它来维护一个位集,允许我们跟踪所有处理过的元素,从而产生一个简单的线性时间算法。但我不认为这可以被视为O(1)空间算法。所以我假设索引数组没有备用位。

索引数组仍可为我们提供一些空间(比单个字大得多),用于前瞻位集。因为此数组定义了一个排列,所以它包含的信息比相同大小的任意数组少得多。 ln(n!)的斯特林近似给出n ln n位信息,而数组可以存储n log n位。自然对数和二进制对数之间的差异为我们提供了大约30%的潜在自由空间。如果阵列的大小不完全是2的幂,或者换句话说,如果仅部分使用高阶位,我们还可以提取高达1/64 = 1.5%或1/32 = 3%的可用空间。 (这些1.5%可能比保证30%更有价值)。

我们的想法是将所有索引压缩到当前位置的左侧(因为算法从不使用它们),使用压缩数据和当前位置之间的部分可用空间来存储前瞻位集(以提高性能)主要算法),使用自由空间的其他部分来提高压缩算法本身的性能(否则我们只需要二次压缩时间),最后将所有索引解压缩回原始形式。

要压缩索引,我们可以使用阶乘数系统:扫描索引数组以查找其中有多少小于当前索引,将结果放入压缩流,并使用可用空间一次处理多个值。

这种方法的缺点是,当算法进入阵列末端时会产生大部分可用空间,而当我们处于开始时,这个空间通常是需要的。因此,最坏情况的复杂性可能仅略低于O(n 2 )。如果不是这个简单的技巧,这也可能增加预期的复杂性:使用原始算法(没有压缩),虽然它足够便宜,然后切换到“压缩”变体。

如果数组的长度不是2的幂(并且我们有部分未使用的高阶位),我们可以忽略索引数组包含排列的事实,并将所有索引打包,就像在base中一样 - {{1数字系统。这样可以大大减少最坏情况的渐近复杂度,并在“平均情况下”加速算法。

答案 2 :(得分:1)

此提案使用了Evgeny Kluev的answer

如果已经处理了所有元素,但是索引没有达到零,我为更快的处理做了扩展。这是通过另一个变量count完成的,该变量对每个被替换的元素进行倒计时。如果所有元素都在正确的位置(count = 0),则用于离开主循环。

这对环很有帮助,就像第一个带

的例子一样
["A", "B", "C", "D", "E", "F"]
[ 4,   0,   5,   2,   1,   3 ]

index 5: 3 -> 2 -> 5 -> 3
index 4: 1 -> 0 -> 4 -> 1

两个环首先重新排列两个循环,而每个环有3个元素,count现在为零。这导致外部while循环短路。

&#13;
&#13;
function rearrange(values, indices) {
    var count = indices.length, index = count, next;

    main: while (count && index--) {
        next = index;
        do {
            next = indices[next];
            if (next > index) continue main;
        } while (next !== index)
        do {
            next = indices[next];
            count--;
            values[index] = [values[next], values[next] = values[index]][0];
        } while (next !== index)
    }
}

function go(values, indices) {
    rearrange(values, indices);
    console.log(values);
}

go(["A", "B", "C", "D", "E", "F"], [4, 0, 5, 2, 1, 3]);
go(["A", "B", "C", "D", "E", "F"], [1, 2, 0, 4, 5, 3]);
go(["A", "B", "C", "D", "E", "F"], [5, 0, 1, 2, 3, 4]);
go(["A", "B", "C", "D", "E", "F"], [0, 1, 3, 2, 4, 5]);
&#13;
&#13;
&#13;

答案 3 :(得分:0)

此答案已更新,以满足OP的条件

在这个答案中没有临时数组,并且ind数组不会以任何方式重新排序或排序。所有替换操作都在一次通过中完成。 getItemIndex函数只接收要使用的ind数组的浅部分。这只是通过利用隐藏在ind数组中的所有信息来完成的。

理解ind数组为我们保留所有历史记录是关键。

我们通过检查ind数组来获得以下信息。

  1. 通过查看项目,我们可以找到arr数组中相应项目的索引图。
  2. 每个项目索引告诉我们之前完成了多少交换。我们得到了历史。
  3. 每个项目索引还会告知是否存在与当前索引位置相关的先前交换,前一个元素的位置是什么。我们可以像ind.indexOf(i)那样做;无论如何这里是代码;
  4. 我添加了一些函数,例如Array.prototype.swap(),以便更轻松地解释代码。这是代码。

    Array.prototype.swap = function(i,j){
      [this[i],this[j]] = [this[j],this[i]];
      return this;
    };
    
    function getItemIndex(a,i){
      var f = a.indexOf(i);
      return f !=-1 ? getItemIndex(a,f) : i;
    }
    
    function sort(arr,ind){
      ind.forEach((n,i,x) => x.indexOf(i) > i ? arr.swap(i,x[i]) // item has not changed before so we can swap 
                                              : arr.swap(getItemIndex(ind.slice(0,i),i),x[i])); // item has gone to somwhere in previous swaps get it's index and swap
      return arr;
    }
    
    var arr = ["A", "B", "C", "D", "E", "F"],
        ind = [4, 0, 5, 2, 1, 3];
    
    
    console.log(sort(arr,ind),ind);

    好的,这段代码的最终版本是这样的。它非常简化,包括一个包含26个字母的测试用例。每次运行时,您将获得不同的纯随机唯一索引图。

    Array.prototype.swap = function(i,j){
      i !=j && ([this[i],this[j]] = [this[j],this[i]]);
      return this;
    };
    
    Array.prototype.shuffle = function(){
      var i = this.length,
          j;
      while (i > 1) {
        j = ~~(Math.random()*i--);
        [this[i],this[j]] = [this[j],this[i]];
      }
    return this;
    };
    
    var   arr = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"],
          ind = (new Array(arr.length)).fill("").map((e,i) => e = i).shuffle();
    console.log(JSON.stringify(arr));
    console.log(JSON.stringify(ind));
    
    function getItemIndex(a,i,j){
      var f = a.indexOf(i);
      return f < j ? getItemIndex(a,f,j) : i;
    }
    
    function sort(arr,ind){
      ind.forEach((n,i,x) => arr.swap(getItemIndex(ind,i,i),n));
      return arr;
    }
    console.log(JSON.stringify(sort(arr,ind)));
    console.log(JSON.stringify(ind));

    根据Trincot的评论,这很正常,它带有迭代getItemIndex()函数。

    Array.prototype.swap = function(i,j){
      i !=j && ([this[i],this[j]] = [this[j],this[i]]);
      return this;
    };
    
    Array.prototype.shuffle = function(){
      var i = this.length,
          j;
      while (i > 1) {
        j = ~~(Math.random()*i--);
        [this[i],this[j]] = [this[j],this[i]];
      }
    return this;
    };
    
    var   arr = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"],
          ind = (new Array(arr.length)).fill("").map((e,i) => e = i).shuffle();
    console.log(JSON.stringify(arr));
    console.log(JSON.stringify(ind));
    
    function getItemIndex(a,i){
      var f = a.indexOf(i),
          j;
      if (f >= i) return i; // this element hasn't been moved before.
      while (f < i) {       // this element has been swapped so get this elements current index
      	j = f;
      	f = a.indexOf(f);
      }
      return j;
    }
    
    function sort(arr,ind){
      ind.forEach((n,i,x) => arr.swap(getItemIndex(ind,i),n));
      return arr;
    }
    console.log(JSON.stringify(sort(arr,ind)));
    console.log(JSON.stringify(ind));

答案 4 :(得分:0)

var arr = ["A", "B", "C", "D", "E", "F"];
var ind = [4, 0, 5, 2, 1, 3];

function rearrange(arr, ind){
  var map = [];
  for (var i = 0; i < arr.length; i++)   map[ind[i]] = arr[i];
  for (var i = 0; i < arr.length; i++)   arr[i] = map[i];
}

rearrange(arr, ind);

console.log(arr);

这样可行但是,因为我不是一个聪明的开发者,我认为它可能不是最快的算法。

答案 5 :(得分:0)

下面,我们可以找到一个PARTIAL解决方案,用于我们只有一个周期的情况,即

    delete oldNode;

为了使这个解决方案适用于一般情况,我们需要找到每个独特循环的总数和一个索引。

对于var arr = ["A", "B", "C", "D", "E", "F"]; var ind = [4, 2, 5, 0, 1, 3]; function rearrange( i, arr, ind, temp ){ if( temp ){ if( arr[ind[i]] ){ var temp2 = arr[ind[i]]; arr[ind[i]] = temp; rearrange( ind[i], arr, ind, temp2 ); } else{ // cycle arr[ind[i]] = temp; // var unvisited_index = ...; // if( unvisited_index ) rearrange( unvisited_index, arr, ind, "" ); } } else{ if( i == ind[i] ){ if( i < arr.length ) rearrange( i + 1, arr, ind, temp ); } else{ temp = arr[ind[i]]; arr[ind[i]]=arr[i]; arr[i] = ""; i = ind[i]; rearrange(i, arr, ind, temp ); } } } rearrange( 0, arr, ind, "" ); 示例:

OP

有两个独特的周期:

var arr = ["A", "B", "C", "D", "E", "F"];
var ind = [4, 0, 5, 2, 1, 3];

如果运行

4 -> 1 -> 0 -> 4
5 -> 3 -> 2 -> 5

S(他)将获得rearrange( 0, arr, ind, "" ); rearrange( 5, arr, ind, "" ); 问题的理想输出。

答案 6 :(得分:0)

我当时不确定,但地图功能似乎确实按照要求进行了操作。这是一个选项,但由于我不知道.map的内部工作原理,所以我不能肯定这是你正在寻找的。

var arr = ["A", "B", "C", "D", "E", "F"];
var ind = [4, 0, 5, 2, 1, 3];

var temp = [];

for (var i = 0, ind_length = ind.length; i < ind_length; i++) 
{ 
    var set_index = ind[i];
    temp.push(arr[set_index]); 
    delete arr[set_index]; 
}

arr = temp;

另一个不使用map函数的解决方案可能如下所示:

{{1}}

这可以通过使用delete选项来充分利用空间,这也可以防止索引移位。由于它只进行一个循环,我想象执行速度相当快。由于命令非常简单和简单,因此这应该是一个可行的解决方案。这并没有被问到什么是交换没有使用额外的空间,但它非常接近。我很想回答像这样的问题,所以请......建设性的批评。

答案 7 :(得分:0)

试试这个:

var result = new Array(5);
for (int i = 0; i < result.length; i++) {
    result[i] = arr[ind[i]];
}
console.log(arr);

答案 8 :(得分:-2)

我在其自己的订单中使用ind作为索引

var arr = ["A", "B", "C", "D", "E", "F"];
var ind = [4, 0, 5, 2, 1, 3];
var obj = {}
for(var i=0;i<arr.length;i++)
    obj[ind[i]]=arr[i];
console.log(obj);

Working Fiddle