- 快速排序
- 归并排序
- 计数排序
- 堆排序
排序算法实现及效率分析
快速排序
首先快排是不稳定排序,因为它是通过交换元素来实现的。快排适用于完全乱序的序列,不适用于基本有序或基本逆序的序列,STL
中对快排的优化方法:
- 优化一,在选择哨兵的时候使用三数取中法,即取序列第一个元素、中间元素以及最后一个元素,再取这三个元素的中位数作为哨兵,这样可以避免取到边缘值而导致每次分割不均匀的情况;
- 优化二,因为在递归分割序列时,序列的长度会越来越短,又因为短序列排序中插入排序效率最高,所以可以设置一个阈值,当序列长度分割到阈值时切换到插入排序,提高效率;
- 优化三,当序列中存在大量相同元素,或者全是相同元素时,使用快排仍然会得到最低的效率,这时可以采用聚集相等元素的方法,每次选择哨兵后,将与哨兵相同的元素放到序列中间,下次分割时中间这些值不参与分割;
- 优化四,当递归层数过深的时候改用堆排序,虽然第一个优化方法避免了取到边缘哨兵,但还是有可能取到较边缘的哨兵,最坏的情况会导致递归层数过深,从而降低快排效率,所以每次递归要判断递归层数,达到一定深度就改用堆排序。之所以要用堆排序,我猜想可能是因为快排和归并都是基于递归的排序,此时递归深度已经太深,肯定不能再用了,而对于较长的序列使用插入排序效率也不是太高,所以选择了堆排序。
最后,快排的时间复杂度最好情况为O(NlogN)
,最坏为O(N^2)
,空间复杂度最好的情况为O(logN)
,因为递归时函数压栈需要空间,最坏情况为O(N)
。
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 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| void shuffle(vector<int> &nums) { int n = nums.size(); for (int i = 0 ; i < n; i++) { int j = rand() % ((n - 1) - i + 1) + i; swap(nums[i], nums[j]); } }
int partition(vector<int> &nums, int lo, int hi) { shuffle(nums.begin(), nums.end(), default_random_engine(time(nullptr))); int i = lo; int j = hi + 1; while(1) { while(nums[++i] < nums[lo]) { if(i == hi) { break; } } while(nums[lo] < nums[--j]) { if(j == lo) { break; } } if(j <= i) { break; } swap(nums[i], nums[j]); } swap(nums[lo], nums[j]); return j; }
int partition(vector<int> &nums, int lo, int hi) { shuffle(nums.begin(), nums.end(), default_random_engine(time(nullptr))); int i = lo; int j = lo; while(++j <= hi) { if(nums[j] < nums[lo]) { swap(nums[++i], nums[j]); } } swap(nums[lo], nums[i]); return i; }
void quickSort(vector<int> &nums, int lo, int hi) { if(lo >= hi) { return; } int pivot = partition(nums, lo, hi); quickSort(nums, lo, pivot - 1); quickSort(nums, pivot + 1, hi); }
void quickSort(vector<int> &nums) { quickSort(nums, 0, nums.size() - 1); }
|
归并排序(Top-down Approach
)
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
|
void merge(vector<int> &nums, int lo, int mi, int hi) { vector<int> helper(nums.begin() + mi + 1, nums.begin() + hi + 1); int i = mi; int j = helper.size() - 1; int k = hi; while(i >= lo && j >= 0) { if(nums[i] > helper[j]) { nums[k--] = nums[i--]; } else { nums[k--] = helper[j--]; } } while(j >= 0) { nums[k--] = helper[j--]; } }
void mergeSort(vector<int> &nums, int lo, int hi) { if(hi - lo < 1) { return; } int mi = lo + (hi - lo) / 2; mergeSort(nums, lo, mi); mergeSort(nums, mi + 1, hi); merge(nums, lo, mi, hi); }
void mergeSort(vector<int> &nums) { mergeSort(nums, 0, nums.size() - 1); }
|
归并排序(Bottom-up Approach
)
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
|
void merge(vector<int> &nums, int lo, int mi, int hi) { vector<int> helper(nums.begin() + mi + 1, nums.begin() + hi + 1); int n = helper.size(); int i = mi; int j = n - 1; int k = hi; while(i >= lo && j >= 0) { if(nums[i] > helper[j]) { nums[k--] = nums[i--]; } else { nums[k--] = helper[j--]; } } while(j >= 0) { nums[k--] = helper[j--]; } }
void mergeSort(vector<int>& nums) { int sz = nums.size(); for(int step = 1; step <= sz; step *= 2) { for(int lo = 0; lo < sz; lo += 2 * step) { merge(nums, lo, min(lo + step - 1, sz - 1), min(lo + 2 * step - 1, sz - 1)); } } return nums; }
|
计数排序
- 计数排序要求待排序的
n
个元素的大小在[0, k]
之间,并且k
与n
在一个数量级上,即k = O(n)
,此时使用计数排序可以把时间复杂度降到O(n)
上;
- 计数排序不是基于比较的排序算法,它基于计数策略;
- 写计数排序算法时,应该把它写成稳定排序的;
- 计数排序还是原址排序,但需要借助额外的内存空间;
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
|
void CountSort(vector<int> &nums) { int minVal = *min_element(nums.begin(), nums.end()); int maxVal = *max_element(nums.begin(), nums.end()); int valRange = maxVal - minVal; vector<int> count(valRange + 1, 0);
for(auto x : nums) { count[x - minVal]++; } for(int i = 1; i <= valRange; ++i) { count[i] += count[i - 1]; } vector<int> backup(nums); for (int i = nums.size() - 1; i >= 0; i--) { int index = count[backup[i] - minVal] - 1; nums[index] = backup[i]; count[backup[i] - minVal]--; } }
|
堆排序
秩为i
的元素,其父节点若存在,其秩必为(i - 1) / 2
;其左孩子若存在,其秩必为2 * i + 1
;其右孩子若存在,其秩必为左孩子秩加1
。
下滤:将首元素依次和其孩子中较大者交换。上滤:将末元素依次和其父亲交换。时间复杂度均为O(logn)
。
批量建堆:可以是所有的内部节点,自右往左依次下滤(复杂度正比于所有下滤节点的高度),或者是末元素依次上滤(复杂度是正比于所有上滤节点的深度),相比之下,前者的时间复杂度为O(n)
,后者时间复杂度为O(nlogn)
。第一个内部节点其实就是末元素的父亲,之后的内部节点依次递减即可。
堆排序:先执行批量建堆操作,时间复杂度为O(n)
,然后进行n
次取最大值操作,时间复杂度为O(nlogn)
。具体实现可以是建完堆后,反复进行将首元素和未排序的末元素交换,然后对首元素进行下滤的操作。总体时间复杂度为O(n) + O(nlogn) = O(nlogn)
。
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
| void sortArray(vector<int> &nums) { auto down = [&](int hole, int n) { int holeVal = nums[hole]; for(int lc = 2 * hole + 1; lc < n; lc = 2 * hole + 1) { if(lc + 1 < n && nums[lc] <= nums[lc + 1]) { lc++; } if(nums[lc] <= holeVal) { break; } nums[hole] = nums[lc]; hole = lc; } nums[hole] = holeVal; }; int n = nums.size(); for(int i = (n - 1 - 1) / 2; i >= 0; i--) { down(i, n); } for(int i = n - 1; i > 0; i--) { swap(nums[i], nums[0]); down(0, i); } }
|