目录
近来有空闲,把前几个学期做的实验上传上来。如有错误的地方欢迎大佬批评指正,有更好的方法也期待您的分享~
实验内容
输入一组关键字序列,分别实现下列排序算法:
1.编写函数,实现简单选择排序、直接插入排序和冒泡排序算法。
2.编写函数,实现希尔排序算法。
3.编写函数,实现快速排序算法。
4.编写函数,实现堆排序算法。
5.编写函数,实现折半插入排序算法。
6.编写一个主函数,在主函数中设计一个简单的菜单,分别调试上述算法。
一、实验目的
1.学习排序的基本概念,包括排序的稳定性、内排序和外排序之间的差异;
2.掌握常见的排序算法(插入排序、交换排序、选择排序、归并排序、基数排序等)的思想、特点及其适用条件和算法实现;
3.灵活运用各种排序算法解决一些综合应用问题。
二、问题分析及数据结构设计
本次开发任务要求我们编写一个能调试七种排序算法的系统。该任务需要实现以下功能:简单选择排序、直接插入排序、冒泡排序、希尔排序、快速排序、堆排序、折半插入排序。
其次是数据结构设计,为了实现这些功能,我们需要构建数据模型。每次排序前随机生成10个1-1000之间的元素的数组array。
对于每个功能,我们需要设计相应的函数,函数的输入参数和返回值根据具体的需求进行设计。例如,简单选择排序的函数使用数组array和元素个数作为输入,返回一个根节点的指针;希尔排序函数使用节点指针arr和元素个数作为输入,输出遍历的结果等。另外,在实现堆排序功能的过程中,需要使用辅助的数组来建立堆。从最后一个非叶子节点开始向上遍历,建立堆,每交换一次之后,就把最大值拿出,不再参与调整堆。
三、算法设计
1.简单选择排序 selectSort():
在待排序的数列中寻找最大(或者最小)的一个数,与第 1 个元素进行交换,接着在剩余的待排序的数列中继续找最大(最小)的一个数,与第 2 个元素交换。以此类推,一直到待排序的数列中只有一个元素时为止。
步骤:
(1)新建一个索引k为标记索引,用于保存最大值或者最小值的索引;
(2)将第一个元素索引用k保存,将k索引所在元素和剩下元素一一比较,将其中的较小值用k保存其索,遍历结束后可以取得最小值的索引k;
(3)将索引为k的元素和第一个元素进行位置交换,重复步骤2从第二个元素继续,直到遍历完成。
2.冒泡排序 popSort():
将关键字较大的记录向序列尾部移动,关键字较小的记录向序列前部移动。
步骤:
(1)比较相邻的元素。如果第一个比第二个大,就交换他们两个;
(2) 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数;
(3) 针对所有的元素重复以上的步骤,除了最后一个;
(4) 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
3. 直接插入排序 InsertSort():
将原序列分为两个序列,一个序列是有序序列,一个序列为无序序列。假设一共有n个无序数,那么,我们第一次选择一个数据元素作为有序序列(我们知道,一个元素本身肯定是有序的),将其他n-1个元素都作为无序序列,那么,我们每一次从无序序列中取出一个数,通过某种方法找到合适的位置插入,这样,有序序列的元素总数就多了一个,无序序列的元素总数就少了一个。通过n-1次放置之后,就可以得到数据元素总数为n的有序序列了,排序完成。
步骤:假设有一组无序序列 R0, R1,…, RN-1;
(1)我们先将这个序列中下标为 0 的元素视为元素个数为 1 的有序序列。
(2)然后,我们要依次把 R1, R2,…, RN-1 插入到这个有序序列中。所以,我们需要一个外部循环,从下标 1 扫描到 N-1 ;
(3)接下来描述插入过程。假设这是要将 Ri 插入到前面有序的序列中。由前面所述,我们可知,插入Ri时,前 i-1 个数肯定已经是有序了;
所以我们需要将Ri 和R0 ~ Ri-1 进行比较,确定要插入的合适位置。这就需要一个内部循环,我们一般是从后往前比较,即从下标 i-1 开始向 0 进行扫描。
4. 希尔排序 ShellSort():
希尔排序是记录按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法便终止。
步骤:初始时,有一个大小为 10 的无序序列。
(1)在第一趟排序中,我们不妨设 gap1 = N / 2 = 5,即相隔距离为 5 的元素组成一组,可以分为 5 组;
(2)接下来,按照直接插入排序的方法对每个组进行排序;
在第二趟排序中,我们把上次的 gap 缩小一半,即 gap2 = gap1 / 2 = 2 (取整数)。这样每相隔距离为 2 的元素组成一组,可以分为 2 组;
(3)按照直接插入排序的方法对每个组进行排序;
(4)在第三趟排序中,再次把 gap 缩小一半,即gap3 = gap2 / 2 = 1。 这样相隔距离为 1 的元素组成一组,即只有一组;
(5)按照直接插入排序的方法对每个组进行排序。此时,排序已经结束。
5. 快速排序 QuickSort():
通过一趟划分将要排序的序列分割成独立的三个部分,即左部、基准值、右部。其中左部的所有数据都比基准值小,右部的所有数据都比基准值大。然后再按此方法分别对左部和右部进行划分,整个排序过程通过递归进行。
步骤:
(1)首先我们从数组的left位置取出该数(2)作为参照物(base);
(2)从数组的right位置向前找,一直找到比(base)小的数,如果找到,将此数赋给left位置(也就是将1赋给2),此时数组为:1,4,5,1,3,left和right指针分别为前后的1;
(3)从数组的left位置向后找,一直找到比(base)大的数,如果找到,将此数赋给right的位置(也就是4赋给1),此时数组为:1,4,5,4,3,left和right指针分别为前后的4;
(4):重复“第二,第三“步骤,直到left和right指针重合,最后将(base)插入到4的位置,此时数组值为: 1,2,5,4,3,至此完成一次排序。第五步:此时2已经潜入到数组的内部,2的左侧一组数都比2小,2的右侧作为一组数都比2大,以2为切入点对左右两边数按照"第一,第二,第三,第四"步骤进行。
6. 堆排序 HeapSort():
堆排序是在直接选择排序的基础上借助于堆的一种排序方法。在选择排序中,为找出最小的记录需要作n-1次比较,但比较的信息没有保存下来,然后为寻找关键字次小的记录要对剩下的n-1个记录进行n-2次比较,如此反复多次,增加了时间的开销。堆排序是在寻找当前最大元素的同时,还保存了本趟排序过程所产生的其他比较信息,这些信息就存放在堆中。
7. 折半插入排序 binaryInsertSort():
先取已经排序的序列的中间元素,与待插入的数据进行比较,如果中间元素的值大于待插入的数据,那么待插入的数据属于数组的前半部分,否则属于后半部分。依次类推,不断缩小范围,确定要插入的位置。
步骤:设R[low..high]是当前的非空查找区间(下界为 low,上界为 high),首先确定该区间的中点位置 mid=[(low+high)/2](或者 mid=(low+high)>>1),然后将待查的k值与R[mid]比较:
(1)D若k=R[mid],则查找成功并返回该元素的序号 mid;
(2)若k<R[mid],则由表的有序性可知 R[mid..high]均大于k,因此若表中存在关键字等于k的元素.则该元素必定在左区间中,故新查找区间为 R[low..mid-1],即下界不变,上界改为 mid-1 ;
(3)若h>R[mid],则要查找的k 必在右区间中,即新查找区间为 R[mid+l..high],即下界改为 mid+1,上界不变;
(4)下一次查找是针对非空新查找区间进行的,其过程与上述过程类似。若新查找区间空.表示查找失败,返回 -1 。
四、功能模块程序流程图
1.简单选择排序 selectSort():
2.冒泡排序 popSort():
3.直接插入排序 InsertSort():
4.希尔排序 ShellSort():
5.快速排序 QuickSort():
6.堆排序 HeapSort():
7.折半插入排序 binaryInsertSort():
五、实验结果
菜单如下:
1.简单选择排序:
2.冒泡排序:
3. 直接插入排序:
4. 希尔排序:
5.快速排序:
6.堆排序:
7.折半插入排序:
六、算法分析
1.简单选择排序
(1)时间复杂度O(n 2):当N个数进行排序时,将进行T=[n*(n-1)]/2次,根据计算方法保留最高次n 2,因此选择排序的时间复杂度为O(n 2);
(2)空间复杂度O(1):排序中始终只用到了数组大小的空间,为常数,因此空间复杂度为O(1);
(3)稳定性(不稳定):选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n - 1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果当前元素比一个元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了,所以选择排序不是一个稳定的排序算法。
2.冒泡排序
(1)时间复杂度O(n 2):外层循环为n,内层所需要循环比较的次数为(n-1)、(n-2)…1由等差数列求和得时间花销为:[n(n-1)] / 2;所以时间复杂度为:O(n 2);
(2)空间复杂度O(n):冒泡排序的辅助变量空间仅仅是一个临时变量,并且不会随着排序规模的扩大而进行改变,所以空间复杂度为O(1);
(3)稳定性(稳定):两个大小相同的元素在排序过程中会相互靠近,一旦这两个大小相同的元素在排序过程中处于相邻位置,那么后续的排序操作就不会对该元素进行任何相对位置的改变。所以冒泡排序是稳定的。
3. 直接插入排序
(1)时间复杂度O(n)~O(n 2):①最好情况:当初始序列为正序时,只需要外循环n-1次,每次进行一次比较,无需移动元素。此时比较次数(C min)和移动次数(M min)达到最小值,此时时间复杂度为O(n);②最坏情况:初始序列为反序时,需要外循环n-1次,每次排序中待插入的元素都要和[0,i-1]中的i个元素进行比较且要将这i个元素后移i次,此时比较次数和移动次数达到最大值,时间复杂度为O(n 2);
(2)空间复杂度O(1):在直接插入排序中只使用了i,j,tmp这三个辅助元素,与问题规模无关,空间复杂度为O(1);
(3)稳定性(稳定):插入排序过程中,大小相同的元素,偶尔会由于交换而相离,但总体上会相互靠近,直到它们相互毗邻后,插入排序便不再改变它们的相互位置,所以插入排序是稳定的。
4. 希尔排序
(1)时间复杂度O(n 1.3)~O(n 2):
希尔排序的时间复杂度依赖增量序列的函数(和增量序列有关)这个涉及数学上尚未解决的问题,时间复杂度分析比较困难;当n在某个特定范围时,希尔排序的时间复杂度约为O(n 1.3);最坏的情况下,时间复杂度为O(n 2);
(2)空间复杂度O(1):仅仅使用了常数个辅助单元,空间复杂度是O(1);
(3)稳定性(不稳定):一次插入排序是稳定的,不会改变相同元素的相对顺序,但在不同的插入排序过程中,相同的元素可能在各自的插入排序中移动,最后其稳定性就会被打乱,所以希尔排序是不稳定的。
5.快速排序
(1)时间复杂度O(n):O(nlogn)~O(n 2):①最好情况:每一次取到的元素都刚好平分整个数组,此时的时间复杂度公式为:T[n] = 2T[n/2] + f(n),时间复杂度为:O(nlogn);
②最坏情况:每一次取到的元素就是数组中最小/最大的,T[n] = n * (n-1) = n^2 + n,此时时间复杂度为:O(n 2);
(2)空间复杂度O(logn):①最好情况:其树的深度为:log2(n),空间复杂度为 O(logn);②最坏情况:需要n-1次调用,每2个数都需要交换,此时退化为冒泡排序,空间复杂度为 O(n);
(3)稳定性(不稳定):相等元素可能会因为分区而交换顺序,所以它是不稳定的算法。
6.堆排序
(1)时间复杂度O(nlogn):初始化堆的时间复杂度为O(n),n-1次删除操作的时间复杂度为O(nlogn),所以总操作时间复杂度为O(nlogn);
(2)空间复杂度O(1):因为堆排序是就地排序,空间复杂度为常数:O(1);
(3)稳定性(不稳定):在堆的调整过程中,关键字进行比较和交换所走的是该结点到叶子结点的一条路径,因此对于相同的关键字就可能出现排在后面的关键字被交换到前面来的情况,所以堆排序是一种不稳定的排序方法。
7.折半插入排序
(1)时间复杂度O(n 2):折半插入排序的对象移动次数与直接插入排序相同,依赖于对象的初始排列,在平均情况下,折半插入排序仅减少了关键字的比较次数,而记录的移动次数不变,因此,折半插入排序的时间复杂度为O(n 2);
(2)空间复杂度O(1):折半插入排序所需附加存储空间和直接插入排序相同,只需要一个记录的辅助空间r[0],所以空间复杂度为O(1)。
(3)稳定性(稳定):当待插入数与mid位置的值相等时,接下来相当于进入了有序序列的右半区,mid+1到high,之后经过多次折半查找,该元素所找到的合适位置就是前一个与之相等元素的后一位,所以说两者相对位置没有发生变化,因此折半插入排序是稳定的。
七、操作说明
1.进入排序系统后,会看见如下提示:
************************* 实验四 排序 ***********************
* *
* 每次排序随机生成10个1-1000之间的元素 *
* *
* 1:实现简单选择排序 *
* 2:实现冒泡排序 *
* 3:实现直接插入排序 *
* 4:实现希尔排序 *
* 5:实现快速排序 *
* 6:实现堆排序 *
* 7:实现折半插入排序 *
* 8:退出菜单 *
* *
**************************************************************
☆请输入一个数字选项:
2.若想实现简单选择排序,则输入1。
3.若想实现冒泡排序,则输入2。
4.若想实现直接插入排序,则输入3。
5.若想实现希尔排序,则输入4。
6.若想实现快速排序,则输入5。
7.若想实现堆排序,则输入6。
8.若想实现折半插入排序,则输入7。
9.若想退出菜单,则输入8。
八、源代码
#include <iostream>
#include <ctime>
#include <cstdlib>
using namespace std;
void printarr(int array[], int n);
//选择排序
void selectSort(int arr[], int n) {
int i, index, j;
for (i = 0; i < n; i++)//执行第i遍扫描操作
{
index = i;
for (j = i + 1; j < n; j++)//比较无序序列中的记录
{
if (arr[index] > arr[j])//记录序列中最小值的位置
{
index = j;
}
}
if (index != i)//如果无序序列中第一个记录不是最小值,则进行交换
{
int temp = arr[index];
arr[index] = arr[i];
arr[i] = temp;
}
for (int k = 0; k < n; k++)
{
cout << arr[k] << " ";
}
cout << endl;
}
}
//冒泡排序
void popSort(int arr[], int n)
{
for (int i = 0; i < n - 1; i++)
{
for (int j = 0; j < n - i - 1; j++)
{
int temp = 0;
if (arr[j] > arr[j + 1])
{
temp = arr[j + 1];
arr[j + 1] = arr[j];
arr[j] = temp;
}
}
for (int k = 0; k < n; k++)
{
cout << arr[k] << " ";
}
cout << endl;
}
}
//直接插入排序
void InsertSort(int a[], int n)
{
int temp;
int j;
cout << "直接插入排序过程" << endl;
for (int i = 1; i < n; i++)
{
if (a[i] < a[i - 1])
{
temp = a[i];
for (j = i - 1; j >= 0 && temp < a[j]; j--)
{
a[j + 1] = a[j];
}
a[j + 1] = temp;
}
for (int k = 0; k < n; k++)
cout << a[k] << " ";
cout << endl;
}
}
//希尔排序
void ShellSort(int* arr, int len) {
int grp = len / 2; // 计算增量
for (; grp > 0; grp = grp / 2) { // 每次计算完一轮,增量再除以二
// 选择排序
for (int i = grp; i < len; i++) {
int cur = arr[i]; // 先空出第二个数的位置
int j = 0;
// 根据增量往前比较,小的在前,大的在后
for (j = i - grp; j >= 0 && arr[j] > cur; j = j - grp) {
arr[j + grp] = arr[j]; // 符合条件将前面的数据移动到空出的位置
}
arr[j + grp] = cur; // 最后将待插入数据cur插入到前面空出的位置
}
for (int k = 0; k < len; k++)
{
cout << arr[k] << " ";
}
cout << endl;
}
}
//快速排序
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];
for (int k = 0; k < 10; k++)
{
cout << A[k] << " ";
}
cout << endl;
}
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);
}
}
//堆排序算法
void HeapAdjust(int* array, int i, int length) { //调整堆
int leftChild = 2 * i + 1; //定义左右孩子
int rightChild = 2 * i + 2;
int max = i; //初始化,假设左右孩子的双亲节点就是最大值
if (leftChild<length && array[leftChild]>array[max]) {
max = leftChild;
}
if (rightChild<length && array[rightChild]>array[max]) {
max = rightChild;
}
if (max != i) { //若最大值不是双亲节点,则交换值
swap(array[max], array[i]);
HeapAdjust(array, max, length); //递归,使其子树也为堆
}
}
void HeapSort(int* array, int length) { //堆排序
for (int i = length / 2 - 1; i >= 0; i--) { //从最后一个非叶子节点开始向上遍历,建立堆
HeapAdjust(array, i, length);
}
cout << "堆排序过程为" << endl;
for (int j = length - 1; j > 0; j--) { //调整堆 ,此处不需要j=0
swap(array[0], array[j]);
HeapAdjust(array, 0, j);
//因为每交换一次之后,就把最大值拿出(不再参与调整堆),第三个参数应该写j而不是length
printarr(array, length);
}
}
//折半插入排序
void binaryInsertSort(int arr[], int len) {
int front, end, temp;
for (int i = 1; i < len; i++) {
temp = arr[i];
front = 0;
end = i - 1;
while (front <= end) {
int mid;
mid = (front + end) / 2;
if (temp < arr[mid]) {
end = mid - 1;
}
else {
front = mid + 1;
}
}
for (int z = i; z > front; z--) {
arr[z] = arr[z - 1];
}
arr[front] = temp;
for (int k = 0; k < len; k++)
{
cout << arr[k] << " ";
}
cout << endl;
}
}
//生成随机数组1-100
void Random(int* a, int n)//生成范围在l~r的随机数
{
srand(time(0)); //设置时间种子
for (int i = 0; i < n; i++) {
a[i] = rand() % (1000) + 1;//生成区间r~l的随机数
}
}
void printarr(int array[], int n)
{
for (int i = 0; i < n; i++)
{
cout << array[i] << " ";
}
cout << endl;
}
int main()
{
cout << " ************************* 实验四 排序 ***********************\n";
cout << " * *\n";
cout << " * 每次排序随机生成10个1-1000之间的元素 *\n";
cout << " * *\n";
cout << " * 1:实现简单选择排序 *\n";
cout << " * 2:实现冒泡排序 *\n";
cout << " * 3:实现直接插入排序 *\n";
cout << " * 4:实现希尔排序 *\n";
cout << " * 5:实现快速排序 *\n";
cout << " * 6:实现堆排序 *\n";
cout << " * 7:实现折半插入排序 *\n";
cout << " * 8:退出菜单 *\n";
cout << " * *\n";
cout << " **************************************************************\n";
int array[10];
int choice;
bool flag = true;
while (flag)
{
Random(array, 10);
cout << "☆请输入一个数字选项:";
cin >> choice;
switch (choice)
{
case 1:
cout << "排序前:" << endl;
printarr(array, 10);
cout << "简单选择排序为:" << endl;
selectSort(array, 10);
cout << endl;
break;
case 2:
cout << "排序前:" << endl;
printarr(array, 10);
cout << "冒泡排序为:" << endl;
popSort(array, 10);
cout << endl;
break;
case 3:
cout << "排序前:" << endl;
printarr(array, 10);
cout << "直接插入排序为:" << endl;
InsertSort(array, 10);
cout << endl;
break;
case 4:
cout << "排序前:" << endl;
printarr(array, 10);
cout << "希尔排序为:" << endl;
ShellSort(array, 10);
cout << endl;
break;
case 5:
cout << "排序前:" << endl;
printarr(array, 10);
cout << "快速排序为:" << endl;
QuickSort(array, 0, 9);
cout << endl;
break;
case 6:
cout << "排序前:" << endl;
printarr(array, 10);
cout << "堆排序为:" << endl;;
HeapSort(array, 10);
cout << endl;
break;
case 7:
cout << "排序前:" << endl;
printarr(array, 10);
cout << "折半插入排序为:" << endl;
binaryInsertSort(array, 10);
cout << endl;
break;
case 8:
flag = false;
break;
}
if (flag)
{
cout << endl;
cout << "排序后:" << endl;
printarr(array, 10);
cout << endl;
}
}
system("pause");
return 0;
}