四、重温数据结构之外部排序

      排序在各大公司面试时是常考的类型,感觉排序没有什么特别要说明的,主要是在这些排序的叙述中需要理解“记录”、“关键字”、“一趟排序”的概念,然后大排序算法思想,在练练代码,应该问题不大。按照排序过程中涉及的存储器不同可分为:

  • 内部排序:待排序记录放在随机存储器中进行排序的过程。(内存中的排序)
  • 外部排序:待排序记录数量很大,内存一次性不能容纳全部,需要对外存进行排序的过程。

以下是各大排序的一个总概括:

排序过程的两个基本操作:

(1)两个关键字比较;(必要操作)

(2)记录从一个位置移到另一个位置;(可通过改变存储方式来避免)

可有的3中存储方式:

(1)待排序的一组记录存储在地址连续的一组存储单元上;

(2)待排序的一组记录存储在静态链表上;

(3)待排序记录本身存储在一组地址连续的存储单元内,同时设一个指示各个记录存储位置的地址向量;

下面介绍一些排序:

1、冒泡排序

/**
	 * 思想:设趟数为t,共有n条记录
	 * 第一趟冒泡排序为:将第一个记录的关键字k.1和第二个记录的关键字k.2比较,若k.1>k.2,交换,否则不交换;
	 * 	             再第二个记录的关键字k.2和第三个记录的关键字k.3比较,若k.2>k.3,交换,否则不交换;
	 *                    ......
	 *                   再第n-1个记录的关键字k.n-1和第n个记录的关键字k.n比较,若k.n-1>k.n-2,交换,否则不交换;
	 * 第二趟冒泡排序为:前面和第一趟冒泡排序一样,只是最后一趟无需在进行k.n-1和k.n的比较;
	 * 			  ......
	 *                   再第n-2个记录的关键字k.n-2和第n个记录的关键字k.n-1比较,若k.n-2>k.n-1,交换,否则不交换;
	 * ......
	 * 第n-1趟冒泡排序:将第n-t=n-(n-1)=1个记录的关键字和第n-t+1=n-(n-1)+1=2个记录比较;若k.1>k.2,交换,否则不交换
	 * 第n趟冒泡排序  :将第n-t=n-(n)=0个记录的关键字和第n-t+1=n-(n)+1=1个记录比较;若k.0>k.1,交换,否则不交换  
	 * 综上:n条记录需要进行n-1趟冒泡排序;
	 *            	
	 * @param sequence
	 */
	public static void bubbleSort(int[] sequence){
		int num = sequence.length;
		int i,j,tmp;
		int t = num-1;//趟数
		for(i = 1;i <= t;i++){//趟数限制
			for(j = 0;j<num-i;j++){//每趟元素比较限制 n-t
				if(sequence[j]>sequence[j+1]){
					tmp = sequence[j+1];
					sequence[j+1] = sequence[j];
					sequence[j] = tmp; 
				}
			}
		}
		
	}



 2、快速排序   

         他怎么就能想到这样的排序方式呢,主要步骤,选枢轴、高低位记录互换、重置记录的分界线(将原来的序列分成2个)



/* * 实现功能:对顺序表中字序列sequence[low..high]做快速排序
	 * 思想:快速排序,是对冒泡排序的一种改进;
	 * 通过分治思想,一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的关键字比另一部分记录的关键字小,
	 * 然后可对两部分记录继续进行排序,通过递归实现来达到整个序列有序,其主要核心为一趟快速排序(或一次划分)
	 * @param sequence
	 * @param low
	 * @param high
	 */
	public  void quickSortRecursion(int[] sequence,int low ,int high){
		if(low < high){
			int pivotloc = partition2(sequence,low,high);//置轴
			quickSortRecursion(sequence,low,pivotloc-1);//对低子表进行递归排序
			quickSortRecursion(sequence,pivotloc+1,high);//对高子表进行递归排序
		}
	}
	
	
	/**
	 * 快排核心:一趟快速排序(一次划分)
	 * 思想:一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的关键字比另一部分记录的关键字小,
	 *     然后可对两部分记录继续进行排序,来达到整个序列有序
	 * 具体的操作为:
	 * (1)任意(通常第一个)选取一个记录最为选枢轴(pivot) ,将所有比它小的关键字安置在它的位置之前,较大的安置到它的位置之后;   
	 * (2)设置两个指针low,high;(这个指针不是程序意义上的指针),首先从high所指位置向前搜索找到第一个关键字小于枢轴的记录和枢轴记录互相交换。
	 * 然后从low 所指位置向后搜索找到第一个关键字大于枢轴的记录和枢轴记录互相交换。
	 * (3)重复这两个步骤,直到 low = high;
	 * 综述:实际上是高低位的互换过程的过程
	 * @param sequence
	 * @param low
	 * @param high
	 */
	
	protected  int  partition(int[] sequence,int low,int high){
		//选枢轴
		int pivotkey = sequence[0];//通常选序列中第一个作为枢轴关键字
		int tmp;
		
		while(low < high){//条件
			while(low < high && sequence[high] >= pivotkey) --high;
			tmp = sequence[high];
			sequence[high] = sequence[low];
			sequence[low] = tmp;
			while(low < high && sequence[low] <= pivotkey) ++low;
			tmp = sequence[high];
			sequence[high] = sequence[low];
			sequence[low] = tmp;
		}
		return low;
	}
	/**
	 * 未使用tmp变量来实现一次划分,实际上就是高低位记录互换
	 * @param sequence
	 * @param low
	 * @param high
	 * @return
	 */
	protected  int  partition2(int[] sequence,int low,int high){
		//选枢轴
		int pivotkey = sequence[low];
		
		while(low < high){//条件
			while(low < high && sequence[high] >= pivotkey) --high;
			sequence[low] = sequence[high];//高位值给低位
			sequence[high] = pivotkey;//枢轴值给高位
			while(low < high && sequence[low] <= pivotkey) ++low;
			sequence[high] = sequence[low]; //低位值给高位
			sequence[low] = pivotkey;//枢轴值给低位
		}

		return low;
	}

3、插入排序

   3.1 直接插入排序



/**
	 * 直接插入排序,是一种对简单的排序方法;
	 * 思想:将一个记录插入到已排好序的有序表中,从而得要一个新的,记录加1的有序表
	 * 操作:将序列中的第一个记录看成是一个有序序列,让后第二个记录逐个插入,直到整个序列按照关键字非递减有序序列为止
	 * @param sequence
	 */
	public void straightInsertionSort(int[] sequence){
		int i,j;
		for(i = 1;i < sequence.length;i++){
			if(sequence[i]<sequence[i-1]){
			    int tmp = sequence[i];//使用临时变量存储这个值
				sequence[i] = sequence[i-1];
				for( j = i-1;j >= 0&&tmp<sequence[j];j--){//记录后移,指含有i-1个记录的有序表的元素后移
					sequence[j+1] = sequence[j];
				}
				sequence[j+1] = tmp;//将这个值插入正确的位置
			}
		}
	 
		
	}


   3.2 希尔排序



/**
	 * 思想:先将整个待排序分成若干个子序列分别进行直接插入排序,在对全体进行一次直接插入排序
	 * @param sequence
	 * @param dk增量,不能为1
	 */
	public static void shellSort(int[] sequence,int dk){
		int i,j;
		for(int k = 0;k < dk;k++){ //按增量序列排列
			for(i = k;i < sequence.length;i++){//假定sequence[0]是有序表,所以从1开始
				if(sequence[i]<sequence[i-k]){
					int tmp = sequence[i];//使用临时变量存储这个值
					sequence[i] = sequence[i-k];
					for( j = i-k;j >= 0&&tmp<sequence[j];j-=k){//记录后移,指含有i-1个记录的有序表的元素后移
					sequence[j+k] = sequence[j];
				}
				sequence[j+k] = tmp;//将这个值插入正确的位置
			  }
			}
		}
		out(sequence);
		
	}


4、选择排序

4.1、简单选择排序(Simple Selection Sort)



/**
	 * 思想:一趟简单排序为从i..lenth中选取关键字最小记录k[minLoc]和k[i]比较,若i!=minLoc,则交换两者
	 * @param sequence
	 */
	public static void simpleSelectionSort(int[] sequence){
		int i,j,min,minLoc,tmp;
		for(i = 0;i<sequence.length;i++){
			//从sequence[0..length]中选取最小值
			min = sequence[i];
			minLoc = i;
			for(j=i;j<sequence.length;j++){
				if(sequence[j]< min){
					min = sequence[j];
					minLoc = j;
				} 
			}
			if(i!=minLoc){
				tmp = sequence[minLoc];
				sequence[minLoc] = sequence[i];
				sequence[i] = tmp;
				
			}
		}
		
	}



4.2、堆排序

      说下树形选择排序(Tree Selection Sort)的思想,TSS也叫锦标赛排序。用叶子节点存放待排序记录,两两选择最小关键字作为其根节点,在上一层的根节点在两两选择最小关键字作为其根节点,最后输出的完全二叉树的根节点为这一趟排序的最小值。然后将该该关键字在叶子节点的记录置为无穷大。在进行上面步骤。

    这个方法需要过多的辅助存储空间,与“无穷大”值进行比较多余。为了弥补,J.willioms在1994年提出了堆排序。

   实现堆排序需要解决两个问题:

                                                        (1)一个无序序列如何建成一个堆;

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

首先考虑考虑第二个问题,假设已经有一个初始堆,如何进行调整,看下面的调整过程:







图片来源:内​部​排​序



/**
	 * 思想:堆排序实现主要分成两个步骤:
	 *     (1)根据初始输入的数据利用堆的调整算法形成初始堆;
	 *     (2)通过一序列的对象交换和重新调整堆进行排序。
	 * 堆逻辑结构是一个完全二叉树,满足k(i) <= k(2i),k(i) <= k(2i+1)(小顶堆),注意这实际上就是k(i)是根节点,k(2i)和k(2i+1)是其左右子节点
	 * 存储结构可用顺序表来表示。
	 * 实现注意:
	 * 找出无序序列中第[n/2]取下限的角标(也就是无序序列中最后一个叶子节点的根)作为起始直到第一个做完筛选。
	 * @param sequence
	 */
	public static void heapSort(int[] sequence){
		int tmp;
		//形成初始堆
	
		for(int i = sequence.length/2;i>=0;i--){	
			heapAdjust(sequence,i,sequence.length-1);
		}
		//有了初始堆之后就可以进行排序了
		//每次将堆顶记录和最后一个叶子节点记录进行交换,然后再调整堆
		for(int j=sequence.length-1;j>0;j--){
			tmp = sequence[0];
			sequence[0] = sequence[j];
			sequence[j] = tmp;
			heapAdjust(sequence,0,j-1);//每次都减少了一个元素
		}
		
		for(int i=0;i<sequence.length;i++){
			System.out.print(sequence[i]+",");
		}
		
		
	}
	/**
	 * 
	 * @param sequence 待调整的序列
	 * @param r        当前根的下标
	 * @param length   当前最大堆的位置
	 */
	protected static void heapAdjust(int[] sequence,int r,int loc){
		int tmp = sequence[r];
		
		for(int j = 2*r+1;j<=loc;j=2*j+1){  //因为最初下标为0,所以选择j=2*k+1,若下标为1,则用2*k
			if((j<loc)&&(sequence[j] < sequence[j+1]))++j;//巧妙应用下标移动
			if( tmp >= sequence[j]) break;
			sequence[r] = sequence[j];
			r = j;	
		}
		sequence[r]=tmp;
	}
	

5、归并排序

          归并排序的辅助空间为O(n),也就是需要与序列等长的辅助空间,归并排序是利用空间去换取时间的一个典型算法。使用递归来实现,不是很好理解这个递归。

在网上找了一张图,便可一目了然:


图片来源:归并排序


顺序表合并如何所示:



/**
	 * 归并排序也是采用分治的思想
	 * 思想:	有序表的合并
	 *      初始时刻将每个待排记录当成有序表,然后合并
	 * 
	 * @param sequence   待排序序列
	 * @param orderSequence  辅助序列空间,有序表
	 * @param s          起始下标
	 * @param m          结束下标
	 */
	public static int[] mergeSort(int[] sequence,int[] orderSequence,int s,int m ){
		int t;
		int[] orderSequence1= new int[m+1] ;
		if(s==m) {
			orderSequence[s] = sequence[s];	
		}
		else{
			t = (s+m)/2;
			mergeSort(sequence,orderSequence1,s,t);
			mergeSort(sequence,orderSequence1,t+1,m);
			merge(orderSequence1,orderSequence,s,t,m);//将orderSequence1[s..t]和orderSequence[t+1...m]归并到sequence1[s..m]
		}
		return orderSequence;
	}
	/**
	 *该方法就是顺序表的合并
	 * 将orderSequence1[s..t]和orderSequence1[t+1...m]归并到orderSequence[s..m]
	 * @param orderSequence1
	 * @param orderSequence
	 * @param s   起始下标          
	 * @param t   分界下标
	 * @param m   结束下标
	 */
	protected static void merge(int[] orderSequence1, int[] orderSequence, int s, int t, int m){
		//将orderSequence1中的记录由小到大并入orderSequence
		int k,j;
		for(k=s,j=t+1;k<=t&&j<=m;s++){
			if(orderSequence1[k]<orderSequence1[j]){
				orderSequence[s] = orderSequence1[k];
				k++;
			}else{
				orderSequence[s] = orderSequence1[j];
				j++;
			}
		}
		if(k<=t){  //将orderSequence1[k..t]复制到orderSequence[s..m]
			for(int q = k;k<=t;k++){
				orderSequence[s] = orderSequence1[k];
				s++;
			}
		}
		if(j<=m){//就将orderSequence1[j..m]复制到orderSequence[s..m]
			for(int p = j;p<=m;p++){
				orderSequence[s] = orderSequence1[p];
				s++;
			}
		}
	}


6、基数排序

     理解基数排序之前,可以先理解下桶排序。基数排序是对桶排序的一种改进,这种改进是让“桶排序”适合于更大的元素值集合的情况,而不是提高性能


/**
	 * 
	 * 思想:
	 * 算法的步骤如下:  
	 * 1.找出待排序的数组中最大和最小的元素  
	 * 2.统计数组中每个值为i的元素出现的次数,存入桶的第i项  
	 * 3.对所有的计数累加(从桶中的第一个元素开始,每一项和前一项相加)  
	 * 4.反向填充目标数组:将每个元素i放在新数组的第bucket(i)项,每放一个元素就将bucket(i)减去1
	 * @param sequence  //带排序记录
	 * @param radix     //基数:位数的取值范围
	 * @param digit     //待排序的位数
	 */
	public void radixSort(int[] sequence,int radix,int digit){
		int[] bucket = new int[radix];  //桶
		int[] tmp = new int[sequence.length]; //缓存数组
		int divide = 1;
		for(int i=0;i<=digit;i++){
			//重置bucket数组
			Arrays.fill(bucket, 0);
			// 将sequence中的元素复制tmp数组中  
            System.arraycopy(sequence, 0, tmp, 0, sequence.length);
            
			for(int j=0;j<=sequence.length-1;j++){//按位数上的值分配到桶中
				int digitvalue  = (tmp[j]/divide)%radix;//该位数上的值
				bucket[digitvalue]++;
			}
			// 累加桶计数的值 
			for(int j=1;j< radix;j++){
				bucket[j] = bucket[j]+bucket[j-1];
			}
			//按桶中收集的值对序列进行有序序列
			for(int j=sequence.length-1;j>=0;j--){
				int digitvalue = (tmp[j]/divide)%radix;
				sequence[--bucket[digitvalue]] = tmp[j];
			}
			divide*=radix;
		}
		
	}


参考资料:

         1.严蔚敏 数据结构

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值