目录
- 一、排序的介绍
- 二、排序算法的实现
- 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
前后指针:
创建前后指针,从左往右找比基准值小的进行交换,使得小的都排在基准值的左边。
- 我们会创建两个指针
prev
及cur
,cur
走在前面,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账号,我们一同成长!
(~ ̄▽ ̄)~