算法——实验 1《分治算法实验》

目录

实验 1《分治算法实验》

一、实验目的

二、实验内容

三、算法思想分析

分治法的思想:

适用条件:

实验中具体的分治思想:

四、实验过程分析

遇到的问题及解决

实验体会

改进空间

五、算法源代码

        1、归并排序:

2、快速排序

3、循环赛日程表


实验 1《分治算法实验》

一、实验目的

  1. 了解分治策略算法思想
  2. 掌握快速排序、归并排序算法
  3. 了解其他分治问题典型算法

二、实验内容

  1. 编写一个简单的程序,实现归并排序。
  2. 编写一段程序,实现快速排序。
  3. 编写程序实现循环赛日程表。设有n=2k 个运动员要进行网球循环赛。现要设计一个满足以下要求的比赛日程表:
    1. 每个选手必须与其它 n-1 个选手各赛一次
    2. 每个选手一天只能赛一场
    3. 循环赛进行 n-1 天

三、算法思想分析

分治法的思想

分而治之,关键在于将大问题分割成若干子问题(最好使子问题的规模大致相同),子问题相互独立且与原有问题相同【分】;递归求解出子问题后自底向上合并解,求出原问题的解【治】

适用条件
  • 问题规模缩小到一定程度时容易解决;
  • 问题具有最优子结构性质,即原问题的最优解包含子问题的最优解;
  • 分解出的子问题是相互独立的,即子问题之间不包含公共子问题;
  • 分解出的子问题的解可以合并为原问题的解。

实验中具体的分治思想
  1. 归并排序:先将待排序元素分为两个大小大致相同的子集合,分别对每个集合递归调用归并排序函数,然后将排好序的子集合合并为所要求的排好序的集合;
  2. 快速排序:先确定好基准点(可以通过产生一个随机数来获取),重新划分区间,将比他大的数放到右边,小于或等于的数放到左边(不稳定的排序),递归处理左右两段,直到区间只有一个数(在递归的过程中各个元素已经处于正确位置上,不需要再合并);(挖坑填数+分治)
  3. 循环赛日程表:每个选手与其他选手各赛一次(有对称性,甲与乙比即为乙与甲比),共需赛n*(n-1)场【注意:这个对称规律需要将比赛的选手本人也放进矩阵中,而不是只存他每天的对手!!即不能用第i行表示第i+1个选手,而是要将i+1存在第i行的首元素位置,这样才是一个n*n的矩阵,才有对称一说】。这里的分治即考虑到对角分布特殊——对称,将矩阵不断一分为四(递归),直到矩阵成为一个2*2的矩阵,再利用对角对称,将对角复制赋值,最后合并得到结果。

四、实验过程分析

  • 遇到的问题及解决
  1. 在归并排序中我又一次被边界条件搞糊涂了,其实之前在数据结构的实验中已经写过一次归并了,当时是在二分查找的时候研究了很久边界条件,什么左闭右开区间、左闭右闭区间到底有什么区别,有哪些要注意的点。然后这次写归并的时候又搞糊涂了,为什么一下子是小于,一下又是小于等于。后来搜了好久,也思考了很久,归并与二分查找不同,二分查找可以左闭右开,它的左闭右开是不查找右边界的元素,但是排序的话每一个元素都要排,所以其实一般都是闭区间里排序。归并的话就是分成两个区间,两个区间都成为有序区间后再合并。至于小于还是小于等于其实是决定了这个排序是稳定还是不稳定。
  2. for循环中注意如果是小于则是用right-left+1,如果是小于等于则是用right-left。其实这些细节以前都已经踩过坑注意过了,但是好久没写这种代码了,又忘了这些该注意的细节,所以说各种算法的代码还是要经常练,代码熟练度不是说说而已,要多练。
  3. 快排与并归不同,并归是分成大小大致相同的两部分,分别排,而快排是以找出的基准点正确的排序位置,以它的位置为基准,左右两边再分别排序,而不是平均分成两部分,这也是为什么快排的最好情况和最坏情况差距很大的原因。
  4. 循环赛中犯了个细节错误,我想用制表符将各个数据隔开,结果写成了
    System.out.print(table[i][j] + '\t');//Java中与字符串拼接才能成为字符串,这里误写成了单引号,表示字符,所以此处执行了加法,将制表符对应的ASCII码与table的值加起来后输出了
  5. 循环赛中根据对称性进行复制时应该以矩阵大小(length)为基础进行复制,由于刚开始是以一个4*4的矩阵为单位,所以一直没有注意到这个问题,后来我输入k=3时数组越界了,找了好久才发现这个问题,再复制时下标应该是以已知矩阵的大小为基础来加减,而不是仅仅以已知矩阵的元素下标,并且在将左下矩阵复制到右上时,左下矩阵的length应该是last – mid,而不是last – mid – 1!!!

  • 实验体会

        本次实验中有许多与边界有关的问题需要注意,其实不止本次实验,在每一次的编程中都是如此:用到数组就要注意数组是否越界;用到for循环就要注意循环的判断条件是否正确,循环是否执行了预期执行次数;用到while循环就要注意退出循环的条件何时能满足,什么情况下会退出,会不会造成死循环等等。这些都是有一定编程积累后就明白自己该注意哪些地方,除此之外,在与算法相关的边界条件就更加要谨慎处理了,而且犯错是在所难免的,重要的是我们要学会在犯错后通过调试找出问题所在,解决问题并引以为戒,在日后的学习工作中提醒自己注意不要再犯。本次实验中我就是通过逐步调试慢慢找到了循环赛排序问题所在,并通过自己画图找出下标应该怎么变化。虽然以后可能还会犯此类错误,但只要掌握了方法,就能将其改正。所以学会调试对我们来说是非常重要的。

  • 改进空间
  1. 由于本次实验主要是学习分治思想,学会实现几种排序算法,所以对于程序的健壮性没有特别在意,可以将只对整数排序改成对浮点数排序。
  2. 对于用户的输入没有判断是否合法,只是在让用户输入时通过提示信息规范用户输入,之后就默认用户输入正确且合法了,其实可以加入一些错误判断及处理信息,但那不是本次实验的重点,所以也没有写这些,主要是要理清各种算法的思想,感觉多考虑这些,代码一长,那些算法的重点逻辑就乱了,所以我将主要注意力集中在其实现思想上,没有考虑这些。
  3. 其实使用递归效率相对来说是比较低的,因为用到了栈,额外开销比较大,可以考虑看能不能找出其递推公式,将递归改成递推或者考虑使用迭代,但是分治的精髓就在于将原问题分解成若干规模更小的子问题,且子问题与原问题同构,用递归更能显示出这个特点,所以就没有考虑进一步改进,将递归改为递推等来提升效率了。

五、算法源代码

        1、归并排序:
import java.util.Scanner;

public class MergeSort {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n;//待排序序列有多少个元素
        System.out.print("请输入待排序序列元素个数:");
        n = input.nextInt();
        System.out.println("请输入待排序序列(要求为整数):");
        int[] data = new int[n];
        for(int i = 0; i < n;i++){
            data[i] = input.nextInt();
        }
        mergeSort(data, 0, n-1);//[0,n - 1]
        System.out.println("归并排序的结果为:");
        for(int i = 0; i < n; i++){
            System.out.print(data[i]+"  ");
        }
    }
    public static void mergeSort(int[] data, int left, int right){
        if(left < right){//相等时区间只有一个元素,已经有序,不用再排
            int mid = (left + right) / 2;
            mergeSort(data, left, mid);//将[left, mid]的元素排序
            mergeSort(data, mid + 1, right);//[mid + 1, right]
            merge(data, left, mid, right);//将上述两个区间合并
        }
    }
    public static void merge(int[] data, int left, int mid, int right){
        int[] temp = new int[right - left + 1];//申请空间用来存放合并后的序列
        //第一个有序数组是从left到mid---[left, mid]
        int begin1 = left;
        int end1 = mid;
        //第二个有序数组是从mid+1到right---[mid + 1, right]
        int begin2 = mid + 1;
        int end2 = right;
        int i = 0;

        while((begin1 <= end1)&&(begin2 <= end2)){
            if(data[begin1] <= data[begin2]){//若判断条件为<, 则是一个不稳定的排序,因为相等时将后面的排前面了
                temp[i] = data[begin1];
                i++;
                begin1++;
            }else {
                temp[i] = data[begin2];
                i++;
                begin2++;
            }
        }
        //若第一个数组还有剩余
        while(begin1 <= end1){
            temp[i] = data[begin1];
            i++;
            begin1++;
        }
        //若第二个还有剩余
        while(begin2 <= end2){
            temp[i] = data[begin2];
            i++;
            begin2++;
        }

        for(i = 0; i < right - left + 1; i++){//注意要+1
            data[left + i] = temp[i];
        }

    }
}

2、快速排序
import java.util.Scanner;

public class QuickSort {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        int n;//待排序序列有多少个元素
        System.out.print("(quicksort)\n请输入待排序序列元素个数:");
        n = input.nextInt();
        System.out.println("请输入待排序序列(要求为整数):");
        int[] data = new int[n];
        for(int i = 0; i < n;i++){
            data[i] = input.nextInt();
        }
        quickSort(data, 0, n-1);//[0,n - 1]
        System.out.println("归并排序的结果为:");
        for(int i = 0; i < n; i++){
            System.out.print(data[i]+"  ");
        }
    }
    public static void quickSort(int[] data, int left, int right){
        if(left < right){
            int index = partition(data, left, right);//index处元素已经正确排序,接下来只需要将其左右分别进行排序
            quickSort(data, left, index - 1);
            quickSort(data, index + 1, right);
        }
    }
    public static int partition(int[] data, int left, int right){
        int pivotIndex = (int)(Math.random()*right) + 1;
        int pivot = data[pivotIndex];//随机产生一个下标作为基准,并将基准与第一个元素交换
        data[pivotIndex] = data[left];
        data[left] = pivot;
        while(left < right){
            while (data[right] > pivot && right > left){
                right--;
            }
            //退出上面的while循环时有data[right] <= pivot
            if(left < right){
                data[left] = data[right];
            }
            while (data[left] <= pivot && left < right) {
                left++;
            }
            //退出上面的while循环时有left > pivot
            if(left < right){
                data[right] = data[left];
            }

        }
        //退出时有left = right,这个位置就是pivot排好序时应该排的位置
        data[left] = pivot;
        return left;
    }
}
3、循环赛日程表
import java.util.Scanner;

public class RoundRobinTable {
    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);
        System.out.print("循环赛日程表:总运动员数量为2的k次方\n请输入k的值(要求为整数):");
        int k = input.nextInt();
        int n = (int)Math.pow(2, k);
        int[][] table = new int[n][n];//日程表为n行n-1列,每一列表示一天,每一行表示每个运动员每天的对手
        for(int i = 0; i < n; i++){
            table[i][0] = i + 1;//将每一行的第一个元素赋值,从1-n,表示是第几位选手
        }
        roundRobin(table, 0, n - 1);
        for(int i = 0; i < n; i++){
            for (int j = 0; j < n; j++){
                System.out.print(table[i][j] + "\t");
            }
            System.out.print('\n');
        }
    }
    public static void roundRobin(int[][] table, int first, int last){
        //first:表中第一个选手的编号,last:表中最后一个选手的编号
        if(last - first == 1){//当一共只有两个选手时直接通过对称就能得到结果
            table[first][1] = table[last][0];
            table[last][1] = table[first][0];
            return;
        }
        int mid = (first + last) / 2;
        roundRobin(table, first, mid);//左上角
        roundRobin(table, mid + 1, last);//左下角
        //右上和右下不用再求了,由对称性可以直接复制
        //右下 = 左上
        int length = mid + 1 - first;
        for(int i = first; i < mid + 1 ; i++){
            for(int j = 0; j < mid + 1 - first; j++){
                table[length + i][length + j] = table[i][j];
            }
        }
        //右上 = 左下
        length = last - mid;
        for(int i = mid + 1; i < last + 1; i++){
            for(int j = 0; j < mid + 1 - first; j++){
                table[i - length][length + j] = table[i][j];
            }
        }
    }
}

  • 15
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值