目录
1.冒泡排序
冒泡排序应该是从C语言阶段就开始使用的最为简单的排序,其思想为单趟两两比较,将大的数放到最后,之后再对剩余的进行排序再放第二大的数。
动画演示:
//冒泡排序
void BubbleSort(int* a, int n)
{
//n-1趟
for (int i = 0; i < n-1; i++)
{
//单趟 右边已经排好了顺序所以不用比较
for (int j = 0; j < n - i-1; j++)
{
if (a[j] > a[j + 1])
{
Swap(&a[j], &a[j + 1]);
}
}
}
}
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
2.插入排序
插入排序的单趟思想是一个元素依次与前面的元素进行比较,如果比前面的元素小就交换。
单趟演示:
那么总体思路就是从第二个元素开始排,以保证前面的元素一定是有序的。
动画演示:
//插入排序
void InsertSort(int* a, int n)
{
//tmp储存要比较的数,因此到n-1为止,
for (int i = 0; i < n - 1; i++)
{
//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的下一个位置给tmp
a[end+1] = tmp;
}
}
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
3.希尔排序
希尔排序是插入排序的plus版,其在排序之前要先进行预排序,通过使用gap将文件以gap间距分为gap组,之后再使用插入排序。预排序之后再进行一次插入排序。
gap的值是可以改的,上面是以3为例
gap越大,越不接近有序。
gap越小,越接近有序。如果gap==1就是直接插入排序。
//希尔排序
void ShellSort(int* a, int n)
{
//这里进行了一些改进,使gap的值可以变化,并且gap的值可以为1
int gap = n;
while (gap>1)
{
//+1是为了防止出现gap==0
gap = gap / 3 + 1;
//分组不会每次都恰好,防止越界,在n-gap及之后的元素都已经分好组了
for (int i = 0; i < n - gap; i++)
{
int end = i;
//每组的元素间距是gap,故end+gap
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,甚至下面的代码就是插入排序。希尔排序是插入排序的优化版本,其速度也比插入排序快很多
- 时间复杂度:O(N^1.3)
- 空间复杂度:O(1)
- 稳定性:不稳定
4.堆排序
堆排序是根据数据结构中的堆来设计的,利用堆删除思想来进行排序。
建堆和堆删除中都使用向下调整,堆顶元素是当前堆中的最大值或最小值,将堆顶元素与堆中最后一个元素交换,然后将剩余元素重新调整成堆,再取出堆顶元素。重复上述步骤,直到所有元素都被取出,即完成了排序。
升序:建大堆
降序:建小堆
//堆排序
void Adjustdown(int* a, int size, int parent)
{
int child = parent * 2 + 1;
while (child<size)
{
if (child+1 <size && a[child+1] < a[child ] ) // <小堆 >大堆
{
child++;
}
if (a[child] < a[parent]) // <小堆 >大堆
{
swap(&a[parent], &a[child]);
parent = child;
child = child * 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)
- 稳定性:不稳定
5.选择排序
选择排序单趟选出最小的元素,再与选择的元素位置交换
总体就是左边是已经排好序的,右边在依次排序。整体十分简单。因此我们加上一些优化,左右两边都开始,把小的放在左边,大的放在右边,
//选择排序
void SelectSort(int* a, int n)
{
int begin = 0;
int end = n - 1;
while (begin<=end)
{
//mini记录最小元素的位置 maxi记录最大元素的位置
int mini = begin;
int 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]);
//判断最大元素是否在最左边,如果在就更新位置
if (maxi == begin)
{
maxi = mini;
}
//把最大元素放到右边
Swap(&a[maxi], &a[end]);
begin++;
end--;
}
}
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
6.快速排序
它敢这么叫就一定有它的原因
快排的思想是取一元素为基准,将其余元素小的放在它左边,大的放在它右边。使左右两边局部有序
1.hoare版本
这样就会有一些疑问,为什么相遇的地方一定是key要在的位置?那我们分析一下
相遇无非就两种,L去遇R,R去遇L
L去遇R:L去遇R,因为R是先走,所以说明此时R已经找到小了,并且L这一路上都没有大了。那么相遇的位置左边都是小的,右边都是大的,那么key的位置就找到了
R去遇L:R先走,这就说明R一路上都是比key大的元素,极端一点,LR在最左侧相遇,那么此时右边都比key大,那么key的位置也就在那
总结:
右边找小,左边找大,且右边先走
因为快排是递归左右区间,所以最好key不大不小,不然左右区间有的大,有的小,造成效率上的损失。因此在代码中加入了三数取中的优化
//三数取中
int GetMidi(int* a, int begin, int end)
{
int midi = (begin + end) / 2;
//begin end midi三个数选中位数
if (a[begin] > a[midi])
{
if (a[midi] > a[end])
{
return midi;
}
else if (a[begin] > a[end])
{
return end;
}
else
return begin;
}
else
{
if (a[begin] > a[end])
{
return begin;
}
else if (a[midi] < a[end])
{
return midi;
}
else
return end;
}
}
//快速排序
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int midi = GetMidi(a, begin, end);
Swap(&a[midi], &a[begin]);
int left = begin;
int right = end;
int keyi = begin;
while (left < right)
{
//右边找小,右边先走
while (left < right && a[right] >= a[keyi])
{
right--;
}
//左边找大
while (left < right && a[left] <= a[keyi])
{
left++;
}
Swap(&a[left], &a[right]);
}
//找的了key的位置
Swap(&a[left], &a[keyi]);
keyi = left;
//此时的数组可以划分为:[begin,keyi-1] keyi [keyi+1,end]
//递归key的左右区间
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 < 10)
{
InsertSort(a + begin, end - begin + 1);
}
else
{
int midi = GetMidi(a, begin, end);
Swap(&a[midi], &a[begin]);
int left = begin;
int right = end;
int keyi = begin;
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;
//[begin,keyi-1] keyi [keyi+1,end]
QuickSort1(a, begin, keyi - 1);
QuickSort1(a, keyi + 1, end);
}
}
小区间优化效果有,但不明显
2.挖坑法
对比hoare版本的思想,挖坑法则更为顺畅。但核心思想还是不变。
挖坑法是在key处挖个坑位,R先走找小,把小放在坑中,此时坑位更新位置。再L找大,最后相遇后会有一个坑位,这便是key的位置
//挖坑法
int PartSort2(int* a, int begin, int end)
{
int midi = GetMidi(a, begin, end);
Swap(&a[midi], &a[begin]);
int key = a[begin];
int holei = begin;
while (begin < end)
{
//右边找小
while (begin < end && a[end] >= key)
{
end--;
}
//更新坑位的值和位置
a[holei] = a[end];
holei = end;
//左边找大
while (begin < end && a[begin] <= key)
{
begin++;
}
a[holei] = a[begin];
holei = begin;
}
//将key放到最后的值
a[holei] = key;
return holei;
}
void QuickSort(int* a, int begin, int end)
{
if (begin >= end)
return;
int keyi = PartSort2(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
3.前后指针法
这种方法是思想是,创建前后指针prev和cur,prev指针指向开始,cur指针指向prev指针的后一个位置
cur找小,找到就++prev,交换prev和cur位置的值,++cur
结束标志就是cur到达最右
// 快速排序前后指针法
int PartSort3(int* a, int begin, int end)
{
int midi = GetMidi(a, begin, end);
Swap(&a[midi], &a[begin]);
int keyi = begin;
int cur = begin + 1;
int prev = begin;
while (cur <= end)
{
//cur找小,prev先++再交换
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)
{
if (begin >= end)
return;
int keyi = PartSort3(a, begin, end);
QuickSort(a, begin, keyi - 1);
QuickSort(a, keyi + 1, end);
}
个人感觉前后指针法较为简单
4.非递归快速排序
上面三种都是使用递归来实现的,那还是那个问题,万一要递归的很深,即便加上了小区间优化都栈溢出了怎么办呢?那就得用非递归来实现,非递归要借助栈和循环来实现
假如10个数据,那么0,9入栈,进行一次排序,排序后分为[begin,keyi-1] keyi [keyi+1,end]三部分,也就是[0,4] 5 [6,9]。那么再将0,4,6,9入栈,依次将区间分解。直到栈为空,也就结束了
//非递归快速排序
void QuickSortNonR(int* a, int begin, int end)
{
ST s;
STInit(&s);
//记住栈的特性,因此要按相同的次序入区间的左右位置
STPush(&s, end);
STPush(&s, begin);
while (!STEmpty(&s))
{
int left = STTop(&s);
STPop(&s);
int right = STTop(&s);
STPop(&s);
int keyi = PartSort3(a, left, right);
//[left ,keyi-1] keyi [keyi+1,right]
//判断左右区间长度是否<=1
if (left < keyi - 1)
{
STPush(&s, keyi - 1);
STPush(&s, left);
}
if (keyi+1 < right)
{
STPush(&s, right);
STPush(&s, keyi+1);
}
}
}
- 时间复杂度:O(N*logN)
- 空间复杂度:O(logN)
- 稳定性:不稳定
7.归并排序
归并排序就是先将序列分解再依次比较合并,最终将排序好的序列拷贝回原数组。
//归并排序
void _MergeSort(int* a, int left, int right,int* tmp)
{
//区间中元素<=1时不合并
if (left >= right)
{
return;
}
//划分数组,每次一分为二 // [begin mid] [mid+1 end]
int mid = (left + right) / 2;
_MergeSort(a, left, mid,tmp);//继续分解左区间
_MergeSort(a, mid + 1, right,tmp);//继续分解右区间
int begin1 = left, end1 = mid;//有序序列1
int begin2 = mid + 1, end2 = right;//有序序列2
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++];
}
//将合并后的序列拷贝回原数组
memcpy(a + begin, tmp + begin, sizeof(int) * (end - begin + 1));
}
//
void MergeSort(int* a, int n)
{
assert(a);
//因为需要将两个有序序列合并,需借助额外数组
int* tmp = (int*)malloc(sizeof(int) * n);
if (tmp == NULL)
{
perror("malloc");
exit(-1);
}
_MergeSort(a, 0, n - 1,tmp);
free(tmp);
tmp = NULL;
}
- 归并排序因为需要额外的数组来存储排好的序列因此需要O(N)的空间复杂度
- 时间复杂度:O(N*logN)
- 空间复杂度:O(N)
- 稳定性:稳定
8.计数排序
计数排序与上面的排序都不太相同,因为其不需要交换元素,而是像它的名字一样通过计数的方式来排序
for(int i=0;i<n;i++)
{
count[a[i]]++;
}
通过计数来排序,将数的值和数组的下标结合,那些数出现了就将相应下标+1
计数排序效率极高,时间复杂度O(aN+countN(范围)),空间复杂度O(countN(范围))
局限性:
1.不适合分散的数据,更适合集中数据
2.不适合浮点数、字符串、结构体数据排序,只适合整数。
但如果要排的数较大,列如都是10000~99999的数,那么前0-9999就被浪费了,因此可以进行一些小优化。找到其最小,并从最小来进行相对映射
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;
//calloc可以将数组初始化为0
int* count = (int*)calloc(range, sizeof(int));
if (count == NULL)
{
perror("calloc fail");
return;
}
//统计次数
for (int i = 0; i < n; i++)
{
count[a[i] - min]++;
}
//排序
int i = 0;
for (int j = 0; j < range; j++)
{
while (count[j]--)
{
//因为最小不是0,而是min,所以最后要加回来
a[i++] = j + min;
}
}
}
- 计数排序在数据范围集中时,效率很高,但是适用范围及场景有限。
- 时间复杂度:O(MAX(N,范围))
- 空间复杂度:O(范围)
- 稳定性:稳定
9.总结
如有错误,感谢斧正