Java数据结构和算法(九)——高级排序

  春晚好看吗?不存在的!!!

  在Java数据结构和算法(三)——冒泡、选择、插入排序算法中我们介绍了三种简单的排序算法,它们的时间复杂度大O表示法都是O(N2),如果数据量少,我们还能忍受,但是数据量大,那么这三种简单的排序所需要的时间则是我们所不能接受的。接着我们在讲解递归 的时候,介绍了归并排序,归并排序需要O(NlogN),这比简单排序要快了很多,但是归并排序有个缺点,它需要的空间是原始数组空间的两倍,当我们需要排序的数据占据了整个内存的一半以上的空间,那么是不能使用归并排序的。

  本篇博客将介绍几种高级的排序算法:希尔排序和快速排序。

1、希尔排序

  希尔排序是基于直接插入排序的,它在直接插入排序中增加了一个新特性,大大的提高了插入排序的执行效率。所以在讲解希尔排序之前,我们先回顾一下直接插入排序。

  ①、直接插入排序

  直接插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所有元素为止。

  

  实现代码为:

package com.ys.sort;

public class InsertSort {
	public static int[] sort(int[] array){
		int j;
		//从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的
		for(int i = 1 ; i < array.length ; i++){
			int tmp = array[i];//记录要插入的数据
			j = i;
			while(j > 0 && tmp < array[j-1]){//从已经排序的序列最右边的开始比较,找到比其小的数
				array[j] = array[j-1];//向后挪动
				j--;
			}
			array[j] = tmp;//存在比其小的数,插入
		}
		return array;
	}

}

  我们可以分析一下这个直接插入排序,首先我们将需要插入的数放在一个临时变量中,这也是一个标记符,标记符左边的数是已经排好序的,标记符右边的数是需要排序的。接着将标记的数和左边排好序的数进行比较,假如比目标数大则将左边排好序的数向右边移动一位,直到找到比其小的位置进行插入。

  这里就存在一个效率问题了,如果一个很小的数在很靠近右边的位置,比如上图右边待排序的数据 1 ,那么想让这个很小的数 1 插入到左边排好序的位置,那么左边排好序的数据项都必须向右移动一位,这个步骤就是将近执行了N次复制,虽然不是每个数据项都必须移动N个位置,但是每个数据项平均移动了N/2次,总共就是N2/2,因此插入排序的效率是O(N2)。

  那么如果以某种方式不必一个一个移动中间所有的数据项,就能把较小的数据项移动到左边,那么这个算法的执行效率会有很大的改进。

  ②、希尔排序图解

  希尔排序应运而生了,希尔排序通过加大插入排序中元素的间隔,并在这些有间隔的元素中进行插入排序,从而使数据项能够大跨度的移动。当这些数据项排过一趟序后,希尔排序算法减小数据项的间隔再进行排序,依次进行下去,最后间隔为1时,就是我们上面说的简单的直接插入排序。

  下图显示了增量为4时对包含10个数组元素进行排序的第一个步骤,首先对下标为 0,4,8 的元素进行排序,完成排序之后,算法右移一步,对 1,5,9 号元素进行排序,依次类推,直到所有的元素完成一趟排序,也就是说间隔为4的元素都已经排列有序。

  

  当我们完成4-增量排序之后,在进行普通的插入排序,即1-增量排序,会比前面直接执行简单插入排序要快很多。

  ③、排序间隔选取

  对于10个元素,我们选取4的间隔,那么100个数据,1000个数据,甚至更多的数据,我们应该怎么选取间隔呢?

  希尔的原稿中,他建议间隔选为N/2,也就是每一趟都将排序分为两半,因此对于N=100的数组,逐渐减小的间隔序列为:50,25,12,6,3,1。这个方法的好处是不需要在开始排序前为找到初始序列的间隔而计算序列,只需要用2整除N。但是这已经被证明并不是最好的序列。

  间隔序列中的数字互质是很重要的指标,也就是说,除了1,他们没有公约数。这个约束条件使得每一趟排序更有可能保持前一趟排序已经排好的结果,而希尔最初以N/2的间隔的低效性就是没有遵守这个准则。

  所以一种希尔的变形方法是用2.2来整除每一个间隔,对于n=100的数组,会产生序列45,20,9,4,1。这比用2会整除会显著的改善排序效果。

  还有一种很常用的间隔序列:knuth 间隔序列 3h+1

  

  但是无论是什么间隔序列,最后必须满足一个条件,就是逐渐减小的间隔最后一定要等于1,因此最后一趟排序一定是简单的插入排序。

  下面我们通过knuth间隔序列来实现希尔排序:

  ④、knuth间隔序列的希尔排序算法实现

//希尔排序 knuth 间隔序列 3h+1
public static void shellKnuthSort(int[] array){
	System.out.println("原数组为"+Arrays.toString(array));
	int step = 1 ;
	int len = array.length;
	while(step <= len/3){
		step = step*3 + 1;//1,4,13,40......
	}
	while(step > 0){
		//分别对每个增量间隔进行排序
		for(int i = step ; i < len ; i++){
			int temp = array[i];
			int j = i;
			while(j > step-1 && temp <= array[j-step]){
				array[j] = array[j-step];
				j -= step;
			}
			array[j] = temp;
		}//end for
		System.out.println("间隔为"+step+"的排序结果为"+Arrays.toString(array));
		step = (step-1)/3;
	}//end while(step>0)

	System.out.println("最终排序:"+Arrays.toString(array));
}

  测试结果:

public static void main(String[] args) {
	int[] array = {4,2,8,9,5,7,6,1,3,10};
	shellKnuthSort(array);
}

  

  ⑤、间隔为2h的希尔排序

//希尔排序 间隔序列2h
public static void shellSort(int[] array){
	System.out.println("原数组为"+Arrays.toString(array));
	int step;
	int len = array.length;
	for(step = len/2 ;step > 0 ; step /= 2){
		//分别对每个增量间隔进行排序
		for(int i = step ; i < array.length ; i++){
			int j = i;
			int temp = array[j];
			if(array[j] < array[j-step]){
				while(j-step >=0 && temp < array[j-step]){
					array[j] = array[j-step];
					j -= step;
				}
				array[j] = temp;
			}
		}
		System.out.println("间隔为"+step+"的排序结果为"+Arrays.toString(array));
	}
}

  测试结果:

  

2、快速排序

  快速排序是对冒泡排序的一种改进,由C. A. R. Hoare在1962年提出的一种划分交换排序,采用的是分治策略(一般与递归结合使用),以减少排序过程中的比较次数。

  ①、快速排序的基本思路

  一、先通过第一趟排序,将数组原地划分为两部分其中一部分的所有数据都小于另一部分的所有数据原数组被划分为2份

  二、通过递归的处理, 再对原数组分割的两部分分别划分为两部分,同样是使得其中一部分的所有数据都小于另一部分的所有数据。 这个时候原数组被划分为了4份

  三、就1,2被划分后的最小单元子数组来看,它们仍然是无序的,但是! 它们所组成的原数组却逐渐向有序的方向前进。

  四、这样不断划分到最后,数组就被划分为多个由一个元素或多个相同元素组成的单元,这样数组就有序了。

  具体实例:

  

  对于上图的数组[3,1,4,1,5,9,2,6,5,3],通过第一趟排序将数组分成了[2,1,1]或[4,5,9,3,6,5,3]两个子数组,且对于任意元素,左边子数组总是小于右边子数组。通过不断的递归处理,最终得到有序数组[1 1 2 3 3 4 5 5 6]

  ②、快速排序的算法实现

  假设被排序的无序区间为[A[i],......,A[j]]

  一、基准元素选取:选择其中的一个记录的关键字 v 作为基准元素(控制关键字);怎么选取关键字?

  二、划分:通过基准元素 v 把无序区间 A[I]......A[j] 划分为左右两部分,使得左边的各记录的关键字都小于 v;右边的各记录的关键字都大于等于 v;(如何划分?)

  三、递归求解:重复上面的一、二步骤,分别对左边和右边两部分递归进行快速排序。

  四、组合:左、右两部分均有序,那么整个序列都有序。

  上面的第 三、四步不用多说,主要是第一步怎么选取关键字,从而实现第二步的划分?

  划分的过程涉及到三个关键字:“基准元素”、“左游标”、“右游标”

  基准元素:它是将数组划分为两个子数组的过程中,用于界定大小的值,以它为判断标准,将小于它的数组元素“划分”到一个“小数值的数组”中,而将大于它的数组元素“划分”到一个“大数值的数组”中,这样,我们就将数组分割为两个子数组,而其中一个子数组的元素恒小于另一个子数组里的元素。

  左游标:它一开始指向待分割数组最左侧的数组元素,在排序的过程中,它将向右移动。

  右游标:它一开始指向待分割数组最右侧的数组元素,在排序的过程中,它将向左移动。

  注意:上面描述的基准元素/右游标/左游标都是针对单趟排序过程的, 也就是说,在整体排序过程的多趟排序中,各趟排序取得的基准元素/右游标/左游标一般都是不同的。

  对于基准元素的选取,原则上是任意的。但是一般我们选取数组中第一个元素为基准元素(假设数组是随机分布的)

  ③、快速排序图示

  

  上面表示的是一个无序数组,选取第一个元素 6 作为基准元素。左游标是 i 哨兵,右游标是 j 哨兵。然后左游标向左移动,右游标向右移动,它们遵循的规则如下:

  一、左游标扫描, 跨过所有小于基准元素的数组元素, 直到遇到一个大于或等于基准元素的数组元素, 在那个位置停下

  二、右游标扫描, 跨过所有大于基准元素的数组元素, 直到遇到一个小于或等于基准元素的数组元素,在那个位置停下。

  第一步:哨兵 j 先开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵 j 先开始出动,哨兵 j 一步一步的向左挪动,直到找到一个小于 6 的元素停下来。接下来,哨兵 i 再一步一步的向右挪动,直到找到一个大于 6 的元素停下来。最后哨兵 i 停在了数字 7 面前,哨兵 j 停在了数字 5 面前。

  

  到此,第一次交换结束,接着哨兵 j 继续向左移动,它发现 4 比基准数 6 要小,那么在数字4面前停下来。哨兵 i 也接着向右移动,然后在数字 9 面前停下来,然后哨兵 i 和 哨兵 j 再次进行交换。

  

  第二次交换结束,哨兵 j 继续向左移动,然后在数字 3 面前停下来;哨兵 i 继续向右移动,但是它发现和哨兵 j 相遇了。那么此时说明探测结束,将数字 3 和基准数字 6 进行交换,如下:

  

  到此,第一次探测真正结束,此时已基准点 6 为分界线,6 左边的数组元素都小于等于6,6右边的数组元素都大于等于6。

  左边序列为【3,1,2,5,4】,右边序列为【9,7,10,8】。接着对于左边序列而言,以数字 3 为基准元素,重复上面的探测操作,探测完毕之后的序列为【2,1,3,5,4】;对于右边序列而言,以数字 9 位基准元素,也重复上面的探测操作。然后一步一步的划分,最后排序完全结束。

  通过这一步一步的分解,我们发现快速排序的每一轮操作就是将基准数字归位,知道所有的数都归位完成,排序就结束了。

  

  ④、快速排序完整代码

package com.ys.high.sort;

public class QuickSort {

	//数组array中下标为i和j位置的元素进行交换
	private static void swap(int[] array , int i , int j){
		int temp = array[i];
		array[i] = array[j];
		array[j] = temp;
	}

	private static void recQuickSort(int[] array,int left,int right){
		if(right <= left){
			return;//终止递归
		}else{

			int partition = partitionIt(array,left,right);
			recQuickSort(array,left,partition-1);// 对上一轮排序(切分)时,基准元素左边的子数组进行递归
			recQuickSort(array,partition+1,right);// 对上一轮排序(切分)时,基准元素右边的子数组进行递归
		}
	}

	private static int partitionIt(int[] array,int left,int right){
		//为什么 j加一个1,而i没有加1,是因为下面的循环判断是从--j和++i开始的.
		//而基准元素选的array[left],即第一个元素,所以左游标从第二个元素开始比较
		int i = left;
		int j = right+1;
		int pivot = array[left];// pivot 为选取的基准元素(头元素)
		while(true){
			while(i<right && array[++i] < pivot){}

			while(j > 0 && array[--j] > pivot){}

			if(i >= j){// 左右游标相遇时候停止, 所以跳出外部while循环
				break;
			}else{
				swap(array, i, j);// 左右游标未相遇时停止, 交换各自所指元素,循环继续
			}
		}
		swap(array, left, j);//基准元素和游标相遇时所指元素交换,为最后一次交换
		return j;// 一趟排序完成, 返回基准元素位置(注意这里基准元素已经交换位置了)
	}

	public static void sort(int[] array){
		recQuickSort(array, 0, array.length-1);
	}

	//测试
	public static void main(String[] args) {
		//int[] array = {7,3,5,2,9,8,6,1,4,7};
		int[] array = {9,9,8,7,6,5,4,3,2,1};
		sort(array);
		for(int i : array){
			System.out.print(i+" ");
		}
		//打印结果为:1 2 3 4 5 6 7 7 8 9
	}
}

  ⑤、优化分析

  假设我们是对一个逆序数组进行排序,选取第一个元素作为基准点,即最大的元素是基准点,那么第一次循环,左游标要执行到最右边,而右游标执行一次,然后两者进行交换。这也会划分成很多的子数组。

  那么怎么解决呢?理想状态下,应该选择被排序数组的中值数据作为基准,也就是说一半的数大于基准数,一般的数小于基准数,这样会使得数组被划分为两个大小相等的子数组,对快速排序来说,拥有两个大小相等的子数组是最优的情况。

  三项取中划分

  为了找到一个数组中的中值数据,一般是取数组中第一个、中间的、最后一个,选择这三个数中位于中间的数。

	//取数组下标第一个数、中间的数、最后一个数的中间值
	private static int medianOf3(int[] array,int left,int right){
		int center = (right-left)/2+left;
		if(array[left] > array[right]){ //得到 array[left] < array[right]
			swap(array, left, right);
		}
		if(array[center] > array[right]){ //得到 array[left] array[center] < array[right]
			swap(array, center, right);
		}
		if(array[center] > array[left]){ //得到 array[center] <  array[left] < array[right]
			swap(array, center, left);
		}

		return array[left]; //array[left]的值已经被换成三数中的中位数, 将其返回
	}
	private static int partitionIt(int[] array,int left,int right){
		//为什么 j加一个1,而i没有加1,是因为下面的循环判断是从--j和++i开始的.
		//而基准元素选的array[left],即第一个元素,所以左游标从第二个元素开始比较
		int i = left;
		int j = right+1;
		int pivot = array[left];// pivot 为选取的基准元素(头元素)

		int size = right - left + 1;
		if(size >= 3){
			pivot = medianOf3(array, left, right); //数组范围大于3,基准元素选择中间值。
		}
		while(true){
			while(i<right && array[++i] < pivot){}

			while(j > 0 && array[--j] > pivot){}

			if(i >= j){// 左右游标相遇时候停止, 所以跳出外部while循环
				break;
			}else{
				swap(array, i, j);// 左右游标未相遇时停止, 交换各自所指元素,循环继续
			}
		}
		swap(array, left, j);//基准元素和游标相遇时所指元素交换,为最后一次交换
		return j;// 一趟排序完成, 返回基准元素位置(注意这里基准元素已经交换位置了)
	}

  处理小划分

  如果使用三数据取中划分方法,则必须遵循快速排序算法不能执行三个或者少于三个的数据,如果大量的子数组都小于3个,那么使用快速排序是比较耗时的。联想到前面我们讲过简单的排序(冒泡、选择、插入)。

  当数组长度小于M的时候(high-low <= M), 不进行快排,而进行插入排序。转换参数M的最佳值和系统是相关的,一般来说, 5到15间的任意值在多数情况下都能令人满意。

	//插入排序
	private static void insertSort(int[] array){
		for(int i = 1 ; i < array.length ; i++){
			int temp = array[i];
			int j = i;
			while(j > 0 && array[j-1] > temp){
				array[j] = array[j-1];
				j--;
			}
			array[j] = temp;
		}
	}

  

原文地址:https://www.cnblogs.com/ysocean/p/8032632.html

时间: 2024-08-03 13:18:13

Java数据结构和算法(九)——高级排序的相关文章

数据结构与算法之--高级排序:shell排序和快速排序【未完待续】

高级排序比简单排序要快的多,简单排序的时间复杂度是O(N^2),希尔(shell)排序的是O(N*(logN)^2),而快速排序是O(N*logN). 说明:下面以int数组的从小到大排序为例. 希尔(shell)排序 希尔排序是基于插入排序的,首先回顾一下插入排序,假设插入是从左向右执行的,待插入元素的左边是有序的,且假如待插入元素比左边的都小,就需要挪动左边的所有元素,如下图所示: ==> 图1和图2:插入右边的temp柱需要outer标记位左边的五个柱子都向右挪动 如图3所示,相比插入排序

Java数据结构和算法(三)——简单排序

单单有数据还不够,对于数据的展现,经常要按照一定的顺序进行排列,越高要求的排序越复杂,这篇只介绍三种大小的简单排序. 1)冒泡排序 模拟:有数组,1,4,2,5,7,3. (1)首先从1开始冒泡,1比4小,不冒,4大于2,冒上去,与2交换位置,4比5小,不冒,7比3大,冒,结果:1,2,4,5,3,7 (2)接下来从2开始,因为第一个元素冒过了,重复(1),结果:1,2,4,3,5,7 (3)从第三位4开始,结果1,2,3,4,5,7 (4)虽然看起来已经排好序,但是还是要继续冒,接下来就是从第

Java数据结构和算法(二)——数组

数组的用处是什么呢?--当你需要将30个数进行大小排列的时候,用数组这样的数据结构存储是个很好的选择,当你是一个班的班主任的时候,每次要记录那些学生的缺勤次数的时候,数组也是很有用.数组可以进行插入,删除,查找等. 1)创建和内存分配 Java中有两种数据类型,基本类型和对象类型,也有人称为引用类型,Java中把数组当成对象,创建数组时使用new操作符. int array[] = new int[10]; 既然是对象,那么array便是数组的一个引用,根据Java编程思想(一) -- 一切都是

Java数据结构和算法之数组与简单排序

一.数组于简单排序 数组 数组(array)是相同类型变量的集合,可以使用共同的名字引用它.数组可被定义为任何类型,可以是一维或多维.数组中的一个特别要素是通过下标来访问它.数组提供了一种将有联系的信息分组的便利方法. 一维数组 一维数组(one‐dimensional array )实质上是相同类型变量列表.要创建一个数组,你必须首先定义数组变量所需的类型.通用的一维数组的声明格式是: type var‐name[ ]; 获得一个数组需要2步: 第一步,你必须定义变量所需的类型. 第二步,你必

Java数据结构与算法之排序

排序从大体上来讲,做了两件事情: 1.比较两个数据项: 2.交换两个数据项,或复制其中一项 一.冒泡排序 大O表示法:交换次数和比较次数都为O(N*N). 算法原理: 1.比较相邻的元素.如果第一个比第二个大,就交换他们两个. 2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对.在这一点,最后的元素应该会是最大的数. 3.针对所有的元素重复以上的步骤,除了最后一个. 4.持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较. /** * 冒泡排序 demo * */

Java数据结构与算法之集合

线性表.链表.哈希表是常用的数据结构,在进行Java开发时,SDK已经为我们提供了一系列相应的类来实现基本的数据结构.这些类均在java.util包中. 一.Collection接口 Collection是最基本的集合接口,一个Collection代表一组Object.一些Collection允许相同元素而另一些不行.一些能排序而另一些不行.Java  SDK不提供直接继承自Collection的类,Java  SDK提供的类都是继承自Collection的"子接口"如List和Set

Java数据结构和算法之递归

四.递归 递归是函数调用自身的一种特殊的编程技术,其应用主要在以下几个方面:   阶乘 在java当中的基本形式是: Public  void  mothed(int n){//当满足某条件时: Mothed(n‐1): } 递归二分查找 Java二分查找实现,欢迎大家提出交流意见.  /** *名称:BinarySearch *功能:实现了折半查找(二分查找)的递归和非递归算法. *说明: *     1.要求所查找的数组已有序,并且其中元素已实现Comparable<T>接口,如Integ

Java数据结构和算法之链表

三.链表 链结点 在链表中,每个数据项都被包含在'点"中,一个点是某个类的对象,这个类可认叫做LINK.因为一个链表中有许多类似的链结点,所以有必要用一个不同于链表的类来表达链结点.每个LINK对象中都包含一个对下一个点引用的字段(通常叫做next)但是本身的对象中有一个字段指向对第一个链结点的引用. 单链表 用一组地址任意的存储单元存放线性表中的数据元素. 以元素(数据元素的映象)  + 指针(指示后继元素存储位置)  = 结点(表示数据元素 或 数据元素的映象) 以"结点的序列&q

Java数据结构和算法(一)——开篇

这篇文章里面不讲技术,抽空讲讲技术和通俗之间有一种奇特的关系,还有驱动力学习的东西. 1)技术与通俗 大学里面那本严蔚敏的数据结构不厚,内容丰富,但是复杂问题的讲解方面篇幅这样就少了,比较难理解,c也不是很擅长,但是基本的思路还是有的. 简单的链表,数组,堆栈,队列,图,几个排序算法. 后面看到知乎涛吴的回答,当时很震撼,这里引用一下他的回答: 如果说 Java 是自动档轿车,C 就是手动档吉普.数据结构呢?是变速箱的工作原理.你完全可以不知道变速箱怎样工作,就把自动档的车子从 A 开到 B,而