欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 明星 > 【八大排序】冒泡、直接选择、直接插入、希尔、堆、归并、快速、计数排序

【八大排序】冒泡、直接选择、直接插入、希尔、堆、归并、快速、计数排序

2025/4/19 17:13:50 来源:https://blog.csdn.net/2401_86785052/article/details/147196454  浏览:    关键词:【八大排序】冒泡、直接选择、直接插入、希尔、堆、归并、快速、计数排序

目录

  • 一、排序的介绍
  • 二、排序算法的实现
    • 2.1 直接插入排序
    • 2.2 希尔排序
    • 2.3 直接选择排序
    • 2.4 堆排序
    • 2.5 冒泡排序
    • 2.6 快速排序
    • 2.7 归并排序
    • 2.8 比较排序算法的性能展示
    • 2.9 计数排序

个人主页<—
数据结构专栏<—

在这里插入图片描述

一、排序的介绍

我们的生活中有很多排序,比如像成绩的高低,身高的高低,体重的胖瘦,价格的高低等,这些都是排序,今天我们这期博客要讲的就是如何去排序,我们一共会讲解8个排序算法,它们分别是:
在这里插入图片描述
这八大排序就是我们常用的主流排序算法,其中大多数都是我们耳熟能详的排序算法,下面我们会逐步实现这八大算法。

二、排序算法的实现

2.1 直接插入排序

在这里插入图片描述
直接插入排序是⼀种简单的插入排序法,基本思想是:把待排序的记录按其关键码值的大小逐个插入到⼀个已经排好序的有序序列中,直到所有的记录插入完为止,得到⼀个新的有序序列 。
这个排序就是依次从前往后扫描建立前k有序序列,之后每次让序列之后的一个元素依次从有序序列的末尾开始与有序序列的元素进行比较,如果序列中的元素比序列之后的第一个元素大,就让序列中的元素向后移动,直到找到小于有序序列之后第一个元素的位置就将它插入。上面动图中标黄的就是排好的有序序列,标红的是有序序列后面的第一个元素,移动的过程就是比较的过程。

//直接插入排序
void InsertSort(int* arr, int n)
{for (int i = 0;i < n-1;i++){int end = i;int tmp = arr[end + 1];while (end >= 0){if (arr[end] > tmp){arr[end + 1] = arr[end];end--;}else{break;}}arr[end + 1] = tmp;}
}

以上代码中,变量end永远指向有序序列的最后一个元素,我们用tmp存储有序序列之后的第一个元素,然后我们让有序序列中的元素和tmp比较,如果大我们就后移,如果小就跳出循环,此时end指向比tmp小的元素或者是下标为-1的元素,那么end+1就是tmp该在的位置。直接插入排序的时间复杂度是:O(n2)。

测试代码:

int main()
{int arr[] = { 9,6,5,1,10,11,35,27 };printf("排序之前:");print(arr, 8);InsertSort(arr, 8);printf("排序之后:");print(arr, 8);return 0;
}

代码中print是我封装的一个打印函数

结果:
在这里插入图片描述

2.2 希尔排序

希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数(通常是gap = n/3+1),把待排序文件所有记录分成各组,所有的距离相等的记录分在同一组内,并对每一组内的记录进行排序,然后gap=gap/3+1得到下一个整数,再将数组分成各组,进⾏插⼊排序,当gap=1时,就相当于直接插入排序。它是在直接插入排序算法的基础上进⾏改进而来的,综合来说它的效率肯定是要高于直接插入排序算法的
在这里插入图片描述
经典动图:
在这里插入图片描述

//希尔排序
void ShellSort(int* arr, int n)
{int gap = n;while (gap > 1){gap = gap / 3 + 1;for (int i = 0;i < n - gap;i++){int end = i;int tmp = arr[end + gap];while (end>=0){if (arr[end] > tmp){arr[end + gap] = arr[end];end -= gap;}else{break;}}arr[end + gap] = tmp;}}
}

以上代码与直接插入排序的函数的交换核心相同,唯一不同的是希尔排序是将要排序的分为好几组数据,假设gap是2,就每隔一个数据进行比较然后排序,假设gap是1就和直接插入排序相同了,都是依次比较,这时候也是最后一次排序。也就是gap>1预排序,gap=1直接插入排序。 关于希尔排序时间复杂度,它一直是一个难题,它的时间是所取“增量”序列的函数,这涉及一些数学上尚未解决的难题,希尔排序的时间复杂度大约是:n1.3

测试结果:
在这里插入图片描述

2.3 直接选择排序

  • 在元素集合array[i]--array[n-1]中选择最大(小)的数据元素;
  • 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换;
  • 在剩余的 array[i]--array[n-2]array[i+1]--array[n-1]) 集合中,重复上述步骤,直到集合剩余 1 个元素;

其排序的核心思想就是同时未排序的序列中找最大值最小值,并将最小值放在前面,把最大值放在后面

//直接选择排序
void SelectSort(int* arr, int n)
{int begin = 0;int end = n - 1;while (begin < end){int max = begin;int min = begin;for (int i = begin+1;i <= end;i++){if (arr[i] > arr[max]){max = i;}if (arr[i] < arr[min]){min = i;}}if (max == begin){max = min;}Swap(&arr[min], &arr[begin]);Swap(&arr[max], &arr[end]);begin++;end--;}
}
void Swap(int* a, int* b)
{int tmp = *a;*a = *b;*b = tmp;
}

直接选择排序的代码比较好理解,需要注意的是,假设我们要对6、5、4进行排升序时,max会指向下标为0的位置,min会指向下标为2的位置,begin指向下标为0,min指向下标为2,如果我们不对max进行if语句判断max是否在begin指向下标位置的话,交换两次之后,还会是6、5、4,序列没有发生改变,就会出错,所以代码中出现了if语句特判

直接选择排序的时间复杂度为:O(n2)。

测试结果:
在这里插入图片描述

2.4 堆排序

堆排序就是利用数据结构堆的思想进行的排序,我们在之前的博客中已经实现了堆排序,所以我们会直接将代码拷贝过来,->堆排序博客

void AdjustDown(int* arr, int parent, int n)
{int child = parent * 2 + 1;while (child < n){if (child + 1 < n && arr[child] < arr[child + 1]){child++;}if (arr[parent] < arr[child]){Swap(&arr[parent], &arr[child]);parent = child;child = parent * 2 + 1;}else{break;}}
}
//堆排序
void HeapSort(int* arr, int n)
{//建大根堆for (int i = (n - 1 - 1) / 2;i >= 0;i--){AdjustDown(arr, i, n);}int end = n - 1;//排序while (end >= 0){Swap(&arr[0], &arr[end]);AdjustDown(arr, 0, end);end--;}
}

虽然代码的风格和之前博客上的略有差异,但思想还是相同的,排升序:建根堆,逐步交换排序;排降序:建根堆,逐步交换排序。堆排序的时间复杂度为:O(nlogn)。

测试结果:
在这里插入图片描述

2.5 冒泡排序

冒泡排序是我们最为熟知,也是最为经典的排序算法,它的代码思路简单且易懂,核心思路就是有n个数字就比较n-1次逐步比较出最大值最小值并交换排序。

//冒泡排序
void BubbleSort(int* arr, int n)
{for (int i = 0;i < n - 1;i++){int flag = 0;for (int j = 0;j < n - i - 1;j++){if (arr[j] > arr[j + 1]){flag = 1;int tmp = arr[j];arr[j] = arr[j + 1];arr[j + 1] = tmp;}}if (flag == 0){break;}}
}

经典动图:
在这里插入图片描述

冒泡排序的时间复杂度为:O(n2)。

测试结果
在这里插入图片描述

2.6 快速排序

快速排序Hoare于1962年提出的⼀种二叉树结构的交换排序方法,其基本思想为:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两子序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准值,然后最左右子序列重复该过程,直到所有元素都排列在相应位置上为止。

经典动图:
在这里插入图片描述
快速排序找基准值递归版本可以分成两个版本,一个是hoare版本,一个是lomuto前后指针;非递归版本会借用数据结构进行实现。

快速排序主体框架:

//快速排序
void QuickSort(int* arr, int left, int right)
{if (left >= right){return;}//找基准值int keyi = _QuickSort(arr, left, right);//左序列[left,keyi-1] 右序列[keyi+1,right]QuickSort(arr, left, keyi - 1);QuickSort(arr, keyi + 1, right);
}

找基准值的过程就是排序的过程。

递归版本:

hoare版本:

  • 创建左右指针,确定基准值
  • right从右向左找出比基准值的数据,left从左向右找出比基准值的数据,左右指针数据交换,进入下次循环
//hoare版本
int _QuickSort(int* arr, int left, int right)
{int keyi = left;left++;while (left <= right){//right:从右往左走,找比基准值要小的while (left <= right && arr[right] > arr[keyi]){right--;}//left:从左往右走,找比基准值要大的while (left <= right && arr[left] < arr[keyi]){left++;}if (left <= right){Swap(&arr[left], &arr[right]);}}Swap(&arr[keyi], &arr[right]);return right;
}

测试结果:
在这里插入图片描述
图解
在这里插入图片描述
lomuto前后指针:

创建前后指针,从左往右找比基准值小的进行交换,使得小的都排在基准值的左边。

  • 我们会创建两个指针prevcurcur走在前面,prev在后面
  • cur找到比基准值要小的之后,++prev让prev和cur指向的下标的值进行交换,cur++
  • cur未找到比基准值要小的数据cur++
//lomuto前后指针法
int _QuickSort1(int* arr, int left, int right)
{int keyi = left;int prev = left;int cur = prev + 1;while (cur <= right){if (arr[cur] < arr[keyi] && ++prev != cur){Swap(&arr[cur], &arr[prev]);}cur++;}Swap(&arr[keyi], &arr[prev]);return prev;
}

运行结果:
在这里插入图片描述
图解:
在这里插入图片描述
非递归版本:

非递归版本的快速排序将会使用到数据结构,我们在之前的博客中已经实现过了数据结构,所以我们会将代码拷贝过来,在利用的同时,我们找基准值的方法依旧使用lomuto前后指针法,【数据结构】栈 相关博客<–

//非递归版本快速排序
void QuickSortNoR(int* arr, int left, int right)
{//定义栈ST st;//初始化栈StackInit(&st);StackPush(&st, left);StackPush(&st, right);while (!StackEmpty(&st)){//取栈顶元素int end = StackTop(&st);//销毁栈顶元素StackPop(&st);int begin = StackTop(&st);StackPop(&st);//[begin,end]找基准值int keyi = begin;int prev = begin;int cur = prev + 1;while (cur <= end){if (arr[cur] < arr[keyi] && ++prev != cur){Swap(&arr[cur], &arr[prev]);}cur++;}Swap(&arr[keyi], &arr[prev]);keyi = prev;//[begin,keyi-1]  [keyi+1,end]if (keyi + 1 < end){StackPush(&st, keyi + 1);StackPush(&st, end);}if (begin < keyi - 1){StackPush(&st, begin);StackPush(&st, keyi - 1);}}//销毁栈StackDestroy(&st);
}

快速排序的时间复杂度为:O(nlogn)

测试结果:
在这里插入图片描述
图解
在这里插入图片描述
在这里插入图片描述

2.7 归并排序

归并排序MERGE-SORT是建立在归并操作上的⼀种有效的排序算法,该算法是采用分治法Divideand Conquer的⼀个非常典型的应用。将已有序的子序列合并,得到完全有序的序列先使每个子序列有序,再使子序列段间有序若将两个有序表合并成一个有序表,称为二路归并

归并排序核心步骤
在这里插入图片描述
从图中就可以看出很强烈的递归思想,所以我们的归并排序依旧使用递归写的。

//归并排序
void _MergeSort(int* arr, int left, int right, int* tmp)
{if (left >= right){return;}int mid = left + (right - left) / 2;//[left,mid] [mid+1,right]_MergeSort(arr, left, mid, tmp);_MergeSort(arr, mid + 1, right, tmp);//第一次运行到这里时,是将数组分解成了一个个单个元素的时候。//合并两个有序序列int begin1 = left;int end1 = mid;int begin2 = mid + 1;int end2 = right;int index = begin1;//[begin1,end1] [begin2,end2]while (begin1 <= end1 && begin2 <= end2){if (arr[begin1] < arr[begin2]){tmp[index++] = arr[begin1++];}else{tmp[index++] = arr[begin2++];}}while (begin1 <= end1){tmp[index++] = arr[begin1++];}while (begin2 <= end2){tmp[index++] = arr[begin2++];}//导入原数组for (int i = left;i <= right;i++){arr[i] = tmp[i];}
}
//归并排序
void MergeSort(int* arr, int n)
{int* tmp = (int*)malloc(sizeof(int) * n);_MergeSort(arr, 0, n - 1, tmp);free(tmp);tmp = NULL;
}

归并排序的时间复杂度为:O(nlogn)

测试结果:
在这里插入图片描述
至此我们已经讲解了七种排序算法,并且这七种排序算法都是比较算法,都是通过一个个比较来排序的,而我们的计数排序不是比较算法那么在讲解计数排序之前呢,我们先来了解一下这七种排序算法的性能如何吧!

2.8 比较排序算法的性能展示

测试代码

// 测试排序的性能对⽐
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);int* a7 = (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];a7[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();int begin7 = clock();BubbleSort(a7, N);int end7 = 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);printf("冒泡排序----->BubbleSort:%d\n", end7 - begin7);free(a1);free(a2);free(a3);free(a4);free(a5);free(a6);free(a7);
}

首先生成了100000个随机数据,然后保证每个排序算法排的数组中的元素相同,再根据clock函数记录开始结束时间,就可以知道每个排序算法耗费的时间了(单位:ms)。

测试结果:
在这里插入图片描述
从结果可以看出,

  • 我们的第一梯队分别是:堆排序、希尔排序、快速排序、归并排序
  • 我们的第二梯队分别是:直接插入排序、直接选择排序。但它们都是以秒为单位的。
  • 我们的第三梯队就是我们熟悉的冒泡排序了。

2.9 计数排序

经典动图:
在这里插入图片描述

//计数排序
void CountSort(int* arr, int n)
{//找最大最小值int max = arr[0];int min = arr[0];for (int i = 1;i < n;i++){if (max < arr[i]){max = arr[i];}if (arr[i] < min){min = arr[i];}}//用max-min+1确定数组大小int* count = (int*)malloc(sizeof(int) * (max - min + 1));if (count == NULL){perror("malloc fail!");return;}//初始化数组countmemset(count, 0, sizeof(int) * (max - min + 1));for (int i = 0;i < n;i++){count[arr[i] - min]++;}//将count数组还原到原数组中,使其有序int index = 0;for (int i = 0;i < max - min + 1;i++){while (count[i]--){arr[index++] = i + min;}}
}

测试结果:
在这里插入图片描述

总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词