排序稳定性
定义:假定在待排序的序列中,存在多个相同值,若经过排序,这些值的相对次序 保持不变,即在原序列中,r[i] = r[j],且 r[i] 在 r[j] 之前,而在排序后的序列中,r[i] 仍在 r[j] 之前,则称这种排序算法是稳定的;否则称为不稳定的。
排序稳定性很容易被认为是排序速度的稳定性,比如插入排序就是速度不稳定的算法。
排序稳定性对于基础类型(如 int、double 等)没有任何意义,而对非基础类型则具有重要意义。设想下面两个场景:
打印整个年级的名册,要求先按班级排序,然后班级内部再按年龄排序,这就要求排序算法具有稳定性。
你在淘宝上购买商品,如果先将价格从低到高排列,再将评价从高到低排列,那么排在前面的就是物美价廉的商品。
那么哪些排序算法具有稳定性呢?注意,排序稳定性取决于具体实现的细节,例如冒泡排序、选择排序以及归并排序,它们既可以是稳定排序,也可以是不稳定排序,关键取决于你如何处理两个相等的值(把前者仍放前面,后者仍放后面) 。其他排序算法的稳定性见下表。
综合比较
排序
时间复杂度
空间复杂度
稳定性
冒泡排序
O(N^2)
O(1)
稳定
选择排序
O(N^2)
O(1)
不稳定
插入排序
O(N^2)
O(1)
稳定
希尔排序
O(N^2)
O(1)
不稳定
归并排序
O(NlogN)
O(N)
稳定
堆排序
O(NlogN)
O(1)
不稳定
快速排序
O(NlogN)
O(logN) 系统栈
不稳定
基数排序
O(N)
O(N)
稳定
基于比较的排序,时间复杂度极限为 O ( N l o g N ) O(NlogN) O ( Nl o g N )
不基于比较的排序,对数据形式有较大要求,且不适合对象排序
时间复杂度 O ( N l o g N ) O(NlogN) O ( Nl o g N ) ,空间复杂度 O ( N ) O(N) O ( N ) ,且稳定的算法是不存在的
随即快排之所以快于归并和堆排序,是因为其常数时间是最小的
追求速度——随即快排,追求空间——堆排序,追求稳定性——归并排序
工程改进
排序算法在工程上的改进可以大致从以下两个角度:
稳定性
比如在 java 中的 arrays.sort() 中,当元素是对象时,它会使用归并排序以追求稳定性;当元素是基础类型时,它会使用快速排序以追求速度。
综合利用
比如 C 语言中的 std::sort 函数,针对大数据量,使用快排;若快排递归深度超过阈值 __depth_limit
,改用堆排序,防止快排递归过深,同时保持时间复杂度仍是 O(NlogN);当数据规模小于阈值 _S_threshold 时,改用插入排序,因为插入排序的常数时间小于快排,因此处理小规模数据时更有优势。
速度比较器
速度比较器(自己取的名字,嘿嘿)用来快速比较两种算法的排序速度,由 chatGPT 提供 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 #include <iostream> #include <chrono> #include <random> #include <functional> #include "radixSort.h" #include <algorithm> using std::sort;void sortFunc1 (int * data, int dataSize) { sort (data, &data[dataSize], [](int a, int b){return a<b;}); } void sortFunc2 (int * data, int dataSize) { radixSort (data, dataSize); } std::pair<double , double > testSortingTime (std::function<void (int *, int )> sortFunc1, std::function<void (int *, int )> sortFunc2, int numComparisons, int dataSize, int maxValue) { int * data = new int [dataSize]; std::random_device rd; std::mt19937 gen (rd()) ; std::uniform_int_distribution<int > dis (0 , maxValue) ; for (int i = 0 ; i < dataSize; ++i) { data[i] = dis (gen); } auto startTime = std::chrono::high_resolution_clock::now (); for (int i = 0 ; i < numComparisons; ++i) { int * sortedData = new int [dataSize]; std::copy (data, data + dataSize, sortedData); sortFunc1 (sortedData, dataSize); delete [] sortedData; } auto endTime = std::chrono::high_resolution_clock::now (); double sortingTime1 = std::chrono::duration <double >(endTime - startTime).count (); startTime = std::chrono::high_resolution_clock::now (); for (int i = 0 ; i < numComparisons; ++i) { int * sortedData = new int [dataSize]; std::copy (data, data + dataSize, sortedData); sortFunc2 (sortedData, dataSize); delete [] sortedData; } endTime = std::chrono::high_resolution_clock::now (); double sortingTime2 = std::chrono::duration <double >(endTime - startTime).count (); delete [] data; return std::make_pair (sortingTime1, sortingTime2); } int main () { auto result = testSortingTime (sortFunc1, sortFunc2, 1 , 100000000 , 10000000 ); std::cout << "Sorting Time for Function 1: " << result.first << " seconds\n" ; std::cout << "Sorting Time for Function 2: " << result.second << " seconds\n" ; return 0 ; }