八大排序算法--归并排序及其优化

归并排序定义

时间复杂度 O(NlogN)
归并排序的整体思想就是把数据 一分为二,然后将两部分数据分别排序后,再合并到一起的过程。可以用递归完成这个过程,看图理解:
在这里插入图片描述

每次将数据分成a/b两组,两组数据分别排序。对a排序时,依然使用归并排序,于是继续将a分成两组,直到每组数据都只有一个时,一个数据本身就是有序的,不需要再排序,然后将讲个数据合并到一起,小的数排前面,大的排后面。不断合并,最后数据就变成有序的了。

这个过程使用了额外的空间,空间复杂度也为O(NlogN)。但是现在,速度往往比空间重要。

代码实现

/**
 * 归并排序
 * 开始实现代码前先定义好边界问题,这里我们采用前闭后闭的结构,对数组的[left, right] 这个范围排序
 * 由于递归排序每次分成两半,这是一个天然递归的结构,所以可以采用递归的方式进行排序,逻辑上会比较简单
 * @param arr 数据集合
 * @param left 要排序数据的左边界
 * @param right 右边界
 */
public static void mergeSort(int[] arr, int left, int right) {
	
	if(left >= right) {
		//左边界已经大于右边界,说明此事已经遍历完成,应该停止递归
		//要注意,采用递归,就一定需要定义好停止递归的条件,不能无限递归
		return ;
	}
	
	int mid = (right + left) / 2;//得到中间的位置
	mergeSort(arr, left, mid); //先对左半部分递归排序
	mergeSort(arr, mid+1, right);//再对右半部分进行递归排序
	merge(arr, left, mid, right);//然后重新合并为一个有序数据集
}

private static void merge(int[] arr, int left, int mid, int right) {
	//从示例图中我们看到,归并的过程中,需要开辟新的空间来放数组,因此先定义一个我们需要的数组
	int[] temp = new int[right - left + 1];
	for(int i=left; i<=right; i++) {
		temp[i-left] = arr[i]; //把arr [left , right] 这个范围的值赋值给temp
	}
	int i = left;
	int j = mid + 1;
	for(int k=left; k<=right; k++) {
		if(i > mid) {
			//当左边部分的指针已经大于中间位置时,说明左边部分已经全部遍历完了
			//此时只需要顺序遍历右边部分即可
			arr[k] = temp[j-left];
			j++;
		} else if(j > right) {
			//当右边部分的指针已经大于最大位置时,说明右边部分已经全部遍历完了
			//此时只需要顺序遍历左边部分即可
			arr[k] = temp[i - left];
			i++;
		} else if(temp[i-left] < temp[j-left]) {
			//比较左右部分哪一个的值比较小,先写入到原数组中,然后对应的左右部分指针后移一位
			arr[k] = temp[i-left];
			i++;
		} else {
			arr[k] = temp[j-left];
			j++;
		}
	}
}

归并排序的优化一

如果要排序的数据本身是接近有序的,或者换句话说,如果在归并过程中,数据已经有序,则不用再进行额外的归并操作了,优化如下:

public static void mergeSort(int[] arr, int left, int right) {
	
	if(left >= right) {
		return ;
	}
	
	int mid = (right + left) / 2;
	mergeSort(arr, left, mid);
	mergeSort(arr, mid+1, right);
	if(arr[mid+1] < arr[mid]) {
		//只有左右两边数据还是无序时,才需要merge
		merge(arr, left, mid, right);
	}
}

通过增加一个判断,就可能少调用mergeSortMerge方法,从而少了很多步操作。当然,因为增加了一个判断,也可能存在降低性能的可能,但是影响不大,特别是对于接近有序的数据,优化效果会特别明显。

归并排序的优化二

前面的代码,我们是在递归到最后一个数时,才返回的:
if(left >= right) {
return ;
}
这里可以在数据比较小时,就不使用递归排序了,通常数据较小时,插入排序会比较快,所以这里可以用插入排序来进行优化:
public static void mergeSort(int[] arr, int left, int right) {

	if(right - left <=15) {
		insertionSort(arr, l, r);//此时用插入排序
		return ;
	}
	
	int mid = (right + left) / 2;
	mergeSort(arr, left, mid);
	mergeSort(arr, mid+1, right);
	if(arr[mid+1] < arr[mid]) {
		//只有左右两边数据还是无序时,才需要merge
		merge(arr, left, mid, right);
	}
}

// 对arr[l...r]范围的数组进行插入排序
private void insertionSort(int[] arr, int l, int r){

    for( int i = l+1 ; i <= r ; i ++ ) {

        int[] e = arr[i];
        int j;
        for (j = i; j > l && arr[j-1] > e; j--)
            arr[j] = arr[j-1];
        arr[j] = e;
    }

    return;
}

自底向上排序

上述例子,我们是自顶向下进行排序的,对于归并排序,我们也可以使用自底向上的排序

public static void mergeSortBU(int[] arr, int n){

    for( int sz = 1; sz <= n ; sz += sz ) {
    	//注意定义好边界问题,不能超过边界n, 所以i +sz < n, Math.min(i+sz+sz-1,n-1)
        for( int i = 0 ; i +sz < n ; i += sz+sz ) {
            // 对 arr[i...i+sz-1] 和 arr[i+sz...i+2*sz-1] 进行归并
            merge(arr, i, i+sz-1, Math.min(i+sz+sz-1,n-1) );
        }
    }
}

从统计意义上将,自底向上排序性能比使用递归的自顶向下稍微差一点点。

另外,从代码中可以看到,自底向上没有使用数组的索引去获取元素,这很重要,因为这意味着链表也可以使用归并排序,只需要修改具体的归并过程即可(即修改merge方法);

自底向上的优化思想和前面两个类似,这里没有写代码了。

附上自动生成测试数组的代码

public static int[] randomArray(int length) {
	int[] arr = new int[length];
	Random random = new Random();
	for(int i=0; i<length; i++) {
		arr[i] = random.nextInt(100);
	}
	//自动生成随机数组,先进行一次原始数据打印
	System.out.println(Arrays.toString(arr));
	return arr;
}

http://www.niftyadmin.cn/n/1541354.html

相关文章

JAVA多线程之线程间的通信方式

线程间的通信方式 ①同步 这里讲的同步是指多个线程通过synchronized关键字这种方式来实现线程间的通信。 ②while轮询的方式 ③wait/notify机制 ④管道通信就是使用java.io.PipedInputStream 和 java.io.PipedOutputStream进行通信 几种进程间的通信方式 # 管道( pipe )&#…

八大排序算法--快速排序及其优化

快速排序定义 快速排序的基本思想是&#xff1a;通过一趟排序将要排序的数据分割成独立的两部分&#xff0c;其中一部分的所有数据都比另外一部分的所有数据都要小&#xff0c;然后再按此方法对这两部分数据分别进行快速排序&#xff0c;整个排序过程可以递归进行&#xff0c;…

Shell初学(一)hello world

精简&#xff1a; 1、创建&#xff1a;可以使用 vi/vim 命令来创建文件如&#xff1a; test.sh &#xff0c;扩展名并不影响脚本执行&#xff0c;写什么都可以。 2、hello_world&#xff1a; #!/bin/bash -------解释此脚本文件的 Shell 程序echo "Hello…

由归并排序和快速排序引申的思考

分治算法 归并排序和快速排序都使用了分支算法的思想。 分治算法&#xff1a;顾名思义&#xff0c;就是将原问题分割为同等结构的子问题&#xff0c;之后将子问题逐一解决后&#xff0c;在解决了各个小问题之后(各个击破之后)合并小问题的解&#xff0c;从而得到整个问题的解…

最值得阅读学习的 10 个 C 语言开源项目代码

1. Webbench Webbench是一个在linux下使用的非常简单的网站压测工具。它使用fork()模拟多个客户端同时访问我们设定的URL&#xff0c;测试网站在压力下工作的性能&#xff0c;最多可以模拟3万个并发连接去测试网站的负载能力。Webbench使用C语言编写, 代码实在太简洁&#xff0…

八大排序算法--堆排序

序言 对于堆排序的学习&#xff0c;实际上就是对于 堆 这一种数据结构的学习&#xff0c;把堆学会了&#xff0c;堆排序自然也就学会了。 1、为什么使用堆这种数据结构 优先队列是一种很常用的队列&#xff0c;比如在游戏中&#xff0c;游戏角色在自动模式下&#xff0c;如何…

字符串的HashCode可能相同

字符串的HashCode可能相同 学习了&#xff1a;http://blog.csdn.net/hl_java/article/details/71511815 转载于:https://www.cnblogs.com/stono/p/8165946.html

八大排序算法--堆排序的优化(原地堆排序、索引堆)

优化一----原地堆排序 前一篇博客我们都需要开辟一个新的数组 来进行堆的存放&#xff0c;下面将讲述原地堆排序。 在前面讲到&#xff0c;堆是存放在一个数组中的&#xff0c;如果我们不想开辟新空间&#xff0c;在原来数组上依然可以实现堆排序&#xff0c;不过索引位置就要…