排序

本分参考自数据结构与算法--java版

排序的基本介绍:

    排序(sorting)的功能是将一个数据元素的任意序列,重新排列成一个按关键字有序的 序列。其确切的定义为:
假设有n个数据元素的序列{R1 , R2 , … , Rn},其相应关键字的序列是{K1 , K2 , … , Kn}, 通过排序要求找出下标 1 , 2 , … , n的一种排列p1 , p2 , … , pn,使得相应关键字满足如下的非 递减(或非递增)关系

Kp1 ≤ Kp2 ≤ … ≤ Kpn


这样,就得到一个按关键字有序的纪录序列:{ Rp1 , Rp2 , … , Rpn }。 根据排序时待排序的数据元素数量的不同,使得排序过程中涉及的存储器不同,可以将
排序方法分为两类。一类是整个排序过程在内存储器中进行,称为内部排序;另一类是由于 待排序元素数量太大,以至于内存储器无法容纳全部数据,排序需要借助外部存储设备才能 完成,这类排序称为外部排序。如果在待排序的序列中存在多个具有相同关键字的元素。假设Ki=Kj(1≤ i≤ n,1≤ j≤ n, i≠j),若在排序之前的序列中Ri在Rj之前,经过排序后得到的序列中Ri仍然在Rj之前,则称所 用的排序方法是稳定的;否则,当相同关键字元素的前后关系在排序中发生变化,则称所用 的排序方法是不稳定的

排序算法分类:内部排序和外部排序。
内部排序:整个排序过程不需要借助于外部存储器(如磁盘等),所有排序操作都在内存中完成。
外部排序:参与排序的数据非常多,数据量非常大,计算机无法把整个排序过程放在内存中完成,必须借助于外部存储器(如磁盘)。外部排序最常见的是多路归并排序。可以认为外部排序是由多次内部排序组成。

本文介绍的排序算法都属于内部排序

     内部排序的方法很多,但是很难说哪一种内部排序方法最好,每一种方法都有各自的优缺点,适合于不同的环境下使用。如果按照排序过程中依据的原则对内部排序进行分类,则 大致上可以分为插入排序、交换排序、选择排序、归并排序等排序方法。


衡量排序算法的优劣:
  1.时间复杂度:分析关键字的比较次数和记录的移动次数
  2.空间复杂度:分析排序算法中需要多少辅助内存
  3.稳定性:若两个记录A和B的关键字值相等,但排序后A、B的先后次序保持不变,则称这种排序算法是稳定的。


插入类排序

插入排序的基本排序思想是:逐个考察每个待排序元素,将每一个新元素插入到前面已 经排好序的序列中适当的位置上,使得新序列仍然是一个有序序列。

在这一类排序中主要介绍三种排序方法:直接插入排序、折半插入排序和希尔排序。

  • 直接插入排序:从最前方开始遍历,只要遇到比r[i]大的就插到这个数的前面然后将记录后移:i后面的不用动,只是将插入位置之前的元素后移即可。比待插入元素大的就不用移了。所以应该是先排好较小的数

    基本思想:把 n 个待排序的元素看成一个有序表和一个无序表,开始时有序表中只有一个元素。无序表中有 n-1个元素;排序过程中每次从无序表中取出第一个元素,把他依次与有序表中的元素从而将他插到合适的位置。直到所有无序表中元素插入到有序表中(只要有比被插入元素大的就后移,直到没有退出, 然后赋值)。

实现过程如下:


实现代码及自我理解:

public class  InsertSort{
	public static void main(String[] args) {
		InsertSort is = new InsertSort( ) ;
		int[] num = new int[400000] ;
		for ( int i = 0 ; i < num.length  ;  i++){
			num[ i ] =(int) Math.round( Math.random() * 100) ;
		}
		Calendar cal = Calendar.getInstance( ) ;
		System.out.println("排序前:" + cal.getTime( )) ;
		is.insertSort(num);
		cal = Calendar.getInstance( ) ;
		System.out.println("排序后:" + cal.getTime( )) ;
	}
	public void insertSort(int[] num){
		for ( int j = 1 ; j < num.length ; j++){//第一个数为有序元素,从第二个数开始插
		
			//insertValue :准备和前一个数比较的数
			int insertValue = num[ j ] ;
			int insertIndex  = j -1 ; //前一个数的下标
			//因为是逐个向前比较的,那么应该是num[ insetIndex ]与待插元素比较(待插元素逐个与比较后的前一个数比较)
				while ( insertIndex >=0 && num[ insertIndex ] > insertValue  ){
					/*这里要先写  insertIndex >=0 因为一假则假,如果num[ insertIndex ] > insertValue那么就不判断 insertIndex >=0
					,会出现下标为-1的ArrayIndexOutOfBoundsException */
					//如果这个数比前一个数小,就将前一个数后移一位,再继续比较,直到找到合适的位置
						num[insertIndex + 1] = num[ insertIndex ] ;
						insertIndex-- ;//从有序数列最后一元素向前比较
				}
				//上面即使找到了也减减了,所以后面赋值时,应该要加1
				num[ insertIndex + 1] = insertValue ;
			} 
			/*for(int a : num){
				System.out.print( a + "\t") ;
			}*/
	}
}

  • 折半插入排序直接插入排序的基本操作是向有序序列中插入一个元素,插入位置的确定是通过对有序 序列中元素按关键字逐个比较得到的。既然是在有序序列中确定插入位置,则可以不断二分 有序序列来确定插入位置,即搜索插入位置的方法可以使用折半查找实现

代码及理解:

/*
折半插入排序:
直接插入排序的基本操作是向有序序列中插入一个元素,插入位置的确定是通过对有序 序列中元素按关键字逐个比较得
到的。既然是在有序序列中确定插入位置,则可以不断二分 有序序列来确定插入位置,即搜索插入位置的方法可以使用
折半查找实现。
*/
import java.util.Calendar ;
public class BinaryInsertSort {
	public static void main(String[] args) {
			int[] num = new int[10] ;
			for ( int k = 0 ; k < num.length  ;  k++ ){
				num[ k] = (int)Math.round(Math.random()*100) ;
			}
			//System.out.println("\n ----------------------------------------------------------------------------------------------");
			BinaryInsertSort bis = new BinaryInsertSort( ) ;
			Calendar cal = Calendar.getInstance( ) ;
			System.out.println("排序前:" + cal.getTime( )) ;
			bis.binaryInsertSort( num, 0 , num.length - 1 ) ;
			cal = Calendar.getInstance( ) ;
			System.out.println("\n排序后:" + cal.getTime( )) ;
			
	}
	public void binaryInsertSort( int[] num , int low , int high ){
		 for ( int i = low + 1 ; i < num.length ; i++ ) {
			 int temp = num[ i ] ; //保存 i ;
			 int hi = i - 1 ; //和 待插入元素比较的第一个元素的下标,作为,二分的高位。
			 int lo = low ; //从 low到 i-1 是要比较的元素,也是二分查找位置的作用范围。
			
			 while ( lo <= hi)
			 {		
				 int mid = (hi + lo) / 2 ;	
				 if (num[ i ] < num[ mid ]){
					 hi = mid - 1 ;
				 }else if ( num[ i ] > num[mid]){
					 lo = mid + 1 ;
				 }	 
			 }
			 //循环出来之后后面的数大于temp,前面的数小于temp,num[ i ] 应该插中间
			
			/*lo 和 hi 的值都是比 i 小的。应该是相对于hi移动的,因为是,从前向后走---因该是插在 hi 的前面,
			将hi后面的都后移
			到不能再取中的位置结束,这个位置就是要插的位置 hi - 1 = lo 的时候
			*/

			 //现在插入num[ i ] ;
			 for ( int j = i - 1 ; j  > hi ;  j--){
					num [ j +1 ] = num [ j ] ;
			 }
			 num[ hi +1] = temp ;
		 }
			
		 for ( int k = 0 ; k < num.length  ;  k++ ){
				System.out.print( num[ k ] + "\t ") ;
			}
	}
}

折半插入排序所需的辅助空间与直接插入排序相同,从时间上比 较,折半插入排序仅减少了元素的比较次数,但是并没有减少元素的移动次数,因此折半插 入排序的时间复杂度仍为O(n2)。

  • 希尔排序

希尔排序又称为“缩小增量排序”,它也是一种属于插入排序类的排序方法,是一种对直 接插入排序的改进,但在时间效率上却有较大的改进。

基本思想:算法先将要排序的一组数按某个增量d(n/2,n为要排序数的个数)分成若干组,每组中记录的下标相差d.对每组中全部元素进行直接插入排序,然后再用一个较小的增量(d/2)对它进行分组,在每组中再进行直接插入排序。当增量减到1时,进行直接插入排序后, 排序完成。其实就是将待排序的序列,分成若干个子序列,对每个子序列采用直接插入排序,待子序列中元素有序后,在对序列进行一次插入排序。

过程分析:


代码实现:

public class  ShellSort{
	public static void main(String[] args) {
			int[] num = new int[100000000] ;
			for ( int k = 0 ; k < num.length  ;  k++ ){
			num[ k] = (int)Math.round(Math.random()*100) ;
			}
			System.out.println("\n ----------------------------------------------------------------------------------------------");
			ShellSort ss = new ShellSort( ) ;
			Calendar cal = Calendar.getInstance( ) ;
			System.out.println("排序前:" + cal.getTime( )) ;
			ss.shellSort( num ) ;
			cal = Calendar.getInstance( ) ;
			System.out.println("排序后:" + cal.getTime( )) ;
			
	}
	public void shellSort( int[] num){
		double d1 = num.length ;//取得数组的长度

		//直接插入排序,将元素逐渐插入到合适的位置,假设前面的元素都是有序的,所以需要一个中间变量

		int temp = 0 ;
		while ( true){

			//直到序列步长为1的时候结束排序,不知道要循环多少次,所以只有制定当步长为1 的时候退出
			//定义步长(决定了子序列元素的个数,步长是指,每隔几个数取一次元素用来组成子序列)

			d1 = Math.ceil(d1/2) ;				 //每次步长都缩减(奇数是四舍五入的)
			int d = (int) d1 ;							 //步长
			//System.out.print(d1 + "\t") ;
			for (int x = 0 ; x < d; x++){		 //子序列的个数是与步长相等的,
				for ( int i = x + d ;  i < num.length ; i += d){ //拆分成子序列

					//这样每次我都能接收一个数,然后我用直接插入排序的思想将他插入到子序列的合适位置,完成
					//子序列的插入排序。

					int index = i - d ; 
					//index保存的应该是 x ,待插入元素前一个元素的索引值(从前一个元素依次向前,到最前或找到插入位置)
					
					temp = num[ i ] ;
					//因此此时temp 保存的是待插入的元素

					while ( index >= 0&& num[i] < num[index]){
						//如果比待插入的元素大就后移 d 个位置
							num[ index + d ] = num [index] ;//如果这里写了 i 就不是后移 d 而是用前面较大的数一直替换 i 处的值
							index -= d ;
					}
					num[ index + d ] = temp ;		//将待插元素插入到合适的位置
				}
			}
			if ( d == 1){
				break ;
			}
		}
			/*for ( int k = 0 ; k < num.length  ;  k++ ){
				System.out.print( num[ k ] + "\t ") ;
			}*/
	}
}

选择类排序

基本原理:将待排序的元素分为已排序(初始为空)和未排序两组,依次将未排序的元素中值最小的元素放入已排序的组中。

基本思想:在要排序的一组数中,选出最小的一个数与第一个位置的数交换;然后在剩下的数当中再找最小的与第二个位置的数交换,如此循环到倒数第二个数和 最后一个数比较为止。 即:
        找到每一次的最小值,与这次的最小下标表进行交换,因此外层循环 第一次应该是从第二个数开始的,此时是将 i= 1及以后的最小的数放在数组下标为0的位置。第二次是将第二小的数放在下标为1的位置。以此类推。。。。。。假设i = 0时是第一次循环的时候最小的数。外层循环是从 i 等于 1开始的,找到每次中最小的数放到这次循环时的最小下标

  • 简单选择排序:

简单直观,但性能略差;堆排序是一种较为高效的选择排序方法,但实现起来略微复杂。

直接选择排序的基本过程为:



代码及个人理解:

public class  SelectSort{
	public static void main(String[] args) {
		int[] num = new int[100000] ;
		for ( int k = 0 ; k < num.length  ;  k++ ){
			num[ k] = (int)Math.round(Math.random()*100) ;
		}
		SelectSort ss = new SelectSort( ) ; 
		Calendar cal = Calendar.getInstance() ;
		System.out.println("排序前 "+ cal.getTime());
		ss.selectSort(num) ;
		cal = Calendar.getInstance() ;
		System.out.print("排序后 "+ cal.getTime());
	}
	public void selectSort( int[] num){
			int temp = 0 ;
			int min = 0 ;
			int index = 0 ;

			for ( int i = 0 ; i < num.length - 1 ; i++ ){
/*
				//这里如果固定式0 就找出了当前数组的最小值。,然后将最小值与num[0]就换,就排好了一个。
				min = num[ i ] ;//假设每次内层循环时最小值都是这次循环中最小的下标
				index = i ;

				//同理可知,i = 1,就是排出了num[1]~num[length-1]之中的最小值,并将最小值与num[1]交换。。以此类推,直到都排好
				
					for ( int j = i + 1 ; j < num.length ; j++ ){//这里是前面较小的数都已经拍好了,所以从 i + 1开始比较。
						if ( min > num[ j ]){
							min = num[ j ] ;	//找出这层循环的最小值
							index = j ;			//记录其下标
						}
					}

				//内层循环之后就找到了下标等于 i 之后的最小值了,使之与这次循环最小下标处的元素交换,就排好了数
						temp = num[ i ] ;
						num[ i ] = num[ index] ;
						num[index ] = temp ;*/

			for(int j = i+1 ; j < num.length ; j++){//找出这轮的最小值
					if(num[j]<num[i]){
						temp = num[i] ;//地址值
						num[i] = num[j];
						num[j] = temp ;	
					}
				}

			 }
			/* for ( int k = 0 ; k < num.length  ;  k++ ){
				System.out.print( num [ k ] + "\t");
			 }*/
		}
}

【效率分析】

空间效率:显然简单选择排序只需要一个辅助空间。 时间效率:在简单选择排序中,所需移动元素的次数较少,在待排序序列已经有序的情

况下,简单选择排序不需要移动元素,在最坏的情况下,即待排序序列本身是逆序时,则移 动元素的次数为 3(n-1)。然而无论简单选择排序过程中移动元素的次数是多少,在任何情况 下,简单选择排序都需要进行n(n-1)/2 次比较操作,因此简单选择排序的时间复杂度为Ο(n2)。

算法改进思想:从上述效率分析中可以看出,简单选择排序的主要操作是元素间的比较

操作,因此改进简单选择排序应从减少元素比较次数出发。在简单选择排序中,首先从 n 个元素的序列中选择关键字最小的元素需要 n-1 次比较,在 n-1 个元素中选择关键字最小的 元素需要 n-2 次比较……,在此过程中每次选择关键字最小的元素都没有利用以前比较操作 得到的结果。欲降低比较操作的次数,则需要把以前比较的结果记录下来,由此得到一种改 进的选择类排序算法,即树型选择排序。


交换类排序:

交换类排序主要是通过两两比较待排元素的关键字,若发现与排序要求相逆,则“交换” 之。在这类排序方法中最常见的是冒泡排序快速排序,其中快速排序是一种在实际应用中 具有很好表现的算法。

  • 冒泡排序

冒泡排序的思想非常简单。首先,将 n 个元素中的第一个和第二个进行比较,如果两个 元素的位置为逆序,则交换两个元素的位置;进而比较第二个和第三个元素关键字,如此类 推,直到比较第 n-1 个元素和第 n 个元素为止;上述过程描述了起泡排序的第一趟排序过程, 在第一趟排序过程中,我们将关键字最大的元素通过交换操作放到了具有 n 个元素的序列的 最一个位置上。然后进行第二趟排序,在第二趟排序过程中对元素序列的前 n-1 个元素进行 相同操作,其结果是将关键字次大的元素通过交换放到第 n-1 个位置上。一般来说,第 i 趟 排序是对元素序列的前 n-i+1 个元素进行排序,使得前 n-i+1 个元素中关键字最大的元素被 放置到第 n-i+1 个位置上。排序共进行 n-1 趟,即可使得元素序列按关键字有序。

过程分析:



算法实现举例:

/*
冒泡排序: 在要排序的一组数中,对当前还未排好序的范围内的全部数,自上而下对相邻的两个数依次进行比较和调整,让较大的数往下沉,较小的往上冒。
 * 即:每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
 * 时间复杂度为O(n*n)
*/
import java.util.Calendar ;
public class BubbleSort {
	public static void main(String[] args) {
		int[] num = new int[100000] ;
		for ( int i = 0 ; i < num.length ;  i++){
			num[ i ] = (int)Math.round( Math.random() * 100 );
		}
		BubbleSort bs = new BubbleSort() ;
		Calendar cal = Calendar.getInstance( ) ;
		System.out.println( "排序前" + cal.getTime());
		bs.bubbleSort( num);
		cal = Calendar.getInstance( ) ;
		System.out.println( "排序后" + cal.getTime());
	}
	public void bubbleSort( int[] num){
		int temp = 0 ;
			/*
				① 每当两相邻的数比较后发现它们的排序与排序要求相反时,就将它们互换。
				② 每个数都要与后面每一个数进行比较,最后一个数前面的数已经与他比较,所以他不需要在参与
				③ i 表示第 i 个数正在找自己的位置,排好了就找到了,决定一共走几趟.
				④ 每一次外层循环之后就将较大的数后移并排好(i = 0 之后最大的数到了最后,i = 1 时第二大的数就到了
				倒数第二的位置,因为你把数组的每两个数都进行了比较)
			*/
			for ( int i = 0 ; i < num.length - 1 ;  i++){
			//j只是控制每个i与后面的数比较的次数。每一次 i 执行完,就有一个数排好,下一个数就少比较一次
				for ( int j = 0 ; j < num.length - 1 - i ; j++ ){
						if ( num [ j ] > num[j + 1]){//从小到大排序
								temp = num[ j ] ;
								num[ j ] = num[ j + 1 ] ;
								num [ j + 1 ]  = temp;
						}
				}
			}
		/*	for ( int i = 0 ; i < num.length ; i++){
				System.out.print( num [ i ] + "\t");
			}*/
			
	}
}

  • 快速排序:

基本思想:选择一个基准元素,通常选择第一个元素或者最后一个元素,通过一趟扫描,将待排序列分成两部分,一部分比基准元素小,一部分大于等于基准元素,此时基准元素在其排好序后的正确位置,然后再用同样的方法递归地 排序划分的两部分。

过程分析:


代码实现:

public class quickSort {  
  
  inta[]={49,38,65,97,76,13,27,49,78,34,12,64,5,4,62,99,98,54,56,17,18,23,34,15,35,25,53,51};  
  
public quickSort(){  
  
    quick(a);  
  
    for(int i=0;i<a.length;i++)  
  
       System.out.println(a[i]);  
  
}  
  
publicint getMiddle(int[] list, int low, int high) {     
  
            int tmp = list[low];    //数组的第一个作为中轴     
  
            while (low < high) {     
  
                while (low < high && list[high] >= tmp) {     
  
                    high--;     
  
                }     
  
                list[low] = list[high];   //比中轴小的记录移到低端     
  
                while (low < high && list[low] <= tmp) {     
  
                    low++;     
  
                }     
  
                list[high] = list[low];   //比中轴大的记录移到高端     
  
            }     
  
           list[low] = tmp;              //中轴记录到尾     
  
            return low;                   //返回中轴的位置     
  
        }    
  
publicvoid _quickSort(int[] list, int low, int high) {     
  
            if (low < high) {     
  
               int middle = getMiddle(list, low, high);  //将list数组进行一分为二     
  
                _quickSort(list, low, middle - 1);        //对低字表进行递归排序     
  
               _quickSort(list, middle + 1, high);       //对高字表进行递归排序     
  
            }     
  
        }   
  
publicvoid quick(int[] a2) {     
  
            if (a2.length > 0) {    //查看数组是否为空     
  
                _quickSort(a2, 0, a2.length - 1);     
  
        }     
  
       }   
  
}  

【效率分析】 时间效率:快速排序算法的运行时间依赖于划分是否平衡,即根据枢轴元素 pivot 将序列划分为两个子序列中的元素个数,而划分是否平衡又依赖于所使用的枢轴元素。下面我们 在不同的情况下来分析快速排序的渐进时间复杂度。

快速排序的最坏情况是每次进行划分时,在所得到的两个子序列中有一个子序列为空。 此时,算法的时间复杂度T(n) = Tp(n) + T(n-1),其中Tp(n)是对具有n个元素的序列进行划分 所需的时间,由以上划分算法的过程可以得到Tp(n) = Θ(n)。由此,T(n) =Θ(n) + T(n-1) =Θ(n2)。 在快速排序过程中,如果总是选择r[low]作为枢轴元素,则在待排序序列本身已经有序或逆 向有序时,快速排序的时间复杂度为Ο(n2),而在有序时插入排序的时间复杂度为Ο(n)。

快速排序的最好情况是在每次划分时,都将序列一分为二,正好在序列中间将序列分成 长度相等的两个子序列。此时,算法的时间复杂度T(n) = Tp(n) + 2T(n/2),由于Tp(n) = Θ(n),所以T(n) = 2T(n/2) +Θ(n),由master method知道T(n) = Θ(n log n)。 在平均情况下,快速排序的时间复杂度 T(n) = kn ㏑ n,其中 k 为某个常数,经验证明,

在所有同数量级的排序方法中,快速排序的常数因子 k 是最小的。因此就平均时间而言,快速排序被认为是目前最好的一种内部排序方法

快速排序的平均性能最好,但是,若待排序序列初始时已按关键字有序或基本有序,则 快速排序蜕化为,冒泡排序,其时间复杂度为Ο(n2)。为改进之,可以采取随机选择枢轴元素 pivot的方法,具体做法是,在待划分的序列中随机选择一个元素然后与r[low]交换,再将r[low] 作为枢轴元素,作如此改进之后将极大改进快速排序在序列有序或基本有序时的性能,在待 排序元素个数n较大时,其运行过程中出现最坏情况的可能性可以认为不存在。

空间效率:虽然从时间上看快速排序的效率优于前述算法,然而从空间上看,在前面讨论的算法中都只需要一个辅助空间,而快速排序需要一个堆栈来实现递归。若每次划分都将 序列均匀分割为长度相近的两个子序列,则堆栈的最大深度为 log  n,但是,在最坏的情况 下,堆栈的最大深度为 n。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值