数据结构的排序算法比较

排序算法属于算法中的一种,而且是覆盖范围极小的一种,但彻底掌握排序算法对程序开发是有很大的帮助的。

对于一种算法,一般从如下3个方面来衡量算法的优劣。

    时间复杂度
    空间复杂度
    稳定行

对于一般的排序,可以使用非常简单的排序来完成,如直接选择,直接插入等,但也有一些非常优秀,但又非常复杂的排序算法,如:快速排序,基数排序等。大致有如下几种:

    选择排序:(直接选择排序,堆排序)
    交换排序(冒泡排序,快速排序)
    插入排序(直接插入排序,折半插入排序,Shell排          序)
    归并排序
    桶排序
    基数排序

按如上的分类,大致有六大类具体有十种排序方法。下面我们一一学习这十种排序方法。
在这里插入图片描述

选择排序

1.直接选择排序:
简单的排序方法,它的基本思想是:第一次从R[0]R[n-1]中选取最小值,与R[0]交换,第二次从R[1]R[n-1]中选取最小值,与R[1]交换,….,第i次从R[i-1]R[n-1]中选取最小值,与R[i-1]交换,……,第n-1次从R[n-2]R[n-1]中选取最小值,与R[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列·
直接选择排序的优点是算法简单,容易实现,缺点是每趟只能确定一个元素,n个数组要进行n-1趟比较

@Test
public void StraightSelectionSort() {
 //测试数据
    int[] data = { 14, 13, 11, 15, 12 }; 
    //遍历实现
     for (int i = 0; i < data.length-1; i++) {
      for (int j = i+1; j < data.length; j++) {
       if (data[i] > data[j]) { 
          int temp = data[i]; 
          data[i] = data[j]; 
          data[j] = temp; 
                } 
            } 
       } 
       //输出验证
     for(int i=0;i<data.length;i++)            
     System.out.println(data[i]);
}

如上代码就是一个最简单的直接排序算法,但我们的程序还有很大的改进空间,因为在n-1趟比较中,每趟比较的目的就是选出本趟中的最小元素而放在第一位,所以我们在每趟比较中找出最小的元素后只与第一位的元素进行一次交换,而不是在每次比较中一旦发现某个数比第一位小就立即交换。
根据刚刚分析的思路,改进上面的排序:

@Test
    public void StraightSelectionSort() {
        //测试数据
        int[] data = { 14, 13, 11, 15, 12 };
        //遍历实现
        for (int i = 0; i < data.length-1; i++) {
            //minIndex永远保留本次比较中的最小值的索引
            int minIndex = i;
            for (int j = i+1; j < data.length; j++) {
                if (data[minIndex] > data[j]) {
                     minIndex = j;
                }
            }
            //每趟比较最多交换一次
            if(minIndex!=i){
            int temp = data[i];
            data[i] = data[minIndex];
            data[minIndex] = temp;
            }
        }
     //输出验证
        for(int i=0;i<data.length;i++)
        System.out.println(data[i]);
    }

直接排序算法中,n项数据中,数据交换的次数为n-1次。但程序比较的次数较多。其时间效率为O(n的平方),空间效率较高O(1),它是不稳定的。

2.堆排序
首先来看什么是堆:
n个关键字序列Kl,K2,…,Kn称为(Heap),当且仅当该序列满足如下性质(简称为堆性质):
(1)ki<=k(2i)且ki<=k(2i+1)(1≤i≤ n/2),当然,这是小根堆,大根堆则换成>=号。

若将此序列所存储的向量R[1…n]看做是一棵完全二叉树的存储结构,则堆实质上是满足如下性质的完全二叉树:
对于小根堆:
树中任一非叶子结点的关键字均不大于其左右孩子,即根节点为最小值。
对于大根堆:
树中任一非叶子结点的关键字均不小于其左右孩子,即根节点为最大值。

通过上面的介绍我们不难发现,对于一个数组进行堆排序的关键就是将其建立为一个堆。只要建立成堆后,就选择出了最大值或者最小值。
堆排序的过程:对于包含n个元素的数组

  • 第1趟将索引0~n-1处的全部数据建成大根堆或小根堆,就可以选择出最大值或者最小值
  • 将上一步建立的堆的根节点与这组数据的最后一个节点交换,就使得其最大值或最小值排在最后。
  • 第2趟将索引0~n-2处的全部数据建成大根堆或小根堆,就可以选择出这组数据中最大值或者最小值
  • 将上一步建立的堆的根节点与这组数据的倒数第2个节点交换,就使得其最大值或最小值排在倒数第2个
  • ……
  • 第K趟将索引0-n~k处的全部数据建成大根堆或小根堆,就可以选择出这组数据中最大值或者最小值
  • 将上一步建立的堆的根节点与这组数据的倒数第k个节点交换,就使得其最大值或最小值排在倒数第k个。

堆排序就是不断重复以下两步:
建堆 拿堆的根节点与最后一个元素比较或交换
不难发现对于n个数据元素的数组,堆排序需要经过n-1次建堆。每次建堆的作用就是选出最值,因为堆排序本身就是一种选择排序。

堆排序与直接选择排序的差别在于,堆排序可通过树形结构及时的保存部分比较结果,可减少比较次数而提升效率

现在我们完成堆排序的重点就在于如何建堆了。

建堆的过程

1.先把数组转换成完全二叉数
    
2.从最后一个非叶子节点开始,比较它于两个子节 点的值。如果某个子节点的值大于父节点的值,就交换两者。
 3.向前逐步提升至根节点,即保证每个父节点的值都大于或等于其两个子节点,建堆完成。

堆排序的实现

  @Test
 public void headSort(){
	 //测试数据
	 int[] data = { 14, 13, 11, 15, 12 }; 
	 //遍历数组   去除让两位数来建堆。所以为i<data.length-1
	 for(int i=0;i<data.length-1;i++){ 
		 //建堆 
         bulidMaxHead(data,data.length-i-1); 
         //交换堆顶和最后一个元素
         swap(data, 0, data.length-i-1); 
     } 
	 //输出验证
	 for(int i=0;i<data.length;i++)
		 System.out.println(data[i]); 
	 } 
 
 //从0~j建堆
 private void bulidMaxHead(int[] data,int j) {
	 //从最后的节点的父节点开始
	 for(int i=(j-1)/2;i>=0;i--){
		 int nowIndex = i; 
		 //判断当前节点的子节点是否存在,一定是while循环来判断。因为要确定在交换之后的节点的子节点依然是它最大
		 while(nowIndex*2+1<=j){ 
			 //定义出nowIndex的左子节点 
			 int bigIndex = nowIndex*2+1; 
			 //如果nowIndex*2+1小于j,则为右子节点 nowIndex*2+2存在 
			 if(bigIndex<j){ 
				 //当右节点存在时,判断右节点和左节点谁更大 
				 if(data[bigIndex]<data[bigIndex+1]){ 
					 //总是让bigIndex指向较大的一个字节点
					 bigIndex++;
					 } 
				 } 
			 //父节点与其子节点中较大的值比较 
			 if(nowIndex<data[bigIndex]){
				 //保证父节点的值大于任何一个子节点 
				 swap(data,nowIndex,bigIndex); 
				 nowIndex = bigIndex; 
				 }else{ 
					 break; 
					 } 
			 } 
		 }
	 } 
 private void swap(int[] data,int i,int j){ 
	 int temp = data[i];
	 data[i] = data[j]; 
	 data[j] = temp;
	 }
				 }
			 }
		 }
	 }
 }

堆排序的关键在于建堆,这样就可以选择出其中的最大元素值。然后放置在最后一个位置上。假设有n个数据,需要进行n-1次建堆。每次建堆消耗时间为以2为第n的对数。时间效率为O(n*以2为第n的对数).堆排序的空间效率很高O(1).堆排序同样是不稳定的。

交换排序

交换排序的主题操作就是对数据中的数据不断进行交换操作。交换排序主要有冒泡排序和快速排序。
1.冒泡排序

  1. 冒泡排序思路简单,易于实现: 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
  2. 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
  3. 针对所有的元素重复以上的步骤,除了最后一个。 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
  4. 在最坏的情况下n个数组进行冒泡排序需要n-1趟比较
@Test
public void bubbleSort(){
	//测试数据
	int[] data = { 14, 13, 11, 15, 12 };
	for(int i=0;i<data.length-1;i++){ 
		//标识是否发生了交换 
		boolean flag = true; 
		for(int j=0;j<data.length-1-i;j++){
			if(data[j]>data[j+1]){
				swap(data,j,j+1);
				flag= false; 
				} 
			}
		//如果在某趟遍历中没有发生交换,说明已经排序好了没必须继续遍历了 
		if(flag){ break; 
		}
		} 
	//输出验证 
	for(int i=0;i<data.length;i++) 
		System.out.println(data[i]);
	} 
private void swap(int[] data,int i,int j){
	int temp = data[i];
	data[i] = data[j]; 
	data[j] = temp;
	}
	}
}

排序算法是稳定的算法。而其时间效率是不确定的,在最好的情况下仅执行1趟冒泡做n-1趟比较,无交换,而在最坏的情况下执行n-1趟冒泡,第i趟做了n-i次比较。并执行n-i-1次对象交换。空间效率很高O(1).

2.快速排序:
快速排序(Quicksort)是对冒泡排序的一种改进。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

一趟快速排序的算法是:

1)设置两个变量i、j,排序开始的时候:i=0,j=N-1; 2)以第一个数组元素作为关键数据,赋值给key,即key=A[0];
3)从j开始向前搜索,即由后开始向前搜索(j–),找到第一个小于key的值A[j],将A[j]和A[i]互换;
4)从i开始向后搜索,即由前开始向后搜索(i++),找到第一个大于key的A[i],将A[i]和A[j]互换;
5)重复第3、4步,直到i=j;(3,4步中,没找到符合条件的值,即3中A[j]不小于key,4中A[i]不大于key的时候改变j、i的值,使得j=j-1,i=i+1,直至找到为止。找到符合条件的值,进行交换的时候i, j指针位置不变。另外,i==j这一过程一定正好是i+或j-完成的时候,此时令循环结束)。

代码实现:

private void swap(int[] data,int i,int j){
	 int temp = data[i];
	 data[i] = data[j];
	 data[j] = temp; 
	 } 
 @Test
 public void quickSort(){
	 //测试数据 
	 int[] data = { 14, 13, 11, 15, 12 };
	 //对data数组从0 ~length-1索引的数据进行整理 
	 subSort(data,0,data.length-1); 
	 //输出验证 
	 for(int i=0;i<data.length;i++)
		 System.out.println(data[i]);
	 }
 }
 private void subSort(int[] data,int start,int end){ 
	 //判断是否需要排序 if(start<end){ 
	 //随意选择一个数作为中间值
	 int key = data[start]; 
	 //i从左边开始搜索,找出小于分界值的元素的索引
	 int i = start; 
	 //j从右边开始搜索,找出大于分界值元素的索引
	 int j = end+1; 
	 while(true){
		 //执行两个while循环直到找出左边大于分界值的  或者  右边大于分界值的 
		 while(i<end&&data[++i]<data[start]);
		 while(j>start&&data[--j]>data[start]); 
		 if(i<j){ 
			 swap(data, i, j);
		 }else{ 
			 break;
			 } 
	  } 
	 //把刚刚循环的中界值放在正确的位置上
	 swap(data,start,j); 
	 //递归左子数 
	 subSort(data, start,j-1);
	 //递归右子数 
	 subSort(data,j+1, end); 
	 }
 }

快速排序的空间效率很好,因为每趟都能确定的元素呈指数增长。
快速排序需要使用递归,而递归使用栈,因此它的空间效率为O(以2为底n的对数)
快速排序含跳跃式交换,因此是不稳定的排序算法。

插入排序

插入排序是一种非常常见的排序方法,包含直接插入,Shell排序和折半插入等等。

1.直接插入排序
每次从无序表中取出第一个元素,把它插入到有序表的合适位置,使有序表仍然有序。
第一趟比较前两个数,然后把第二个数按大小插入到有序表中; 第二趟把第三个数据与前两个数从后向前扫描,把第三个数按大小插入到有序表中;依次进行下去,进行了(n-1)趟扫描以后就完成了整个排序过程。

代码实现:

  @Test 
  public void straightInsertionSort(){
	  //测试数据 
	  int[] data = { 14, 13, 11, 15, 12 };
	  for(int i=1;i<data.length;i++){ 
		  int temp = data[i]; 
		  //如果索引为i比i-1小,则把i插入到i-1处
		  if(temp<data[i-1]){ 
			  int j = i-1 ; 
			  //插入i到i-1时,要把i-1之后的(包含i-1)所以元素都向后移动一遍 
			  for(;j>=0&&data[j]>temp;j--){
				  data[j+1] = data[j];
			  } 
			  //最后把i放在合适的位置 
			  data[j+1] = temp; 
			  } 
		  } 
	  //输出验证 
	  for(int i=0;i<data.length;i++) {
		  System.out.println(data[i]);
	  }
}

直接插入排序空间效率很高O(1).时间复杂度O(n的平方),直接插入排序是稳定的。

2.折半插入排序
折半插入排序(binary insertion sort)是对直接插入排序算法的一种改进,对于直接插入排序算法而言。当第i-1趟需要将第i个元素插入前面的0~i-1个元素序列中时,它总是从i-1个元素开始,逐个比较每个元素,直到找到它的位置。这显然没有利用前面i-1已经是有序的特点,而折半查找排序则改进了这一点。

对于折半插入排序,在第i-1趟需要把第i个元素插入到前面0~i-1个元素序列中时,它不会直接依次比较。具体做法如下:

计算0~i-1索引的中间点,也就是用i索引和(0+i-1)/2处的元素进行比较,如果i索引处的元素大,则在(0+i-1)/2~i-1半个范围内搜索,反之在0~(0+i-1)/2半个范围搜索,这就是所谓的折半。
在确定好的半个范围之类不断的重复第一步的操作,范围不断缩小。1/2,1/4,1/8从而确定第i个元素的插入位置。
之后的做法与前面的大致一样

代码实例:

 @Test
 public void binaryInsertionSort(){
	 //测试数据 
	 int[] data = { 14, 13, 11, 15, 12 }; 
	 //对data数组从0 ~length-1索引的数据进行整理
	 binarySort(data); 
	 //输出验证 
	 for(int i=0;i<data.length;i++) {
		 System.out.println(data[i]); 
		 }
 }
private void binarySort(int[] data){
	int length = data.length;
	for(int i=1;i<length;i++){
		int temp = data[i];
		int low = 0;
		int height = i-1;
		while(low<=height){
			//找到中间值
			int mid = (low+height)/2;
			if(temp>data[mid]){
				//限制在大于中点搜索
				low = mid+1;
				}else{
					//限制在小于中点搜索
					height = mid-1;
					}
			} 
		//将low到i处的所有元素向后整体移一位 
		for(int j=i;j>low;j--){
			data[j] = data[j-1];
			}
		//最后将tmp的值插入合适位置 
		data[low]=temp; 
		}
	}

折半插入排序和直接插入排序效果基本相同,只是更快一些。

3.Shell排序
对于直接插入排序而言,当插入排序执行到一半时,待插值左边的所有数据都已经处于有序状态,直接插入和折半插入都把待插值存储在一个临时变量里。然后,从待插值左边边第一个数据单元开始 ,只要该数据单元的值大于待插值。就把该数据单元右移动一格,直到找到第一个小于待插值的数据单元。接下来,将临时变量里的值放入小于待查值的数据单元之后(前面所有数据都右移过,因此该数据单元有一个空格)。

分析上面的算法我们发现一个问题:如果我们有一个很小的元素位于最右端,则在排序过程中中间所有的数据元素都有向右移动一格。而Shell排序对其做了改进,通过增大插入排序之间的间隔,而使得这些数据项跨度的移动,当这些数据项排过一次序之后,shell排序算法减少数据项的间隔再进行排序,依次下去。进行这些排序时的数据项之间的间隔称为增量。

核心思想是:待排序列有n个元素,先取一个小于n的整数h1作为第一个增量,把待排序列以间隔h1分成若干子序列,子序列内使用插入排序;然后取第二个增量h2(< h1),重复上述的划分和排序,直至所取的增量hl = 1 (h1 > h2 > … > hl)。

这样不管序列多么庞大,在先前较大步长分组下每个子序列规模都不是很大,用直接插入效率很高;后面步长变小,子序列变大,但由于整体有序性越来越明显,排序效率依然很高,大大提高了时间效率。示意图:
在这里插入图片描述从上面介绍可知,最终确定Shell排序算法的关键在于确定h序列的值。而对于h的值,通过按如下计算来产生,h从1开始。

h = 3*h+1

当h很大的时候,每次移动的数据量非常小。因此shell排序效率很高。

 @Test 
 public void shellSort(){ 
	 //测试数据
	 int[] data = { 14, 13, 11, 15, 12 }; 
	 //开始排序
	 int arrayLength = data.length;
	 //h保存增量
	 int h =1; 
	 //按h*3+1得到具体的增量
	 while(h<=arrayLength/3){ 
		 h=h*3+1;
		 } 
	 while(h>0){ 
		 for(int i=h;i<arrayLength;i++){
			 //当整体移动时,保证data[i]的值不变 
			 int temp = data[i]; 
			 //当i索引的值大于前面的值表示无需插入,而此时i-1之前的数据都是有序的,i-1索引元素的值就是最大值
			 if(data[i]<data[i-h]){
				 int j =i-h; 
				 //整体后移h格 
				 for(;j>=0&&data[j]>temp;j-=h){
					 data[j+h]=data[j]; 
					 } 
				 //最后把tmp的值插入合适位置 d
				 ata[j+h] = temp; 
				 }
			 }
		 h=(h-1)/3; 
		 } 
	 //输出验证 
	 for(int i=0;i<data.length;i++)
		 System.out.println(data[i]);
 }

Shell排序是一种不稳定的排序算法。

归并排序

归并基本思想是将两个或两个以上有序的序列合并成一个新的有序序列。
而归并排序先将长度为n的无序序列看成是n个长度为1的有序子序列,然后两两合并,得到n/2个长度为2的有序子序列,再进行两两合并….不断重复,最终得到一个长度为n的有序序列。

设有数列{6,202,100,301,38,8,1}

初始状态: [6] [202] [100] [301] [38] [8] [1] 比较次数

i=1 [6 202 ] [ 100 301] [ 8 38] [ 1 ] 3

i=2 [ 6 100 202 301 ] [ 1 8 38 ] 4

i=3 [ 1 6 8 38 100 202 301 ] 4

总计: 11次

代码实例:

 @Test 
 public void testMerge(){
//测试数据
	 int[] data = { 14, 13, 11, 15, 12 };
	 mergeSort(data,0,data.length-1); 
	 //输出验证
	 for(int i=0;i<data.length;i++)
		 System.out.println(data[i]); 
} 
 public void mergeSort(int[] data,int left,int right){
	 if (left>= right) 
		 return; 
	 // 找出中间索引
	 int center= (left + right) / 2; 
	 // 对左边数组进行递归 
	 mergeSort(data,left, center);
	 // 对右边数组进行递归 
	 mergeSort(data,center + 1, right);
	 // 合并
	 merge(data,left, center, right); } 
 /**
 }

     * 将两个数组进行归并,归并前面2个数组已有序,归并后依然有序

     *

     * @param data 数组对象

     * @param left 左数组的第一个元素的索引

     * @paramcenter 左数组的最后一个元素的索引,center+1是右数组第一个元素的索引

     * @param right右数组最后一个元素的索引

     */
 public void merge(int[] data, int left, int center, int right) {
	 // 临时数组
	 int[]tmpArr = new int[data.length];
	 // 右数组第一个元素索引
	 int mid =center + 1;
	 // third 记录临时数组的索引
	 int third =left; 
	 // 缓存左数组第一个元素的索引
	 int tmp =left; 
	 while (left<= center && mid <= right) {
		 // 从两个数组中取出最小的放入临时数组 
		 if(data[left] <= data[mid]) {
			 tmpArr[third++] = data[left++];
			 } else{ 
				 tmpArr[third++] = data[mid++];
				 } 
		 }
	 // 剩余部分依次放入临时数组(实际上两个while只会执行其中一个)
	 while (mid<= right) { 
		 tmpArr[third++] = data[mid++];
		 } 
	 while (left<= center) {
		 tmpArr[third++] = data[left++];
		 } 
	 // 将临时数组中的内容拷贝回原数组中
	 // (原left-right范围的内容被复制回原数组)
	 while (tmp<= right) { 
		 data[tmp] = tmpArr[tmp++];
		 }
 }

基数排序

(radixsort)则是属于“分配式排序”(distribution sort),基数排序法又称“桶子法”(bucket sort)或bin sort,顾名思义,它是透过键值的部份资讯,将要排序的元素分配至某些“桶”中,藉以达到排序的作用,基数排序法是属于稳定性的排序,其时间复杂度为O (nlog®m),其中r为所采取的基数,而m为堆数,在某些时候,基数排序法的效率高于其它的比较性排序法。

基数排序(Radix sort)是一种非比较型整数排序算法,其原理是将整数按位数切割成不同的数字,然后按每个位数分别比较。由于整数也可以表达字符串(比如名字或日期)和特定格式的浮点数,所以基数排序也不是只能使用于整数。基数排序的发明可以追溯到1887年赫尔曼·何乐礼在打孔卡片制表机(Tabulation Machine)上的贡献[1]。

它是这样实现的: 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零. 然后, 从最低位开始, 依次进行一次排序.这样从最低位排序一直到最高位排序完成以后, 数列就变成一个有序序列.
基数排序的方式可以采用LSD(Least significantdigital)或MSD(Most significantdigital),LSD的排序方式由键值的最右边开始,而MSD则相反,由键值的最左边开始。

以LSD为例,假设原来有一串数值如下所示:

73,22, 93, 43, 55, 14, 28, 65, 39, 81

首先根据个位数的数值,在走访数值时将它们分配至编号0到9的桶子中:

0

1 81

2 22

3 73 93 43

4 14

5 55 65

6

7

8 28

9 39

接下来将这些桶子中的数值重新串接起来,成为以下的数列:

81,22, 73, 93, 43, 14, 55, 65, 28, 39

接着再进行一次分配,这次是根据十位数来分配:

0

1 14

2 22 28

3 39

4 43

5 55

6 65

7 73

8 81

9 93

接下来将这些桶子中的数值重新串接起来,成为以下的数列:

14,22, 28, 39, 43, 55, 65, 73, 81, 93

这时候整个数列已经排序完毕;如果排序的对象有三位数以上,则持续进行以上的动作直至最高位数为止。

LSD的基数排序适用于位数小的数列,如果位数多的话,使用MSD的效率会比较好,MSD的方式恰与LSD相反,是由高位数为基底开始进行分配,其他的演算方式则都是相同。

代码说明:

 public class RadixSort {
	 publicstatic void sort(int[] number, int d) {
		 int k= 0;
		 int n= 1;
		 int m= 1; 
		 int[][] temp = new int[number.length][number.length]; 
		 int[]order = new int[number.length];
		 while(m <= d) { 
			 for (int i = 0; i < number.length; i++) {
				 int lsd = ((number[i] / n) % 10);
				 temp[lsd][order[lsd]] = number[i]; 
				 order[lsd]++; 
				 } 
			 for (int i = 0; i < d; i++) { 
				 if (order[i] != 0)
					 for (int j = 0; j < order[i]; j++) {
						 number[k] = temp[i][j];
						 k++; 
						 } 
				 order[i] = 0;
				 } 
			 n*= 10; 
			 k= 0;
			 m++; 
			 } 
		 } 
	 publicstatic void main(String[] args) {
		 int[]data = { 73, 22, 93, 43, 55, 14, 28, 65, 39, 81, 33, 100 };
		 for(int i = 0; i < data.length; i++) {
			 System.out.print(data[i] + " ");
			 } 
		 System.out.println("\n排序后:");
		 RadixSort.sort(data, 10); 
		 for(int i = 0; i < data.length; i++) { 
			 System.out.print(data[i] + " ");
			 }
	 } 
}

猜你喜欢

转载自blog.csdn.net/weixin_38361153/article/details/88106854
今日推荐