用插入排序改进归并排序算法,并对其性能进行测试

一、实验目的

根据文档,要求我们编写插入排序算法、归并排序算法和用插入排序改进的归并排序算法代码。
改进的归并排序算法在处理元素较少的子数组时,使用插入排序算法来提高效率。k值表示当子数组元素个数少于等于k时,采用插入排序。并通过实验和测试不同的k值,找到合适的k值获得最佳的性能表现。

二、实验步骤

实验硬件:Apple MacBook Pro
CPU:M1 Pro
系统:MacOS Ventura 13.5.1
软件:IntelliJ IDEA 2023.1.3 (Ultimate Edition)
Java运行时版本: 17.0.7+10-b829.16 aarch64
VM: OpenJDK 64-Bit Server VM by JetBrains s.r.o.
GC: G1 Young Generation, G1 Old Generation
Java虚拟机内存: 4096MB
核心数: 8核

  1. 生成测试数据

    	  /**
         * 创建一个包含随机整数的测试文件
         */
        public static void createTestFile() {
            String filename = "test.txt"; // 文件名
            int count = 10000000; // 随机整数的数量
    
            try (BufferedWriter writer = new BufferedWriter(new FileWriter(filename))) {
                Random random = new Random();
    
                for (int i = 0; i < count; i++) {
                    int randomNumber = random.nextInt(count + 1); // 生成随机整数
                    writer.write(Integer.toString(randomNumber)); // 将随机整数写入文件
                    writer.newLine(); // 写入换行符
                }
    
                System.out.println("Random numbers have been written to " + filename);
            } catch (IOException e) {
                System.out.println("An error occurred while writing to the file: " + e.getMessage());
            }
        }
    

    向一个名为 “test.txt” 的文本文件写入向其中写入一千万个随机数,以测试排序性能。重复使用相同的测试样例,避免因每次生成数据不一致导致的误差。

  2. 编写排序算法代码

    插入排序

    	/**
         * 使用插入排序对给定数组进行排序
         *
         * @param array 要排序的数组
         * @param <T>   实现了 Comparable 接口的泛型类型
         */
        public static <T extends Comparable<? super T>> void insertionSort(T[] array) {
            // 从第二个元素开始遍历数组
            for (int i = 1; i < array.length; i++) {
                // 将当前元素存储在临时变量 key 中
                T key = array[i];
    
                // 向前比较并移动比 key 大的元素
                int j = i - 1;
                while (j >= 0 && array[j].compareTo(key) > 0) {
                    array[j + 1] = array[j];
                    j--;
                }
    
                // 将 key 插入到合适的位置
                array[j + 1] = key;
            }
        }
    

    归并排序

        /**
         * 使用归并排序对给定数组进行排序
         *
         * @param array 要排序的数组
         * @param <T>   实现了 Comparable 接口的泛型类型
         */
        public static <T extends Comparable<? super T>> void mergeSort(T[] array) {
            // 如果数组长度小于等于1,则已经有序,直接返回
            if (array.length <= 1) return;
    
            // 求中间位置的索引
            int mid = array.length / 2;
    
            // 将原数组分割为左右两个子数组
            T[] leftArray = Arrays.copyOfRange(array, 0, mid);
            T[] rightArray = Arrays.copyOfRange(array, mid, array.length);
    
            // 递归调用归并排序对左右子数组进行排序
            mergeSort(leftArray);
            mergeSort(rightArray);
    
            // 将排好序的左右子数组合并到原数组中
            merge(leftArray, rightArray, array);
        }
    
        /**
         * 将两个有序数组合并到目标数组中
         *
         * @param leftArray  左子数组
         * @param rightArray 右子数组
         * @param resultArray 目标数组
         * @param <T>        实现了 Comparable 接口的泛型类型
         */
        public static <T extends Comparable<? super T>> void merge(T[] leftArray, T[] rightArray, T[] resultArray) {
            int i = 0, j = 0, k = 0;
    
            // 比较左右子数组的元素,将较小的元素放入目标数组中
            while (i < leftArray.length && j < rightArray.length) {
                if (leftArray[i].compareTo(rightArray[j]) <= 0) {
                    resultArray[k++] = leftArray[i++];
                } else {
                    resultArray[k++] = rightArray[j++];
                }
            }
    
            // 将剩余的元素复制到目标数组中
            while (i < leftArray.length) {
                resultArray[k++] = leftArray[i++];
            }
            while (j < rightArray.length) {
                resultArray[k++] = rightArray[j++];
            }
        }
    

    用插入排序改进的归并排序(部分代码直接调用了插入排序和归并排序中已定义好的方法)

        /**
         * 使用归并排序对给定数组进行排序
         *
         * @param array 要排序的数组
         * @param <T>   实现了 Comparable 接口的泛型类型
         * @param k     数组大小小于k时开始使用插入排序
         */
        public static <T extends Comparable<? super T>> void combineSort(T[] array, int k) {
            // 如果数组长度小于等于 k,使用插入排序
            if (array.length <= k) {
                insertionSort(array);
                return;
            }
    
            // 将数组分为两半
            int mid = array.length / 2;
            T[] leftArray = Arrays.copyOfRange(array, 0, mid);
            T[] rightArray = Arrays.copyOfRange(array, mid, array.length);
    
            // 递归地对左右两部分数组进行排序
            combineSort(leftArray, k);
            combineSort(rightArray, k);
    
            // 合并左右两个有序数组
            merge(leftArray, rightArray, array);
        }
    

三、数据收集

编写测试代码,获得实验结果
经过预测试发现,当数据规模小于十万数量级时,运行速度太快,耗时均为0ms或1ms,无法得到运行速度变化,所以我选取的数据规模n从100000开始。
当数据规模达到一亿数量级规模时,运行速度太慢,进行一次排序需要1.5min,很难测试不同k下的运行速度变化,所以我选取的数据规模n最大到10000000。
对于每个k值,进行十次排序取平均值,减小由于某次排序可能存在的异常情况或错误对结果的影响,提高数据的稳定性和可靠性。

public static void main(String[] args) {
//        createTestFile(); // 创建测试数据文件(已注释)
        try {
            PrintWriter writer = new PrintWriter(new FileWriter("result.txt"));
            //测试MergeSort效率,获得基准运行时间
          	for (int n = 100000; n <= 10000000; n *= 10) {
                Integer[] nums = TestFile.readTestFile(n); // 读取测试数据
                long runtime = 0;
                for(int i=0;i<10;i++){ // 进行十次排序并取平均运行时间
                    runtime += testMergeSort(nums);
                }
                writer.println("MergeSort: n=" + n + "  runtime:" + runtime/10 + "ms"); // 将结果写入文件
                System.out.println("MergeSort: n=" + n + "  runtime:" + runtime/10 + "ms"); // 在控制台输出结果
            }
         	  //测试不同数据规模和不同k取值下CombineSort效率
            for (int n = 100000; n <= 10000000; n *= 10) {
                Integer[] nums = TestFile.readTestFile(n); // 读取测试数据
                for (int k = 1; k <= 100; k++) { // 遍历不同的k值
                    long runtime = 0;
                    for(int i=0;i<10;i++){ // 进行十次排序并取平均运行时间
                        runtime += testCombineSort(nums,k);
                    }
                    writer.println("CombineSort: n=" + n + "  k=" + k + "  runtime:" + runtime/10 + "ms"); // 将结果写入文件
                    System.out.println("CombineSort: n=" + n + "  k=" + k + "  runtime:" + runtime/10 + "ms"); // 在控制台输出结果
                }
            }
            writer.close(); // 关闭文件写入流
        } catch (IOException e) { // 异常处理
            System.out.println("Failed to write results to file. Error message: " + e.getMessage());
        }
        System.out.println("TEST FINISH"); // 输出测试结束信息
    }

    public static <T extends Comparable<? super T>> long testCombineSort(T[] arr, int k) {
        long startTime = System.currentTimeMillis();
        combineSort(arr,k); // 合并排序
        long endTime = System.currentTimeMillis();

        return endTime - startTime; // 返回运行时间
    }

    public static <T extends Comparable<? super T>> long testMergeSort(T[] arr) {
        long startTime = System.currentTimeMillis();
        mergeSort(arr); // 归并排序
        long endTime = System.currentTimeMillis();

        return endTime - startTime; // 返回运行时间
    }
	
		/**
     * 从测试文件中读取指定数量的整数
     *
     * @param n 要读取的整数数量
     * @return 包含读取整数的数组
     */
    public static Integer[] readTestFile(int n){
        String filename = "test.txt"; // 文件名
        Integer[] arr = new Integer[n]; // 存放读取整数的数组
        try (BufferedReader reader = new BufferedReader(new FileReader(filename))) {
            List<Integer> numbers = new ArrayList<>();

            String line;
            while ((line = reader.readLine()) != null && numbers.size() < n) {
                int number = Integer.parseInt(line); // 将字符串转换为整数
                numbers.add(number); // 将整数添加到列表中
            }
            numbers.toArray(arr); // 将列表转换为数组

        } catch (IOException e) {
            System.out.println("An error occurred while reading the file: " + e.getMessage());
        }
        return arr;
    }

结果导出到result.txt中,将其转换到result.xlsx中,方便后续分析数据。

四、结果与讨论

  1. 理论分析
    归并排序的时间复杂度为 O ( n log ⁡ 2 n ) O(n\log_{2}n) O(nlog2n)
    插入排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2)
    当待排数组的规模足够大时,归并排序比插入排序更快。但归并排序为递归算法,存在堆栈调用的额外开销,而插入排序为原地排序,所以当数组长度比较小时,插入排序的常量因子比较小,排序速度比归并排序更快。
    因此,我使用插入排序对归并排序进行优化,自顶向下,指定需要使用插入排序的数组大小k,当递归排序的数组大小小于等于k时,不再递归调用归并排序,而是直接调用插入排序。
    自此,该问题形成一个递归树,若是完全的归并排序,递归树的高度为 O ( log ⁡ 2 n + 1 ) O(\log_{2}n+1) O(log2n+1)
    而在我的算法中当子问题长度小于等于指定长度k时,不再递归,此时递归树的高度为 O ( log ⁡ 2 ( n / k ) + 1 ) O(\log_{2}(n/k)+1) O(log2(n/k)+1)
    归并算法的总时间为 O ( n log ⁡ 2 ( n / k ) O(n\log_{2}(n/k) O(nlog2(n/k)需要执行插入排序的子数组有 n / k n/k n/k个,每个子数组长度为 k k k,所以插入排序的总时间为 n / k ∗ O ( k 2 ) = O ( n k ) n/k*O(k^2)=O(nk) n/kO(k2)=O(nk)
    总的时间复杂度为 O ( n k + n log ⁡ 2 ( n / k ) ) O(nk+n\log_{2}(n/k)) O(nk+nlog2(n/k))

    接下来讨论 k k k的理论最大值,当 k k k趋近1时,时间复杂度趋近归并排序的复杂度 O ( n log ⁡ 2 n ) O(n\log_{2}n) O(nlog2n),当 k k k趋近于 n n n时,时间复杂度趋近于插入排序的时间复杂度 O ( n 2 ) O(n^2) O(n2) 。把 k k k作为 n n n的函数,观察到当 k k k log ⁡ 2 n \log_{2}n log2n时,代入 O ( n k + n log ⁡ 2 ( n / k ) ) O(nk+n\log_{2}(n/k)) O(nk+nlog2(n/k))得:
    O ( n log ⁡ 2 n + n log ⁡ 2 ( n / log ⁡ 2 n ) ) O(n\log_{2}n + n\log_{2}(n/\log_{2}n)) O(nlog2n+nlog2(n/log2n)) 化简后得: O ( 2 n log ⁡ 2 n + n log ⁡ 2 ( l o g 2 n ) ) O(2n\log_{2}n + n\log_{2}(log_{2}n)) O(2nlog2n+nlog2(log2n))
    其中 O ( n log ⁡ 2 ( l o g 2 n ) ) O(n\log_{2}(log_{2}n)) O(nlog2(log2n))部分的时间复杂度相对较小,通常不会影响整体的时间复杂度。因此可以近似表示为 O ( n log ⁡ 2 n ) O(n\log_{2}n) O(nlog2n)
    即归并排序复杂度,所以可以得出当 k k k 等于 log ⁡ 2 n \log_{2}n log2n 时整个算法的复杂度趋近于归并排序复杂度,因此 k k k 理论最大值为 log ⁡ 2 n \log_{2}n log2n
    当n=100000时k的理论最大值为16.6
    当n=1000000时k的理论最大值为19.9
    当n=10000000时k的理论最大值为23.3

  2. 实验结果

    在Matlab中使用result.xlsx的数据画图

    % n=100000 
    % 读取 Excel 文件
    [num, txt, raw] = xlsread('result.xlsx');
    
    % 提取数据
    k = num(1:100, 1);  % 提取 "k" 列数据
    runtime = num(1:100, 2);  % 提取 "runtime" 列数据
    
    % 绘制散点图
    scatter(k, runtime, 'filled');
    
    % 添加未优化时的参考线
    hold on;
    y_ref = 8;
    ref_line = line([min(k), max(k)], [y_ref, y_ref], 'Color', 'red');
    hold off;
    
    % 添加标题、轴标签和网格线
    title('CombineSort Efficiency (n=100000)');
    xlabel('k');
    ylabel('Runtime(ms)');
    grid on;
    

    n=1000000和n=10000000时同理,得到以下图像

     n=100000
     n=1000000
     n=10000000

    由图,其中红色线为单独使用归并排序的运行耗时,可以发现,使用插入排序对归并排序进行优化确实可以使算法的运行效率变快。分析散点图,n=100000时,k=67~74时算法运行效率较高;n=1000000时,k=76的算法效率最高;n=10000000时,k=63的算法效率最高。
    但是实验获得的k大于理论k值,分析是因为归并排序存在堆栈调用的额外开销,而现代处理器在执行原地排序的算法时效率更快,所以最终实验得到的k值大于理论值。

    在图中也会发现在每次增大数据规模时,前几次排序耗时总是高于平均耗时,推测是因为CPU对于一定规模的数据有实时的对应优化,导致增大数据规模时前几次排序效率较低。另外,观察硬件设备活动监视器发现在排序算法运行过程中CPU负载普遍在30%左右,且由于系统资源管理的特性,CPU频率并不稳定。同时排序算法的执行过程中可能会对内存进行读操作,内存的读写速度出现波动或延迟也会影响最终的排序耗时,最终表现为排序算法的耗时并不稳定,没有一个十分明显的根据k的变化而变化的趋势。

五、结论

理论分析和实验结果表明,通过对归并排序进行优化,使用插入排序来处理较小的子数组,可以进一步提高排序算法的效率。优化后的算法的时间复杂度为 O ( n k + n log ⁡ 2 ( n / k ) ) O(nk+n\log_{2}(n/k)) O(nk+nlog2(n/k))其中 k k k为指定的阈值。理论上, k k k的最大值为 log ⁡ 2 n \log_{2}n log2n,即当 k k k等于 log ⁡ 2 n \log_{2}n log2n时,算法的复杂度趋近于归并排序的复杂度。
实验结果显示,在不同数据规模下,选择适当的阈值 k k k可以使排序算法的运行效率最高。然而,排序耗时受到多种因素的影响,包括CPU优化、内存读写速度等,因此排序耗时可能会出现波动和不稳定的情况。
综上所述,通过合理选择阈值k,优化后的归并排序算法可以在一定程度上提高排序效率。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值