目录
排序接口
//插入排序
void InsertSort(int* a, int n);
//希尔排序
void ShellSort(int* a, int n);
//冒泡排序
void BubbleSort(int* a, int n);
//选择排序
void SelectSort(int* a, int n);
//堆排序
void HeapSort(int* a, int n);
//快速排序
int GetMidIndex(int* a, int left, int right);//三数取中
int PartSort2(int* a, int left, int right);//对一段,无递归
void QuickSort(int* a, int begin, int end);//对整个数组,有递归,有小区间优化 //递归法
int PartSort2(int* a, int left, int right);//对一段,无递归
void QuickSortNonR(int* a, int begin, int end);//含栈 //非递归法
//归并排序
void _MergeSort(int* a, int begin, int end, int* tmp);
void MergeSort(int* a, int n); //有小区间优化(插入排序) //递归法
void MergeSortNonR(int* a, int n); //非递归法
//计数排序
void CountSort(int* a, int n);
排序方法
插入排序 O(N^2)
void InsertSort(int* a, int n)
{
for (int i = 0; i < n - 1; ++i) //以end为标准 从1开始插入
{
// [0, end] 有序,插入tmp依旧有序
int end = i;
int tmp = a[i + 1]; //tmp为end后一位
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
--end;
}
else
{
break;
}
}
a[end + 1] = tmp;
}
}
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高
- 时间复杂度:O(N^2)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
希尔排序 O(N^1.3)
运用多组并排的方式,且一定会存在gap=1的情况,这时候就是插入排序
希尔排序思想:就是对插入排序的优化,若为升序,是让小的数更快的在前面,是让大的数更快的在后面,且最后一次gap=1,就是插入排序,总之希尔排序大大减少了插入排序一步步挪的次数。时间复杂度:O(N^1.3)
void ShellSort(int* a, int n)
{
// 1、gap > 1 预排序
// 2、gap == 1 直接插入排序
int gap = n;
while (gap > 1)
{
gap = gap / 3 + 1; // +1可以保证最后一次一定是1 最后一定是两种情况:2/3 3/3
// gap = gap / 2; //最后一定是一种情况:2/2
for (int i = 0; i < n - gap; ++i) //保证tmp为数组中的数,没有越界情况
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end -= gap;
}
else
{
break;
}
}
a[end + gap] = tmp;
}
}
}
选择排序 O(N^2)
思路:
1.先通过begin到end的遍历,找到该区间最大值和最小值
2.将最小值移到区间最左边,将最大值移到区间最右边
void SelectSort(int* a, int n)
{
int begin = 0, end = n - 1;
while (begin < end)
{
int maxi = begin, mini = begin;
for (int i = begin; i <= end; i++)
{
if (a[i] > a[maxi])
{
maxi = i;
}
if (a[i] < a[mini])
{
mini = i;
}
}
Swap(&a[begin], &a[mini]);
// 如果maxi和begin重叠,修正一下即可
if (begin == maxi)
{
maxi = mini;
}
Swap(&a[end], &a[maxi]);
++begin;
--end;
}
}
堆排序 O(N*logN)
排升序,建大堆,排降序,建小堆
向下调整和向上调整:时间复杂度O(logN)
利用向下调整建大/小堆:时间复杂度O(N)
堆排序:时间复杂度O(N*logN)
void AdjustDown(int* a, int n, int parent)
{
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]);
AdjustDown(a, end, 0);
--end;
}
}
冒泡排序 O(N)
void BubbleSort(int* a, int n)
{
for (int j = 0; j < n; ++j)
{
bool exchange = false;
for (int i = 1; i < n - j; i++) //防越界:保后面,舍前面 所以a[i-1]为前面一位,a[i]为后面一位
{
if (a[i - 1] > a[i]) //每一趟都是从a[0]开始换,无论换没换,都会从a[0]移动到a[n-1-j]
{ //该代码中移动的是a[i-1]
int tmp = a[i];
a[i] = a[i - 1];
a[i - 1] = tmp;
exchange = true;
}
}
if (exchange == false) //若一趟中没发生任何交换,则数组有序
{
break;
}
}
}
快速排序(O(logN*N))
递归法
下面三种方法其实本质一样,形式不同
时间复杂度: O(logN*N)
空间复杂度:O(logN)
可以小区间优化,只有有递归,就可以小区间优化
1.1hoare
三数取中作用:防止a[keyi]为数组最小值
如果keyi每次取得都是中间值,则效率很好,但如果数组有序,a[keyi]每次取值都是最小值,则易出现下图情况,效率低。
所以三数取中就是来解决这个问题,注意:三数取中是间中间大小的数换掉最左边,而不是将keyi移到中间数位置,keyi(下标)永远在最左边
//三数取中
int GetMidIndex(int* a, int left, int right) //left, right都是下标
{
int mid = (left+right)/2;
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return right;
}
else
{
return left;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return right;
}
else
{
return left;
}
}
}
// hoare
int PartSort1(int* a, int left, int right) //left, right都是下标
{
int midi = GetMidIndex(a, left, right); //三数取中
Swap(&a[left], &a[midi]);
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[keyi], &a[left]);
return left;
}
void QuickSort(int* a, int begin, int end) //begin, end都是下标
{
if (begin >= end)
return;
// 小区间优化
/*if (end - begin + 1 < 10)
{
InsertSort(a+begin, end - begin + 1);
return;
}*/
int keyi = PartSort1(a, begin, end);
//int keyi = PartSort2(a, begin, end);
//int keyi = PartSort3(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
hoare思路:先右边找小,再左边找大,左右交换,直到相遇,再相遇点和最左边交换。
其实相遇点一定是比keyi小,理由如下:
1.2 挖坑法
思路:
1. 先把最左边的数作为keyi存起来,且最左边为坑(hole)
2. 右边找小,找到小,小放入坑中,小的位置变成坑
左边找大,找到大,大放入坑中,大的位置变成坑,重复直到相遇
3. 相遇后,相遇点一定是坑,因为left和right有一个一定是坑,将keyi放入坑中
// 挖坑法,也有三数取中
int PartSort2(int* a, int left, int right) //left, right是下标
{
int midi = GetMidIndex(a, left, right);
Swap(&a[left], &a[midi]);
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;
}
void QuickSort(int* a, int begin, int end)//begin,end是下标
{
if (begin >= end)
return;
// 小区间优化
/*if (end - begin + 1 < 10)
{
InsertSort(a+begin, end - begin + 1);
return;
}*/
//int keyi = PartSort1(a, begin, end);
int keyi = PartSort2(a, begin, end);
//int keyi = PartSort3(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
1.3前后指针法
思路:1.起始位置:prev在最左边,cur在其后一位
2.若a[cur]>=a[keyi],cur++,prev不加
若a[cur]<a[keyi],cur++,prev++,a[cur]和a[prev]交换,直到cur>right
(保证了a[prev]永远<keyi,除起始点)(两端区间的维护)
3.cur>right后,a[prev]与keyi交换
// 前后指针法,也有三数取中
int PartSort3(int* a, int left, int right)//left, right是下标
{
int midi = GetMidIndex(a, left, right);
Swap(&a[left], &a[midi]);
int prev = left;
int cur = left + 1;
int keyi = left;
while (cur <= right)
{
if (a[cur] < a[keyi] && ++prev != cur)
{
Swap(&a[prev], &a[cur]);
}
++cur;
}
Swap(&a[prev], &a[keyi]);
keyi = prev;
return keyi;
}
void QuickSort(int* a, int begin, int end)// begin, end是下标
{
if (begin >= end)
return;
// 小区间优化
/*if (end - begin + 1 < 10)
{
InsertSort(a+begin, end - begin + 1);
return;
}*/
//int keyi = PartSort1(a, begin, end);
//int keyi = PartSort2(a, begin, end);
int keyi = PartSort3(a, begin, end);
// [begin, keyi-1] keyi [keyi+1, end]
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
2.三路划分
目的:防止出现数组全为一个数或每个数很多的情况
//随机数三数取中,改mid
int GetMidIndex(int* a, int left, int right) //left, right都是下标
{
//int mid =(left+right)/2;
int mid= left+ (rand() %(right-left)); //与固定三数取中唯一不同点,还要记得加srand(time(0));
if (a[left] < a[mid])
{
if (a[mid] < a[right])
{
return mid;
}
else if (a[left] < a[right])
{
return right;
}
else
{
return left;
}
}
else // a[left] > a[mid]
{
if (a[mid] > a[right])
{
return mid;
}
else if (a[left] > a[right])
{
return right;
}
else
{
return left;
}
}
}
void Swap(int* p1,int* p2)
{
int t=(*p1);
(*p1)=(*p2);
(*p2)=t;
}
void QuickSort(int* a,int begin,int end)
{
srand(time(0));
if(begin>=end)
{
return;
}
int midi=GetMidIndex(a,begin,end); //三数取中:防止a[keyi]为最小值,但这个三数取中是随机数取中,是进阶版的三数取中,前面的三数取中也可以用进阶版
Swap(&a[midi],&a[begin]);
int key=a[begin];
int cur=begin+1;
int right=end;
int left=begin;
while(cur<=right)
{
if(a[cur]<key)
{
Swap(&a[cur],&a[left]);
cur++; left++;
}
else if(a[cur]>key)
{
Swap(&a[cur],&a[right]);
right--;
}
else
{
cur++;
}
}
//[begin,left-1] [left,right] [right+1,end]
QuickSort(a,begin,left-1);
QuickSort(a,right+1,end);
}
这道题排序用普通快排过不了,三路划分+随机取中可以过,归并排序也可以过。
非递归法(也含有1.1,1.2,1.3)
非递归法:我是用栈实现的,栈的相关函数在我的博客栈和队列的实现的源码里有,该方法也用到了hoare,挖坑法,前后指针法。
// 挖坑法
int PartSort2(int* a, int left, int right) //left, right是下标
{
int midi = GetMidIndex(a, left, right);
Swap(&a[left], &a[midi]);
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;
}
void QuickSortNonR(int* a, int begin, int end)// begin, end是下标
{
ST st;
STInit(&st);
STPush(&st, end);
STPush(&st, begin);
while (!STEmpty(&st))
{
int left = STTop(&st);
STPop(&st);
int right = STTop(&st);
STPop(&st);
int keyi = PartSort2(a, left, right); //重点
//int keyi = PartSort1(a, left, right);
//int keyi = PartSort3(a, left, right);//1,2,3种方法都可以
// [left, keyi-1] keyi [keyi+1, right]
if (keyi + 1 < right) //确保右区间的个数>=2
{
STPush(&st, right);
STPush(&st, keyi + 1);
}
if (left < keyi - 1) 确保左区间的个数>=2
{
STPush(&st, keyi - 1);
STPush(&st, left);
}
}
STDestroy(&st);
}
归并排序 O(logN * N)
时间复杂度:O(logN * N)
空间复杂度:O(N)
可以用小区间优化,在递归法里
递归法
归并排序不是左右同时进行的,是有左右顺序的,因为递归是有左右顺序的
含小区间优化
void _MergeSort(int* a, int begin, int end, int* tmp) //先分割成两部分,两部分分别归并,成为有序的两部分,这两部分再一起归并,成为一个有序的整体
{
if (begin == end)
return;
// 小区间优化
/*if (end - begin + 1 < 10)
{
InsertSort(a+begin, end - begin + 1);
return;
}*/
int mid = (begin + end) / 2;
// [begin, mid] [mid+1, end]
_MergeSort(a, begin, mid, tmp);
_MergeSort(a, mid + 1, end, tmp); //分割,分割后依然两部分有序
// 归并两个区间
// ...
int begin1 = begin, end1 = mid; //归并,将有序的两部分归并,使两部分变为一个整体,归并后该整体是有序的
int begin2 = mid + 1, 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);
_MergeSort(a, 0, n - 1, tmp);
free(tmp);
}
非递归法
我们可以发现,其实每一个数最后一趟都会归并到,不会落数。归并两部分,不需要两部分数量相同,只需要两部分有序
接下来,讲讲在某一趟中,边界的情况分析:
总的来说,就是只有第二部分有,就归并,无就break
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
// 1 2 4 ....
int gap = 1; //有gap个数
while (gap < n)
{
int j = 0;
for (int i = 0; i < n; i += 2 * gap)
{
// 每组的合并数据
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
//printf("[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
if (end1 >= n || begin2 >= n)
{
break;
}
// 修正
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));
}
printf("\n");
//memcpy(a, tmp, sizeof(int) * n);
//在这里,为什么不可以归并一趟,拷贝一趟,是因为数组末尾有些直接break,这些数都没有赋值到tmp数组上,那tmp缺数了,不能拷贝n个数
gap *= 2;
}
free(tmp);
}
若当区间2不存在时,将区间2设为不存在区间,那么那一组就可以进行归并,那就可以实现归并一趟,拷贝一趟,代码如下:(不过,更推荐上面的归并一组,拷贝一组的方法)
void MergeSortNonR(int* a, int n)
{
int* tmp = (int*)malloc(sizeof(int) * n);
// 1 2 4 ....
int gap = 1;
while (gap < n)
{
int j = 0;
for (int i = 0; i < n; i += 2 * gap)
{
// 每组的合并数据
int begin1 = i, end1 = i + gap - 1;
int begin2 = i + gap, end2 = i + 2 * gap - 1;
//printf("修正前:[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
if (end1 >= n)
{
end1 = n - 1;
// 不存在区间
begin2 = n;
end2 = n - 1;
}
else if (begin2 >= n)
{
// 不存在区间
begin2 = n;
end2 = n - 1;
}
else if(end2 >= n)
{
end2 = n - 1;
}
//printf("修正后:[%d,%d][%d,%d]\n", begin1, end1, begin2, end2);
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);
}
计数排序 O(N+Range)
时间复杂度:O(N+Range)
空间复杂度:O(Range)
缺陷1:依赖数据范围,适用于范围集中的数组
缺陷2:只能用于整形
void CountSort(int* a, int n)
{
int min = a[0], max = a[0];
for (int i = 0; i < n; i++)
{
if (a[i] < min)
{
min = a[i];
}
if (a[i] > max)
{
max = a[i];
}
}
int range = max - min + 1;
int* countA = (int*)malloc(sizeof(int) * range);
memset(countA, 0, sizeof(int) * range);
// 统计次数
for (int i = 0; i < n; i++)
{
countA[a[i] - min]++;
}
// 排序
int k = 0;
for (int j = 0; j < range; j++)
{
while (countA[j]--)
{
a[k++] = j + min;
}
}
}