Space complexity is defined as how much additional space the algorithm needs in terms of the N elements. And even though according to the docs, the sort method sorts a list in place, it does use some additional space, as stated in the description of the implementation:
timsort can require a temp array containing as many as N//2 pointers, which means as many as 2*N extra bytes on 32-bit boxes. It can be expected to require a temp array this large when sorting random data; on data with significant structure, it may get away without using any extra heap memory.
Therefore the worst case space complexity is O(N) and best case O(1)
Space complexity is defined as how much additional space the algorithm needs in terms of the N elements. And even though according to the docs, the sort method sorts a list in place, it does use some additional space, as stated in the description of the implementation:
timsort can require a temp array containing as many as N//2 pointers, which means as many as 2*N extra bytes on 32-bit boxes. It can be expected to require a temp array this large when sorting random data; on data with significant structure, it may get away without using any extra heap memory.
Therefore the worst case space complexity is O(N) and best case O(1)
Space complexity is defined as how much additional space the algorithm needs in terms of the N elements. And even though according to the docs, the sort method sorts a list in place, it does use some additional space, as stated in the description of the implementation:
timsort can require a temp array containing as many as N//2 pointers, which means as many as 2*N extra bytes on 32-bit boxes. It can be expected to require a temp array this large when sorting random data; on data with significant structure, it may get away without using any extra heap memory.
Therefore the worst case space complexity is O(N) and best case O(1)
Python's built in sort method is a spin off of merge sort called Timsort, more information here - https://en.wikipedia.org/wiki/Timsort.
It's essentially no better or worse than merge sort, which means that its run time on average is O(n log n) and its space complexity is ฮฉ(n)
Videos
It seems that Python uses Timsort to sort lists which in the worse case requires O(n) extra space, but there seem to exist in place sorts like heap sort which are O(n log n) but only require O(1) extra memory. Why does Python use Timsort which in the worse case requires O(n) extra space and not an algorithm like heap sort which only requires O(1) extra space?
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).
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.


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...