Noob
Noob

Reputation: 2807

What is Array.prototype.sort() time complexity?

According to Mozilla documentation :

The time and space complexity of the sort cannot be guaranteed as it depends on the implementation.

Is it at least safe to assume that it's not O(n^2)? Is there anywhere more detailed data about how it's implemented ? Thanks.

Upvotes: 71

Views: 78742

Answers (3)

Trentium
Trentium

Reputation: 3719

Theory and practice: In theory there is no difference between theory and practice, but in practice there is.

  • Theory: everything is clear, but nothing works;
  • Practice: everything works, but nothing is clear;
  • Sometimes theory meets practice: nothing works and nothing is clear.

Big O notation is great for assessing the scalability of an algorithm, but does not provide a means of direct comparison of performance between implementations of an algorithm...

Case in point is the implementation of Array.sort() within browsers. Despite Timsort having a better Big O profile than Merge sort (see https://www.bigocheatsheet.com/), empirical testing shows that the implementation of Timsort in Chrome's V8 engine is clearly outperformed on average by the implementation of Merge sort in Firefox.

The charts below each show two sets of data points:

  • The blue data is the performance of Array.sort() for 500 test cases of random length arrays (from 100 to 500,000 elements) randomly filled with integers. The curved line shows the median of the data in the form of N * Log( N ), and the dotted curves show the 95% bounds, again in the form of N * Log( N ). That is to say, 95% of the data points lie between these curves.
  • The orange data shows the performance of Array.sort() for a mostly sorted array. Specifically, ~2% of the values in the array are re-randomized and then Array.sort() is applied again. In this case, the solid and dotted lines are linear measures of performance, not logarithmatic.

Furthermore, Big O notation provides a general rule of thumb to expect from the scalability of the algorithm, but does not address the variability. The Chrome V8 implementation of the Timsort algorithm has a wider variability in its execution than the Firefox Merge sort, and despite Timsort's better Big O profile, even Timsort's best times are not better than the Merge sort's worst times. At the risk of starting a religious war, this does not mean that the Timsort is worse than Merge sort, as this could simply be a case of better overall performance by Firefox's implementation of JavaScript.

Chrome Array.sort() Performance

Firefox Array.sort() Performance

The data for the charts above was generated from the following code on my Acer Aspire E5-5775G Signature Edition having an Intel Core i5-7200U CPU @2.50GHz and 8GB of RAM. The data was then imported into Excel, analyzed for the 95% bounding range, and then charted. The axes scales on the charts are normalized for ease of visual comparison.

  function generateDataPoints( qtyOfTests, arrayRange, valueRange, nearlySortedChange ) {
  
    let loadingTheArray = [];
    let randomSortMetrics = [];
    let nearlySortedMetrics = [];
    
    for ( let testNo = 0; testNo < qtyOfTests; testNo++ ) {
      if ( testNo % 10 === 0 ) console.log( testNo );
      
      // Random determine the size of the array given the range, and then
      // randomly fill the array with values.
      let testArray = [];      
      let testArrayLen = Math.round( Math.random() * ( arrayRange.hi - arrayRange.lo ) ) + arrayRange.lo;
      
      start = performance.now();
      for ( let v = 0; v < testArrayLen; v++ ) {
        testArray[ v ] = Math.round( Math.random() * ( valueRange.hi - valueRange.lo ) ) + valueRange.lo;
      }
      end = performance.now();
      
      loadingTheArray[ testNo ] = { x: testArrayLen, y: Math.floor( end - start ) };
      
      // Perform the sort and capture the result.
      start = performance.now();
        testArray.sort( (a, b ) => a - b );
      end = performance.now();
      
      randomSortMetrics[ testNo ] = { x: testArrayLen, y: Math.floor( end - start ) };
      
      // Now, let's change a portion of the sorted values and sort again.
      let qtyOfValuesToChange = testArrayLen * nearlySortedChange;
      for ( let i = 0; i < qtyOfValuesToChange; i++ ) {
        let v = Math.round( Math.random() * testArrayLen );
        testArray[ v ] = Math.round( Math.random() * ( valueRange.hi - valueRange.lo ) ) + valueRange.lo;
      }
      
      start = performance.now();
        testArray.sort( (a, b ) => a - b );
      end = performance.now();
      
      nearlySortedMetrics[ testNo ] = { x: testArrayLen, y: Math.floor( end - start ) };

    }
    
    return  [ loadingTheArray, randomSortMetrics, nearlySortedMetrics ];
    
  }
  
  // Let's start running tests!
  let arraySizeRange = { lo: 100, hi: 500000 };
  let valueRange = { lo: 0, hi: 2 ** 32 - 1 };
  let results = generateDataPoints( 500, arraySizeRange, valueRange, 0.02 );
  
  
  let tabFormat = 'No Of Elements\tTime to Load Array\tFull Sort\tMostly Sorted\n';
  for ( let i = 0; i < results[0].length; i++ ) {
    tabFormat += `${results[0][i].x}\t${results[0][i].y}\t${results[1][i].y}\t${results[2][i].y}\n`;
  }
  console.log( tabFormat );

The takeaway is that the performance of an algorithm, ostensibly being better based on Big O notation, has many factors that drive its overall performance, and a better Big O does not necessarily translate to better performance...

Upvotes: 43

Patrik Bego
Patrik Bego

Reputation: 4115

It depends on the browser/engine.

Since V8 v7.0 and Chrome 70 Timsort algorithm is used.
Which, for smaller arrays, has a time complexity of O(n) and space complexity of 0(1). And for larger arrays, it has a time complexity of O(nlog(n)) and space complexity of O(n).

The older version of V8 uses Quick Sort, and it has a check for small arrays (up to 10 elements). So for smaller arrays, the time complexity is O(n^2), and space complexity is O(1).
For larger arrays, time complexity is Θ(n log(n)) (average case), and space complexity is O(log(n))

Upvotes: 7

CertainPerformance
CertainPerformance

Reputation: 370759

Firefox uses merge sort. Chrome, as of version 70, uses a hybrid of merge sort and insertion sort called Timsort.

The time complexity of merge sort is O(n log n). While the specification does not specify the sorting algorithm to use, in any serious environment, you can probably expect that sorting larger arrays does not take longer than O(n log n) (because if it did, it would be easy to change to a much faster algorithm like merge sort, or some other log-linear method).

While comparison sorts like merge sort have a lower bound of O(n log n) (i.e. they take at least this long to complete), Timsort takes advantages of "runs" of already ordered data and so has a lower bound of O(n).

Upvotes: 119

Related Questions