归并排序及优化详解

  本系列文章共有十一篇:
    冒泡排序及优化详解
    快速排序及优化详解
    插入排序及优化详解
    希尔排序及优化详解
    选择排序及优化详解
    归并排序及优化详解
    堆排序详解
    计数排序及优化详解
    桶排序详解
    基数排序及优化详解
    十大排序算法的比较与性能分析

一、归并排序基础

  归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
  归并排序是一种稳定的排序方法

1.1 归并排序图示

  归并排序的步骤:
   1>创建临时数组,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;
   2>设定两个指针,最初位置分别为两个已经排序序列的起始位置;
   3>比较两个指针所指向的元素,选择相对小的元素放入到合并空间,并移动指针到下一位置;
   4>重复步骤 3 直到某一指针达到序列尾;
   5>将另一序列剩下的所有元素直接复制到合并序列尾。
  此处引用网上一张比较经典的gif来展示归并排序的整个过程:

  如果上面的图,不够直观的话,可以看下面的这张图:

1.2 归并排序实现

  将上述过程用代码表示,示例如下:

public class MergeSort {
	public static void main(String[] args) { 
		int[ ] array = new int[ ]{5,2,3,9,4,7};
		int[ ] temp = new int[6];
		/*借助temp数组对array数组中的0 - array.length-1位置的元素进行排序*/
		mergeSort(array,0,array.length-1,temp);
		for(int i = 0;i<array.length;i++)
			System.out.print(array[i]+" "); 
	}
	
    static void mergeSort(int arr[],int start,int end,int temp[]){
	     if(start<end){
		     int mid = (start+end)/2;
		     /*拆分左右序列*/
		     mergeSort(arr,start,mid,temp);	 
		     mergeSort(arr,mid+1,end,temp);	 
		     /*将每个拆分的序列进行合并*/
		     mergeArray(arr,start,mid,end,temp);
	     }
    }

    /*将两个数组归并排序*/ 
	static void mergeArray(int arr[],int start,int mid,int end,int temp[])	{
		/*将每个子序列,即arr[start] - arr[end],拆分成左右两部分:下标i到m为左部分,
		 *下标j到n为右部分
		 */
		int i = start,j = mid+1;
		int m = mid,n = end;
		/*k代表临时数组元素的下标*/
		int k = 0;
		/*左右两个子序列都有元素*/
		while(i<=m && j<=n){
			/*左右,哪个子序列的数据小,就放入临时数组temp*/
			if(arr[i]<arr[j])
				temp[k++] = arr[i++];
			else
				temp[k++] = arr[j++];
		}
		/*左边序列还有数据,直接追加到temp数组中*/
		while(i<=m)
			temp[k++] = arr[i++];
		/*右边序列还有数据,直接追加到temp数组中*/
		while(j<=n)
			temp[k++] = arr[j++];
		/*将临时数组temp的元素追加在原数组arr中*/
		for(i=0;i<k;i++)
			arr[start+i] = temp[i];
	}
}

二、归并排序优化

  归并排序常用的优化方式有3种:
   1>对于小数组可以使用插入排序或者选择排序,避免递归调用。
   2>在合并两个子数组之前,可以比较左边子数组的最后一个值是否小于右边子数组的第一个值,如果小于,则不用再逐个比较左右子数组的元素,因为左右子数组都已有序,直接合并就行。
   3>为了节省将元素复制到辅助数组作用的时间,可以在递归调用的每个层次交换原始数组与辅助数组的角色。

2.1 小数组使用插入排序

  该种优化方式比较容易理解,实现方式为在mergeArray方法的开始位置,加入对子数组的数量判断即可,示例代码如下:

		/*优化方式1:对于小序列,使用插入排序,比如小序列数量<=7时*/ 
		if((end-start) >= 7){
			insertionSort(arr,end-start);
		}

2.2 避免多余比较

  第二种优化,针对的是某一子数组已经有序,并且完全大于(小于)另一子数组,在本次合并时,则不需要再对该有序子序列中的元素挨个进行比较的情况,此时,直接返回即可。
  我们可以先将初始的数组改成{4,2,3,5,7,9},然后在mergeArray的while循环中,加入一些语句,打印一下当时的子数组中的元素,示例如下:

		while(i<=m && j<=n){
			System.out.print("arr["+i+"]:"+arr[i]+",arr["+j+"]:"+arr[j]); 
			System.out.println(); 
			/*左右,哪个子序列的数据小,就放入临时数组temp*/
			if(arr[i]<arr[j])
				temp[k++] = arr[i++];
			else
				temp[k++] = arr[j++];
		}

  此时,打印结果如下:

arr[0]:4,arr[1]:2
arr[0]:2,arr[2]:3
arr[1]:4,arr[2]:3
arr[3]:5,arr[4]:7
arr[3]:5,arr[5]:9
arr[4]:7,arr[5]:9
arr[0]:2,arr[3]:5
arr[1]:3,arr[3]:5
arr[2]:4,arr[3]:5

  从上面的打印可以看出,数组的后三个元素组成的子数组是{5,7,9},即有序后,还是在和前面的{2,3,4}挨个比较,这个过程完全没用必要,可以规避掉。这也是此种优化方式的目的,示例代码如下:

	public static void main(String[] args) { 
		int[ ] array = new int[ ]{4,2,3,5,7,9};
		int[ ] temp = new int[6];
		/*借助temp数组对array数组中的0 - array.length-1位置的元素进行排序*/
		mergeSort(array,0,array.length-1,temp);
		System.out.println("排序后的结果:"); 
		for(int i = 0;i<array.length;i++)
			System.out.print(array[i]+" "); 
	}
	
    static void mergeSort(int arr[],int start,int end,int temp[]){
	     if(start<end){
		     int mid = (start+end)/2;
		     /*拆分左右序列*/
		     mergeSort(arr,start,mid,temp);	 
		     mergeSort(arr,mid+1,end,temp);	 
		     /*将每个拆分的序列进行合并*/
		     mergeArray(arr,start,mid,end,temp);
	     }
    }
    
	
    /*将两个数组归并排序*/ 
	static void mergeArray(int arr[],int start,int mid,int end,int temp[])	{		
		/*将每个子序列,即arr[start] - arr[end],拆分成左右两部分:下标i到m为左部分,
		 *下标j到n为右部分
		 */
		int i = start,j = mid+1;
		int m = mid,n = end;
		/*k代表临时数组元素的下标*/
		int k = 0;
		if(arr[mid]<arr[mid+1]){
			return;
		}
		/*左右两个子序列都有元素*/
		while(i<=m && j<=n){
			System.out.print("arr["+i+"]:"+arr[i]+",arr["+j+"]:"+arr[j]); 
			System.out.println(); 
			/*左右,哪个子序列的数据小,就放入临时数组temp*/
			if(arr[i]<arr[j])
				temp[k++] = arr[i++];
			else
				temp[k++] = arr[j++];
		}
		/*左边序列还有数据,直接追加到temp数组中*/
		while(i<=m)
			temp[k++] = arr[i++];
		/*右边序列还有数据,直接追加到temp数组中*/
		while(j<=n)
			temp[k++] = arr[j++];
		/*将临时数组temp的元素追加在原数组arr中*/
		for(i=0;i<k;i++)
			arr[start+i] = temp[i];
	}

  测试结果如下:

arr[0]:4,arr[1]:2
arr[0]:2,arr[2]:3
arr[1]:4,arr[2]:3
排序后的结果:
2 3 4 5 7 9

2.3 节省元素拷贝时间

  此种优化,是想节省在原数组和辅助数组之间拷贝元素的时间,做法是:先克隆原数组到辅助数组,然后对克隆出来的辅助数组进行拆,拆完后合并时,替换原有数组中对应位置的元素,示例代码如下:

	public static void main(String[] args) { 
		int[ ] array = new int[ ]{4,2,3,5,7,9,8};
		 /*拷贝一个和a所有元素相同的辅助数组*/
		int[] arrTemp = array.clone();
		sort(array,arrTemp,0,array.length-1);
		System.out.println("排序后的结果:"); 
		for(int i = 0;i<array.length;i++)
			System.out.print(array[i]+" "); 
	}

	/*基于递归的归并排序算法*/
	static void sort (int a[], int temp[], int start,int end) {
		if(end > start){
	        int mid =  start+(end-start)/2;  
	        /*对左右子序列递归*/
	        sort(temp, a,start,mid);  
	        sort(temp, a,mid+1,end);  
	        /*合并左右子数组*/
	        merge(a, temp, start,mid,end);  
		}
	}
		 
	/*arr[low...high] 是待排序序列,其中arr[low...mid]和 a[mid+1...high]已有序*/
	static void merge (int arr[],int temp[],int start,int mid,int end) {
		/*左边子序列的头元素*/
		int i = start; 
		/*右边子序列的头元素*/
	    int j = mid+1;
	    
	    for(int k = start;k <= end;k++){
	      if(i>mid){
	    	  /*左边子序列元素用尽*/
	    	  arr[k] = temp[j++]; 
	      }else if(j>end){
	    	  /*右边子序列元素用尽*/
	    	  arr[k] = temp[i++]; 
	      }else if(temp[j]<temp[i]){
	    	  /*右边子序列当前元素小于左边子序列当前元素, 取右半边元素*/
	    	  arr[k] = temp[j++]; 
	      }else {
	    	  /*右边子序列当前元素大于等于左边子序列当前元素,取左半边元素*/
	    	  arr[k] = temp[i++]; 
	      }
	    }
    }

三、稳定性、复杂度及适用场景

3.1 稳定性

  归并排序是一种稳定的排序算法,因为在拆、合的过程中,如果多个元素相等,不用移动其相对位置,就可完成排序过程。

3.2 时间复杂度

  因为归并排序使用的是分治的思想,所以其时间复杂度为O(nlogn)。

3.3 适用场景

  待排序序列中元素较多,并且要求较快的排序速度时。

猜你喜欢

转载自blog.csdn.net/m0_37741420/article/details/107057592