先上测试的PTA例题:
给定N个(长整型范围内的)整数,要求输出从小到大排序后的结果。
本题旨在测试各种不同的排序算法在各种数据情况下的表现。各组测试数据特点如下:
- 数据1:只有1个元素;
- 数据2:11个不相同的整数,测试基本正确性;
- 数据3:1e3个随机整数;
- 数据4:1e4个随机整数;
- 数据5:1e5个随机整数;
- 数据6:1e5个顺序整数;
- 数据7:1e5个逆序整数;
- 数据8:1e5个基本有序的整数;
- 数据9:1e5个随机正整数,每个数字不超过1000。
输入格式:
输入第一行给出正整数N(≤1e5),随后一行给出N个(长整型范围内的)整数,其间以空格分隔。
输出格式:
在一行中输出从小到大排序后的结果,数字间以1个空格分隔,行末不得有多余空格。
输入样例:
11
4 981 10 -17 0 -20 29 50 8 43 -5
输出样例:
-20 -17 -5 0 4 8 10 29 43 50 981
代码长度限制
16 KB
时间限制
10000 ms
内存限制
64 MB
排序法稳定:如果a原本在b前面,且a=b,排序之后a仍然在b的前面。
一、冒泡排序
因为冒泡排序法解决正序排列的数组最快,解决倒序排列的数组最慢。所以部分测试点没有通过,但这并不代表冒泡排序法不快。
#include
#include
void sort(int a[], int n){
for(int i = 0;i < n;i++){
bool flag = true;//设置一个点
for(int j = 0;j < n - i - 1;j++){
if(a[j] > a[j + 1]){//升序还是降序的控制点
int x = a[j];
a[j] = a[j + 1];
a[j + 1] = x;
flag = false;//如果没有进行交换说明排序已经完成
}
}
if(flag) break;
}
}
int main(){
int n, j;
scanf("%d", &n);
int a[n];
for(int i = 0;i < n;i++)
scanf("%d", &a[i]);
sort(a, n);
for(int i = 0;i < n;i++)
printf("%d%c", a[i], i == n - 1?'\n':' ');
}
总结:冒泡排序法的原理是,通过比较相邻的两个元素的大小,然后不断交换的过程,看图就可以很好的理解到冒泡排序的精髓。这个代码已经做了优化,就是设定一个bool或者一个随意的值,当循环结束时,交换代码仍没有触发,说明已经完成排序。
平均时间复杂度:O(n^2)
空间复杂度:O(1)
算法稳定性:稳定
#include
void selectsort(int a[], int n){
for(int i = 0;i < n - 1;i++){
int temp = i;
for(int j = i + 1;j < n;j++){
if(a[j] < a[temp]) temp = j;//逆序还是正序的关键点
}
int x = a[i];
a[i] = a[temp];
a[temp] = x;
}
}
int main(){
int n;
scanf("%d", &n);
int a[n];
for(int i = 0;i < n;i++)
scanf("%d", &a[i]);
selectsort(a, n);
for(int i = 0;i < n;i++)
printf("%d%c", a[i], i == n - 1?'\n':' ');
}
总结:选择排序法就是先确定一个值,然后比较后边的值与该值的大小,比较出来一个最大的或者最小的,进行交换,往次循环,直到结束。
平均时间复杂度:O(n^2)
空间复杂度:O(1)
算法稳定性:不稳定
#include
void insert(int a[], int n){
for(int j, i = 1;i < n;i++){
int temp = a[i];//保留数值
for(j = i;j > 0 && a[j - 1] > temp;j--){//如果前方数值大于保留数值就移动,否则就退出。逆序还是正序的关键点
a[j] = a[j - 1];
}
a[j] = temp;//将保留数值插入
}
}
int main(){
int n;
scanf("%d", &n);
int a[n];
for(int i = 0;i < n;i++)
scanf("%d", &a[i]);
insert(a, n);
for(int i = 0;i < n;i++)
printf("%d%c", a[i], i == n - 1?'\n':' ');
}
总结:插入排序就是不断保留第一个值,用这个值与前边的所有值进行对比,如果大(小),那么就移动位置,直到找到合适的位置,将保留的值插入进去。
平均时间复杂度:O(n^2)
空间复杂度:O(1)
算法稳定性:稳定
#include
void shellsort(int a[], int n){
int i, j, d;
for(d = n / 2;d > 0;d /= 2){//先定义增量值进行分组
for(i = d;i < n;i++){//每+1,都会进入下一组,直到所有分组都完成排序
int temp = a[i];//插入排序原理,先存储当前值
for(j = i - d;j >= 0 && temp < a[j];j -= d){//决定因素在于temp
a[j + d] = a[j];//排序的顺序决定于temp
}
a[j + d] = temp;//找到合适的位置将数值进行插入
}
}
}
int main(){
int n;
scanf("%d", &n);
int a[n];
for(int i = 0;i < n;i++)
scanf("%d", &a[i]);
shellsort(a, n);
for(int i = 0;i < n;i++)
printf("%d%c", a[i], i == n - 1?'\n':' ');
}
总结:希尔排序是进行分组,对每一组进行插入排序,通过不断的分组来减少插入排序的时间复杂度。希尔排序只能基于顺序表不能基于链表实现,因为需要有一个增量值。如果有两个相同的数值,希尔排序会导致这两个数值出现的次序发生变化所以希尔排序不稳定。
算法复杂度:O(nlog2n)
算法空间复杂度:O(1)
算法稳定性:不稳定
#include
void HeapAdjust(int a[], int i, int len){
a[0] = a[i];//先用a[0]保存可能需要移动的值
for (int k = 2 * i; k <= len; k *= 2){
if (k < len && a[k] < a[k + 1])//比较左右子树的大小。
k++;//k的值大于等于len说明k是最后一个结点,无法进行k++
if (a[0] >= a[k]) break;//如果该结点已经是最大的了,那就不需要继续下滤了
else {
a[i] = a[k];//如果子结点中最大值比该结点大,那就交换
i = k;//位置也交换
}
}
a[i] = a[0];//把下滤的最终位置填入该结点
}
/*最小堆的写法,只需要在最大堆的基础上该两个符号即可。
void HeapAdjust(int a[], int i, int len){
a[0] = a[i];//先用a[0]保存可能需要移动的值
for (int k = 2 * i; k <= len; k *= 2){
if (k < len && a[k] > a[k + 1])//比较左右子树的大小
k++;//k的值大于等于len说明k是最后一个结点,无法进行k++
if (a[0] <= a[k]) break;//如果该结点已经是最大的了,那就不需要继续下滤了
else {
a[i] = a[k];//如果子结点中最大值比该结点大,那就交换
i = k;//位置也交换
}
}
a[i] = a[0];//把下滤的最终位置填入该结点
}*/
void BuildMaxHeap(int a[], int len){
for (int i = len / 2; i > 0; i -- )
HeapAdjust(a, i, len);//对每一个有子结点的父结点进行调整
}
void Heapsort(int a[], int len){
BuildMaxHeap(a, len);//先创建最大堆
for(int i = len; i >= 1; i -- ){//对堆中的元素进行排序
int temp = a[i];//找到最大值进行交换,将最大值放在最后的位置上
a[i] = a[1];
a[1] = temp;
HeapAdjust(a, 1, i - 1);//每次交换完都需要进行堆的整理
}
}
int main(){
int n;
scanf("%d", &n);
int a[n];
for(int i = 1;i <= n;i++)
scanf("%d", &a[i]);
Heapsort(a, n);
for(int i = 1;i <= n;i++)
printf("%d%c", a[i], i == n?'\n':' ');
}
总结:堆排序主要分为三个部分,堆的创建,堆的调整,堆的排序。整个过程中堆的调整是最重要的,也是决定是大根堆还是小根堆的关键点。堆的调整代码中有详解。
算法复杂度:O(nlog2n)
算法空间复杂度:O(1)
算法稳定性:不稳定
快排对于正序数组还是逆序数组都是比较慢的。因为要整个数组进行遍历一次。
#include
int Partition(int a[], int low, int high){
int pivot = a[low];//选择第一个作为枢纽元素
while(low < high){//直到两个指针相逢,循环结束条件
while(low < high && a[high] >= pivot) --high;
a[low] = a[high];//找到比枢纽元素小的值就放在左边空位
while(low < high && a[low] <= pivot) ++low;
a[high] = a[low];//找到比枢纽元素大的值就放在右边空位
//这个循环位置一定不能颠倒,因为是左边第一个先为空,所以要从右边开始!
}
a[low] = pivot;
return low;
}
void quicksort(int a[], int low, int high){
if(low < high){//递归跳出条件,说明排序完成
int pivotpos = Partition(a, low, high);//返回最后结束的枢纽下标
quicksort(a, low, pivotpos - 1);//进入左递归进行排序
quicksort(a, pivotpos + 1, high);//进入右递归进行排序
}
}
int main(){
int n;
scanf("%d", &n);
int a[n];
for(int i = 0;i < n;i++)
scanf("%d", &a[i]);
quicksort(a, 0, n - 1);
for(int i = 0;i < n;i++)
printf("%d%c", a[i], i == n - 1?'\n':' ');
}
总结:快速排序就是将原数组分成两部分,然后以中间值为标准,如果左指针指向作为枢纽元素,那么右指针先进行左移,找到比它小的就放其左边枢纽元素位置,此时右指针所指为空,然后左指针右移,找到比枢纽元素大的值,放在右指针空位。在左右两边又以相同的方式继续排序。
算法复杂度:O(n^2)
算法空间复杂度:O(n)
算法稳定性:不稳定
#include
void merge(int a[], int low, int mid, int high){
int i, j, k, s[high];
for(i = low;i <= high;i++)
s[i] = a[i];//将数组a复制到数组s
for(i = low, j = mid + 1, k = i;i <= mid && j <= high;k++){//判定条件为,数组结束。
if(s[i] <= s[j]) a[k] = s[i++];//比较两数组中的大小,小的进a中
else a[k] = s[j++];
}
while(i <= mid) a[k++] = s[i++];
while(j <= high) a[k++] = s[j++];
}
void mergesort(int a[], int low, int high){
if(low < high){//只要能继续分组,那么就继续分
int mid = (low + high) / 2;
mergesort(a, low, mid);//分为左组,并进行归并排序
mergesort(a, mid + 1, high);//分为右组,并进行归并排序
merge(a, low, mid, high);
}
}
int main(){
int n;
scanf("%d", &n);
int a[n];
for(int i = 0;i < n;i++)
scanf("%d", &a[i]);
mergesort(a, 0, n - 1);
for(int i = 0;i < n;i++)
printf("%d%c", a[i], i == n - 1?'\n':' ');
}
总结:归并排序是通过分组,两组又进行递归的分组,每一组都会与另一组进行归并排序,创建一个存储数组,比较依次比较两组的值,小的(大的)就放进存储数组中,最后将存储数组存入原数组。归并排序和快速排序有点像,都是利用双指针进行排序。
算法复杂度:O(nlog2n)
算法空间复杂度:O(n)
算法稳定性:稳定
有一个人博客讲的挺好的,这里可以点击基数排序进行链接。此基数排序只适用于无符号值,也就是非负整数的排序。能不能进行负数排序其实问题不大,主要是掌握到排序算法的技巧和原理。这里没有好的动图可以进行演示。所以截取了一些王道考研数据结构的图。
在这里插入图片描述
第一次收集将所有个位数的值依次排列。
第二次收集将所有十位数的值依次排列。
第三次收集将所有百位数的值依次排列。
#include
int temp[100000];
int bucket[10];//bucket相当于桶,记录每个桶中放了多少元素
int radix = 1;//设定位数,每循环一次扩大十倍,对应着个位,十位,百位...
int MaxBit(int a[], int n){
int max = a[0];
for (int i = 1; i < n; i ++ )//找到数组中的最大值
if (a[i] > max) max = a[i];
int count = 1;//位数计算
while(max >= 10){
max /= 10;
count++;
}
return count;
}
void radixsort(int a[], int n){
int count = MaxBit(a, n);//找到需要排序的数组中最多的位数,也就是排序次数
for (int i = 0; i < count; i ++ ){//需要进行排序的总次数
//bucket相当于桶,记录每个桶中放了多少元素
for(int j = 0; j < 10 ;j ++ )
bucket[j] = 0;//进行桶的重置
for(int j = 0; j < n; j ++ )
bucket[(a[j] / radix) % 10]++;//进行元素计数
/*
将元素个数全部加起来,和后边循环进行结合之后
等于将每个数的尾数已经从小到大安排了固定位置。
*/
for(int j = 1; j < 10; j ++ )
bucket[j] = bucket[j - 1] + bucket[j];
for(int j = n - 1; j >= 0; j -- ){
int bit = (a[j] / radix) % 10;
temp[bucket[bit] - 1] = a[j];
bucket[bit]--;
}
/*
这个bucket[bit]已经是将前边所有的元素个数加和的结果
这个加起来的和正好是数组元素的个数,然后按照这个个数进行
存储,恰好能够将所有的元素按照尾数从小到大进行排列
*/
for(int j = 0; j < n; j ++ )
a[j] = temp[j];//将temp数组中排序好的数复制给a中
radix *= 10;
}
}
int main(){
int n;
scanf("%d", &n);
int a[n];
for(int i = 0;i < n;i++){
scanf("%d", &a[i]);
}
radixsort(a, n);
for(int i = 0;i < n;i++)
printf("%d%c", a[i], i == n - 1?'\n':' ');
}
总结:基数排序又叫做“桶排序”,像桶一样,通过关键词进行采用不同的分配。本代码是将0-9个数字作为关键词分配,依次按照个位十位百位进行比较大小和排列,有几位数就进行几次排序。
算法复杂度:O(d(n + r)
算法空间复杂度:O®
算法稳定性:稳定
空间复杂度为O( r ),r指的是每个关键词部分可能取得的值的多少。时间复杂度为O(d(n + r)),d是指可以分为几个关键词。
本人软工小白,因为最近数据结构已经结课,现在正在复习排序法,正好总结一下每个排序法的使用方法,如果能够帮助到你,那将会令我十分开心。
文章引用的所有图片如果有侵权,请联系我,我会道歉并且删除文章。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)