文章目录
插入
InsertSort
//1.先写单趟
//2.再写多趟
//画图!!
void InsertSort(int* a, int n)
{
assert(a);
assert(n >= 0);
//多躺排序
//end下标从0~n-2
for (int i = 0; i < n-1; i++)
{
//1.先写单趟排序
//把tmp插入到数组[0,end]的有序区间中
int end = i;
int tmp = a[end+1];
while (end >= 0)
{
if (tmp < a[end])
{
a[end + 1] = a[end];
end--;
}
else
{
break;
}
}
//end移到-1时,需要把tmp放到a[0]里面
//tmp>=a[end]也需要把tmp放到a[end]后面
a[end + 1] = tmp;
}
}
//时间复杂度O(N^2)
ShellSort
gap越大,大的数和小的1数可以更快的挪动到对应的方向
gap越大,越不接近有序gap越小,大的数和小的1数可以更慢的挪动到对应的方向
gap越小,越接近有序gpa==1 就是插入排序,接近O(N)
1.预排序->接近有序(先分组,对分组的数据插入排序)
2.直接插入排序
void ShellSort(int* a, int n)
{
int gap = n;
while (gap > 1) // n/3/3/3.../3 == 1 -》 3^x = n x就是这个while循环跑的次数
{
//+1保证最后依次gap是1
gap = (gap / 3 + 1);
//先预排序,gap=1时直接插入排序
// 最坏的情况:逆序,gap很大的时-》O(N)
// ...
//gap很小时本来应该是O(N*N),但是经过前面的预排序,数组已经很接近有序的,所这里还是O(N)
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;
}
//end移到<0时,需要把tmp放到a[end+gap]里面
//tmp>=a[end]也需要把tmp放到a[end+gap]后面
a[end + gap] = tmp;
}
}
}
}
//平均复杂度O(N^1.3) 因为gap不确定
// gap为3的情况:O(log3(N) * N) 这里log3(N)是以3为底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);
}
InsertSort:1118
ShellSort:8
注意:要在release版本下测试才公平,这样编译器优化发挥到极致
选择
SelectSort
void SelectSort(int* a, int n)
{
int left = 0;
int right = n - 1;
while (left < right)
{
int minIndex = left;
int maxIndex = left;
//选出最大的值和最小的值
for (int i = left; i <= right; i++)
{
if (a[i] < a[minIndex])
{
minIndex = i;
}
if (a[i] > a[maxIndex])
{
maxIndex = i;
}
}
Swap(&a[left], &a[minIndex]);
//极端情况下需要修正,第一个数恰好是最大的数,但它又要进行交换,改变了maxIndex的位置
if (left == maxIndex)
{
maxIndex = minIndex;
}
Swap(&a[right], &a[maxIndex]);
++left;
--right;
}
}
//N N-2 N-4
//O()
HeapSort
//排大根堆
void AdjustDown(int* a, int n, int root)
{
int parent = root;
int child = parent * 2 + 1;
while (child < n)
{
//选出大的孩子
//先检查是否有越界再访问
if (child+1 < n && a[child+1] > a[child])
{
++child;
}
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]);
//选次大的
//左右子树均为大堆,只需一次向下调整就能从n-1选出原来n里面次大的数
AdjustDown(a, end, 0);
end--;
}
对比测试
InsertSort:1144
ShellSort:8
SelectSort:5162
HeapSort:7
交换
BubbleSort
void BubbleSort(int* a, int n)
{
/*for (int i = 0; i < n; i++)
{
for (int j = 1; j < n-i; j++)
{
if (a[i-1]>a[i])
{
Swap(&a[i - 1], &a[i]);
}
}
}*/
//或者优化一下
for (int end = n; end > 0; end--)
{
int exchange = 0;
for (int i = 1; i < end; i++)
{
if (a[i - 1] > a[i])
{
Swap(&a[i - 1], &a[i]);
exchange = 1;
}
}
if (exchange == 0)
{
//说明没有发生交换,也就是前面的恰好是有序的
break;
}
}
}
冒泡和插入相比谁更好?
顺序有序,一样好
接近有序,插入更好
QuickSort
单趟排序
选出一个key,一般是最左边的,或者是最右边的
key放到他正确的位置上,左边的比key小,右边的比key大选左边做key,让right先走
right找小,left找大,交换
直到相遇
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int left = begin, right = end;
int keyi = left;
while (left < right)
{
//要保证相遇位置的值一定比key先小,要让right先走
//找小
//left < right防止越界
while (left < right && a[right] >= a[keyi])
{
--right;
}
//找大
while (left < right && a[left] <= a[keyi])
{
++left;
}
//交换
Swap(&a[left], &a[right]);
}
int meeti = left;
Swap(&a[keyi], &a[left]);
//分治思想
//[begin,meeti-1] meeti [meeti+1,end]
QuickSort(a, begin, meeti - 1);
QuickSort(a, meeti+1, end);
}
PartSort
void QuickSort(int* a, int begin, int end)
{
//区间有多个值时才继续,一个值都没有就return
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)
//空间复杂度O(logN) 树的高度
用QuickSort去排一个有序的序列,比InsertSort还慢
InsertSort:1143
ShellSort:7
SelectSort:5092
HeapSort:7
QuickSort:1309
左右指针法
//单趟排序 hoare版本 -- 左右指针法
int PartSort1(int* a, int left, int right)
{
int keyi = left;
while (left < right)
{
//要保证相遇位置的值一定比key先小,要让right先走
//找小
//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[keyi], &a[left]);
return left;
}
挖坑法
//挖坑法
int PartSort2(int* a, int left, int right)
{
//先把left作为key保存起来,left的位置天生是一个坑
int key = a[left];
while (left < right)
{
//找小
while (left < right && a[right] >= key)
{
--right;
}
//放到左边的hole中,右边就形成新的hole
a[left] = a[right];
//找大
while (left < right && a[left] <= key)
{
++left;
}
//放到右边的hole中,左边就形成新的hole
a[right] = a[left];
}
//相遇
a[left] = key;
return left;
}
前后指针法
cur prev 一前一后
cur去找比keyi位置小的值
找到小之后,++prev,再交换prev和cur位置的值
直到数组尾
最后交换prev和keyi位置的值
思想: 把小的往左边留,大的往右边留,prev++是为了跟上cur
//前后指针法
int PartSort3(int* a, int left, int right)
{
//三数取中优化
int midIndex = GetMidIndex(a, left, right);
Swap(&a[left], &a[midIndex]);
int keyi = left;
int prev = left, cur = left + 1;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
//prev++;
//prev和cur相同时,交换也没影响
Swap(&a[prev], &a[cur]);
}
cur++;
}
Swap(&a[keyi], &a[prev]);
return prev;
}
优化
三数取中优化
三数取中 key如果越接近中位数,效率就越高
int GetMidIndex(int* a, int left, int right)
{
int mid = (left + right) >> 1;//位运算效率比除法高
//left mid right
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;
}
}
}
//单趟排序 hoare版本 -- 左右指针法
int PartSort1(int* a, int left, int right)
{
int midIndex = GetMidIndex(a, left, right);
Swap(&a[left], &a[midIndex]);
int keyi = left;
while (left < right)
{
//要保证相遇位置的值一定比key先小,要让right先走
//找小
//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[keyi], &a[left]);
return left;
}
三数取中优化后,恰好是有序的数据
InsertSort:1134
ShellSort:8
SelectSort:5144
HeapSort:7
QuickSort:1
1000w的随机数据排序:
InsertSort:0
ShellSort:1217
SelectSort:0
HeapSort:2530
QuickSort:593
小区间优化
void QuickSort(int* a, int begin, int end)
{
//区间有多个值时才继续,一个值都没有就return
if (begin >= end)
return;
//1.如果这个子区间数据较多,继续选key单趟,分割子区间分治递归
//2.如果子区间数据较少,再去分治递归不太划算
if (end-begin > 10)
{
int keyi = PartSort3(a, begin, end);
//[begin,keyi-1] keyi [keyi+1,end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
else
{
//考虑到分割中间的较小子区间
InsertSort(a + begin, end - begin + 1);
}
}
面试时写快排,不用写三数取中的优化,小区间优化
写完了可以讲一下优化方案
小区间优化优化效果不明显, 本质是减少递归树的最后几层,减少递归调用的次数,但是使用插入排序也是有消耗的
三数取中 本质是防止最坏的情况发生
排数据时,可以根据数据量的大小调整小区间优化,用ShellSort HeapSort等也行
非递归
递归
现代编译器优化很好,性能已经不是大问题
最大的问题–>递归深度太深,程序本身没问题,但是栈空间不够,导致栈溢出改成非递归:
1.直接改循环–>斐波那契数列
2.树遍历非递归和快排非递归,只能用Stack存储数据模拟非递归过程
void QuickSortNonR(int* a, int begin, int end)
{
Stack st;
StackInit(&st);
StackPush(&st, begin);
StackPush(&st, end);
while (!StackEmpty(&st))
{
int left, right;
//先入左后入右,出来时就先出右后出左
right = StackTop(&st);
StackPop(&st);
left = StackTop(&st);
StackPop(&st);
int keyi = PartSort3(a, left, right);
if (left<keyi-1)
{
StackPush(&st, left);
StackPush(&st, keyi-1);
}
if (keyi+1<right)
{
StackPush(&st, keyi + 1);
StackPush(&st, right);
}
}
StackDestroy(&st);
}
对比测试
InsertSort:1137
ShellSort:8
SelectSort:5149
HeapSort:8
QuickSort:5
//优化过后:
InsertSort:1129
ShellSort:8
SelectSort:5112
HeapSort:8
QuickSort:1
归并
MergeSort
假设如果左边右边均有序了,那么一归并,整体就有序了
取小的尾插到下面的数据,直到一个区间结束
再把另一个区间剩下的数据尾插到最后
归并完之后拷回去分治思想
void _MergeSort(int* a, int left, int right, int* tmp)
{
if (left >= right)
{
return;
}
int mid = (left + right) >> 1;
//[left, mid] [mid+1, right]
_MergeSort(a, left, mid, tmp);
_MergeSort(a, mid+1, right, tmp);
//两段有序子区间归并到tmp再拷贝回去
int begin1 = left, end1 = mid;
int begin2 = mid+1, end2 = right;
int i = left;
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++];
}
//拷贝回去
for (int j = left; j <= right; j++)
{
a[j] = tmp[j];
}
}
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);
}
时间复杂度O(N*logN)
每一层归并都是N
logN层
空间复杂度O(N)
InsertSort:1141
ShellSort:7
SelectSort:5085
HeapSort:7
QuickSort:1
MergeSort:8
MergeSortNonR
一一归并 二二归并 四四归并
void _Merge(int* a, int* tmp, int begin1, int end1, int begin2, int end2)
{
//两段有序子区间归并到tmp再拷贝回去
int i = begin1;
int j = 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++];
}
//拷贝回去
for (; j <= end2; j++)
{
a[j] = tmp[j];
}
}
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;//gap=1表示一一归并
while (gap < n)
{
for (int i = 0; i < n; i += 2 * gap)
{
//[i,i+gap-1] [i+gap, i+2*gap-1]
_Merge(a, tmp, i, i + gap - 1, i + gap, i + 2 * gap - 1);
}
gap *= 2;
}
free(tmp);
}
程序小bug:
1.最后一个小组归并时,第二个小区间不存在,不需要归并
2.最后一个小组归并时,第二个小区间存在,但不够gap个
3.最后一个小组归并时,第一个小区间不够gap个,不需要归并
13问题可以合并,第二个小区间不存在即可判断
//非递归
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;//gap=1表示一一归并
while (gap < n)
{
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, begin2 = i + gap, end2 = i + 2 * gap - 1;
//分析知,如果第二个小区间不存在就不需要归并
if (begin2 >= n)
{
break;
}
//如果第二个小区间存在,但是不够gap个,结束位置越界
//修正一下
if (end2 >= n)
{
end2 = n - 1;
}
_Merge(a, tmp, begin1, end1, begin2, end2);
}
gap *= 2;
}
free(tmp);
}
非比较排序
CountSort
绝对映射
统计出每个数出现的次数
A[i]是几就对Count数组对应位置的值++
统计出次数再排序
相对映射
映射时减去最小的那个值
void CountSort(int* a, int n)
{
int max = a[0], min = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] > max)
max = a[i];
if (a[i] < min)
min = a[i];
}
int range = max - min + 1;
int* count = (int*)malloc(sizeof(int) * range);
if (count == NULL)
{
printf("malloc fail\n");
exit(-1);
}
//数组初始化为0
memset(count, 0, sizeof(int) * range);
//统计次数
for (int i = 0; i < n; i++)
{
//相对映射
count[a[i] - min]++;
}
//把数据写回去 count数组里放的是数据出现的次数
int i = 0;
for (int j = 0; j < range; j++)
{
while (count[j]--)
{
a[i++] = j + min;
}
}
free(count);
}
//时间复杂度O(2*N+range)
//只适合一组数据,数据的范围比较集中
//空间复杂度O(range)
//如果数据集中,效率很高
//并且只适合整数 浮点数,字符串不行 因为没有用到比较
内排序
数据量相对少一些,可以放到内存中排序
外排序
数据量较大,内存中放不下,数据放到磁盘文件中排序
归并排序既可以用于内排序,也可以用于外排序
十亿个整数,放到文件A中,需要排序?
假设只有512M内存
10亿int 约等于4G (实际上小于4G)
每次读文件A的1/8 也就是512M左右到内存中,进行排序,然后写到一个小文件,再继续读1/8,重复过程
再将8个有序小文件归并排序核心:在内存中排序效率会很高
在内存中排序时不能用归并,归并有O(N)的空间复杂度 用快排就行
总结
稳定性
数组中相同的值,排完序以后,相对顺序不变,就是稳定的,否则就是不稳定的
可以做到稳定就是稳定
注意: 简单选择排序是不稳定的,很多书上都是错的
选择排序找到最大的后,会去交换,可能影响其他值
希尔排序不稳定,相同的值在预排时有可能分到不同的组里面
快排不稳定,选的key要换到中间,有可能原来左边有和key相同的值,排完之后就被换过去了
冒泡插入归并是稳定