理论与实践:理论上没有理论区别
和实践,但实际上是有的。
- 理论:一切都很清楚,但没有任何作用;
- 练习:一切正常,但没有什么是清楚的;
- 有时理论与实践相结合:没有什么是有效的,也没有什么是清楚的。
Big O 表示法非常适合评估算法的可扩展性,但不提供直接比较算法实现之间性能的方法...
典型的例子是 Array.sort() 在浏览器中的实现。尽管 Timsort 的 Big O 配置文件比 Merge 排序更好(请参阅 https://www.bigocheatsheet.com/),但经验测试表明,在 Chrome 的 V8 引擎中实现 Timsort 的平均性能明显优于在 Firefox 中实现 Merge 排序。
下面的图表分别显示了两组数据点:
- 蓝色数据是 Array.sort() 对随机填充整数的随机长度数组(从 100 到 500,000 个元素)的 500 个测试用例的性能。曲线以 N * Log( N ) 的形式显示数据的 中值,而虚线则以 N * Log( N ) 的形式显示 95% 的界限。也就是说,95% 的数据点位于这些曲线之间。
- 橙色数据显示了 Array.sort() 对大多数排序数组的性能。
具体来说,数组中约 2% 的值被重新随机化,然后再次应用 Array.sort()。在这种情况下,实线和虚线是性能的线性度量,而不是对数。
此外,Big O 表示法提供了一般的经验法则,可以预期算法的可扩展性,但不解决可变性问题。 Timsort 算法的 Chrome V8 实现在执行上比 Firefox Merge 排序具有更大的可变性,尽管 Timsort 的 Big O 配置文件更好,但即使 Timsort 的最佳时间也不比 Merge 排序的最差时间好。冒着引发宗教战争的风险,这并不意味着 Timsort 比 Merge 排序更差,因为这可能只是 Firefox 的 JavaScript 实现的整体性能更好的一个例子。
上面图表的数据是根据我的 Acer Aspire E5-5775G 签名版的以下代码生成的,该版本具有 Intel Core i5-7200U CPU @2.50GHz 和 8GB RAM。然后将数据导入 Excel,分析 95% 的边界范围,然后绘制图表。图表上的坐标轴刻度已标准化,以便于视觉比较。
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 );
要点是,一个算法的性能,表面上基于大 O 表示法更好,有许多因素推动其整体性能,更好的大 O 不一定转化为更好的性能......