1.排序的意义
1.1排序的概念
1.2排序的应用
1.3常见的排序算法
2.插入排序
2.1 基本思想
2.2直接插入排序
// 插入排序
// 时间复杂度是多少?O(N^2)
// 什么情况下最坏?逆序 1+2+3+...+n-1
// 什么情况下最好?顺序有序 O(N)
void InsertSort(int* a, int n)
{
// [0, end]有序 end+1位置的值插入[0, end],让[0, end+1]有序
for (int i = 0; i < n-1; ++i) /// 注意这里的 i< n-1
{ /// 从 第二个数开始与前面的比,一个元素时就是有序
int end = i;
int tmp = a[end + 1];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
// tmp 插入中间,或tmp比所有的值大插入最前面,或tmp比所有的值小插入最后面
// 无论哪一个画图都可以看到tmp要插到end后面
a[end + 1] = tmp;
}
}
2.3希尔排序(缩小增量排序)
// 直接插入排序的基础上的优化
// 1、先进行预排序,让数组接近有序
// 2、直接插入排序
// 时间复杂度:O(logN*N) 或者 O(log3N*N)
// 平均的时间复杂度是O(N^1.3)
[9 1 2 5 7 4 8 6 3 5]
第一层循环用于分gap,第一趟5,第二趟2,第三趟 1
第二个循环用于遍历数组n-gap个元素 比如 gap=2 [4 1 2 5 9 8 6 5 7]会遍历到6结束
第三个循环当为i时,第i+gap与前面的间隔gap的所有元素个数比较 与i,i-gap...比
比如 gap=2 [4 1 8 0 9 6 5 2 7] a[i]=9 < a[i+2]=5
原来【4<8<9】但是9与5交换后 【4<8<5<9】破坏了原来的顺序,所以 5还需要继续往前比较,直到遇到一个不满住交换条件直接跳出,因为更前面一定满足
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1)
{
//gap = gap / 2; // logN
gap = gap / 3 + 1; // log3N 以3为底数的对数
// gap > 1时都是预排序 接近有序
// gap == 1时就是直接插入排序 有序
// gap很大时,下面预排序时间复杂度O(N)
// gap很小时,数组已经很接近有序了,这时差不多也是(N)
// 把间隔为gap的多组数据同时排
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
a[i] < a[i+2] 9与5交换 [4 1 8 0 5 6 9 2 7] 但是 a[i]=5,a[i-2]=8
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
3.选择排序
3.1基本思想
3.2直接选择排序
下面的直接选择排序优化了一下,一次循环找最小和最大的两个元素,而上面的图解是一次循环只找最大的元素丢到后面去。
下面的找最小和最大要注意:begin跟maxi重叠,需要修正一下maxi的位置
// 直接选择排序,时间复杂度O(N*N)
// 很差,因为最好情况也是O(N*N)
// N
// N-2
// N-4
// ...
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int mini = begin, maxi = begin;
for (int i = begin; i <= end; ++i)
{
if (a[i] < a[mini])
{
mini = i;
}
if (a[i] > a[maxi])
{
maxi = i;
}
}
Swap(&a[begin], &a[mini]);
// 如果begin跟maxi重叠,需要修正一下maxi的位置
if (begin == maxi)
{
maxi = mini;
}
Swap(&a[maxi], &a[end]);
++begin;
--end;
}
}
3.3堆排序
void Swap(int* a, int* b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
void AdjustDwon(int* a, int n, int root)
{
int parent = root;
int child = parent * 2 + 1; // 默认是左孩子
while (child < n)
{
// 1. 选出左右孩子中大的哪一个
// 注意判断条件 child + 1 < n
if (child + 1 < n && a[child + 1] > a[child])
{
child += 1;
}
if (a[child] > a[parent])
{
Swap(&a[child], &a[parent]);
parent = child;
child = parent * 2 + 1;
}
else
{
break;
}
}
}
// 小堆要求:树中所有的父亲都大于等于孩子
// 大堆要求:树中所有的父亲都小于等于孩子
/// 建小堆--向下调整算法--前提:左右子树都是小堆
void HeapSort(int* a,int n)
{
// 当左右子树不是小堆,使用下面方法
// 倒着从最后一颗子树开始调,叶子不需要调,从倒数最后一个非叶子的子树开始调
/// (n-1)最后一个孩子,(n-1-1)/2 倒数第一个根
/// 循环 从后依次找到每个根 ,用向下调整算法
/// O(N) 建堆
for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
AdjustDwon(a, n, i);
}
///如过建小堆,最小数在堆顶,已经被选出来了,那么在剩余的数中再去选数,但是剩余数结构
///都乱了,需要重建堆才能选出下一个数,建堆的时间复杂度是O(N),那么这样不是可以
/// 但是堆排序就没有优势了
// 升序建大堆 降序建小堆
// 第一个和最后一个交换 把他不看做堆里面
/// 排序
int end = n - 1;
while (end > 0)
{
Swap(&a[0], &a[end]);
AdjustDwon(a, end, 0);
--end;
}
}
4.交换排序
4.1基本思想
4.2冒泡排序
/// 冒泡排序 对局部有序插入排序比冒泡排序的适应性更强
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n - 1; j++) // n-1趟
{
int exchange = 0;
for (int i = 1; i < n - j; i++) // 每趟要比较的个数
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
{
break;
}
}
}
4.3快速排序
4.3.1递归法
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int begin = left, end = right;
int pivot = begin;
int key = a[begin];
// ... 单趟排序
while (begin < end)
{
// 找小
while (begin < end && a[end] >= a[keyi])
{
--end;
}
// 找大
while (begin < end && a[begin] <= a[keyi])
{
++begin;
}
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[keyi]);
// [left, right]
// [left, pivot-1] pivot [pivot+1, right]
// 左子区间和右子区间有序,我们就有序了,如果让他们有序呢? 分治递归
QuickSort(a, left, pivot - 1);
QuickSort(a, pivot + 1, right);
}
4.3.2递归+三数取中+小区间优化
这里有三种单躺排序
- 挖坑法
- 前后指针法
- 左右指针法
// 三数取中
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) >> 1;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return left;
}
else
{
return right;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return left;
}
else
{
return right;
}
}
}
// 挖坑法
int PartSort1(int* a, int left, int right)
{
int index = GetMidIndex(a, left, right);
Swap(&a[left], &a[index]); // // 为了让key还是选第一个 注意 &a[left]中 是left
int begin = left, end = right;
int pivot = begin;
int key = a[begin];
// O(N)
while (begin < end)
{
// 右边找小,放到左边(注意判断条件 begin < end)
while (begin < end && a[end] >= key) // 注意 a[end] >= key 没有等于可能会死循环
--end; // 注意 begin < end 没有 可能会越界
// 小的放到左边的坑里,自己形成新的坑位
a[pivot] = a[end];
pivot = end;
// 左边找大
while (begin < end && a[begin] <= key)
++begin;
// 大的放到左边的坑里,自己形成新的坑位
a[pivot] = a[begin];
pivot = begin;
}
pivot = begin;
a[pivot] = key;
return pivot;
}
// 前后指针
int PartSort2(int* a, int left, int right)
{
int index = GetMidIndex(a, left, right);
Swap(&a[left], &a[index]);
int begin = left, end = right;
int keyi = begin;
while (begin < end)
{
// 找小
while (begin < end && a[end] >= a[keyi])
{
--end;
}
// 找大
while (begin < end && a[begin] <= a[keyi])
{
++begin;
}
Swap(&a[begin], &a[end]);
}
Swap(&a[begin], &a[keyi]);
return begin;
}
/// 左右指针
int PartSort3(int* a, int left, int right)
{
int index = GetMidIndex(a, left, right);
Swap(&a[left], &a[index]);
int keyi = left;
int prev = left, cur = left + 1;
while (cur <= right) // 闭区间小于等于
{
//if (a[cur] < a[keyi])
//{
// prev++;
// Swap(&a[prev], &a[cur]);
//}
if (a[cur] < a[keyi]
&& ++prev != cur) /// 与前面注释的部分相比,这种方法不用自己交换自己
{ /// a[cur] < a[keyi] 注意是小于,与前面的a[end]<=key不同
Swap(&a[prev], &a[cur]);
}
++cur;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
void QuickSort(int* a, int left, int right)
{
if (left >= right)
return;
int keyIndex = PartSort3(a, left, right);
// [left, right]
// [left, keyIndex-1] keyIndex [keyIndex+1, right]
// 左子区间和右子区间有序,我们就有序了,如果让他们有序呢? 分治递归
// QuickSort(a, left, keyIndex - 1);
// QuickSort(a, keyIndex + 1, right);
// 小区间
// 当数据量很大的时候,最后几层递归的数量很大
// 使用小区间优化
if (keyIndex - 1 - left > 10)
{
QuickSort(a, left, keyIndex - 1);
}
else
{
InsertSort(a + left, keyIndex - 1 - left + 1);
}
if (right - (keyIndex + 1) > 10)
{
QuickSort(a, keyIndex + 1, right);
}
else
{
InsertSort(a + keyIndex + 1, right - (keyIndex + 1) + 1);
}
}
4.3.3非递归+三数取中
递归的缺陷:栈帧深度太深,栈空间不够用,可能会溢出
- 递归改非递归:1、直接改循环(简单) 2、借助数据结构栈模拟递归过程(复杂一点)
- 要用到栈的接口函数:栈的函数实现博客
void QuickSortNonR(int* a, int n)
{
ST st;
StackInit(&st);
StackPush(&st, n - 1);
StackPush(&st, 0);
while (!StackEmpty(&st))
{
int left = StackTop(&st);
StackPop(&st);
int right = StackTop(&st);
StackPop(&st);
int keyIndex = PartSort1(a, left, right);
// [left, keyIndex-1] keyIndex [keyIndex+1, right]
if (keyIndex + 1 < right)
{
StackPush(&st, right);
StackPush(&st, keyIndex + 1);
}
if (left < keyIndex - 1)
{
StackPush(&st, keyIndex-1);
StackPush(&st, left);
}
}
StackDestory(&st);
}
5.归并排序
5.1基本思想
5.2代码
归并的递归形式几乎是典型的二分
// 时间复杂度:O(N*logN)
// 空间复杂度:O(N)
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, end1 = mid;
int begin2 = mid + 1, end2 = end;
int i = begin1;
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, (end - begin + 1)*sizeof(int));
}
void MergeSort(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
5.3归并非递归
5.3.1为什么不能使用栈
归并的非递归不能用栈和队列来实现,栈和队列实现非递归适合前序不适合后续。
想一想前面的非递归快速排序,它是使用栈来实现的,思路是选出一个key分出左区间和右区间,左区间右区间都处理后就有序了,不需要回来后再做其他事
情。而归并排序递归回来还要做一些事情。非递归快速排序相当于前序,归并的非递归相当与后续
5.3.2非递归实现思路
5.3.3注意下面两个代码实现非递归memcpy在代码的位置
代码一(注意归并完后一起拷贝回元素组)
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
int gap = 1;
while (gap < n)
{
printf("gap=%d->", gap);
for (int i = 0; i < n; i += 2 * gap)
{
// [i,i+gap-1][i+gap, i+2*gap-1]
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
// 越界-修正边界
if (end1 >= n)
{
end1 = n - 1;
// [begin2, end2]修正为不存在区间
begin2 = n;
end2 = n - 1;
}
else if (begin2 >= n)
{
// [begin2, end2]修正为不存在区间
begin2 = n;
end2 = n - 1;
}
else if (end2 >= n)
{
end2 = n - 1;
}
printf("[%d,%d] [%d, %d]--", begin1, end1, begin2, end2);
int j = begin1;
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++];
}
}
printf("\n");
memcpy(a, tmp, sizeof(int) * n);
gap *= 2;
}
free(tmp);
}
代码二(归并一部分拷贝一部分)
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int)*n);
if (tmp == NULL)
{
printf("malloc fail\n");
exit(-1);
}
// 休息11:48继续
int gap = 1;
while (gap < n)
{
//printf("gap=%d->", gap);
for (int i = 0; i < n; i += 2 * gap)
{
// [i,i+gap-1][i+gap, i+2*gap-1]
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
// end1越界或者begin2越界,则可以不归并了
if (end1 >= n || begin2 >= n)
{
break;
}
else if (end2 >= n)
{
end2 = n - 1;
}
//printf("[%d,%d] [%d, %d]--", begin1, end1, begin2, end2);
int m = end2 - begin1 + 1;
int j = begin1;
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)* m);
}
gap *= 2;
}
free(tmp);
}
5.3.4归并排序的特性总结
- 归并的缺点在于需要O(N)的空间复杂度,归并排序的思考更多的是解决在磁盘中的外排序问题。
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
6. 测试排序的性能对比
每个函数的接口实现前面都有相关代码
// 排序实现的接口
// 插入排序
void InsertSort(int* a, int n);
// 希尔排序
void ShellSort(int* a, int n);
// 选择排序
比特就业课void SelectSort(int* a, int n);
// 堆排序
void AdjustDwon(int* a, int n, int root);
void HeapSort(int* a, int n);
// 冒泡排序
void BubbleSort(int* a, int n)
// 快速排序
void QuickSort(int* a, int left, int right);
// 归并排序
void MergeSort(int* a, int n)
// 测试排序的性能对比
void TestOP()
{
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);
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];
}
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 begin5 = clock();
QuickSort(a5, 0, N-1);
int end5 = clock();
int begin6 = clock();
MergeSort(a6, 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("QuickSort:%d\n", end5 - begin5);
printf("MergeSort:%d\n", end6 - begin6);
free(a1);
free(a2);
free(a3);
free(a4);
free(a5);
free(a6);
}
排序的稳定性
非比较排序
注意前面的排序都是比较排序,元素之间会通过比较大小进行排序
下面的排序时非比较排序--其排序只试用与整数
- 桶排序
- 计数排序
- 基数排序
计数排序
思想:计数排序又称为鸽巢原理,是对哈希直接定址法的变形应用。 操作步骤:
- 统计相同元素出现次数
- 根据统计的结果将序列回收到原来的序列中
局限性:
- 如果是浮点数或字符串就不能用了
- 如果数据范围很大,空间复杂度就会很高
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
// 时间复杂度:O(max(range, N))
// 空间复杂度:O(range)
void CountSort(int* a, int n)
{
int min = a[0], max = a[0];
for (int i = 1; i < n; ++i)
{
if (a[i] < min)
min = a[i];
if (a[i] > max)
max = a[i];
}
// 统计次数的数组
int range = max - min + 1;
int* count = (int*)malloc(sizeof(int)*range);
if (count == NULL)
{
printf("malloc fail\n");
exit(-1);
}
memset(count, 0, sizeof(int)*range);
// 统计次数
for (int i = 0; i < n; ++i)
{
count[a[i] - min]++;
}
// 回写-排序
int j = 0;
for (int i = 0; i < range; ++i)
{
// 出现几次就会回写几个i+min
while (count[i]--)
{
a[j++] = i + min;
}
}
}