//实验十 排序算法
//本实验是解决元素的数组排序问题。实验涉及了多种排序算法,这些算法包含了一些重要的算法设计和优化思想,
//值得认真分析和充分掌握。为简单起见, 实验中假设 数组中包含N个整数,当然排序算法也适用于更复杂的数据对象。
//(166)实验目的
//(167)掌握各种排序算法的设计思想;
//(168)分析各种排序算法的运行效率及其适用情形;
//(169)掌握上机调试排序算法的基本方法。
//(170)实验内容
//(171)排序算法的实现
//目标:使用不同的排序算法,对一组数据进行排序。
//(172)算法填空:根据功能提示,完善下划线处的代码。
/*(173)直接插入排序
排序思想:整个排序由N - 1趟排序组成。
对于第p = 1, 2, ..N - 1趟排序,算法把
第p + 1(= 2,3,4,..N)个数据(初始位置分别是r[1],r[2],..r[N-1]位置上的数)插入到数组中下标为0, 1, 2, ..., p (<=N-1),共n个位置.的恰当位置,使得数组中的前p + 1(<=N)个元素有序。*/
void insertionSort(int r[], int n)
{
/**********************************************************
对数组r[]中的n个元素进行直接插入排序
*********************************************************/
int p, j;/*p和j都是数组r的下标变量.
/*r[0]上的数可认为已经是有序数,循环从p=1开始.*/
for (p = 1; p < n; p++) // N-1趟排序(从r[1]上的数据开始排.
{
const int tmp = r[p]; /* 取出第p+1个元素数据(从1计数),将其保存 而反映到数组下标则要减一,刚好为p,亲测允许const 变量初始化 */
// 从数组中的第p-1个位置开始(从0开始计数)(一直往前比较,找到合适的位置,若tmp比该位置的元素小,则
// 把该元素往后移(对于每次只插入一个元素的插入排序而言这个变更位置的缓冲单元由当前被比较元素空出来,刚好不会影响还未排序的后面部分
for (j = p; j > 0 && tmp < r[j - 1]; j--)/*找到合适的插入位置j;同时把元素后移;这个过程中,
p作为右值,不被改变,类似于选择排序(tmp即r[p],每一趟排序共用一个tmp,数组内的元素r[j-1]和数组外的临时常量tmp作比较.);
而内部j与j-1则类似于冒泡的特点,但这里不是比较,而是直接覆盖值
测试:r[0],r[1],*/
{
r[j] = r[j - 1];/*0<=j-1,则j>=1;*/
}
r[j] = tmp;// 把tmp放入到数组中的恰当位置
}
}
//(174)谢尔(Shell)排序
/*排序思想:进行多趟排序。(该算法包含了三重循环:第一重循环是为了让gap能够取不同的值;内层的for则是为了对各个gap下所得到的分组子序列并行进行插入排序
在每趟排序时,按照给定的元素位置增量对元素进行分组,在每组内对元素进行直接插入排序。
每趟排序所用的位置增量随着算法进行而减少,直到最后一趟,所有元素均分在一组。因此,Shell排序也称为“缩减增量排序”。*/
void shellSort(int r[], int n)
{
/**********************************************************
使用Shell增量,对数组r[]中的n个元素进行Shell排序
*********************************************************/
int i, j, gap;
int tmp;
for (gap = n; gap > 0; gap /= 2) // Shell增量序列:N/2, N/4,..., 1
/*对根据当前gap所分组的子序列进行并行的插入排序*/
for (i = gap; i < n; i++) { // 以增量gap对元素进行分组(只是想法上的分组,并不直接体现在代码上(存储结构上),并排序,并且,每次排序都是几个子序列一同开始(每次经过for都为某个序列的有序部分增加一个元素,而不是先把某一个序列一次性排完才取排下一个子序列
// 把r[i] 插入到组中的恰当位置,使得下面的元素序列有序: // (r[i], r[i-gap], r[i-2*gap]...)
tmp = r[i];
//i增加1,就是进入下一分组序列进行排序;
// 依次检查r[j-gap], r[j-2*gap]...与tmp的关系,若tmp小于当前位置的元素,则把当前位置元素r[j-n*gap]往后移,即放到 后面的第gap个位置
for (j = i; j >= gap && tmp < r[j - gap]; j -= gap)/*我们需要访问的最小索引 j-gap >= 0,故j>=gap*/
{
r[j] = r[j - gap];/*location_1处 ;将相对于当前元素的前gap个间隙位置的元素r[j-gap]赋值到当前元素*/
}/*离开此处时,j比loacation_1处又小了gap*/
r[j] = tmp;/*location_2 (可以填充到(对应分组)正确位置上)*/
}
}
//(175)简单选择排序
//排序思想:整个排序由N - 1趟排序组成。第一趟排序,从N条记录中找到最小的记录,将它与第一条记录交换;第二趟排序,从剩余的N - 1条记录中,找到次小的记录,将它与第二条记录交换;重复上述排序过程。
void selSort(int r[], int n)
{
/**********************************************************
对数组r[]中的n个元素进行简单选择排序
*********************************************************/
int i, j, k;
int tmp;
for (i = 0; i < n; i++) // N-1 趟排序
{ // 从记录i, i+1, ..., N-1中找到最小记录
k = i;/*保存最小元素所在位置的索引*/
for (j = i + 1; j < n; j++)
if (r[j] < r[k]) k = j;
/*该趟排序结束,将k索引上的元素调到有序部分(正确的位置上)*/
if (i != k) // 通过与记录i交换,把最小记录放在位置i处
{
tmp = r[i];
r[i] = r[k];
r[k] = tmp;
}
}
}
//(176)堆排序
/* 排序思想:首先,通过筛选 为 待排序数据 构建一个二叉堆。
然后,整个排序由N - 1趟排序组成。每次排序时,找到堆中最小元素
(即堆顶元素),并放入排序数组的恰当位置;然后通过筛选把剩余的元素重建成一个堆。*/
void percDown(int r[], int i, int n)/*筛选函数(具有较好的通用性 从第i个元素开始,对数组r进行“筛选”(以最大堆为例)*/
{
/**********************************************************
参数要求:元素从数组下标为1的地方开始存储
*********************************************************/
int tmp,/*暂存元素*/
child;/*保存大孩子元素的索引*/
/*填写别的挖空代码,一定要尽快搞清楚,他所引入的辅助变量的含义/用意,并为变量注释好含义,不然还是容易在使用的过程中出现前后含义不一致的情况:
比如child是索引值还是元素元素值.*/
/*进入循环体前的准备工作(初始化)*/
tmp = r[i]; // 存储待筛选的第i个元素的值
child = 2 * i; // 第i个元素的左孩子索引
/*该函数功能的核心:循环:(传入的节点编号是非叶子节点的时候,才能进入循环)*/
while (2 * i <= n) { // 如果待筛选元素存在(左)孩子结点,若还能满足2*i+1<=n,那么该结点r[i]左右孩子都有.
// 从r[i], r[2i], r[2i+1] 这三个元素中,找到最大元素
if (child != n && r[child] < r[child + 1]) /* 让child指向较大的孩子结点(当然前一个条件可以为 child+1 <=n (child != n 的写法也是为了照顾child+1的取值范围)
如果第一个条件不满足,说明只有一个孩子结点(左孩子),那就直接在下面作双亲结点于与左孩子节点的比较.*/
child++;
/*判断并处理交换(如果需要的话)*/
if (tmp < r[child]) // 如果较大孩子结点的值大于待筛选元素,交换
r[i] = r[child];
else/*temp>=child*/
break; // 否则,筛选结束(已经是大顶堆了)
i = child; child = 2 * i; //往下找, 从较大孩子结点开始,迭代i,继续筛选
}
r[i] = tmp; // 把最开始筛选的元素值放入最终交换到的结点位置
}
void heapSort(
int r[],
int n)
{
/*在测试排序算法的函数时,建议先将形参注释掉,测试数据直接在函数内部定义(这样方便再调试的时候观察变化,尤其时内部的数组,观察方便,而若通过参数出传入数组,就不方便观察整个数组的变化情况.测试完毕后,注释/删除掉内部的测试数据,并恢复形参
不过要使用全局变量的话也可,若在不同源文件中,声明一下,若是结构体/自定义类型,就写个头文件吧(但要注意,所在项目的各个项(源文件可能分散在不同的文件夹中,不方便包含.)
typedef int elemtype;
elemtype r[] = { -99, 19,15,13,1,6,7,0,3,2,4 };
int n = 10;*/
/**********************************************************
对数组r[]中的n个元素进行堆排序
******************************************************/
int i = n / 2, temp;
// 创建堆,从第n/2个元素开始到第一个元素,反复筛选
for (; i >= 1; i--)
{
percDown(r, i, n);
}
/*执行N-1趟排序即可,所以i>1(或写作i>=2 */
for (i = n; i >= 2; i--)
{
// 删除堆顶,即把堆顶(堆中最大元素)与堆尾交换
temp = r[i];
r[i] = r[1];
r[1] = temp;
// 完成交换后,从堆顶开始,对堆进行“筛选”调整
percDown(r, 1, i - 1);/*此时i已经是全局最大值,放到最大编号处,不再参与堆的调整.*/
}
}
// (177)调试
// 使用上述排序算法,完成下面代码,运行并调试程序。
// void main()
// {
// // 构造一个包含50个随机数的数组,并对其进行排序
// int r[50], i;
//
// // 1. 构造随机数数组
// {
// // 以当前时间作为随机数种子,需要包含"time.h"以及"stdlib.h"
// srand(time(NULL));
// for (i = 0; i < 50; i++)
// r[i] = rand(); // 随机数
// }
//
// // 2. 依次调用上述排序算法对数组中的数据进行排序,并输出
// // 注意:在堆排序中,数组中数据默认是从下标为1的地方开始存储。 // 因此,r[50]只能存放49个数,可适当地修改上面的数据构造代码
// {
//
// }
// }
/*归并排序
* 对n个元素的表,将这n个元素看作叶结点,若将叶子两两归并生成的 子表 看作它们的父结点,则归并过程对应由叶向根生成一棵二叉树的过程。所以归并趟数约等于二叉树的高度-1
* 即log2n,每趟归并需移动记录n次,故时间复杂度为O(nlog2n)
排序思想:如果N = 1,那么只有一个元素需要排序,答案显然;
否则,将数据分成前后两半,分别对前后两部分进行归并排序,再把排序好的前后两部分合并在一起。
该排序算法采用了经典的分治策略,可通过递归求解。
*/
//合并前后两部分的排序数据的函数。函数原型如下:
/**********************************************************
给定两个有序数组A和B。其中,数组A存放在r[leftPos ~ rightPos-1],
数组B存放在r[rightPos ~ rightEnd]。合并这两个有序数形成一个新的有
序数组, 并存放在位置: r[leftPos ~ rightEnd]。提示:在合并形成新的有
序数组时,可先存放于临时位置tmpArray,再复制到r[leftPos ~ rightEnd]。
*********************************************************/
void merge(int r[], int tmpArray[],
int leftPos, /*第一部分序列的开头;(结尾尾rightPos-1)*/
int rightPos, /*第二部分序列的开头*/
int rightEnd)/*第而部分序列的结尾*/
{
/*数组A存放在r[leftPos ~ rightPos-1],
数组B存放在r[rightPos ~ rightEnd]。*/
int i = leftPos,
j = rightPos,
k;/*指示写在tmpArray[]索引k处*/
//l;/*可用于将有剩余部分的序列循环复制到tmpArray[]里*/
k = leftPos;
for (; i <= rightPos-1/*j-1 和 辅助变量混淆就糟糕了*/ && j <= rightEnd;)/*j只是被初始化为rightPos,但难以保持不变,要写实参*/
{
if (r[i] < r[j])
{
tmpArray[k] = r[i++];
}
else
{
tmpArray[k] = r[j++];
}
k++;
}
/*若第一部分有剩余*/
if (i <= rightPos - 1)
{
while (i <= rightPos - 1)/*rightPos不能写成j(j已经被改变了,不再是初始化时的第二部分左边界)*/
{
tmpArray[k++] = r[i++];
}
}
/*若第二部分有剩余*/
if (j <= rightEnd)
{
while (j <= rightEnd)
{
tmpArray[k++] = r[j++];
}
}
/*将排序结果从辅助数组拷贝(遍历拷贝)回原序列所在数组,以便将结果带回.*/
for (i = leftPos; i <= rightEnd; i++)
{
r[i] = tmpArray[i];
}
}//merge()
/*归并排序核心算法(递归版本):
将排序结果存值tmpArray[]*/
void mergeSortImplemnt(
int r[],
int tmpArray[],/*临时存放数据空间*/
int left,
int right)
{
/**********************************************************
对数组r[]中下标在[left, right]之间的数据进行归并排序
*********************************************************/
if (left < right)/*else:(left==right)递归出口_1(简单出口)此出口不做元素调整*/
{ // N > 1
int center = (left + right) / 2; // 中间位置,把待排序数据分成两半
/*父级调用要等待子级调用回归后再触发它的兄弟级调用(如果还有后面的兄弟级调用的话);当它的最后一个兄弟级调用完成后就回到了父级调用,执行父级调用还未完成的操作;(叶子级别的调用一般时使用简单递归出口,父级调用一般使用复杂的递归出口(会有更多的处理)*/
mergeSortImplemnt(r, tmpArray, left, center); // 前半部分归并排序(递归进入)
mergeSortImplemnt(r, tmpArray, center + 1, right); // 后半部分归并排序(对末次调用:递归出来(返程);而对于初次调用,他是后半部分的处理开端)
merge(r, tmpArray, left, center + 1, right); /*递归出口_2(回程复杂出口(一般执行元素调整操作) ,合并前后两部分有序子序列为更大范围的大有序子序列)这一调用使得原序列数组r变得局部有序,调整完有序的部分是从父级调用的左右边界范围内*/
}
}//mergeSortImplemnt()
void mergeSort(int r[], int n)
{
/**********************************************************
对数组r[]中的n个元素进行归并排序
*********************************************************/
int* tmpArray = (int*)malloc(sizeof(int) * n); // 临时存放数据空间
/*调用核心算法:*/
mergeSortImplemnt(r, tmpArray, 0, n - 1); // 归并排序的具体实现
free(tmpArray);
}
/*(180)快速排序
排序思想:与归并排序一样,采用分治策略。
如果N = 0或1,答案显然;
否则:(1) 取任一元素v,称之为枢纽元;
(2) 将r - {v}(数组中剩余元素)分割成前后两部分r1和r2,其中r1中元素均小于v,而r2中元素均大于v;
(3) 返回 { r1的快速排序结果,后跟v,继而r2的快速排序结果 }。
核心算法实现实现如下:
*/
void swap(int* a, int* b)
{
int tmp = 0;
tmp = *a;
*a = *b;
*b = tmp;
}
/*为给定范围内的元素序列,返回枢轴元素值,并将枢纽搬到右边界*/
int median3(int r[], int left, int right)
{
/**********************************************************
通过 三数中值分割法 选取待排序元素r[left ~ right]的枢纽
*********************************************************/
int center = (left + right) / 2,/*中间索引*/
pivot;
/*不难知道,中间值元素的所映无非就是left/center/right*/
/*若三个索引上的元素满足:大中小/小中大*/
if (r[center] <= r[left] && r[center] >= r[right] ||
r[center] >= r[left] && r[center] <= r[right])
pivot = center;/*center对应的元素就是中间值*/
/*若三个索引上的元素满足:中大小/中小大*/
else if (r[left] <= r[center] && r[left] >= r[right] ||
r[left] >= r[center] && r[left] <= r[right])
pivot = left;
/*若三个索引上的元素满足:小大中/大小中*/
else pivot = right;
/*已求得合适的枢轴元素(的索引位置piovt)*/
/* 把枢纽元素放置在尾部(?):为了方便安排从那一侧先开始收缩边界*/
if (pivot != right)
swap(&r[pivot], &r[right]);
return r[right];/*返回枢轴元素值*/
}
/*主要的排序算法:对数组r[]中下标在[left, right]之间的数据进行快速排序*/
void quickSortImplement(int r[], int left, int right)/*implement:执行*/
{
int pivot_index;
int saveLeft = left,
saveRight = right;
/*递归出口*/
if (left < right)
{
int pivot = median3(r, left, right); //三数中值分割法 选取枢纽元(元素值而非位置)
// 根据枢纽元,把待排序元素分割成前后两部分,且算出枢轴位置,并分别对前后两部分进行快速排序
/*对序列分区(交换调整元素),并求出枢轴的位置保存在pivot_index中*/
int tmp = 0;
while (left < right)
{
/*根据枢轴元素所在位置的不同,不能够将while()a 和while()b 的代码顺序对调!(yin'gai)应该将枢轴的对面侧安排在前!
因为如果从枢轴的同一侧开始调整,那枢轴所在的位子就不会被移动(指针直接就扫过它);而从另一侧开始,那枢轴就可以被调整到(正确的中间位置)*/
/*左边界收缩的理想情况*/
while (left < right && r[left] <= pivot)
{
left++;/*先后推++*/
}//while():b
SWAP(r[left], r[right], tmp);
/*右边界收缩的理想情况*/
while (left < right && r[right] >= pivot)/*取等号时还不需(不必)要交换,继续测试下一个边界,提高稳定性*/
{
right--;/*往前进--*/
}//while():a
SWAP(r[left], r[right], tmp);
}//while;到此为止,可以完成一次分区(与交换元素)的工作
pivot_index = left;/*记录枢轴元的索引位置,以便递归*/
/*递归:*/
/*递归出口:*/
/*递归调用分区交换的功能模块while();使得该函数能够实现全局的快速排序.*/
quickSortImplement(r, saveLeft, pivot_index - 1);/*pivot_index - 1*/
/*after left side have accomplished,then start the right side */
quickSortImplement(r, pivot_index + 1, saveRight);/*pivot_index + 1*/
}//if
}//quickSortImplement()
/*该函数不是必须(唯一的一点儿作用就是减少一个参数(整合两个参数))
对数组r[]中的n个元素进行快速排序;参数为待排序列+元素个数*/
void quickSort(int r[], int n)
{
/*测试数组:建议先用小规模的连续数组:5,4,3,2,1或其它连续值来初步调试基本错误.
调试的基本原则是:先简单(已于看出错误(如果有的话),更进一步才是更一般,更刁钻的测试数据(如果连简单数据都过不了,那如和过得了复杂数据呢)*/
//int r[] = { /*90,80,70,*/60,50,40,30,20,10 };int n = 6;
//int r[] = { 19,15,13,1,6,7,0,3,2,4 }; int n = 10;
quickSortImplement(r, 0, n - 1); // 快速排序的具体实现(编写复合函数原型时,以传入实参的方式调用(复合函数的)内存函数)
}
// /*(181)调试
// 构建测试数据,在主函数中调用上述排序算法,运行并调试程序。
// (182)思考
// (183)通过课外阅读,了解各种排序算法的稳定性、效率及其适用情况。
// (184)快速排序的改进
// 在上述排序算法中,快速排序是实践中最快的排序算法。但是,当待排序数据规模较小时,简单的排序算法效率更优。改进快速排序算法的代码实现,要求在算法运行过程中,当待排序数据个数在20以内时,选用直接插入排序算法进行排序:
// */
// void quickSortImplement(int r[], int left, int right)
// {
// /**********************************************************
// 对数组r[]中下标在[left, right]之间的数据进行快速排序
// *********************************************************/
// int pivot = median3(r, left, right); // 选取枢纽元
//
// if (right >= left + 20) { // 快速排序
// // 根据枢纽元,把待排序元素分割成前后两部分,并分别对前后两 // 部分进行快速排序
// {
// ;
// }
// }
// else { // 对数组r[]中下标在[left, right]之间的数据进行直接插入排序
// ;
// }
//
// }
//