九大排序之归并排序--实现及注意点

归并排序是一个非常常用的排序算法,基础是对一个数组的两个已排序子数组的排序。
用归并排序对n个元素的数组进行排序时,当n为2的幂时,元素比较次数在(n log n)/2到n log n - n +1之间,元素被赋值次数为2n log n。时间复杂度O(n log 2n),速度仅次于快排,比较稳定。
现在分块来解释算法实现过程:
一、算法思想
归并排序的算法思想基于对一个数组的两个已排序子数组的排序–Merge。归并排序先将数组进行分割,直到每个子数组只有一个元素,这样就可以将相邻的两个子数组看成是两个已排序的数组,构成Merge算法的先决条件,就可以用Merge算法进行排序,构成一个长度翻倍的子数组。对整个数组进行一次小长度的Merge算法后,可以构成一个长度翻倍的Merge算法的条件而进行Merge算法,最终对整个数组实现排序。
二、基础–Merge
(一)Merge算法的前提:一个数组可以划分为两个已排序的子数组,如1 4 7 8 2 5 10,此数组可以划分为两个已排序的子数组:1 4 7 82 5 10
(二)Merge算法的思想:
1、定义一个临时数组,长度为两个子数组的长度之和。int[] B = new int[high - low]
2、从两个子数组的头部开始进行比较,将较小的元素放入临时数组

int s = 0,t = 0,i = 0;
int l1 = mid - low;//第一部分的长度
int l2 = high - mid;//第二部分的长度
//从两个数组的头开始比较,将较小值填入临时数组
while(s < l1 && t < l2){
    if(A[low + s] >= A[mid + t]){
        B[i] = A[mid + t];
        t ++;
    }
    else{
        B[i] = A[low + s];
        s ++;
    }
    i ++;
}

直到某一个数组全部放入临时数组,然后将另一个数组的剩余部分放入临时数组,构成一个长度与原数组相同,且已排序的数组

//某一数组全部填入临时数组之后,将另一个数组的余下部分填入临时数组
if(s != l1){
    for(;s < l1;s++){
        B[i] = A[low + s];
        i++;
    }
}
else{
    for(;t < l2;t++){
        B[i] = A[mid + t];
        i++;
    }
}

因两个子数组是已排序的,所以各选一个进行比较之后就可确定更小元素在排好序的数组中的位置,而无需考虑其他的问题。
三、分割数组
归并排序的第一步是对数组进行分割,只有分割到满足Merge算法的前提,才能使算法正确运行。
为了构成Merge算法的前提,所以必须分割到最小限度。
我用一个step来表示分割后的子数组长度,step是一个2的整数次幂,如:step:1->2->4->8->..
当step为1的子数组排序完之后,就自然构成了多个满足Merge算法前提的step为2的子数组,如此迭代即可完成算法。

//因归并排序的第一步是划分,步长一步步翻倍
//因待排序的数组的长度可能是奇数,而步长总是2的整数倍,故将step的上限定为数组长度的一半并向上取整,即c.length/2 + 1
while(step <= c.length/2 + 1){
    //以步长为基础,对原数组进行切分,分别排序
    for(int i = 0;i < c.length;i = i + step *2){
        //因数组长度可能为奇数,故可能最后存在不满步长的分割情况,需要单独判断
        if((i + step * 2) > c.length){
            merge(c,i,i+step,c.length);
            continue;       
        }
        //若不是上述情况,则正常分割
        merge(c,i,i+step,i+step * 2);
    }
    step = step * 2;//步长翻倍增长
}

此处有一个非常关键的问题,就是step的界限控制,及传递给Merge算法的参数控制问题。
(一)step的界限控制
step是用来控制分割的关键参数,因原数组的长度可能为奇数,而step总是2的整数次幂,所以若不进行区别控制,将会导致最后结果为一个可以分割成两个已排序的子数组的新数组,而没有进行最后的一步归并排序,原因就在于step的界限控制。
这里我将step的界限控制在step <= c.length/2 + 1即step的上限定为数组长度的一半并向上取整,这样即使存在奇数的原数组长度,也可进行完全的归并排序。
例子:
1、原数组:5 2 4,数组长度为3,是奇数,且3/2 + 1 = 2,即须排序2次。
验证:第一次排序:step = 15 2 =>2 5=>2 5 4;第二次排序:step = 22 5 4=>2 4 5,排序2次,结果正确。
若不向上取整,则3/2 = 1,只排序1次,所以结果必然是错误的。
2、原数组:5 2 4 6,数组长度为4,是偶数,且4/2+1 = 3,而step为2的整数次幂,所以排序次数为2次。
验证:第一次排序:step = 15 2 4 6 =>2 5 4 6;第二次排序:step = 22 5 4 6=>2 4 5 6,排序2次,结果正确。
在对所有元素进行一次归并后,需要将step翻倍,即step = step * 2
(二)传递给Merge算法的参数控制
1、参数解释
Merge算法的参数可以根据需求设置,此处我设置为4个参数merge(int[] A,int low,int mid,int high),A为原数组,low为在数组A中须排序的部分的最小位置,mid为两个已排序的子数组的分割,high为在数组A中须排序的部分的最大位置。如2 5 4 6,在次数组中,low为0,mid为2,high为4。所以明显的,第一个子数组的长度为mid-low = 2,第二个子数组的长度为high-mid = 2,结果正确。
2、参数控制
因为原数组的长度可能为奇数,而step为2的幂,所以会存在第一次排序时,最后一个子数组没有归并对象,在之后的排序中,两边数组的长度不等的情况,若不加区别控制,则会造成数组越界的问题。
例子:
原数组:2 5 4
过程:
一次merge:step=1,第一部分2 5构成可用merge(c,i,i+step,i+step * 2)进行排序的数组,即merge(c,0,1,2),结果正确且未数组越界;第二部分4,没有归并对象,若用merge(c,i,i+step,i+step * 2)merge(c,2,3,4),明显数组越界。
所以需做如下控制:

if((i + step * 2) > c.length){
    merge(c,i,i+step,c.length);
    continue;       
}
//若不是上述情况,则正常分割
merge(c,i,i+step,i+step * 2);

在此处,我加上了区别控制。
当发现i + step * 2,即参数high超过了原数组的长度,则表明最后一个子数组不能满足两个子数组长度相等的情况,故不能用普遍的参数merge(c,i,i+step,i+step * 2)来处理,而需要将最后一个参数改为c.length,确保在后续操作时,不出现数组越界的情况。
Merge算法并不需要两个子数组的长度相等,所以这样不会造成算法的失败。
四、具体算法
附上我写的归并排序算法

public class BottomUpSort {
    int[] c;

    void merge(int[] A,int low,int mid,int high){
        //对一个数组的两部分已排序的部分进行排序
        int s = 0,t = 0,i = 0;
        int[] B = new int[high - low];//定义一个新数组
        int l1 = mid - low;//第一部分的长度
        int l2 = high - mid;//第二部分的长度
        //从两个数组的头开始比较,将较小值填入临时数组
        while(s < l1 && t < l2){
            if(A[low + s] >= A[mid + t]){
                B[i] = A[mid + t];
                t ++;
            }
            else{
                B[i] = A[low + s];
                s ++;
            }
            i ++;
        }
        //某一数组全部填入临时数组之后,将另一个数组的余下部分填入临时数组
        if(s != l1){
            for(;s < l1;s++){
                B[i] = A[low + s];
                i++;
            }
        }
        else{
            for(;t < l2;t++){
                B[i] = A[mid + t];
                i++;
            }
        }
        //将原数组被排序部分用临时数组替换
        for(i = 0;i < high - low;i++){
            A[low + i] = B[i];
        }
    }

    void combine(int step){
        //因归并排序的第一步是划分,步长一步步翻倍
        //因待排序的数组的长度可能是奇数,而步长总是2的整数倍,故将step的上限定为数组长度的一半并向上取整,即c.length/2 + 1
        while(step <= c.length/2 + 1){
            //以步长为基础,对原数组进行切分,分别排序
            for(int i = 0;i < c.length;i = i + step *2){
                //因数组长度可能为奇数,故可能最后存在不满步长的分割情况,需要单独判断
                if((i + step * 2) > c.length){
                    merge(c,i,i+step,c.length);
                    continue;       
                }
                //若不是上述情况,则正常分割
                merge(c,i,i+step,i+step * 2);
            }
            step = step * 2;//步长翻倍增长
        }
    }

    public static void main(String[] args){
        //初始化类及原数组
        BottomUpSort b = new BottomUpSort();
        b.c = new int[]{42,661,32,7,62,38,3};
        System.out.print("data: ");
        for(int i = 0;i < b.c.length;i++){
            System.out.print(b.c[i] + " ");
        }
        //进行归并排序
        b.combine(1);
        System.out.print("\nafter sort,result: ");
        for(int i = 0;i < b.c.length;i++){
            System.out.print(b.c[i] + " ");
        }
    }
}

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值