数据结构与算法分析实验14 实现基本排序算法
实现基本排序算法
- 1. 常用的排序算法简介
- 2. 上机要求
- 3. 上机环境
- 4.程序清单(写明运行结果及结果分析)
- 4.1 程序清单
- 4.1.1 头文件 sort.h 内容如下:
- 4.1.2 实现文件 sort.cpp 内容如下:
- 4.1.3 源文件 main.cpp 内容如下:
- 4.2 实现展效果示
- 5.上机体会
1. 常用的排序算法简介
- 冒泡排序
冒泡排序是一种简单的排序算法,通过重复地遍历要排序的列表,比较相邻的元素并交换它们的位置,直到没有需要交换的元素为止。冒泡排序的时间复杂度为 O(n²),适用于小规模数据。 - 选择排序
选择排序通过每次从未排序的部分中选择最小(或最大)的元素,将其放到已排序部分的末尾。选择排序的时间复杂度为 O(n²),适用于小规模数据。 - 插入排序
插入排序通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序的时间复杂度为 O(n²),适用于小规模或部分有序的数据。 - 快速排序
快速排序是一种分治算法,通过选择一个“基准”元素,将数组分为两部分,一部分小于基准,另一部分大于基准,然后递归地对这两部分进行排序。快速排序的平均时间复杂度为 O(n log n),适用于大规模数据。 - 归并排序
归并排序也是一种分治算法,通过将数组分成两半,分别对它们进行排序,然后将排序后的两半合并。归并排序的时间复杂度为 O(n log n),适用于大规模数据。 - 堆排序
堆排序利用堆这种数据结构进行排序,通过构建最大堆或最小堆,将堆顶元素与末尾元素交换,然后调整堆,重复此过程直到排序完成。堆排序的时间复杂度为 O(n log n),适用于大规模数据。
………………
2. 上机要求
编写一个程序,实现排序的相关运算,并完成如下功能:
(1)直接插入排序
(2)希尔排序
(3)直接选择排序
(4)堆排序
(5)冒泡排序
(6)快速排序
(7)二路归并排序
(8)基数排序
3. 上机环境
visual studio 2022
Windows11 家庭版 64位操作系统
4.程序清单(写明运行结果及结果分析)
4.1 程序清单
4.1.1 头文件 sort.h 内容如下:
#pragma once
#include<stdio.h>
#include<malloc.h>
#include<stdlib.h>
#include<iostream>
//采用从大到小的顺序typedef int Data;//交换两个元素(值交换)
void swap(Data* a, Data* b);//直接插入排序
void Sort_insert(Data* arr,int len);//希尔排序
void Sort_shell(Data* arr,int len,int* way,int size);
void Sort_jump(Data* arr, int len, int jump); //shell sort 内置函数//直接选择排序,每次确定两个元素
void Sort_choose(Data* arr, int len);//堆排序
void Sort_heap(Data* arr, int len);
void min_down(Data* arr, int index); //一次遍历 arr 数组 将最小元素放在index中
int Lcd(int index); //index 节点“左子树”
int Rcd(int index); //index 节点“右子树”
int Pat(int index); //index 节点“双亲”//冒泡排序
void Sort_bubble(Data* arr, int len);//快速排序
void Sort_quick(Data* arr, int low,int high);//利用二路归并排序的想法将一个数组排序
void Sort_merge(Data* arr,Data* tmp, int low, int high);
void Merge(Data* arr, Data* tmp, int start, int mid, int end); //Sort_merge内置函数//基数排序,需要用链表结构以提升时间,节约空间
void Sort_bucket(Data* arr, int len,int exp);
void distribute_collect(Data* arr, int len,int exp); //发到筒里去,收回去typedef struct Node {Data data;struct Node* next;
}node, * pnode;void push(node* aim, Data data);
Data pop(node* aim);
4.1.2 实现文件 sort.cpp 内容如下:
#include"sort.h"
#include<math.h>void swap(Data* a, Data* b) {Data c = *a;*a = *b;*b = c;
}void Sort_insert(Data* arr, int len){for (int i = 1; i < len; i++) {int tmp = arr[i];for (int j = i-1; j >=0; j--) {if (arr[j] < tmp) arr[j + 1] = arr[j]; else { arr[j+1] = tmp; break; }if (j == 0)arr[j] = tmp;}}
}
希尔排序实现:
希尔排序(Shell Sort)是一种基于插入排序的算法,通过将数组分成多个子序列进行排序,逐步减少子序列的长度,最终完成整个数组的排序。以下是希尔排序的实现步骤:
- 选择增量序列
希尔排序的核心是选择一个增量序列(gap sequence),用于将数组分成多个子序列。常见的增量序列有希尔原始序列(n/2, n/4, …, 1)或更高效的序列如Hibbard序列、Sedgewick序列等。
void Sort_shell(Data* arr, int len, int* way, int size){for (int i = 0; i < size; i++) {Sort_jump(arr, len, way[i]); }
}
//arr 就是选择的序列
- 分组插入排序
根据选定的增量序列,将数组分成若干子序列,对每个子序列进行插入排序。随着增量逐渐减小,子序列的长度逐渐增加,最终当增量为1时,整个数组被当作一个子序列进行插入排序。
void Sort_jump(Data* arr, int len, int jump){for (int i = 1; i < len; i++) {int tmp = arr[i];for (int j = i - jump; j >= 0; j-=jump) {if (arr[j] < tmp) arr[j + jump] = arr[j];else { arr[j + jump] = tmp; break; }if (j <jump)arr[j] = tmp;}}
}
选择排序
void Sort_choose(Data* arr, int len){int maxflag = 0;int minflag = 0;for (int i = 0; i < len; i++) {maxflag = i;minflag = len - 1 - i;for (int j = i; j <= len - 1 - i; j++) {if (arr[maxflag] < arr[j])maxflag = j;if (arr[minflag] > arr[j])minflag = j;}swap(&arr[i], &arr[maxflag]);if (i == minflag) minflag = maxflag; swap(&arr[len - 1 - i], &arr[minflag]);}
}
堆排序
堆排序通过构建一个最大堆或最小堆,将堆顶元素(最大或最小)与堆的最后一个元素交换,然后调整剩余元素使其重新满足堆的性质,重复这一过程直到所有元素有序。
- Sort_heap 函数:这是堆排序的主函数,负责对整个数组进行排序。它从数组的最后一个元素开始,逐个调用 min_down 函数来调整堆。
void Sort_heap(Data* arr, int len){for (int i = len - 1; i >= 0; i--) {min_down(arr, i);}
}
- min_down 函数:该函数用于调整堆,确保从指定索引开始的子树满足最小堆的性质。它通过比较父节点和子节点的值,并在必要时交换它们的位置。
void min_down(Data* arr, int index) {if (Pat(index) >= 0) {int pos = Pat(index);while (pos>=-1) {if (arr[Lcd(pos)] < arr[pos]&&Lcd(pos)<=index) swap(&arr[Lcd(pos)], &arr[pos]);if (arr[Rcd(pos)] < arr[pos]&&Rcd(pos)<=index) swap(&arr[Rcd(pos)], &arr[pos]);pos--;}swap(&arr[index], &arr[0]);}
}
- Lcd 和 Rcd 函数:这两个函数分别用于计算给定索引的左子节点和右子节点的索引。
int Lcd(int index){return 2 * index + 1;
}int Rcd(int index){return 2 * index + 2;
}
- Pat 函数:该函数用于计算给定索引的父节点索引。
int Pat(int index){return (index - 1) / 2;
}
冒泡排序
void Sort_bubble(Data* arr, int len){for (int i = 0; i < len; i++) {for (int j = i + 1; j < len; j++) {if (arr[i] < arr[j])swap(&arr[i], &arr[j]);}}
}
快速排序
快速排序是一种高效的排序算法,采用分治法策略。其基本思想是通过选择一个“标杆”元素(通常称为“pivot”),将数组分为两部分:一部分小于标杆,另一部分大于标杆。然后递归地对这两部分进行排序。
void Sort_quick(Data* arr, int low, int high){int flag = low; //标杆元素下标if (low == high) return;for (int i = flag + 1; i <= high; i++) {if (arr[flag] >= arr[i])continue;if (arr[flag] < arr[i]) {swap(&arr[flag + 1], &arr[i]);swap(&arr[flag], &arr[flag + 1]);flag++;}}if (flag > low) Sort_quick(arr, low, flag - 1);if (flag < high) Sort_quick(arr, flag + 1, high);
}
归并排序
Sort_merge 函数是归并排序的递归部分。它将数组 arr 从 start 到 end 的部分分成两个子数组,分别对这两个子数组进行排序,然后调用 Merge 函数将两个有序的子数组合并。
Merge 函数负责将两个有序的子数组合并成一个有序的数组。它使用两个指针 i 和 j 分别指向两个子数组的起始位置,比较两个指针所指向的元素,将较小的元素放入临时数组 tmp 中。最后,将临时数组中的内容复制回原数组 arr。
void Sort_merge(Data* arr, Data* tmp, int start, int end){int mid;if (start < end) {mid = (start + end) / 2;Sort_merge(arr, tmp, start, mid);Sort_merge(arr, tmp, mid+1, end);Merge(arr, tmp, start, mid, end);}
}void Merge(Data* arr, Data* tmp,int start, int mid, int end){int move = start; int i = start; int j = mid + 1;while (i!=mid+1 && j!= end+1) {if (arr[i] > arr[j]) {tmp[move++] = arr[i++];}else {tmp[move++] = arr[j++];}}while (i != mid + 1) { tmp[move++] = arr[i++]; }while (j != end + 1) { tmp[move++] = arr[j++]; }for (i = start; i <= end; i++) {arr[i] = tmp[i];}
}
桶排序
桶排序(Bucket Sort)是一种分布式排序算法,它将元素分配到有限数量的桶中,然后对每个桶中的元素进行排序,最后将桶中的元素按顺序合并。以下是对代码的详细解析和优化建议。
Sort_bucket函数通过循环调用 distribute_collect 函数,对数组进行多次排序。exp 参数表示排序的轮数,通常与数据的位数相关。
void Sort_bucket(Data* arr, int len, int exp){for (int i = 1; i <= exp; i++) {//printf("exp=%d: \n", i);distribute_collect(arr, len, i);}
}void distribute_collect(Data* arr, int len,int exp){pnode bucket = (pnode)malloc(10 * sizeof(node));for (int i = 0; i < 10; i++) {bucket[i].next = NULL;}int lab = 0;for (int i = len-1; i >=0; i--) { //对到每个元素 从大到小排序就要从小到大塞进去 反之反之lab = arr[i];for (int j = 0; j < exp - 1; j++) lab /= 10;lab %= 10; //找到桶的下标push(&bucket[lab], arr[i]); //塞到桶里去//printf("push %d to lab %d\n", arr[i],lab);}//收回来int index = 0;for (int i = 9; i >= 0; i--) {while (bucket[i].next != NULL) {arr[index++] = pop(&bucket[i]);}}
}void push(node* aim, Data data){node* fresh = (node*)malloc(sizeof(node));fresh->next = NULL;fresh->data = data;fresh->next = aim->next;aim->next = fresh;
}Data pop(node* aim){pnode tmp = aim->next;Data ret = aim->next->data;aim->next = tmp->next;free(tmp);return ret;
}
4.1.3 源文件 main.cpp 内容如下:
#include"sort.h"
//#define size 10000
#define size 3200
#define filename "testdata.txt"
#define tm 123
//注意,在进行快速排序的时候,由于递归深度限制,对于给出的数据量有一个限制
//对于我自己生成的数据,Sort_quick极端情况为3218个数据。
//在实际操作中,Sort_quick数据量应控制在3200以下以确保运行稳定。
//在多次重复排序过程中,由于在后续重复中数据已经排好顺序,快速排序尚且不占优势。
int main() {FILE* fp;fopen_s(&fp, filename, "r");Data* arr = new Data[size];for (int i = 0; i < size; i++) {if (fp)fscanf_s(fp, "%6d", &arr[i]);}clock_t start = clock();Data* ret = NULL;for (int i = 0; i < tm; i++) {//Sort_insert(arr, size);//int way[7] = { 203,101,47,23,7,3,1 };//Sort_shell(arr, size, way, 7);//Sort_choose(arr, size);//Sort_quick(arr, 0, size-1);//Data* tmp = new Data[size];//Sort_merge(arr, tmp, 0, size - 1);//Sort_bucket(arr, size, 5);//Sort_heap(arr, size);}clock_t end = clock();double during = difftime(end, start) / tm;for (int i = 0; i < size; i++) {printf_s("%d\n", arr[i]);}printf_s("用时%.2fms", during);return 0;
}
4.2 实现展效果示
取消注释对应的代码段,将size 定义成10000(快速排序除外),对排序时间进行考察,给出一些现象产生的原因。
5.上机体会
我的收获主要包括:在上理论课之前,我对于排序算法的理解尚且不到位,就写了一些相关的排序算法,然而在很多地方都忽视了优化问题,直到理论课上完,才重新审视了代码的不足,实现了可读性的增强和基本原理的改进。同样的,对于堆排序时间为什么这么长也是在理论课之后才找到了答案,这也提醒我,不同的排序算法在不同的情况下具有不同的性能。通过实验,可以比较不同算法在处理相同数据集时的执行时间和效率,从而了解它们的相对性能,通过对关键参数的选取,例如枢轴量,增量数组,也更体会到好的算法应当由良好的数学分析而得到。数据结构也会对对算法性能产生影响:排序算法通常需要使用一些数据结构来辅助操作,如数组、链表等。不同的数据结构对算法的性能也会有影响。