C/C++ 的十大排序法对比
文章将从低级到高级讲解c/c++的几种排序算法;并附上代码和说明。
文章偏长(博主也是写了一周才完成),不想看太多解释的可以直接看代码;
(博主试了一下手机用户看表格对齐有点怪,所以建议手机用户看图片,电脑用户看表格)
排序算法 | 平均时间复杂度 | 最好情况 | 最坏情况 | 空间复杂度 | 稳定性 |
---|---|---|---|---|---|
冒泡排序法 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
选择排序法 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
插入排序法 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
希尔排序法 | O(nlog n) | O(nlog n) | O(n^2) | O(1) | 不稳定 |
归并排序法 | O(nlog n) | O(nlog n) | O(nlog n) | O(n) | 稳定 |
快速排序法 | O(nlog n) | O(nlog n) | O(n^2) | O(log n) | 不稳定 |
堆 排 序 法 | O(nlog n) | O(nlog n) | O(nlog n) | O(1) | 不稳定 |
计数排序法 | O(n+k) | O(n+k) | O(n+k) | O(k) | 稳定 |
桶 排 序 法 | O(n+k) | O(n+k) | O(n^2) | O(n+k) | 稳定 |
基数排序法 | O(n+k) | O(n+k) | O(n*k) | O(n+k) | 稳定 |
排序稳定性是指:通俗地讲就是能保证排序前2个相等的数其在序列的前后位置顺序和排序后它们两个的前后位置顺序相同。
不稳定的排序算法:快、选、希、堆。
几种常用的排序算法性能比较:
一千数据量:
快速排序 > 希尔排序 > 堆排序 > 归并排序 > 插入排序 > 选择排序 > 冒泡排序
一万数据量:
快速排序 > 堆排序 > 希尔排序 > 归并排序 > 插入排序 > 选择排序 > 冒泡排序
十万数据量:
堆排序 > 希尔排序 > 快速排序 > 归并排序 > 插入排序 > 选择排序 > 冒泡排序
百万数据量:
快速排序 > 堆排序 > 归并排序 > 希尔排序 > 插入排序 > 选择排序 > 冒泡排序
ps:谈不上那种排序法就是最好的,要看应用场景,每种算法都有自己的优势。
通常来说,快速排序在数据量较小数组特别混乱的情况时,表现得最优秀,而在数据量较大时堆排序表现得更优秀,平均来说希尔排序会比归并排序和快速排序快一点,堆排序、归并排序最坏情况都不会超过O(nlogn);
1.冒泡排序
思路: 相邻数据两两比较,不断循环轮次,每轮冒出最大(最小)的数放到有序区
稳定性解释: 冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个
元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,我想你是不会再无聊地把他们俩交换一下的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
伪代码:
//两两比较,不断循环轮次,每轮冒出最大(最小的数放到有序区)
int buf[10] = { 1,3,5,4,6,8,7,9,10,2 };
int len = sizeof(buf) / sizeof(int);
for (int i = 0; i < len-1; i++)
{
for (int j = 0; j < len-1-i; j++)
{
if(buf[j]>buf[j+1])
swap(buf[j], buf[j + 1]);
}
}
2.选择排序
思路: 顾名思意,就是直接从待排序数组里选择一个最小(或最大)的数字,每次都拿一个最小数字出来,顺序放入新数组,直到全部拿完。再简单点,对着一群数组说,你们谁最小出列,站到最后边,然后继续对剩余的无序数组说,你们谁最小出列,站到最后边,再继续刚才的操作,一直到最后一个,继续站到最后边,现在数组有序了,从小到大。
稳定性解释: 选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。
比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中2个5的相对前后顺序就被破坏了,所以选择排序不是一个稳定的排序算法。
伪代码:
//选择 选择第一个,与后面所有的数比较;如果小就交换,然后前面变成有序区,后面变成无序区
for (int i = 1; i < len - 1; i++)
{
for (int j = i+1; j < len ; j++)
{
if (buf[i] > buf[j])
{
swap(buf[i], buf[j]);
}
}
}
3.插入排序
思路: 插入排序就是每一步都将一个待排数据按其大小插入到已经排序的数据中的适当位置,直到全部插入完毕。
插入排序方法分直接插入排序和折半插入排序两种。
直接插入:1.数据的第一个数是有序树,其他为无序数;2.遍历无序数,把无序数逐个和有序数进行比较;3.定义一个临时变量,存储无序数,循环,把无序数赋值给有序树
折半插入:排序基本思想和直接插入排序一样,区别在于寻找插入位置的方法不同,折半插入排序采用折半查找法来寻找插入位置。折半查找法只能对有序的序列使用。基本思想就是查找插入位置的时候,把序列分成两半(选择一个中间数mid),如果带插入数据大于mid则到右半部分序列去在进行折半查找;反之,则到左半部分序列去折半查找。
稳定性解释:
- 插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。
- 比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。
- 如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序。所以插入排序是稳定的。
伪代码:
//方法一
for (int i = 1; i < Max; i++)
{
if (p[i - 1] > p[i])
{
int tmp = p[i];
int j = i - 1;
for (; j >= 0 && p[j] > tmp; --j)
{
p[j + 1] = p[j];
}
p[j + 1] = tmp;
}
}
//方法二
for (size_t i = 1; i < n; ++i)//用end的位置控制边界
{
//单趟排序
int end = i - 1;
int tmp = a[i];
while (end >= 0)//循环继续条件
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
--end;
}
else
break;1
}
a[end + 1] = tmp;
}
//方法三
//从第二个元素开始,加入第一个元素是已排序数组
for (int i = 1; i < N; i++) {
//待插入元素 array[i]
if (array[i] < array[i - 1]) {
int wait = array[i];
int j = i;
while (j > 0 && array[j - 1] > wait) {
//从后往前遍历已排序数组,若待插入元素小于遍历的元素,则遍历元素向后挪位置
array[j] = array[j - 1];
j--;
}
array[j] = wait;
}
}
4.希尔排序
思路: 希尔排序其实就是跨固定步长的插入排序,然后依次缩减步长再进行排序,待整个序列中的元素基本有序(步长变1)时,演变成对全体元素进行一次直接插入排序。当刚开始元素很无序的时候,步长最大,所以插入排序的元素个数很少,速度很快;当元素基本有序了,步长很小,插入排序对于有序的序列效率很高。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的,因此希尔排序在时间效率有较大提高。
稳定性解释: 希尔排序是按照不同步长对元素进行插入排序,所以,希尔排序的时间复杂度会比o(n^2)好一些。由于多次插入排序,我们知道一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱。 所以shell排序是不稳定的。
伪代码:
// 希尔排序 时间复杂度O(nlogn)~O(n^2) 空间复杂度O(1)
void shell_sort(vector<int>& array)
{
int n = array.size();
//间隙每次都变小一般;知道步长为1是变成插入排查,这时候数据大部分已经有序了,使用插入排序效率很高
for (int gap = n / 2; gap >= 1; gap /= 2)
{
for (int i = gap; i < n; i++)
{
// 使用插入排序算法,将元素依次插入所在小组的已排序列表中
int tmp = array[i];// 待插入元素
int j = i - gap;
for (; j >= 0 && array[j] > tmp; j -= gap)
{
array[j + gap] = array[j];
}
array[j + gap] = tmp;
}
}
}
5.归并排序法
思路: 归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用,归并排序将两个已排序的表合并成一个表。类似二叉排序树原理
第一, 分解: 把待排序的 n 个元素的序列分解成两个子序列, 每个子序列包括 n/2 个元素.
第二, 治理: 对每个子序列分别调用归并排序__MergeSort, 进行递归操作
第三, 合并: 合并两个排好序的子序列,生成排序结果.
如图,把最初的数据拆分成n个有序的列(长度为1),然后两两归并
如:数组 { 4,5,0,7,1,3 }
看成七个独立的有序列:[4] [5] [0] [7] [1] [3]
每趟都得到长度为n/2^n 个长度为=<2n的有序列。然后继续归并
稳定性解释: 归并排序最好、最差和平均时间复杂度都是O(nlogn),稳定算法
缺点: 需要分配额外的空间
伪代码:
//方法一 易懂
void Merge_sort(int array[], int low, int high) {
int mid = (low + high) / 2;
if (low < high) {
Merge_sort(array, low, mid);
Merge_sort(array, mid + 1, high);
//i 遍历第一区间[low mid]
int i = low;
//j 遍历第二区间[mid+1 high]
int j = mid + 1;
int* temp = new int[high - low + 1];//堆区空间不随着本函数结束而结束
memset(temp, 0, sizeof(temp));
int count = 0;
while (i <= mid && j <= high) {
//依次比较两个区间较小的数,然后装入temp数组
if (array[i] <= array[j]) {
temp[count++] = array[i++];
}
else {
temp[count++] = array[j++];
}
}
//比较完成后,假如第一区间还有剩余,继续装载
while (i <= mid) {
temp[count++] = array[i++];
}
//比较完成后,假如第二区间还有剩余,继续装载
while (j <= high) {
temp[count++] = array[j++];
}
//将归并排好序的元素赋值给原数组
for (int i = low, k = 0; i <= high; i++, k++) {
array[i] = temp[k];
}
delete[]temp;
}
}
// 方法二:归并排序容器
void merge_Sort(vector<int>& V, vector<int>& copyArray, int left, int right)
{
if (left < right)
{
int mid = (left + right) / 2;
merge_Sort(V, copyArray, left, mid);
merge_Sort(V, copyArray, mid + 1, right);
int i = left, j = mid + 1, k = 0;
while (i <= mid && j <= right)
{
if (i > mid)
{
copyArray[k] = V[j];
j++;
}
else if (j > right)
{
copyArray[k] = V[i];
i++;
}
else if (V[i] > V[j])
{
copyArray[k] = V[j];
j++;
}
else
{
copyArray[k] = array[i];
i++;
}
k++;
}
for (size_t i = left; i <= right; i++)
{
array[i] = copyArray[i - left];
}
}
}
void mergeSort(vector<int>& V)
{
vector<int> copyArray(V);
merge_Sort(V, copyArray, 0, array.size() - 1);
}
int main()
{
vector<int> V;
srand((unsigned)time(NULL));
for (int i = 0; i < 10; i++)
{
V.push_back(rand());
}
mergeSort(V);
for (auto it : V)
{
cout << it << " ";
}
}
6.快速排序法
思路: 快速排序的原理就是先选择一个哨兵(为了方便理解可以直接选择中间数),然后将序列的值与哨兵值比较,小于哨兵的放在左边,大于哨兵的放在右边从而将序列分成两部分,再重复对这两部分进行排序直到所有序列有序。
稳定性解释: 最坏情况演变成冒泡排序法,不稳定排序
伪代码:
//快速排序 -递归法
template <class T>
void Quick_Sort(T* array, int left, int right)
{
if (left < right)
{
int i = left - 1, j = right + 1;//为了使用前置++和--在这里前后移动一位
T mid = array[(left + right) / 2];//取中间作为基准
while (true)
{
while (array[++i] < mid);//移动前迭代器,大于哨兵记录该迭代器
while (array[--j] > mid);//移动后迭代器,小于哨兵记录该迭代器
if (i >= j)//直到前后迭代器相遇就退出循环
{
break;
}
//交换前大于哨兵和后小于哨兵的值;循环交换直到小于哨兵都在左边,大于哨兵都在右边
swap(array[i], array[j]);
}
Quick_Sort(array, left, i - 1);//break的时候中间认为是有序了,所以可以往前后移动一位重新排序
Quick_Sort(array, j + 1, right);
}
}
7.堆排序
此排序比较难,需要读者花时间去理解;如果之前没有了解过堆排,建议先去了解下堆区结构。
思路: 堆排序的原理就是先构造一个最大堆(完全二叉树),父结点大于左右子结点,然后取出根结点(最大值)与最后一个结点交换,重复调整剩余的结点成最大堆,得到有序的序列。
堆分为大根堆和小根堆,是完全二叉树。大根堆的要求是每个节点的值都不大于其父节点的值,即A[PARENT[i]] >= A[i]。在数组的非降序排序中,需要使用的就是大根堆,因为根据大根堆的要求可知,最大的值一定在堆顶。
既然是堆排序,自然需要先建立一个堆,而建堆的核心内容是调整堆,使二叉树满足堆的定义(每个节点的值都不大于其父节点的值)。调堆的过程应该从最后一个非叶子节点开始
稳定性解释: 我们知道堆的结构是节点i的孩子为2i和2i+1节点,大顶堆要求父节点大于等于其2个子节点,小顶堆要求父节点小于等于其2个子节点。在一个长为n 的序列,堆排序的过程是从第n/2开始和其子节点共3个值选择最大(大顶堆)或者最小(小顶堆),这3个元素之间的选择当然不会破坏稳定性。但当为n /2-1, n/2-2, …1这些个父节点选择元素时,就会破坏稳定性。有可能第n/2个父节点交换把后面一个元素交换过去了,而第n/2-1个父节点把后面一个相同的元素没有交换,那么这2个相同的元素之间的稳定性就被破坏了, 所以堆排序不是稳定算法。
代码:
ps:我封装了一个堆排的类,由于有点长,这里只给了伪代码,有需要的可以私聊我
#include <iostream>
#include <vector>
#include <time.h>
using namespace std;
#define Max 10
void Rand(vector<int> &V)
{
srand((unsigned)time(NULL));
while (this->V.size() < 10)
{
int tmp = rand() % 20;
vector<int>::iterator it = find(V.begin(), V.end(), tmp);
if (it == V.end())
this->V.push_back(tmp);
}
for (auto it : V)
{
cout << it << " ";
}
cout << endl;
}
//方法一
void MaxHeap(vector<int>& nums, int beg, int end)
{
int curr = beg;
int child = curr * 2 + 1;
while (child < end)
{
if (child + 1 < end && nums[child] < nums[child + 1])
child++;
if (nums[curr] < nums[child]) {
swap(nums[curr], nums[child]);
curr = child;
child = 2 * curr + 1;
}
else
break;
}
}
void heap_sort(vector<int>& nums)
{
int n = nums.size();
for (int i = n / 2 + 1; i >= 0; i--)
{
MaxHeap(nums, i, nums.size() - 1);
}
for (int i = n - 1; i > 0; i--)
{
swap(nums[0], nums[i]);
MaxHeap(nums, 0, i);
}
}
int main()
{
vector<int> V;
Rand(V);
heap_sort(V);
for (auto it : V)
{
cout << it << " ";
}
}
8.计数排序
思路:
- 遍历待排序数组A,找出其最小值min和最大值max;
- 创建一个长度为max-min+1的数组B,其所有元素初始化为0,数组首位对应数组A的min元素,索引为i位置对应A中值为min+i的元素;
- 遍历数组A,在B中对应位置记录A中各元素出现的次数;
- 遍历数组B,按照之前记录的出现次数,输出几次对应元素;
稳定性解释: 稳定排序算法;
代码:
// 计数排序
void count_Sort(vector<int>& array)
{
if (array.empty()){
return;
}
//找出最大最小值
int min = array.front(),max = array.front();
for (int i = 1; i < array.size(); i++)
{
if (min > array[i])
{
min = array[i];
}
else if (max < array[i])
{
max = array[i];
}
}
// 记录各元素出现次数
vector<int> counts(max - min + 1);
for (int i = 0; i < array.size(); i++)
{
counts[array[i] - min]++;
}
// 根据记录的次数输出对应元素
int index = 0;
for (int j = 0; j < counts.size(); j++)
{
int n = counts[j];
while (n--){
array[index] = j + min;
index++;
}
}
}
9.桶排序
思路:
- 设置固定数量的空桶;
- 找出待排序数组的最大值和最小值;
- 根据最大最小值平均划分各桶对应的范围,并将待排序数组放入对应桶中;
- 为每个不为空的桶中数据进行排序(例如,插入排序);
- 拼接不为空的桶中数据,得到排序后的结果。
稳定性解释: 常见排序算法中最快的一种稳定算法;可以计算大批量数据,符合线性期望时间;外部排序方式,需额外耗费n个空间;
代码:
// 桶排序
void bucketSort (vector<int>& array, int bucketCount)
{
if (array.empty())
{
return;
}
// 找出最大最小值
int max = array.front(), min = array.front();
for (int i = 1; i < array.size(); i++)
{
if (min > array[i])
{
min = array[i];
}
else if (max < array[i])
{
max = array[i];
}
}
// 将待排序的各元素分入对应桶中
vector<vector<int>> buckets(bucketCount);
int bucketSize = ceil((double)(max - min + 1) / bucketCount);
for (int i = 0; i < array.size(); i++)
{
int bucketIndex = (array[i] - min) / bucketSize;
buckets[bucketIndex].push_back(array[i]);
}
// 对各桶中元素进行选择排序
int index = 0;
for (vector<int> bucket : buckets)
{
if (!bucket.empty())
{
// 使用选择排序算法对桶内元素进行排序
selectSort(bucket);
for (int value : bucket)
{
array[index] = value;
index++;
}
}
}
}
// 桶排序
void bucketSort (vector<int>& array)
{
bucketSort (array, array.size() / 2);
}
10.基数排序
思路: 将各待比较元素数值统一数位长度,即对数位短者在前补零;根据个位数值大小,对数组进行排序;重复上一步骤,依次根据更高位数值进行排序,直至到达最高位;
稳定性解释: 稳定算法;适用于正整数数据(若包含负数,那么需要额外分开处理);对于实数,需指定精度,才可使用此算法。
代码:
// 基数排序,对array的left到right区段,按照curDigit位进行排序
void radixSortImprove(vector<int>& array, int left, int right, int curDigit)
{
if (left >= right || curDigit < 10)
{
return;
}
// 将各元素按当前位数值大小分入各桶
vector<vector<int>> buckets(10);
for (int i = left; i <= right; i++)
{
int bucketIndex = (array[i] % curDigit - array[i] % (curDigit / 10)) / (curDigit / 10);
buckets[bucketIndex].push_back(array[i]);
}
// 按照桶的顺序,将桶中元素拼接
// 对于元素个数大于1的桶,桶内元素按照更低位来进行排序
int index = 0;
for (vector<int> bucket : buckets)
{
int newLeft = index, newRight = index;
for (int value : bucket)
{
array[index] = value;
index++;
}
newRight = index - 1;
radixSortImprove(array, newLeft, newRight, curDigit / 10);
}
}
// 基数排序(从高位开始)
void radix_Sort(vector<int>& v)
{
// 计算当前数组最大数位数
int curDigit = 10;
for (autovalue : v)
{
if (value / curDigit) {
curDigit *= 10;
}
}
radixSortImprove(array, 0, array.size() - 1, curDigit);
}
参考:https://blog.csdn.net/DeepLies/article/details/52593597
参考:https://blog.csdn.net/kuaizi_sophia/article/details/87954222
参考:https://blog.csdn.net/sunmc1204953974/article/details/39396449
版权声明:拷贝、转载请附上本文连接