【问题标题】:What is the most efficient way to reverse an array in Javascript?在Javascript中反转数组的最有效方法是什么?
【发布时间】:2011-07-13 17:07:48
【问题描述】:

最近有人问我,在 Javascript 中反转数组的最有效方法是什么。目前,我建议使用 for 循环并摆弄数组,但后来意识到有一个原生的 Array.reverse() 方法。

出于好奇,任何人都可以通过展示示例或指出正确的方向来帮助我探索这一点,以便我可以阅读吗?任何关于如何衡量性能的建议也很棒。

【问题讨论】:

  • 致未来读者的注意事项:此线程中的大多数答案效率低下(二次方)、不必要地难以阅读、错误或完全不正确。继续时要格外小心!我已经保护了这个问题(这个问题太宽泛了)以避免额外的低质量帖子。许多性能结论和基准已经过时或不正确。那些拥有关闭/删除投票权的人,我强烈建议清除此线程!

标签: javascript arrays performance


【解决方案1】:

基于此设置:

var array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
var length = array.length;

Array.reverse(); 是第一个或第二个最慢的!

基准在这里:

https://jsperf.com/js-array-reverse-vs-while-loop/9

跨浏览器,交换循环更快。有两种常见类型的交换算法(请参阅Wikipedia),每一种都有两种变体。

两种交换算法是临时交换和异或交换。

这两种变体以不同方式处理索引计算。第一个变体比较当前左索引和右索引,然后递减数组的右索引。第二种变体比较当前左索引和长度除以一半,然后为每次迭代重新计算右索引。

您可能会也可能不会看到这两种变体之间的巨大差异。例如,在 Chrome 18 中,临时交换和异或交换的第一个变体比第二个变体慢 60% 以上,但在 Opera 12 中,临时交换和异或交换的两个变体具有相似的性能。

临时交换:

第一个变体:

function temporarySwap(array)
{
    var left = null;
    var right = null;
    var length = array.length;
    for (left = 0, right = length - 1; left < right; left += 1, right -= 1)
    {
        var temporary = array[left];
        array[left] = array[right];
        array[right] = temporary;
    }
    return array;
}

第二个变体:

function temporarySwapHalf(array)
{
    var left = null;
    var right = null;
    var length = array.length;
    for (left = 0; left < length / 2; left += 1)
    {
        right = length - 1 - left;
        var temporary = array[left];
        array[left] = array[right];
        array[right] = temporary;
    }
    return array;
}

异或交换:

第一个变体:

function xorSwap(array)
{
    var i = null;
    var r = null;
    var length = array.length;
    for (i = 0, r = length - 1; i < r; i += 1, r -= 1)
    {
        var left = array[i];
        var right = array[r];
        left ^= right;
        right ^= left;
        left ^= right;
        array[i] = left;
        array[r] = right;
    }
    return array;
}

第二个变体:

function xorSwapHalf(array)
{
    var i = null;
    var r = null;
    var length = array.length;
    for (i = 0; i < length / 2; i += 1)
    {
        r = length - 1 - i;
        var left = array[i];
        var right = array[r];
        left ^= right;
        right ^= left;
        left ^= right;
        array[i] = left;
        array[r] = right;
    }
    return array;
}

还有另一种交换方法称为解构赋值: http://wiki.ecmascript.org/doku.php?id=harmony:destructuring

解构赋值:

第一个变体:

function destructuringSwap(array)
{
    var left = null;
    var right = null;
    var length = array.length;
    for (left = 0, right = length - 1; left < right; left += 1, right -= 1)
    {
        [array[left], array[right]] = [array[right], array[left]];
    }
    return array;
}

第二个变体:

function destructuringSwapHalf(array)
{
    var left = null;
    var right = null;
    var length = array.length;
    for (left = 0; left < length / 2; left += 1)
    {
        right = length - 1 - left;
        [array[left], array[right]] = [array[right], array[left]];
    }
    return array;
}

目前,使用解构赋值的算法是所有算法中最慢的。它甚至比Array.reverse(); 还要慢。然而,使用解构赋值和Array.reverse(); 方法的算法是最短的例子,它们看起来最干净。希望他们以后的表现越来越好。


另外值得一提的是,现代浏览器正在提高数组pushsplice 操作的性能。

在 Firefox 10 中,这种使用数组 pushsplicefor 循环算法可与临时交换和 XOR 交换循环算法相媲美。

for (length -= 2; length > -1; length -= 1)
{
    array.push(array[length]);
    array.splice(length, 1);
}

但是,您可能应该坚持使用交换循环算法,直到许多其他浏览器匹配或超过它们的数组 pushsplice 性能。

【讨论】:

  • 我认为应该注意上面所有的函数,除了临时交换,只适用于整数数组而不是其他类型的数组(字符串、对象等)。
  • 您的基准测试的“for push then slice”测试破坏了全局length,使其之后的所有测试都毫无意义。
  • 鲍里斯是对的:jsperf.com/js-array-reverse-vs-while-loop/9。此外,在谷歌浏览器中,array.reverse 比其他方法更快
  • 是的,在 2016 年的 Google Chrome 中,array.reverse 至少比其他任何东西快两倍。请参阅其他评论中的 jsperf 链接。
  • 这些代码建议质量很差(像 ANSI C 一样编写),并且基准不正确。不要过早地优化,如果你想要就地使用内置的array.reverse(),或者如果你想要一个副本使用array.slice().reverse()。标准库可能已经是最快的了,无疑是最容易理解的,而且随着时间的推移,它们的速度会随着时间的推移而免费提高。
【解决方案2】:

您可以通过简单的方式使用地图来做到这一点。

let list = [10, 20, 30, 60, 90]
let reversedList = list.map((e, i, a)=> a[(a.length -1) -i]) // [90, 60...]

【讨论】:

  • 这是一个不会改变原始数组的解决方案! +1
【解决方案3】:

原生方法总是更快。

所以尽可能使用Array.reverse。否则,在O(1) 中运行的实现将是最好的;)

否则就使用这样的东西

var reverse = function(arr) {
   var result = [],
       ii = arr.length;
   for (var i = ii - 1;i !== 0;i--) {
       result.push(arr[i]);
   }
   return result;
}

Benchmark!

如果您使用 for 构造的所有三个阶段而不是只使用一个阶段,那么有趣的循环会更快。

for(var i = ii - 1; i !== 0;i--)var i = ii - 1;for(;i-- !== 0;)

【讨论】:

  • @Matt McDonald 有趣,我们刚刚在大学里做了一个项目,证明不是这样,哈哈。尽管我们测试了各种功能,例如冒泡排序、堆排序、快速排序。像这样的事情的时间安排与阵列的布局方式有很大关系。 (是否已经排序,是否已经随机?)不同类型的排序/反向排序通常取决于数组的初始状态。很难证明哪个是最快的,因为它们有许多不同的场景。
  • 改进您的算法,然后重试。现在,Array.reverse(); 是第一个或第二个最慢的!
  • 在 Javascript 中,原生方法几乎总是较慢,因为语言的设计是 fundamentally broken。见bind vs. closurefor loop versus map; for versus .forEach。这是因为内置函数无法对数组做出您可以做出的假设,因此必须执行更多的测试。如果您担心性能,请考虑自己做循环或 lodash;使用本机方法以提高可读性。
  • @Raynos 你的例子坏了;它不会迭代整个数组。它应该是var i = ii - 1;i &gt;= 0;i--
  • for (var i = ii - 1;i !== 0;i--) { 在数组为空时给出一个无限循环。 ii = arr.length; 是一个毫无意义的变量。这是草率的代码。避免。 “否则,在 O(1) 中运行的实现将是最好的;)”是不可能的和令人困惑的,我认为这是一个笑话,因为眨眼,但为什么呢? “for(var i = ii - 1; i !== 0;i--)var i = ii - 1;for(;i-- !== 0;) 更快”只是不好的基准测试——初始化和递减是在循环初始化程序内部还是外部都无关紧要。
【解决方案4】:

opened a Firefox bug 关于 Firefox 中缓慢的反向性能。来自 Mozilla 的某个人查看了已接受帖子中使用的基准,并说它非常具有误导性——在他们的分析中,本机方法通常更适合反转数组。 (应该是这样!)

【讨论】:

  • 如果对这个主题感兴趣,人们需要通读整个错误案例。自从您发布答案以来,似乎还有更多内容,但我认为我肯定会坚持使用原生 Array.reverse()
【解决方案5】:

这是使用三元运算符反转数组的最有效和最简洁的方法。

function reverse(arr) {
  return arr.length < 2 ? arr : [arr.pop()].concat(reverse(arr));
}
console.log(reverse([4, 3, 3, 1]));

【讨论】:

  • 这根本不是有效的——它是二次的(在循环中调用concat,基本上为每个递归调用重新分配和构建整个结果数组)并且如果数组爆破堆栈由于递归而太大(尝试使用reverse(Array(10000).fill(0)))。
【解决方案6】:

交换函数是最快的。这是我编写的一个反向函数,它与上面提到的交换函数略有相似,但执行速度更快。

function reverse(array) {
  var first = null;
  var last = null;
  var tmp = null;
  var length = array.length;

  for (first = 0, last = length - 1; first < length / 2; first++, last--) {
    tmp = array[first];
    array[first] = array[last];
    array[last] = tmp;
  }
}

你可以在这里找到基准测试http://jsperf.com/js-array-reverse-vs-while-loop/19

【讨论】:

    【解决方案7】:

    由于没有人想出它并完成反转数组的方法列表......

    array.sort(function() {
        return 1;
    })
    

    它的速度是两种方法的两倍,但除此之外,速度非常慢。

    http://jsperf.com/js-array-reverse-vs-while-loop/53

    【讨论】:

    • 它不会反转数组;也许它曾经这样做过,但它依赖于实现。现在我在最新的稳定 V8 中对其进行了测试,它并没有逆转。事实上,它的顺序保持不变。但是,当我返回 -1 时,它反转了它。这可能是因为在 ES2019 中需要 .sort 方法来执行稳定的排序。但是,我仍然不会依赖它。
    • 有趣。它可能取决于用于排序的算法,所以是的,它本质上是特定于实现的。只要每个元素只被访问一次,-1 应该颠倒顺序,1 保持当前顺序。否则将是任意的。
    • 排序是 O(n log(n)) 但反向是 O(n)。如上所述,这不会扩展并且似乎依赖于实现。在 Chrome 94 中对我来说失败了。即使它确实有效并且有效,这充其量也是完全令人困惑和毫无意义的:sort(() =&gt; 1) 如何将“反转数组”传达给代码的任何读者?
    【解决方案8】:

    您可以使用.slice().reverse()

    const yourArray = ["first", "second", "third", "...", "etc"];
    const reversedArray = yourArray.slice().reverse();
    console.log(reversedArray);

    请注意,.slice() 用于防止修改 yourArray,因为 .reverse() 已就地。

    【讨论】:

    • 这里.slice()是多余的!
    • @JayeshChandrapal 不,不是。 .reverse() 改变了参数,所以 slice 防止 yourArray 被修改。
    【解决方案9】:

    这是一个 java 示例 http://www.leepoint.net/notes-java/data/arrays/arrays-ex-reverse.html 展示如何反转数组。很容易转换为 javascript。

    我建议使用简单地捕获函数被调用之前和函数被调用之后的时间的东西。哪个花费最少的时间/时钟周期将是最快的。

    【讨论】:

    • 链接已经死了,即使没有,这也不会为线程提供太多价值。
    【解决方案10】:

    这是另一个永久修改数组反转其元素的示例:

    var theArray = ['a', 'b', 'c', 'd', 'e', 'f'];
    
    function reverseArrayInPlace(array) {
      for (var i = array.length - 1; i >= 0; i -= 1) {
        array.push(array[i]);
      }
      array.splice(0, array.length / 2);
      return array;
    };
    reverseArrayInPlace(theArray);
    console.log(theArray); // -> ["f", "e", "d", "c", "b", "a"]
    

    【讨论】:

      【解决方案11】:

      另一个建议,与上面类似,但使用 splice 代替:

      var myArray=["one","two","three","four","five","six"];
      console.log(myArray);
      for(i=0;i<myArray.length;i++){
      myArray.splice(i,0,myArray.pop(myArray[myArray.length-1]));
      }
      console.log(myArray);

      【讨论】:

      【解决方案12】:

      如果你想复制一个反转版本的数组并保持原样:

      a = [0,1,2,3,4,5,6,7,8,9];
      b = []
      for(i=0;i<a.length;i++){
          b.push(a.slice(a.length-i-1,a.length-i)[0])
      }
      

      b 的输出:

      [ 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
      

      【讨论】:

      • 或者只是b = a.slice().reverse() :)
      • 或者只是b = [...a].reverse() :)
      • a.slice(a.length-i-1,a.length-i)[0] 是一种非常奇怪的索引数组单个元素的方法。不要使用这个。
      • 另外,始终使用varletconst,而不是声明全局变量。 for(i=0;... 永远不应该这样做。
      • @ggorlen,与其编辑内容并添加更改,不如将投票作为无用的答案并评论他本可以做出的更正。
      【解决方案13】:

      将以下内容粘贴到浏览器或 node.js 上的任何 javascript 运行时控制台中,将对大量数字进行直接的基准测试。

      在我的情况下说 40,000

      var array = Array.from({ length: 40000 }, () =>
        Math.floor(Math.random() * 40000)
      );
      var beforeStart = Date.now();
      var reversedArray = array.map((obj, index, passedArray) => {
        return passedArray[passedArray.length - index - 1];
      });
      console.log(reversedArray);
      var afterCustom = Date.now();
      console.log(array.reverse());
      var afterNative = Date.now();
      console.log(`custom took : ${afterCustom - beforeStart}ms`);
      console.log(`native took : ${afterNative - afterCustom}ms`);

      您也可以直接运行 sn-p 来查看自定义版本的效果。

      【讨论】:

        【解决方案14】:

        为了完整起见,还应该从更广泛的角度考虑效率,而不仅仅是执行时性能。

        因此,我想添加一个交换功能,它比accepted answer 中的功能慢(请使用性能工具自行测试),但它具有以下其他有益特性:

        • 它不需要临时变量来交换
        • 它使输入数组保持不变
        • 它也适用于数值数组以外的值,因为它只是交换引用
        • 它不会在每个迭代步骤中启动函数调用
        • 它一直是可读的(尽管这仍有待改进)
        function fastSwapReverse (array) {
          // half length for odd arrays is added by 1
          var len = array.length;
          var half = len % 2 === 0 ? len / 2 : Math.ceil(len / 2)
          var k = 0, A = new Array(len)
        
          // from 0 to half swap the elements at
          // start: 0 + i
          // end: len - i -1
          for (k = 0; k < half; k++) {
            A[k] = array[len - k - 1];
            A[len - k - 1] = array[k];
          }
        
          return A;
        }
        

        虽然原始的Array.prototype.reverse 方法也会改变数组

        【讨论】:

          【解决方案15】:

          使用for 循环从背面开始复制数组并返回该新数组。这是高效的并且具有 O(n) 时间复杂度。

          var array1 = ["yes", "no", "maybe", "always", "sometimes", "never", "if"];
          var array2 = [5,8,2,9,5,6,3,1];
          
          function reverseArray(arr) {
            var newArray = [];
            for (var x = arr.length - 1; 0 <= x; --x) {
              newArray.push(arr[x]);
            }
            return newArray;
          }
          
          console.log(reverseArray(array1)); // ["if", "never", "sometimes", "always", "maybe", "no", "yes"]
          console.log(reverseArray(array2)) // [1, 3, 6, 5, 9, 2, 8, 5]

          【讨论】:

            【解决方案16】:

            这是我发现的一些技巧。归功于 Codemanx 的原始解决方案

            array.sort(function() {
               return 1;
            })
            

            在 Typescript 中,这可以简化为一行

            array.sort(() => 1)
            

            var numbers = [1,4,9,13,16];
            
            console.log(numbers.sort(() => 1));

            既然这将是 JavaScript 的未来,我想我会分享一下。

            如果你的数组只有 2 个元素,这里还有一个技巧

            array.push(array.shift());
            

            【讨论】:

            • 排序方法不会反转数组;也许它曾经这样做过,但它依赖于实现。现在我在最新的稳定 V8 中对其进行了测试,它并没有逆转。事实上,它的顺序保持不变。但是,当我返回 -1 时,它反转了它。这可能是因为在 ES2019 中需要 .sort 方法来执行稳定的排序。但是,我仍然不会依赖它。
            • 此代码已损坏,正如其他线程中所指出的那样。它依赖于内部排序算法的实现依赖特性,因此在不同的环境和引擎版本之间你会得到不同的结果。基本上,如果它似乎在某些环境中工作,那不应该被信任,因为它违反了排序比较器的合同。
            【解决方案17】:

            您还可以使用 reduceRight 来遍历数组的每个值(从右到左)

            const myArray = [1, 2, 3, 4, 5]
            const reversedArray = myArray.reduceRight((acc, curr) => [...acc, curr], [])
            console.log(reversedArray) // [5, 4, 3, 2, 1]

            【讨论】:

            • 这是无缘无故的二次方。避免。
            【解决方案18】:
            // array-reverse-polyfill.js v1
            Array.prototype.reverse = Array.prototype.reverse || function() {
                return this.sort(function() {
                    return -1;
                });
            };
            

            如果您使用的是现代浏览器,则可以使用本机反向功能。如果它不是现代的,这个 polyfill 会为你添加这个功能。所以就用它吧:

            我还没有发现任何情况下 Firefox 的行为与其他浏览器相反。无论如何,如果你想确保函数按字母顺序排序,只需先做一点测试。

            // array-reverse-polyfill.js v1.1
            ;(function(){
                var order = ['a', 'b'].sort(function(a, b){
                    return -1;
                });
                order = order[0] === 'b' ? -1 : 1;
                Array.prototype.reverse = Array.prototype.reverse || function() {
                    return this.sort(function() {
                        return order;
                    });
                };
            })();
            

            使用示例:

            let a = [1,2,3,4,5];
            a.reverse();
            console.log(a);
            

            【讨论】:

            • 这不起作用,不可扩展且不符合语义。和其他两个帖子一样。
            • 没什么私人的,但如果代码像这样被严重破坏,它是对社区的一项服务,可以在其他用户在生产中使用它之前提醒他们,最终把事情搞得一团糟。我确实在 Chrome 94 中运行了代码,它根本没有修改数组([1,2,3,4].sort(() =&gt; 1) 是确切的代码)。它在 FF 95 中“有效”,但这不应该被信任。 .sort(() =&gt; 1) 滥用依赖于实现的代码,所以如果它有效,那完全是由于特定的内部排序算法可以随时改变。因此,无论出于何种意图和目的,它都完全崩溃了。
            • 您必须在 Chrome 94 中执行 [1,2,3,4].sort(() =&gt; -1) 才能反转数组,因此在撰写本文时,他们使用相反的比较器作为 FF。但说真的,也不要这样做,因为未来的 V8 引擎可能会选择不同的排序算法,并且代码会中断。这似乎适用于某些浏览器中的某些引擎,这一事实使得它如此危险且值得一票(并希望最终删除)以警告未来的访问者。
            • @ggorlen 他们可以,而且更有可能的是,他们永远不会改变并永远保持这种状态。没有意义。
            • 我不知道如何回应——很明显,代码现在已经坏了,即使算法没有改变(尽管它们会——V8 switched from quicksort to timsort a few years ago 并且可以从 timsort 取得进展随时)。当您现在在 FF 和 Chrome 中运行相同的代码时,会出现完全相反的行为。我想不出比这更糟糕的事情了。依赖于引擎实现的代码总是错误的,即使在当前版本中这是正确的(事实并非如此)。除此之外,即使这有效,它仍然是 O(n log(n)) 并且没有语义。
            猜你喜欢
            • 2018-06-08
            • 2011-10-09
            • 2014-09-23
            • 1970-01-01
            • 1970-01-01
            • 2021-06-25
            • 2017-01-21
            • 2010-11-20
            • 2015-11-08
            相关资源
            最近更新 更多