写在前面
虽然这是课本以及教辅书的最后一章,但我相信数据结构起码半年内是不会离开我了。回归正题,这是排序算法。排序算法历来也是考试中的重点与难点,不仅要求实现过程,也会要求代码。因此这里对常见的内部排序算法进行实现,外部排序尽做了了解,并没有敲代码。
按照惯例,上一节地址:数据结构(6)—— 查找
插入排序
这部分主要包括直接插入排序、折半插入排序与希尔排序,其中希尔排序的时间复杂度是最优的,且三种算法的空间复杂度均是O(1),即常数个空间。
注意点
这里直接把这俩非常简单的算法拿来直接说了。折半插入就是在寻找被插入位置的时候使用了折半查找,实现的难度并不大。需要注意的是在我们之前实现的折半查找中的判断条件和这里不一样的。这里为了确保稳定性,当相等的时候会查找右字表,这样就可以保证算法的稳定性。
关于希尔排序,这里实现的是非常简单的方法,以长度的一半为初始的增量,然后不断进行缩小。
代码
/*
* @Description: 插入排序算法 空间复杂度均为O(1)
* @version: 1.0
* @Author: Liuge
* @Date: 2021-07-31 21:01:25
*/
#include<bits/stdc++.h>
// 直接插入排序(带哨兵) 默认排序成升序 时间复杂度:O(n2) 稳定排序算法
void insertSort(int A[],int n){
int i;
int j;
// 比较次数
int count;
for(i = 2;i <= n;i++){
count++;
if(A[i] < A[i-1]){
// 把0这个位置空出来当哨兵
A[0] = A[i];
// 查找插入位置,然后整体后移
for(j = i - 1;A[0] < A[j];--j){
count++;
A[j+1] = A[j];
}
// 插入
A[j+1] = A[0];
}
}
printf("直接插入排序比较次数为:%d\n",count);
}
// 折半插入排序(带哨兵) 升序 时间复杂度:O(n2) 稳定排序算法
void insertSort2(int A[],int n){
int i,j,low,high,mid;
int count;
for(i = 2;i<=n;i++){
A[0] = A[i];
// 设置折半查找的范围
low=1;
high = i - 1;
// 当low > high时停止
// 与折半查找不同的是,为了保证算法的稳定性,这里在相等时不停止,而是继续查找右半部分
while(low <= high){
mid = (low + high) / 2;
count++;
if(A[mid] > A[0]){
high = mid - 1;
}else{
low = mid + 1;
}
}
// 后移元素
for(j = i-1;j >= high + 1;--j){
A[j+1] = A[j];
}
// 插入
A[high+1] = A[0];
}
printf("折半插入排序比较次数为:%d\n",count);
}
// 希尔排序 升序 时间复杂度:当n在某个范围时为O(n1.3) 不稳定排序算法
void shellSort(int A[],int n){
// 这里的A[0]只是暂存单元,并不是哨兵,当j<=0时,插入位置已到
int dk,i,j;
int count;
// 步长变化
for(dk = n / 2;dk >= 1;dk = dk / 2){
// 对每个子表进行直接插入排序
for(i = dk + 1;i <= n;i++){
count++;
if(A[i] < A[i-dk]){
A[0] = A[i];
for(j = i - dk;j > 0 && A[0] < A[j];j -= dk){
count++;
A[j + dk] = A[j];
}
A[j + dk] = A[0];
}
}
}
printf("希尔排序比较次数:%d\n",count);
}
// 生成随机数的函数
int getRand() {
// 设置随机数范围1-100
return rand() % 100 + 1;
}
// 生成随机数的数组
void getRandArray(int A[]){
// 随机生成十个随机数
for(int i = 1;i <= 10;i++){
A[i] = getRand();
}
}
// 数组输出函数
void printArray(int A[]){
for(int i = 1;i <= 10;i++){
printf("%d ",A[i]);
}
printf("\n");
}
// 主函数测试
int main(){
// 以系统时间为种子
srand((unsigned)time(NULL));
int A1[11],A2[11],A3[11];
getRandArray(A1);
getRandArray(A2);
getRandArray(A3);
// 打印出这十个数
printf("原始排序序列为:\n");
printArray(A1);
// 进行直接插入排序
insertSort(A1,10);
printf("直接插入排序后:\n");
printArray(A1);
printf("原始排序序列为:\n");
printArray(A2);
insertSort2(A2,10);
printf("折半插入排序后:\n");
printArray(A2);
printf("原始排序序列为:\n");
printArray(A3);
shellSort(A3,10);
printf("希尔排序后:\n");
printArray(A3);
return 0;
}
交换排序
这部分主要包括冒泡排序和快速排序。其中冒泡排序的时间复杂度会达到O(n2),但跟初始序列是有关的,而快排的平均时间复杂度能到O(nlog2n),还是十分快的。
注意点
冒泡排序十分简单,这里就不再多说。只需要两两对比,不断把最大或最小的元素“冒”到最终位置即可。
快速排序是一种基于分治法的算法,实际上快排也是我们要学习的各种算法中最快的一个了。快排将待排序的序列分成两块,确定一个枢轴,通过跟这个枢轴进行对比,来不断交替搜索和交换,最后达到有序。
代码
/*
* @Description: 交换排序(冒泡排序和快速排序)
* @version: 1.0
* @Author: Liuge
* @Date: 2021-08-02 20:12:44
*/
#include<bits/stdc++.h>
// 交换顺序
void swap(int &m,int &n){
int temp = m;
m = n;
n = temp;
}
// 冒泡排序 升序 时间复杂度:O(n2) 稳定
void bubbleSort(int A[],int n){
int flag;
int count;
for(int i = 0;i< n - 1;i++){
flag = false;
// 一趟冒泡过程
for(int j = n - 1;j > i;j--){
// 逆序则交换
if(A[j-1] > A[j]){
swap(A[j-1],A[j]);
count++;
flag = true;
}
}
// 没有进行交换,说明全都有序了,直接跳出
if(flag == false){
printf("冒泡排序交换了 %d 次\n",count);
return;
}
}
printf("冒泡排序交换了%d次\n",count);
}
// 快速排序的分区函数
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;
}
// 快速排序 升序 时间复杂度(nlog2n) 不稳定 递归 空间复杂度O(log2n)
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 getRand() {
// 设置随机数范围1-100
return rand() % 100 + 1;
}
// 生成随机数的数组
void getRandArray(int A[]){
// 随机生成十个随机数
for(int i = 0;i < 10;i++){
A[i] = getRand();
}
}
// 数组输出函数
void printArray(int A[]){
for(int i = 0;i < 10;i++){
printf("%d ",A[i]);
}
printf("\n");
}
// 主函数测试
int main(){
// 以系统时间为种子
srand((unsigned)time(NULL));
int A1[10],A2[10];
getRandArray(A1);
getRandArray(A2);
printf("原始关键字序列为:\n");
printArray(A1);
bubbleSort(A1,10);
printf("冒泡排序后的序列为:\n");
printArray(A1);
printf("原始关键字序列为:\n");
printArray(A2);
quickSort(A2,0,9);
printf("快速排序后的序列为:\n");
printArray(A2);
return 0;
}
选择排序
选择排序主要包括简单选择排序和堆排序,其中堆排序的时间复杂度不管在何种情况都能达到O(nlog2n)的数量级,也是十分优秀的算法了。
注意点
简单选择排序,正如他的名字,简单。没什么好说的。选出最小的位置,然后把小的元素放到最终位置。
堆排序,可以看成是一颗完全二叉树,分为大根堆和小根堆,每次输出一个元素后就要再次调整,成为堆。具体的实现看代码吧。
代码
/*
* @Description: 选择排序(简单选择排序,堆排序)
* @version: 1.0
* @Author: Liuge
* @Date: 2021-08-03 19:58:46
*/
#include<bits/stdc++.h>
// 交换顺序
void swap(int &m,int &n){
int temp = m;
m = n;
n = temp;
}
// 简单选择排序 升序 O(n2) 不稳定
void selectSort(int A[],int n){
int min;
for(int i = 0;i < n - 1;i++){
// 记录最小元素下标
min = i;
for(int j = i + 1;j < n;j++){
// 更新最小元素位置
if(A[j] < A[min]){
min = j;
}
}
// 将最小元素和当前元素交换位置
if(min != i){
swap(A[i],A[min]);
}
}
}
// 调整堆 O(n)
void headAdjust(int A[],int k,int len){
// 用A[0]暂存根结点
A[0] = A[k];
// 进行调整
for(int i = 2 * k;i <= len;i *= 2){
// 筛选出较大的子结点
if(i < len && A[i] < A[i+1]){
i++;
}
if(A[0] >= A[i]){
// 筛选完毕,结束
break;
}else{
// 没有筛选完,将孩子放到双亲结点上
A[k] = A[i];
// 修改k值,以便接着进行筛选
k = i;
}
}
// 把被筛选结点的值放入最终位置
A[k] = A[0];
}
// 建立大根堆
void buildMaxHeap(int A[],int len){
// 从非叶子结点开始挨个调整
for(int i = len / 2;i > 0;i--){
headAdjust(A,i,len);
}
}
// 堆排序 升序 O(nlog2n) 不稳定
void heapSort(int A[],int len){
buildMaxHeap(A,len);
for(int i = len;i > 1;i--){
// 和堆底元素进行交换,相当于输出
swap(A[i],A[1]);
// 对剩下的i-1个元素进行调整
headAdjust(A,1,i-1);
}
}
// 生成随机数的函数
int getRand() {
// 设置随机数范围1-100
return rand() % 100 + 1;
}
// 生成随机数的数组
void getRandArray(int A[],int len,bool isZeroBlank){
// 随机生成len个随机数
if(isZeroBlank){
for(int i = 1;i <= len;i++){
A[i] = getRand();
}
}else{
for(int i = 0;i < len;i++){
A[i] = getRand();
}
}
}
// 数组输出函数
void printArray(int A[],int len,bool isZeroBlank){
if(isZeroBlank){
for(int i = 1;i <= len;i++){
printf("%d ",A[i]);
}
}else{
for(int i = 0;i < len;i++){
printf("%d ",A[i]);
}
}
printf("\n");
}
// 主函数测试
int main(){
int A1[10],A2[11];
getRandArray(A1,10,false);
getRandArray(A2,10,true);
printf("原始关键字序列为:\n");
printArray(A1,10,false);
selectSort(A1,10);
printf("简单选择排序后序列为:\n");
printArray(A1,10,false);
printf("原始关键字序列为:\n");
printArray(A2,10,true);
heapSort(A2,10);
printf("堆排序后序列为:\n");
printArray(A2,10,true);
return 0;
}
2路归并排序
注意点
2路归并排序是归并排序的一种,归并排序包含着一种递归思想,将每个子表进行两两归并,通过若干趟归并后就可以获得一个有序的序列。由于使用了递归,所以实现还是非常简单的。2路归并排序的时间复杂度也能到O(nlog2n)。
代码
/*
* @Description: 二路归并排序
* @version: 1.0
* @Author: Liuge
* @Date: 2021-08-03 21:19:53
*/
#include<bits/stdc++.h>
// 合并函数
void merge(int A[],int low,int mid,int high){
// 定义辅助数组B,长度和A数组相同
int *B = (int *) malloc((high-low + 1) * sizeof(int));
// 把A的所有元素复制到B中
for(int k = low;k <= high;k++){
B[k] = A[k];
}
// 开始合并
int i,j,k;
for(i = low,j = mid + 1,k = i;i <= mid && j <= high;k++){
// 比较B两个子表,把小的复制到A里
if(B[i] <= B[j]){
A[k] = B[i++];
}else{
A[k] = B[j++];
}
}
// 把表中剩余部分直接复制过去
while(i <= mid){
A[k++] = B[i++];
}
while(j <= high){
A[k++] = B[j++];
}
}
// 二路归并排序 时间复杂度(Olog2n),空间复杂度O(n) 升序
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 getRand() {
// 设置随机数范围1-100
return rand() % 100 + 1;
}
// 生成随机数的数组
void getRandArray(int A[]){
// 随机生成十个随机数
for(int i = 0;i < 10;i++){
A[i] = getRand();
}
}
// 数组输出函数
void printArray(int A[]){
for(int i = 0;i < 10;i++){
printf("%d ",A[i]);
}
printf("\n");
}
// 主函数测试
int main(){
int A[10];
getRandArray(A);
printf("原始关键字序列为:\n");
printArray(A);
mergeSort(A,0,9);
printf("二路归并排序后的序列为:\n");
printArray(A);
return 0;
}
总结
长达一个月左右的数据结构学习就差不多该结束了。磨磨蹭蹭的一个多月,基本上把每章的重点算法都基本实现了一遍,虽然实现过程并不是完全脱稿,只是简单的照着书上的思路学习了一下。之后会复习C++,然后尝试用C++来自己手动敲一些常见的算法,因此这个系列估计会更新到考研的最后几天吧。加油奥利给!