归并排序超级细致讲解

归并排序概述:

归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。

归并操作实现思路

所谓归并操作就是将所有分割后的序列按照顺序合并一起。使用分治的思想解决问题。

归并操作的实现思路就是先将所有元素进行分割成子序列,直到不能在分割,再使每个子序列有序,再使子序列段间有序

将已有序的子序列合并,得到完全有序的序列

所有合并操作完成后便实现了归并排序。

例如:

归并操作步骤图
该图片可以很清楚的看出,先将无规则元素全部进行分割,直到形成单个元素。

此时开始合并,先是合并两个元素,确保两个元素有序

合并4个元素,确保4个元素有序

重复合并操作直到完成所有所有元素的合并,解决问题。

因此其实现步骤总结有三步:

  1. 分割所有元素,直到元素成单个分布
  2. 从单个开始合并元素,合并的过程确保合并元素有序
  3. 重复第二步操作,直到所有元素合并完成

总的来说,归并排序的思路是先分后合

代码实现:

在分析图中可以很清晰看出来,用递归的做法就能解决本分割后再回来合并的元素标号问题。

因此采用递归的思路来解决问题。

首先明确实现需要走的三个步骤:

  1. 分割所有元素,直到元素成单个分布
  2. 从单个开始合并元素,合并的过程确保合并元素有序
  3. 重复第二步操作,直到所有元素合并完成

我们根据以上步骤一步一步来实现代码:

1 . 分割所有元素,直到元素成单个分布

	//通过递归的方式来分割数组,分割到元素成单个后不再分割
	//这里我们用两个下标变量来定义元素区间的左右边界,当元素区间左右边界重合时说明只有一个元素
    public static void mergeSort1(int beginIndex,int endIndex){
    
    		
        if (beginIndex == endIndex){
    
    		//左右区间重合,说明只有一个元素,此时退出递归
            return ;
        }
        int mid = (beginIndex + endIndex) / 2;		//分割区间,用每次分割区区间的中点为分隔线
        int rightIndex = mid + 1;					//记录右边区间开始边界,这里将中间分割线向右移动一个下标,来定义右边区间开始线,中间分割线作为左边区间的结束线
        mergeSort1(beginIndex ,mid );			//开始分割左边
        mergeSort1(rightIndex,endIndex);		//开始分割右边
    }

2 . 从单个开始合并元素,合并的过程确保合并元素有序

由于递归的特点,在分割完后会退回到上一次分割的位置,因此在执行完所有分割操作后接下来的代码就是合并操作的代码,也就是从单个开始合并元素的代码。

对于合并操作基本思路是:

  1. 建立一个新数组,用来合并归并后排序的元素。

  2. 由于此时返回的子序列已经有序了,因此从两个子序列的第一个元素开始遍历,寻找符合要求的元素,并且放入新数组中。

例如:

归并操作图例
此时合并的子序列如图。

假如我们要构建升序序列,此时从两个子序列的首个元素开始遍历寻找符合要求的元素。

首个元素开始遍历
此时符合要求的第一个元素就是最大的元素,因此将4元素放入新数组。

插入新数组
此后再将4的遍历指针向后移,重复此操作,若出现了某个序列遍历完了之后,将另一个序列直接放入新数组的末端。
遍历3

没有了
此时右边已经遍历完成,将左边的所有剩余元素放到新数组的最后:

完成
合并完成后,再将新数组的值复制到原来的数组的对应位置上

直接上代码:

    public static void mergeSort(int beginIndex, int endIndex) {
    
    
        if (beginIndex == endIndex) {
    
    
            return;
        }
        int resIndex = beginIndex;				//定义一个开始标志,用于在合并的最后记录新数组应该复制的位置
        int length = endIndex - beginIndex + 1;
        int mid = (beginIndex + endIndex) / 2;
        int rightIndex = mid + 1;
        mergeSort(beginIndex, mid);
        mergeSort(rightIndex, endIndex);
/*	以上是分割的代码
*	-------------------------------------------------------------------------------------
*	以下是合并的代码
*/      
        boolean flag = false;					//定义一个标志位,用于区分哪个区域元素先遍历完
        int tempArr[] = new int[length];		//定义一个新数组用于存放
        int tempIndex = 0;						//定义一个临时下表用于新数组存放的元素位置
        while (beginIndex <= mid) {
    
    				//开始遍历
            if (arr[beginIndex] > arr[rightIndex]) {
    
    	//如果左边元素大于右边元素,左边元素放入新数组并且向后移动
                tempArr[tempIndex++] = arr[beginIndex++];
            } else {
    
    							//如果右边元素大于左边元素,右边元素放入新数组并且向后移动
                if (rightIndex + 1 <= endIndex) {
    
    			//做一个边界处理
                    tempArr[tempIndex++] = arr[rightIndex++];
                } else {
    
    									//若到达边界,标志位设置为true,此时标志着右边遍历先结束
                    tempArr[tempIndex++] = arr[rightIndex++];
                    flag = true;        //若此时右边先遍历完,将标志位设置一下
                    break;
                }
            }
        }
        if (flag) {
    
    					//根据标志位信息,将剩余的区域元素放到新数组最后
            while (beginIndex <= mid) {
    
    
                tempArr[tempIndex++] = arr[beginIndex++];
            }

        } else {
    
    
            while (rightIndex <= endIndex) {
    
    
                tempArr[tempIndex++] = arr[rightIndex++];
            }
        }
        for (int i = 0; i < tempIndex; i++) {
    
    			//将新数组复制到合并的区间中
            arr[resIndex++] = tempArr[i];
        }
    }

由于遍历的特点,经过以上代码后,合并便完成,不再赘述。

测试代码:

    public static void main(String[] args) {
    
    
        int arr[] = {
    
    1, 23, 456, 48, 897, 456, 12, 456, 789, 165, 32132, 456, 897, 156, 56, 41, 2, 4, 6, 8, 7};
        mergeSort(arr,0, arr.length - 1);
        System.out.println(Arrays.toString(arr));
    }

运行结果:

运行结果

复杂度:

时间复杂度(平均) 时间复杂度(最坏) 时间复杂度(最好) 空间复杂度 是否稳定
O(n Log n) O(n Log n) O(n Log n) O(n)

如果觉得讲的不错,欢迎点赞支持!

Guess you like

Origin blog.csdn.net/Nimrod__/article/details/114266243