目录
- ☀️直接插入排序
- ☀️希尔排序
- ☀️直接选择排序
- ☀️堆排序
- ☀️冒泡排序
- ☀️快速排序
- ☀️归并排序
- ☀️排序算法复杂度及稳定性分析
☀️直接插入排序
1、基本思想
把待排序的数按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所以的记录插入完为止,得到一个新的有序序列。
实际中我们玩扑克牌时,就用到了插入排序的思想
基本步骤:
当插入第i个元素时,前面的arr[0]、arr[2]…arr[n-1]已经排好序,此时用arr[i]待排序的值与前面的数进行比较,找到插入的位置,将arr[i]插入,原来位置上的元素依次向后移动。
2、代码实现
void insertSort(int* a, int n)
{
for (int i = 0; i < n - 1; i++)
{
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
插入排序特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高。
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),是一种稳定的排序算法。
- 稳定性:稳定
☀️希尔排序
1、基本思想
希尔排序又称缩小增量法,其基本思想:
- 先选定一个整数gap,把待排序的数列分成gap组,对每一组内的元素进行排序
- 然后逐渐减小gap,重复排序
- 直到gap=1,再进行直接插入排序,完成排序
第一步进行预排序,分组排序,把大的排后面,小的排前面
第二步进行直接插入排序
动态演示:
2、代码实现
void shellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1;
for (int i = 0; i < n - gap; i++)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
希尔排序特性总结:
- 希尔排序是对直接插入排序的优化
- 当gap>1时都是预排序,目的是让数列更接近有序。当gap==1时,数列就已经接近有序,再进行直接插入排序就会很快,进而到达优化效果
- 由于gap取值不唯一,希尔排序的时间复杂度不好计算,因此在希尔排序的时间复杂度不固定。
- 时间复杂度:O(N^1.3)
- 稳定性:不稳定
☀️直接选择排序
1、基本思想
- 每次从待排序的数列中选出最小(或最大)的一个元素,存放在数列的起始位置,直到全部待排序的元素排完。
基本步骤:
- 在待排序的数列中选择最小(或最大)的一个元素。
- 若它不是这组待排序的数列中的第一个(或最后一个)元素,则将它与这组数列的第一个(或最后一个)元素交换。
- 对剩余的数据,重复上述操作,直到数列排完。
2、代码实现
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin < end)
{
int min = begin;
int max = begin;
for (int i = begin + 1; i <= end; i++)
{
if (a[i] < a[min])
{
min = i;
}
if (a[i] > a[max])
{
max = i;
}
}
Swap(&a[begin], &a[min]);
if (max == begin)
{
max = min;
}
Swap(&a[end], &a[max]);
begin++;
end--;
}
}
直接选择排序特性总结:
- 直接选择排序效率不好,实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
☀️堆排序
1、基本思想
堆排序分两个步骤:
- 建堆
升序:建大堆
降序:建小堆 - 利用堆删除思想来进行排序
把堆顶数据和最后一个数据进行交换,把最后一个数不看做堆里面的,相当于n-1个数,向下调整,选出次大的数。
2、代码如下
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//向下调整
void AdjustDown(int* a, int n, int parent)
{
int child = parent * 2 + 1;
while (child < n)
{
// 确认child指向大的那个孩子
if (child + 1 < n && a[child + 1] > a[child])
{
++child;
}
// 1、孩子大于父亲,交换,继续向下调整
// 2、孩子小于父亲,则调整结束
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
void HeapSort(int* a, int n)
{
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDown(a, n, i);
}
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDown(a, end, 0);
--end;
}
}
堆排序特性总结:
- 堆排序效率比直接选择排序高
- 时间复杂度:O(N*logN)
- 空间复杂度:O(1)
- 稳定性:不稳定
☀️冒泡排序
1、基本思想
将待排序数列中的元素两两进行比较,直到将最大的数移动到末尾,一共进行n-1趟。
2、代码实现
void Swap(int* pa, int* pb)
{
int tmp = *pa;
*pa = *pb;
*pb = tmp;
}
void bubbleSort(int* a, int n)
{
for (int i = 0; i < n; i++)
{
int exchange = 0;
for (int j = 1; j < n - i; j++)
{
if (a[j - 1] > a[j])
{
Swap(&a[j - 1], &a[j]);
exchange = 1;
}
}
//一趟冒泡过程中,没有发生交换,说明已经有序了,不需要再处理
if (exchange == 0)
{
break;
}
}
}
冒泡排序特性总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
☀️快速排序
1、基本思想
快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法,其基本思想:
- 任意取待排序数列中的某一元素key,按照所取元素key值将待排数列分割成两个子数列
- 左子数列小于所取元素key值,右子数列大于所取元素key值
- 然后左右子数列重复该过程,直到排序完成
基本步骤:
- 首先进行单趟排序,左边比key小,右边比key大
- 然后递归排序左右两边子数列
全程图解:
将区间按照所取元素划分为左右两部分的常见方式有三种
第一种:hoare版
若左边做key,右边先走,保证相遇位置比key小
若右边做key,左边先走,保证相遇位置比key大
相遇的两种情况:
1、R停住,L遇到R,相遇的位置就是R停住的位置
2、L挺住,R遇到L,相遇的位置就是L停住的位置
第二种:挖坑版
第三种:前后指针版
2、代码实现及优化
第一种:hoare版
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//hoare版
int PartSort1(int* a, int begin, int end)
{
int left = begin;
int right = end;
int keyi = left;
while (left < right)
{
// 右边先走,找小
while (left < right && a[right] >= a[keyi])
{
--right;
}
// 左边再走,找大
while (left < right && a[left] <= a[keyi])
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
keyi = left;
return keyi;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = PartSort1(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
快排在理想状态下,时间复杂度O(N*logN)
最坏情况下,时间复杂度O(N^2)
为解决最坏情况,可以对快排进行优化,即三数取中。
加入三数取中后,快排瞬间从最坏变成最好,快排几乎不会出现最后情况
代码如下:
void Swap(int* p1, int* p2)
{
int tmp = *p1;
*p1 = *p2;
*p2 = tmp;
}
//三数取中
int GetMidIndex(int* a, int begin, int end)
{
int mid = (begin + end) / 2;
if (a[begin] < a[mid])
{
if (a[mid] < a[end])
{
return mid;
}
else if (a[begin] > a[end])
{
return begin;
}
else
{
return end;
}
}
else
{
if (a[mid] > a[end])
{
return mid;
}
else if (a[begin] < a[end])
{
return begin;
}
else
{
return end;
}
}
}
int PartSort1(int* a, int begin, int end)
{
int mid = GetMidIndex(a, begin, end);
Swap(&a[begin], &a[mid]);
int left = begin;
int right = end;
int keyi = left;
while (left < right)
{
// 右边先走,找小
while (left < right && a[right] >= a[keyi])
{
--right;
}
// 左边再走,找大
while (left < right && a[left] <= a[keyi])
{
++left;
}
Swap(&a[left], &a[right]);
}
Swap(&a[left], &a[keyi]);
keyi = left;
return keyi;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
int keyi = PartSort1(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
还有一种优化方式,即递归到小的区间时,有直接插入排序,减少递归调用次数
代码如下:
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
{
return;
}
if ((end - begin + 1) < 15)
{
//小区间有直接插入排序
insertSort(a + begin, end - begin + 1);
}
else
{
int keyi = PartSort1(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
}
第二种:挖坑版
//挖坑版
int PartSort2(int* a, int begin, int end)
{
int mid = GetMidIndex(a, begin, end);
Swap(&a[begin], &a[mid]);
int left = begin;
int right = end;
int key = a[left];
int hole = left;
while (left < right)
{
// 右边找小,填到左边坑里面
while (left < right && a[right] >= key)
{
right--;
}
a[hole] = a[right];
hole = right;
// 左边找大,填到右边坑里面
while (left < right && a[left] <= key)
{
left++;
}
a[hole] = a[left];
hole = left;
}
a[hole] = key;
return hole;
}
第三种:前后指针版
//前后指针版
int PartSort3(int* a, int begin, int end)
{
int mid = GetMidIndex(a, begin, end);
Swap(&a[begin], &a[mid]);
int keyi = begin;
int prev = begin;
int cur = begin + 1;
while (cur <= end)
{
// 找到比key小的值时,跟++prev位置交换,小的往前翻,大的往后翻
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
return keyi;
}
若深度太深,就会造成溢出,快排的非递归就是次问题
代码如下:
//非递归快排
void QuickSortNonR(int* a, int begin, int end)
{
//创建栈
ST st;
StackInit(&st);
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))
{
int right = StackTop(&st);
StackPop(&st);
int left = StackTop(&st);
StackPop(&st);
int keyi = PartSort3(a, left, right);
// [left, keyi-1] keyi [keyi+1, right]
if (keyi+1 < right)
{
StackPush(&st, keyi + 1);
StackPush(&st, right);
}
if (left < keyi-1)
{
StackPush(&st, left);
StackPush(&st, keyi - 1);
}
}
StackDestroy(&st);
}
快速排序特性总结:
- 快速排序整体的综合性能和使用场景都是比较好的
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
☀️归并排序
1、基本思想
归并排序是建立在归并操作上的一种排序算法,该算法是采用分治法的一个典型的应用。其基本思想:
- 先使每个子序列有序,再使子序列段间有序
- 将有序的子序列合并成一个有序序列
若将两个有序表合并成一个有序表,称为二路归并。
归并全过程如下图:
动态演示:
2、代码实现
void _MergeSort(int* a, int begin, int end, int* tmp)
{
if (begin >= end)
return;
int mid = (begin + end) / 2;
// [begin, mid] [mid+1, end] 递归让子区间有序
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp);
// 归并[begin, mid] [mid+1, end]
int begin1 = begin;
int end1 = mid;
int begin2 = mid + 1;
int end2 = end;
int i = begin;
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[i++] = a[begin1++];
}
else
{
tmp[i++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[i++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[i++] = a[begin2++];
}
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
tmp = NULL;
}
非递归实现:
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL)
{
perror("malloc fail");
exit(-1);
}
// 归并每组数据个数,从1开始,因为1认为是有序的,可以直接归并
int rangeN = 1;
while (rangeN < n)
{
for (int i = 0; i < n; i += 2 * rangeN)
{
// [begin1,end1][begin2,end2] 归并
int begin1 = i, end1 = i + rangeN - 1;
int begin2 = i + rangeN, end2 = i + 2 * rangeN - 1;
int j = i;
if (end1 >= n)
{
break;
}
else if (begin2 >= n)
{
break;
}
else if (end2 >= n)
{
end2 = n - 1;
}
while (begin1 <= end1 && begin2 <= end2)
{
if (a[begin1] <= a[begin2])
{
tmp[j++] = a[begin1++];
}
else
{
tmp[j++] = a[begin2++];
}
}
while (begin1 <= end1)
{
tmp[j++] = a[begin1++];
}
while (begin2 <= end2)
{
tmp[j++] = a[begin2++];
}
// 归并一部分,拷贝一部分
memcpy(a + i, tmp + i, sizeof(int)*(end2 - i + 1));
}
rangeN *= 2;
}
free(tmp);
tmp = NULL;
}
归并排序特性总结:
- 归并的缺点在于需要O(N)空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
☀️排序算法复杂度及稳定性分析
测试排序的性能对比,代码如下:
int main()
{
srand(time(0));
const int N = 100000;
int* a1 = (int*)malloc(sizeof(int) * N);
int* a2 = (int*)malloc(sizeof(int) * N);
int* a3 = (int*)malloc(sizeof(int) * N);
int* a4 = (int*)malloc(sizeof(int) * N);
int* a5 = (int*)malloc(sizeof(int) * N);
int* a6 = (int*)malloc(sizeof(int) * N);
int* a7 = (int*)malloc(sizeof(int) * N);
int j = 0;
for (int i = 0; i < N; ++i)
{
a1[i] = rand();
a2[i] = a1[i];
a3[i] = a1[i];
a4[i] = a1[i];
a5[i] = a1[i];
a6[i] = a1[i];
a7[i] = a1[i];
}
int begin1 = clock();
InsertSort(a1, N);
int end1 = clock();
int begin2 = clock();
ShellSort(a2, N);
int end2 = clock();
int begin3 = clock();
SelectSort(a3, N);
int end3 = clock();
int begin4 = clock();
HeapSort(a4, N);
int end4 = clock();
int begin7 = clock();
BubbleSort(a5, N);
int end7 = clock();
int begin5 = clock();
QuickSort(a6, 0, N - 1);
int end5 = clock();
int begin6 = clock();
MergeSort(a7, N);
int end6 = clock();
printf("InsertSort:%d\n", end1 - begin1);
printf("ShellSort:%d\n", end2 - begin2);
printf("SelectSort:%d\n", end3 - begin3);
printf("HeapSort:%d\n", end4 - begin4);
printf("BubbleSort:%d\n", end5 - begin5);
printf("QuickSort:%d\n", end6 - begin6);
printf("MergeSort:%d\n", end7 - begin7);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
free(a7);
return 0;
}
性能测试如下: