《排序思想》

 
 
 

一、介绍

排序是我们工作中经常碰到的一件事,基本每个项目都涉及到排序运算。一般,排序操作在数据处理过程中要话费许多时间。为了提高计算机的运行效率,人们提出不断改进各种各样的排序算法,而这些算法也从不同角度展示了算法设计的某些重要原则和技巧。

 

排序就是将一组对象按照规定的次序重新排列的过程,排序往往是为检索服务的。例如,学生档案系统里面的学生成绩信息就是按照学号、年龄或入学成绩等排序后的结果,在排好序的结果里面检索学生成绩信息效率就高很多了。如下表1-1就是按照年龄升序排列的学生信息列表。

表1-1       按学号升序排序学生成绩表

学号

姓名

性别

年龄

成绩

20060206

吴三

18

523

20060207

李四

19

450

20060208

王五

18

470

20060209

赵柳

17

485.5

 

表1-2       按学号升序排序学生成绩表

学号

姓名

性别

年龄

成绩

20060209

赵柳

17

485.5

20060208

王五

18

470

20060206

吴三

18

523

20060207

李四

19

450

 

可以看出学号为20060206和20060208的两位同学年龄都为18岁,表1-2按照年龄升序后相对位置发生变化了。对于这种相同键值的两个记录在排序前后相对位置变化情况是排序算法研究中经常关注的一个问题,该问题成为排序算法的稳定性。需要注意的是:稳定性是算法本身的特性,与数据无关。

 

排序算法分为内部排序和外部排序。内部排序是将待排序的记录全部放在计算机内存中进行的排序过程,其排序实现方法很多,主要有插入排序、选择排序、交换排序和归并排序等;如果待排序的记录数量很多,内存不能存储全部记录,需要对外存进行访问排序的过程,本次总结的是内部排序。

 

评价一个算法的优劣,通常是用时间复杂度和空间复杂度这两个指标。由于排序算法的多样性,很难确定一种公认的最好方法,应当根据实际应用选择不同的方法。例如,当待排序序列已基本有序时,插入排序和交换排序比较有效;当待排记录数量较多时,选择排序有效。

二、比较


 
 
 

对20000条记录进行排序:

名称                                               耗时(毫秒)

插入排序(直接插入)                   1469

交换排序(冒泡排序)                   5828

交换排序(快速排序)                   1078

选择排序(直接选择排序)             2796

选择排序(堆排序)                       16

归并排序(二路归并)                    0

可以看出,在记录较多的情况下归并排序是效率最高的,其次是堆排序,再次到快速排序,而算法相对简单的排序方法反而效率很低,尤其是冒泡排序效率最低。

三、内部排序

(一)插入排序

它的基本思想是将记录分为有序区和无序区,将无序区中的记录依次插入到有序区中,并保持有序。常用的插入排序方法有直接插入排序、拆半插入排序、表插入排序和希尔排序等。如果待排记录较少且基本有序的话,可以考虑使用插入排序算法。

 

1.直接插入排序

根据插入排序算法的基本思想,图3-1便是直接插入排序算法的过程,中括号内是有序区,中括号外面是无序区。从无序区的前面或后面依次取出一个元素,在有序区找到一个合适的位置插入。这里所说的合适位置要看是升序还是降序。

图3-1 直接插入排序示意图

 

/**
 * 插入排序——直接插入排序
 * @param arrays 待排序序列
 * @dateTime 2014-2-16 上午09:15:08
 * @author wst
 * @return void
 */
public static void insertSort(int[] arrays){
	int i,j,n=arrays.length-1;
	for(i=1;i<=n;i++){//第一个元素无须排序
		int temp=arrays[i];//当前比较元素
		//在已排好序的序列里找到一个合适的位置存放temp
		for(j=i;j>0&&temp<arrays[j-1];j--){
			arrays[j]=arrays[j-1];//将大的元素后移
		}
		arrays[j]=temp;
	}
}

 

 

(二)交换排序

交换排序的基本思想是两两比较待排序的记录,当记录之间值出现逆序时,则交换两个记录。常用的交换排序有冒泡排序和快速排序等。

1.冒泡排序

冒泡排序的过程是首先将第一个记录和第二个记录进行比较,若为逆序,则将这两个记录交换,然后继续比较第二个和第三个记录。依次类推,直到完成第n-1个记录和第n个记录比较交换为止。上述过程称为第一趟起泡,其结果使最大的记录已到了第n个位置上。重复以上起泡过程,当在一趟起泡过程中没有进行记录交换的操作时,整个排序过程终止。因为每趟起泡都有一个最大的记录沉到水底,所以整个排序过程最多需要进行n-1趟起泡。例如,图3-2是冒泡排序过程的示意图,图中只给出了第一趟起泡的过程,后面直接给出各趟起泡结果。在起泡结果里面,有黄色背景的记录已经排好序。

图3-2       冒泡排序示意图

 

从图3-2中可以看出,当第4趟起泡结束后已经没有需要交换的气泡了,第5、6趟起泡纯属多余,因为此时序列已经有序,因此在第4趟起泡结束后可以终止循环。起泡次数=4<n-1,符合上面的推理。

 

/**
 * 交换排序——冒泡排序
 * @param arrays
 * @dateTime 2014-1-25 上午10:41:58
 * @author wst
 * @return void
 */
public static void upBubbleSort(int[] arrays){
	int i,j,n=arrays.length;
	for(i=0;i<n-1;i++){
		boolean endsort=true;//是否需要交换
		for(j=0;j<n-i-1;j++){
			if(arrays[j]>arrays[j+1]){
				int temp=arrays[j];
				arrays[j]=arrays[j+1];
				arrays[j+1]=temp;
				endsort = false;
			}
		}
		if(endsort){
			break;
		}
	}
}

 

 

2.快速排序

快速排序实质上是对冒泡排序的一种改进。其基本思想是:以选定的记录为基准,将待排序表划分为左、右两段,其中左边所有记录小于等于右边所有记录,然后,对左、右两段记录分别进行快速排序。如下图3-3是快速排序的示意图,下图给出了第一趟快速排序的完整过程,后面相继只给出各趟排序结果。红色记录表示各趟记录划分出的关键字,中括号内的记录表示根据关键字划分的无序区。




 图3-3       快速排序示意图

 

/**
 * 交换排序——快速排序
 * 不稳定。O(nlog2n)~O(n2)。
 * 待排序列基本有序时效率低
 * @param arrays
 * @param low 
 * @param high
 * @dateTime 2014-2-16 上午09:34:24
 * @author wst
 * @return int[]
 */
public static int[] quickSort(int[] arrays,int low,int high){
	if(low<high){
		//划分区域,找出关键字
		int temp=quickPartition(arrays,low,arrays.length-1);
		//在关键字左侧进行排序
		quickSort(arrays,low,temp-1);
		//在关键字右侧进行排序
		quickSort(arrays,temp+1,high);
	}
	return arrays;
}
	
/**
 * 对子序列进行一趟快速排序
 * @param arrays 待排序序列
 * @param low 当前排序序列的首指针
 * @param high 当前排序序列的末指针
 * @dateTime 2014-2-16 上午09:38:01
 * @author wst
 * @return int
 */
private static int quickPartition(int[] arrays,int low,int high){
	int x=arrays[low];//初始值
	while(low<high){
		while(low<high&&(arrays[high]>=x)){
			high--;//从末端找出一个比x小的数
		}
		//将找到的比x小的元素移到low位置
		arrays[low]=arrays[high];
		while(low<high&&(arrays[low]<=x)){
			low++;//从首端找出一个比x大的数
		}
		//将找到的比x大的元素移到high位置
		arrays[high]=arrays[low];
	}
	//一趟快速排序结束后,将x置入low位置
	arrays[low]=x;
	return low;
}

 

(三)选择排序

选择排序的基本思想:每次从待排序列中选取记录最小或最大的放到适当位置。常用的选择排序法有直接选择排序和堆排序法等。选择排序适合待排记录较大的情况。

1.直接选择排序

直接选择排序算法的基本思想:在待排序的无序区中选择最小(大)的记录,并将该记录放到有序区的最前(后)端。图3-4是直接选择排序过程的示意图。中括号内是有序区,每次从中括号外面选择出一个最小或最大的记录放到有序区。该算法简单容易实现。

图3-4       直接选择排序示意图

 

/**
 * 选择排序——直接选择
 * 不稳定。O(n2)。待排记录较多效率非常低下
 * @param arrays
 * @dateTime 2014-2-16 上午09:57:13
 * @author wst
 * @return void
 */
public static void selectSort(int[] arrays){
	int n=arrays.length,minIndex=0,temp=0;
	if(arrays==null||n==0){
		return;
	}
	for(int i=0;i<n;i++){		//每一趟都选择出一个最小值
		minIndex=i;				//待排区的最小元素下标
		for(int j=i+1;j<n;j++){	//在待排区中找出最小元素
			if(arrays[j]<arrays[minIndex]){
				minIndex=j;
			}
		}
		if(minIndex!=i){//将找到的最小元素置入有序区
			temp=arrays[i];
			arrays[i]=arrays[minIndex];
			arrays[minIndex]=temp;
		}
	}
}

 

2.堆排序

堆排序是利用堆来选择最小(大)记录。对直接选择排序的分析我们可以知道,在n个记录中选出最小值,至少要进行n-1次比较。然而继续在剩余的n-1个记录中选出次小记录是否一定要进行n-2次比较呢?若能利用前n-1次比较所得信息,是否可以减少以后各次选择中的比较次数呢?答案是肯定的。堆有最小堆和最大堆,简单定义如下:

序列{k1,k2,…,kn}满足

其中,i=1,2,…,n/2,则称这个n个记录的序列{k1,k2,…,kn}为最小堆(或最大堆)。根据上述定义可以知道,最小堆可以看成是一棵以k1为根的完全二叉树,在这课二叉树中,任一结点的值都不大于它的两个孩子的值(若孩子存在的话);最大堆可以看成是一棵以k1为根的完全二叉树,在这棵二叉树中,任一结点的值都不小于它的两个孩子的值(若存在孩子的话)。

 

由此可知,依次输出堆顶元素就可以得到升序或降序的序列。因此,实现堆排序需要解决两个问题:

(1)如何由一个初始序列建成一个堆?

(2)如何在输出堆顶元素之后调整剩余元素成为一个新堆?

如图3-5可以回答第一个问题,图3-6则回答了第二个问题。以下是由初始序列{65,88,55,34,93,28,18,40}建立堆和调整堆的全排序过程。






图3-5将初始序列建立一个堆

 













图3-6调整剩余元素成为一个新堆

 

/**
 * 堆排序
 * @param a
 * @dateTime 2014-3-5 下午07:12:31
 * @author wst
 * @return void
 */
public static void heapSort(int[] a){
	int n=a.length;
	int temp=0;
	//由初始序列建立一个堆
	for(int i=n/2;i>0;i--){
		sift(a,i-1,n);
	}
	//输出堆顶元素,调整剩余元素成为一个新堆
	for(int i=n-2;i>=0;i--){
		temp=a[i+1];
		a[i+1]=a[0];
		a[0]=temp;
		sift(a,0,i+1);
	}
}

/**
 * 执行一次筛选
 * @param a 待排序列
 * @param k 根元素下标
 * @param n 待排序序列长度
 * @dateTime 2014-3-5 下午07:07:34
 * @author wst
 * @return void
 */
public static void sift(int[] a,int k,int n){
	int temp=a[k];	//根
	int j=2*k+1;	//左孩子
	while(j<=n-1){
		if(j<n-1&&a[j]>=a[j+1]){
			j++;
		}
		if(temp<a[j]){
			break;		//筛选结束
		}
		a[(j-1)/2]=a[j];//根与左孩子交换
		j=2*j+1;		//继续从左孩子筛选
	}
	a[(j-1)/2]=temp;
}

 

 

(四)归并排序

归并排序是将两个或两个以上的有序表合并成一个有序表。归并排序法有二路归并排序等。

1.二路归并

二路归并的基本思想:假设序列中有n个记录,可看成是n个有序的子序列,每个序列的长度为1。首先将每相邻的两个记录合并,得到[n/2]个较大的有序子序列,每个子序列包含2个记录,再将上述序列两两合并,得到[[n/2]/2]个有序子序列,如此反复,直至得到一个长度为n的有序序列为止,排序结束。如图3-7是二路归并排序过程的示意图。

图3-7二路归并排序示意图

 

 

/**
 * 归并排序——二路归并
 * @param a
 * @param n
 * @dateTime 2014-3-4 下午09:15:21
 * @author wst
 * @return void
 */
static void margeSort(int[] a,int n){
	int[] b=new int[n+1];
	int h=1;
	while(h<=n){
		mergePass(a,b,h,n);
		h=2*h;
		mergePass(b,a,h,n);
		h=2*h;
	}
}

/**
 * 执行一次归并
 * 在含有n个记录的序列a中,
 * 将长度各为h的相邻两个有序子序列合并为长度2h的一个有序序列
 * @param a 待排序列
 * @param b 合并后的序列
 * @param h 子序列长度
 * @param n 总长度
 * @dateTime 2014-3-4 下午09:02:39
 * @author wst
 * @return void
 */
static void mergePass(int[] a,int[] b,int h,int n){
	int i=0;
	while(i<n-2*h+1){
		merge(a,b,i,i+h-1,i+2*h-1);
		i+=2*h;
	}
	if(i+h-1<n){
		merge(a,b,i,i+h-1,n);
	}else{
		for(int t=i;t<=n;t++){
			b[t]=a[t];
		}
	}
}


/**
 * 有序序列的合并
 * 将a[h],...,a[m]和a[m+1],...,a[n]两个有序序列合并为一个有序序列
 * @param a 待排序序列
 * @param b 合并后的序列
 * @param h 子序列长度
 * @param m 序列1的结束位置
 * @param n 序列2的结束位置
 * @dateTime 2014-3-4 下午09:07:38
 * @author wst
 * @return void
 */
static void merge(int[] a,int[] b,int h,int m,int n){
	int k=h,j=m+1;//序列1的起始位置和序列2的起始位置
	while((h<=m)&&(j<=n)){
		if(a[h]<a[j]){
			b[k]=a[h];
			h++;
		}else{
			b[k]=a[j];
			j++;
		}
		k++;
	}
	while(h<=m){//将a[h],...,a[m]剩余序列插入末尾
		b[k]=a[h];
		h++;
		k++;
	}
	while(j<=n){//将a[h],...,a[m]剩余序列插入末尾
		b[k]=a[j];
		j++;
		k++;
	}
}

 

辅助类:

package com.wusongti.util;

import java.util.Random;

/**
 * 
 * @Utils.java
 * @description 工具类
 * @date 2014-1-21
 * @time 下午07:14:26
 * @author wst
 *
 */
public class Utils {
	/**
	 * 产生n个不重复的随机数
	 * @param n 产生n个随机数
	 * @param start 起始值
	 * @dateTime 2014-1-21 下午07:13:41
	 * @author wst
	 * @return int[] 返回不重复的n个随机数
	 */
	public static int[] getRandomNumber(int n,int start){
		int[] arrays=new int[n];
		Random rand=new Random();
		boolean flag=false;
		for(int i=0;i<n;i++){
			int num;
			do{
				flag=false;
				num=rand.nextInt(n)+start;
				for(int j=0;j<i;j++){
					if(num==arrays[j]){
						flag=true;
						break;
					}
				}
			}while(flag);
			arrays[i]=num;
		}
		return arrays;
	}
	
	/**
	 * 打印数组
	 * @param arrays
	 * @dateTime 2014-1-21 下午07:23:25
	 * @author wst
	 * @return void
	 */
	public static void printSort(int[] arrays){
		int n=arrays.length;
		for(int i=0;i<n;i++){
			if(i%15==0){
				System.out.println();
			}
			System.out.print(arrays[i]+" ");
		}
	}
}

 

 

四、心得

有总结才会有反思,有反思才会有提高!

个人邮箱:[email protected]

五、参考文献

郑诚.数据结构导论[M].外语教学与研究出版社,2012