算法C++ 实验一 分治策略
一、实验目的和要求
理解分治法的算法思想,阅读实现书上已有的部分程序代码并完善程序,加深对分治法的算法原理及实现过程的理解
内容:
一、用分治法实现一组无序序列的两路合并排序和快速排序。要求清楚合并排序及快速排 序的基本原理,编程实现分别用这两种方法将输入的一组无序序列排序为有序序列后输出。
二、采用基于“五元中值组取中值分割法”(median-of-median-of-five partitioning)的线性 时间选择算法,找出 N 个元素集合 S 中的第 k 个最小的元素,使其在线性时间内解决。(参 考教材 5.5.3 节)
二、实验环境(实验设备)
硬件:微型计算机
软件:Windows 操作系统、Microsoft Visual C++6.0
三、实验原理及内容
实验原理:
分治法得基本思想是将一个规模为n的问题分解为k个规模为m的相互独立且与原问题解法相同的子问题,然后将子问题的解合并得到原问题的解。由此可见,分治法设计出的程序一般是递归算法,设解决一个规模为1的问题需要1个单位时间,再设将k个子问题的解合并为原问题的解所需时间为f(n),则递归算法的时间复杂度为:
两路合并排序是一类时间复杂度为O(nlogn)的排序方法。基本思想为将有n个元素的序列看成是n个长度为1的有序序列,然后两两合并序列,得到 n/2个长度为2或1的有序序列,然后在进行两两合并,直到得到一个长度为n的有序序列。
先将无序序列利用二分法划分为子序列,直至每个子序列只有一个元素(单元素序列必有序),然后再对有序子序列逐步(两两)进行合并排序。合并方法是循环的将两个有序子序列当前的首元素进行比较,较小的元素取出,置入合并序列的左边空置位,直至其中一个子序列的最后一个元素置入合并序列中。最后将另一个子序列的剩余元素按顺序逐个置入合并序列尾部即可完成排序。
快速排序使用分治的思想,通过一趟排序将待排序列分割成两部分,其中一部分记录的关键字均比另一部分记录的关键字小。之后分别对这两部分记录继续进行排序,以达到整个序列有序的目的。最好时间复杂度为:O(nlogn),最坏时间复杂度为:O(n^2),平均时间复杂度为O(nlogn)。
快速排序的三个步骤:
(1)选择基准:在待排序列中,按照某种方式挑出一个元素,作为 "基准"(pivot)
(2)分割操作:以该基准在序列中的实际位置,把序列分成两个子序列。此时,在基准左边的元素都比该基准小,在基准右边的元素都比基准大
(3)递归地对两个序列进行快速排序,直到序列为空或者只有一个元素。
实验内容:
排序是数据处理中常用手段,是指将一个元素序列调整为按一定顺序排列的有序序列,用分治策略解决排序问题的思想是将序列按照某方式分成数个子序列,再分别进行排序,再将已经排序的子序列合并成一个有序序列。两路合并排序和快速排序都是符合分治策略的。
- 两路合并排序:
void mergeSort(int* arr, int lo, int hi) { //0 <= lo < hi <= size
if (hi - lo < 2) return;
//将数组分为两部分
int mid = (lo + hi) / 2; //以中点为界限
mergeSort(arr, lo, mid); //左侧子数组
mergeSort(arr, mid, hi); //右侧子数组
//按照组排序并连接
merge(arr, lo, mid, hi); //归并为有序数组
}
void merge(int* arr, int lo, int mid, int hi) { //有序向量的合并
int* temp = new int[hi - lo]; //汇总两个子向量的临时区域
int left_idx = lo;
int right_idx = mid; //分别对应左侧和右侧的数组当前位置索引
int temp_idx = 0; //临时归并数组的区域索引
//数组长度:① left < right ②.right < left ③.左右两数组所比较的元素都未耗尽,则直接比较两数组当前元素归入temp大数组(一般情况,蕴含了数组长度left = right的情况):
while (left_idx < mid && right_idx < hi) {
if (arr[left_idx] < arr[right_idx])
temp[temp_idx++] = arr[left_idx++];//小的放前面,每次执行成功要让对应的指针后移一位
else
temp[temp_idx++] = arr[right_idx++];
}
//情况①:左侧数组比较元素已耗尽,直接复制
while (left_idx < mid) {
temp[temp_idx++] = arr[left_idx++];
}
//情况②:右侧数组比较情况已耗尽,直接复制
while (right_idx < hi) {
temp[temp_idx++] = arr[right_idx++];
}
for (int i = 0; i < temp_idx; i++) {
arr[lo + i] = temp[i];
}
//所有操作完成后删除左端防止内存泄漏
delete[] temp;
}
两路合并排序运行结果:
- 快速排序
while (1)
{
//lo从左往右遍历,直至找到一个不小于 pivot 的元素
while (arr[lo] < pivot) {
lo++;
};
//hi从右往左遍历,直至找到一个不大于 pivot 的元素
while (hi > 0 && arr[hi] > pivot) {
hi--;
}
//如果 lo≥hi,退出循环
if (lo >= hi)
{
break;
}
else {
//交换 arr[lo] 和 arr[hi] 的值
temp = arr[lo];
arr[lo] = arr[hi];
arr[hi] = temp;
// lo 和 hi 都向前移动一个位置,准备继续遍历
lo++;
hi--;
}
}
//交换 arr[lo] 和 arr[q] 的值
temp = arr[lo];
arr[lo] = pivot;
arr[q] = temp;
//返回中间值所在序列中的位置
return lo;
快速排序运行结果:
四、实验小结(包括问题和解决方法、心得体会、意见与建议等)
(一)实验中遇到的主要问题及解决方法
两路合并排序:
在排序时,将较小的数放在前面时,记得每次执行成功要让对应的指针后移一位。
快速排序:
交换枢纽元素与j元素:为什么不与i交换?
i停下时,i元素总是大于或等于枢纽元素,j停下时,j元素总是小于或等于枢纽元素。与i还是j交换关键看枢纽元素的位置,这里是选第一个元素作为枢纽元素,由于此次排序完成后第一个元素的值应该小于或等于枢纽元素,所以要找一个小于或等于枢纽元素的元素放到start位置,也就是j所指向的元素。
代码中j出现了越界的情况,原因是如果pivot是最小的,则j会一直减到小于0,出现了越界的情况,或者pivot最大,i会越界。于是修改代码,在while循环中,增加判断。最后发现在递归调用中出现了Left大于Right的情况。还是考虑pivot是最小值或最大值的情况,此时i和j同时指向了数组最后一个或第一个元素,i==j,于是跳出for循环。接着递归,此时就出现情况了。试想若i和j都指向第一个元素,则i-1出现了越界,若都指向了最后一个元素,则i+1出现了越界,这样程序就在这个地方出现问题了,解决方法是在前面加上Left<Right的判断,这样就解决了越界的问题。
(二)实验心得
通过实验,不仅使白己更进一-步了解分治法的基本特点,同时也锻炼了白己的逻辑思维。
本实验还算可以,但是一些细节问题还有待解决,比较对二维指针数组的定义和删除还不是很
熟练,在以后会加强这方面的知识积累。
这一次的上机实操让我意识到,算法与软件编程之间有着千丝万缕的联系,使我对学习算法有了新的认知,这次上机实操不仅锻炼了我的思维方式,让我明白对待问题要多方面考虑,同时也让我意识到了对于算法和C、C++等编程语言掌握的不足,在这之后还要花更多的时间去练习学习。
(三)意见与建议
无