(五) 数据结构 - 归并排序

归并排序

归并排序是一种基于分而治之的排序技术。最坏情况下的时间复杂度为O(nlogn),它是最受人尊敬的算法之一。归并排序首先将数组分成相等的两半,然后以排序的方式将它们合并。

核心思想

为了理解合并排序,我们采用未排序的数组,如下所示

我们知道归并排序首先将整个数组迭代地分成相等的一半,除非获得原子值。我们在这里看到一个由8个项目组成的数组分为两个大小为4的数组。

这不会更改原件中项目出现的顺序。现在我们将这两个数组分为两半。

我们进一步划分这些数组,并获得无法再划分的原子值

现在,我们将它们分解时的方式完全相同。请注意提供给这些列表的颜色代码。

我们首先比较每个列表的元素,然后以排序的方式将它们组合到另一个列表中。我们看到14和33处于排序位置。我们比较27和10,在2个值的目标列表中,我们先放置10,然后是27。我们更改19和35的顺序,而将42和44顺序放置。

在合并阶段的下一个迭代中,我们比较两个数据值的列表,然后将它们合并为找到的数据值的列表,将所有数据按排序顺序放置。

最终合并后,列表应如下所示:

代码开发

实现思路

归并排序会继续将列表分为相等的一半,直到无法再对其进行划分为止。根据定义,如果它只是列表中的一个元素,则会对其进行排序。然后,合并排序将合并较小的排序列表,同时也将新列表排序。

Step 1−将列表递归分为两半,直到无法再将其划分为止。
Step 2−将若干个组两两合并,保证合并后的序列有序。
step 3-重复第二步操作直到只剩下一组,排序完成。

伪代码

package lipan.top.notes.basicsort.impl;

import lipan.top.notes.basicsort.ISort;

import java.util.Arrays;

/**
 * <p/>
 * <li>title: 基础排序-归并排序</li>
 * <li>@author: li.pan</li>
 * <li>Date: 2019/12/7 12:15 下午</li>
 * <li>Version: V1.0</li>
 * <li>Description: </li>
 */
public class MergeSort implements ISort<Integer> {


    @Override
    public void sort(Integer[] arr) {
        int n = arr.length;
        sort(arr, 0, n - 1);
    }

    //递归使用归并排序,对arr[l....r]的范围进行进行排序 0,10 0/5 0/2
    private static void sort(Integer[] arr, int l, int r) {

        System.out.println("sort: arr=" + Arrays.toString(arr) + ",l=" + l + ",r=" + r);
        if (l >= r) { //当子序列中只有一个元素递归到底的情况
            System.out.println("------------------------------------------------------------------------------------------------");
            return;
        }

        int mid = (l + r) / 2;  // l=0 r=2
        sort(arr, l, mid);
        sort(arr, mid + 1, r);
        merge(arr, l, mid, r);

    }

    //将arr[l...mid]和arr[mid+1...r]两部分进行归并
    private static void merge(Integer[] arr, int l, int mid, int r) {
        System.out.println("merge: arr=" + Arrays.toString(arr) + ",l=" + l + ",mid=" + mid + ",r=" + r);

        // 开辟临时空间,合并左半部分已经排好序的数组和右半部分已经排好序的数组
        Integer[] aux = Arrays.copyOfRange(arr, l, r + 1);
        System.out.println("merge: arr=" + Arrays.toString(aux) + ",l=" + l + ",mid=" + mid + ",r=" + r);

        // 初始化,i指向左半部分的起始位置索引;j指向右半部分起始索引位置mid+1
        int i = l, j = mid + 1;
        for (int k = l; k <= r; k++) {  // k指向两个元素比较后归并下一个需要放置的位置
            System.out.println("第K=" + k + "趟");

            /**
             * 考虑数组越界
             */
            if (i > mid) {  // 如果左半部分元素已经全部处理完毕,
                arr[k] = aux[j - l];
                j++;
            } else if (j > r) {   // 如果右半部分元素已经全部处理完毕
                arr[k] = aux[i - l];
                i++; // l表示偏移
            }
            /**
             * 真正比较: 两两比较放入新的序列arr
             */
            else if (aux[i - l] < aux[j - l]) {  // 左半部分所指元素 < 右半部分所指元素
                arr[k] = aux[i - l];
                i++;
            } else {  // 左半部分所指元素 >= 右半部分所指元素(右边的放置待排序前面)
                arr[k] = aux[j - l];
                j++;
            }
        }
        System.out.println("\n");
    }

}

执行结果

------------------------------随机数组--------------------------------
插入排序测试1数据量为100排序时长为: 0.001s
插入排序测试2数据量为10000排序时长为: 0.084s
插入排序测试3数据量为100000排序时长为: 5.868s
冒泡排序测试1数据量为100排序时长为: 0.0s
冒泡排序测试2数据量为10000排序时长为: 0.061s
冒泡排序测试3数据量为100000排序时长为: 9.069s
希尔排序测试1数据量为100排序时长为: 0.0s
希尔排序测试2数据量为10000排序时长为: 0.004s
希尔排序测试3数据量为100000排序时长为: 0.008s

代码优化

  • 虽然归并排序是nlogn级别的算法, 但是在数组数据量比较小的时候, 插入排序的效率仍然是高于归并排序的, 所以可以在对数组分解到足够小之后, 使用插入排序, 然后再递归进行归并排序。

  • 如果一个数组是近乎有序的, 或者说是完全有序的, 上述步骤会有很多无用的merge操作, 所以可以在进行merge前增加一个判断, 效率也会有一定的提高。

伪代码

package lipan.top.notes.basicsort.impl.optimize;


import lipan.top.notes.basicsort.ISort;

import java.util.Arrays;

/**
 * <p/>
 * <li>title: 归并排序优化</li>
 * <li>@author: li.pan</li>
 * <li>Date: 2019/12/7 12:15 下午</li>
 * <li>Version: V1.0</li>
 * <li>Description:
 * 优化1: 对于arr[mid] <= arr[mid+1]的情况,不进行merge
 * 优化2: 对于小规模数组, 使用插入排序
 * </li>
 */
public class MergeSortOptimize implements ISort<Integer> {


    @Override
    public void sort(Integer[] arr) {
        int n = arr.length;
        sort(arr, 0, n - 1);
    }

    //递归使用归并排序,对arr[l....r]的范围进行进行排序
    private static void sort(Integer[] arr, int l, int r) {

        /**
         *  优化2: 对于小规模数组, 使用插入排序
         */
        if (r - l <= 15) {
            insertionSort(arr, l, r);
            return;
        }

        int mid = (l + r) / 2;
        sort(arr, l, mid);
        sort(arr, mid + 1, r);

        /**
         * 优化1: 对于arr[mid] <= arr[mid+1]的情况,不进行merge
         * 对于近乎有序的数组非常有效,但是对于一般情况,有一定的性能损失
         */
        if (arr[mid] > arr[mid + 1])
            merge(arr, l, mid, r);

    }

    //将arr[l...mid]和arr[mid+1...r]两部分进行归并
    private static void merge(Integer[] arr, int l, int mid, int r) {

        // 开辟临时空间,合并左半部分已经排好序的数组和右半部分已经排好序的数组
        Integer[] aux = Arrays.copyOfRange(arr, l, r + 1);

        // 初始化,i指向左半部分的起始位置索引;j指向右半部分起始索引位置mid+1
        int i = l, j = mid + 1;
        for (int k = l; k <= r; k++) {  // k指向两个元素比较后归并下一个需要放置的位置

            /**
             * 考虑数组越界
             */
            if (i > mid) {  // 如果左半部分元素已经全部处理完毕,
                arr[k] = aux[j - l];
                j++;
            } else if (j > r) {   // 如果右半部分元素已经全部处理完毕
                arr[k] = aux[i - l];
                i++; // l表示偏移
            }
            /**
             * 真正比较
             */
            else if (aux[i - l] < aux[j - l]) {  // 左半部分所指元素 < 右半部分所指元素
                arr[k] = aux[i - l];
                i++;
            } else {  // 左半部分所指元素 >= 右半部分所指元素
                arr[k] = aux[j - l];
                j++;
            }
        }
    }

    // 对arr[l...r]的区间使用InsertionSort排序
    public static void insertionSort(Integer[] arr, int l, int r) {

        for (int i = l + 1; i <= r; i++) {
            Integer e = arr[i];
            int j = i;
            for (; j > l && arr[j - 1] > e; j--)
                arr[j] = arr[j - 1];
            arr[j] = e;
        }
    }

}

执行结果

------------------------------随机数组--------------------------------
插入排序测试1数据量为100排序时长为: 0.001s
插入排序测试2数据量为10000排序时长为: 0.12s
插入排序测试3数据量为100000排序时长为: 8.578s
归并排序测试1数据量为100排序时长为: 0.0s
归并排序测试2数据量为10000排序时长为: 0.013s
归并排序测试3数据量为100000排序时长为: 0.025s
归并排序优化测试1数据量为100排序时长为: 0.0s
归并排序优化测试2数据量为10000排序时长为: 0.001s
归并排序优化测试3数据量为100000排序时长为: 0.003s
------------------------------近乎有序数组--------------------------------
插入排序测试1数据量为100排序时长为: 0.0s
插入排序测试2数据量为10000排序时长为: 0.039s
插入排序测试3数据量为100000排序时长为: 2.764s
归并排序测试1数据量为100排序时长为: 0.0s
归并排序测试2数据量为10000排序时长为: 0.0s
归并排序测试3数据量为100000排序时长为: 0.009s
归并排序优化测试1数据量为100排序时长为: 0.0s
归并排序优化测试2数据量为10000排序时长为: 0.0s
归并排序优化测试3数据量为100000排序时长为: 0.0s

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值