数据结构(六) -- 高级排序(希尔、快速、归并、基数(桶))

1. 前言:

基本排序算法中我们介绍了三种简单的排序算法,它们的时间复杂度大O表示法都是O(N^2),如果数据量少,我们还能忍受,但是数据量大,那么这三种简单的排序所需要的时间则是我们所不能接受的。
本篇博客将介绍其他高级的排序算法:

  1. 希尔排序
  2. 快速排序
  3. 归并排序
  4. 堆排序

2. 希尔排序

2.1 插入排序存在的问题:

在插入排序中,标记符左边的元素是有序的,右边的是没有排过序的,这个算法取出标记符所指向的数据,存入一个临时变量,接着,在左边有序的数组中找到临时变量应该插入的位置,然后将插入位置之后的元素依次后移一位,最后插入临时变量中的数据。

试想,假如有一个很小的数据项在靠近右端的位置上,把这个数据项插入到有序数组中时,将会有大量的中间数据项需要右移一位,这个步骤对每一个数据项都执行了将近N次复制。虽然不是所有数据项都必须移动N个位置,但是,数据项平均移动了N/2个位置,一共N个元素,总共是N^2/2次复制,这实际上是一个很耗时的过程。

例如如下数组:
在这里插入图片描述

希尔排序就是对这一步骤进行了改进,不必一个个的移动所有中间数据项,就能把较小的数据项移动到左边,大大提高了排序效率。

2.2 基本思想

希尔排序是基于插入排序的,又叫缩小增量排序。

希尔排序通过加大插入排序时元素之间的间隔,并把这些间隔的元素组成一组进行插入排序,从而使数据能大跨度地移动。数据项之间的间隔被称为增量,习惯上还用h表示。随着间隔逐渐减小,每组包含的数组越来越多,当间隔减至1时,整个数组被分为一组,算法结束。

2.2.1 示例解释:

下图是10个数的希尔排序前三轮的解释:

在这里插入图片描述

此时,数据已经基本有序,所有元素离它在最终有序序列中的位置相差都不超过2个单元,通过创建这种交错的内部有序的数据项集合,把完成排序所需的工作量降到了最小,这也是希尔排序的精髓所在。最后只需要再进行一次插入排序即可。

2.3 增量算法

在用java实现希尔排序之前,还有一个问题需要弄清楚,就是这个增量该怎么选择?

最简单的方法是第一轮排序的间隔为N/2,第二趟排序的间隔为N/4,依次类推。但是,实践证明,这种方法有时会使运行时间降到O(N^2),并不比插入排序的效率更高。

保持间隔序列中的数字互质很重要,也就是说,除了1之外它们没有公约数。简单地取间隔为N/2,N/4,N/8…1时,没有遵循这一约束,所以使希尔排序的效率降低。

有很多种有效地生成间隔序列的方法,本文提供一种,下一节的java代码也是按照这种方法来生成间隔序列的。

这种序列生成方法是由Donald Knuth(可以百度一下,图灵奖获得者,一位计算机领域的大牛)提出来的。

数列以逆向的形式从1开始,通过递归表达式:h=3*h+1,来产生后面的间隔。

比如,我们有1000个数据项需要排序,利用h=3*h+1产生的间隔序列为:

1,4,13,40,121,364,1093,3280…

第八个数1093显然超出了要排序的元素总数,所以第一轮排序,应该选取的间隔为364,第二轮为121,第三轮为40……

2.4 代码:

// 希尔排序
public void shellSort() {
    int len = array.length;
    int counter = 1;

    int h = 1;
    while (3 * h + 1 < len) { // 确定第一轮排序时的间隔,如果1000个元素,上限就是333
        h = 3 * h + 1; // 1,4,13,40,121,364
    }

    while (h > 0) {
        for (int i = 0; i < h; i++) {
            shellInsertSort(i, h); // 对间隔为h的元素进行插入排序
        }
        h = (h - 1) / 3; // 下一轮排序的间隔

        System.out.print("第" + counter + "轮排序结果:");
        display();
        counter++;
    }

}

/**
 * - 希尔排序内部使用的插入排序:
 * -  需要进行插入排序的元素为array[beginIndex]、array[beginIndex+increment]、array[beginIndex+2*increment]...
 * - @param beginIndex 起始下标
 * - @param increment 增量
 */
private void shellInsertSort(int beginIndex, int increment) {

    int targetIndex = beginIndex + increment; // 想要插入的元素的下标

    while (targetIndex < array.length) {
        int temp = array[targetIndex];

        int previousIndex = targetIndex - increment; // 前一个元素下标,间隔为increment
        while (previousIndex >= 0 && array[previousIndex] > temp) {
            array[previousIndex + increment] = array[previousIndex]; // 比欲插入数据项大的元素后移一位
            previousIndex = previousIndex - increment;
        }
        array[previousIndex + increment] = temp; // 插入到合适的位置

        targetIndex = targetIndex + increment; // 插入下一个元素
    }

}

2.5 算法分析

在这里插入图片描述
希尔排序不像其他时间复杂度为O(N log2N)的排序算法那么快,但是比选择排序和插入排序这种时间复杂度为O(N^2)的排序算法还是要快得多,而且非常容易实现。它在最坏情况下的执行效率和在平均情况下的执行效率相比不会降低多少,而快速排序除非采取特殊措施,否则在最坏情况下的执行效率变得非常差。

迄今为止,还无法从理论上精准地分析希尔排序的效率,有各种各样基于试验的评估,估计它的时间级介于O(N^ 3/2)与O(N^7/6)之间。我们可以认为希尔排序的平均时间复杂度为O(N*(logN)2)。

2. 快速排序

2.1 基本思想:分治算法

  1. 是对冒泡排序的改进
  2. 选择一个基准元素,通常选择第一个元素或者最后一个元素;
  3. 通过一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小。另一部分记录的 元素值比基准值大;
  4. 此时基准元素在其排好序后的正确位置;
  5. 然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。

在这里插入图片描述

2.2 过程演示:

  1. 假设对以下10个数进行快速排序:
    在这里插入图片描述

  2. 首先,在这个序列中随便找一个数作为基准数,通常为了方便,以第一个数作为基准数。
    在这里插入图片描述

  3. 在初始状态下,数字6在序列的第1位。我们的目标是将6挪到序列中间的某个位置,假设这个位置是k。现在就需要寻找这个k,并且以第k位为分界点,左边的数都小于等于6,右边的数都大于等于6。那么如何找到这个位置k呢?我们要知道,快速排序其实是冒泡排序的一种改进,冒泡排序每次对相邻的两个数进行比较,这显然是一种比较浪费时间的。而快速排序是分别从两端开始”探测”的,先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量r和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i ii指向序列的最左边,指向数字6。让哨兵j指向序列的最右边,指向数字8。
    在这里插入图片描述

  4. 首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要。哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i ++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前
    在这里插入图片描述

  5. 现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下:
    在这里插入图片描述

  6. 到此,第一次交换结束。接下来开始哨兵j jj继续向左挪动(再友情提醒,每次必须是哨兵j先出发)。他发现了4 < 6,停下来。哨兵i也继续向右挪动的,他发现了9 > 6,停下来。此时再次进行交换,交换之后的序列如下
    在这里插入图片描述

  7. 第二次交换结束。哨兵j继续向左挪动,他发现了3 < 6,又停下来。哨兵i继续向右移动,此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。我们将基准数6和3进行交换。交换之后的序列如下
    在这里插入图片描述

  8. 到此第一轮“探测”真正结束。现在基准数6已经归位,此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。回顾一下刚才的过程,其实哨兵j的使命就是要找小于基准数的数,而哨兵i ii的使命就是要找大于基准数的数,直到i和j碰头为止。现在我们将第一轮“探测"结束后的序列,以6为分界点拆分成两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”。接下来还需要分别处理这两个序列。因为6左边和右边的序列目前都还是很混乱的。不过不要紧,我们已经掌握了方法,接下来只要模拟刚才的方法分别处理6左边和右边的序列即可。现在先来处理6左边的序列现吧:
    在这里插入图片描述

  9. 重复第一轮的过程,应该得到如下序列:
    在这里插入图片描述

  10. OK,现在3已经归位。接下来需要处理3左边的序列:
    在这里插入图片描述

  11. 处理之后,2已经归位,序列“1”只有一个数,也不需要进行任何处理,因此“1”也归位:
    在这里插入图片描述

  12. 对于基数右边的序列,采用和左边相同的过程。

细心的同学可能已经发现,快速排序的每一轮处理其实就是将这一轮的基准数归位,直到所有的数都归位为止,排序就结束了。接下来用图示的方法来展示完整的过程:
在这里插入图片描述

2.3 代码:

public static void quickSort3(int[] arr, int left, int right) {
	if (left >= right) return;
	int m = left;
	int n = right;
	int base = arr[left];

	while (m < n) {
		// 先循环右边,再循环左边,保证当m==n时,小的值在左侧
		// 得到比基准小的右边的下标
		while (m < n && arr[n] >= base) n--;
		// 得到比基准大的左边的下标
		while (m < n && arr[m] <= base) m++;
		if (m < n) {
			swap(arr, m, n);
		}
	}
	// 此时的情况是m==n,无需再进行位移,就将基准元素和当前位置元素进行交换
	swap(arr, left, m);
	quickSort3(arr, left, m - 1);
	quickSort3(arr, m + 1, right);
}

//交换
private static void swap(int[] arr, int left, int right) {
	int temp = arr[left];
	arr[left] = arr[right];
	arr[right] = temp;
}

2.4 算法分析:

在这里插入图片描述
快速排序之所以比较快,是因为与冒泡排序相比,每次的交换时跳跃式的,每次排序的时候设置一个基准点,将小于等于基准点的数全部放到基准点的左边,将大于等于基准点的数全部放到基准点的右边。这样在每次交换的时候就不会像冒泡排序一样每次只能在相邻的数之间进行交换,交换的距离就大的多了。因此总的比较和交换次数就少了,速度自然就提高了。当然在最坏的情况下,仍可能是相邻的两个数进行了交换。因此快速排序的最差时间复杂度和冒泡排序是一样的都是O(n^2),它的平均时间复杂度为O(nlog2(底)n) 。

3. 归并排序

3.1 基本思想:

归并排序是利用归并的思想实现的排序方法,该算法采用经典的分治策略(分治法将问题分成一些小的问题然后递归求解,而治的阶段将分的阶段得到的各答案修补在一起,即分而治之)。

在这里插入图片描述

3.2 过程详解

这边详解一下上述治的步骤2,即将[4,5,7,8]和[1,2,3,6]合并成[1,2,3,4,5,6,7,8]的过程:
在这里插入图片描述

  1. 定义一个新的数组arrNew用于存放比较后的元素
  2. 在两个需要合并的数组arr1和arr2左端分别记录下标m和n
  3. 比较arr1[m]和arr2[n]的大小,将小的放入到arrNew中,
  4. 如图arr1[0] > arr2[0],所以将arr2[0]放入到arrNew中,并将n后移一位
  5. 当arr1[0] < arr2[3]时,将arr1[0]放入到arrNew中
  6. 当arr1[2] > arr2[3]时,将arr2[3]放入到arrNew中
  7. 此时右边数组为空,则直接将左边剩余数据放入到arrNew中
  8. 最后将arrNew复制到原数组中

3.3 代码:

public class MergetSort {

	public static void main(String[] args) {
		//int arr[] = { 8, 4, 5, 7, 1, 3, 6, 2 }; //
		
		//测试快排的执行速度
		// 创建要给80000个的随机的数组
		int[] arr = new int[8000000];
		for (int i = 0; i < 8000000; i++) {
			arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数
		}
		System.out.println("排序前");
		Date data1 = new Date();
		SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String date1Str = simpleDateFormat.format(data1);
		System.out.println("排序前的时间是=" + date1Str);
		
		int temp[] = new int[arr.length]; //归并排序需要一个额外空间
 		mergeSort(arr, 0, arr.length - 1, temp);
 		
 		Date data2 = new Date();
		String date2Str = simpleDateFormat.format(data2);
		System.out.println("排序前的时间是=" + date2Str);
 		
 		//System.out.println("归并排序后=" + Arrays.toString(arr));
	}
	
	
	//分+合方法
	public static void mergeSort(int[] arr, int left, int right, int[] temp) {
		if(left < right) {
			int mid = (left + right) / 2; //中间索引
			//向左递归进行分解
			mergeSort(arr, left, mid, temp);
			//向右递归进行分解
			mergeSort(arr, mid + 1, right, temp);
			//合并
			merge(arr, left, mid, right, temp);
			
		}
	}
	
	//合并的方法
	/**
	 * 
	 * @param arr 排序的原始数组
	 * @param left 左边有序序列的初始索引
	 * @param mid 中间索引
	 * @param right 右边索引
	 * @param temp 做中转的数组
	 */
	public static void merge(int[] arr, int left, int mid, int right, int[] temp) {
		
		int i = left; // 初始化i, 左边有序序列的初始索引
		int j = mid + 1; //初始化j, 右边有序序列的初始索引
		int t = 0; // 指向temp数组的当前索引
		
		//(一)
		//先把左右两边(有序)的数据按照规则填充到temp数组
		//直到左右两边的有序序列,有一边处理完毕为止
		while (i <= mid && j <= right) {//继续
			//如果左边的有序序列的当前元素,小于等于右边有序序列的当前元素
			//即将左边的当前元素,填充到 temp数组 
			//然后 t++, i++
			if(arr[i] <= arr[j]) {
				temp[t] = arr[i];
				t += 1;
				i += 1;
			} else { //反之,将右边有序序列的当前元素,填充到temp数组
				temp[t] = arr[j];
				t += 1;
				j += 1;
			}
		}
		
		//(二)
		//把有剩余数据的一边的数据依次全部填充到temp
		while( i <= mid) { //左边的有序序列还有剩余的元素,就全部填充到temp
			temp[t] = arr[i];
			t += 1;
			i += 1;	
		}
		
		while( j <= right) { //右边的有序序列还有剩余的元素,就全部填充到temp
			temp[t] = arr[j];
			t += 1;
			j += 1;	
		}
		
		
		//(三)
		//将temp数组的元素拷贝到arr
		//注意,并不是每次都拷贝所有
		t = 0;
		int tempLeft = left; // 
		//第一次合并 tempLeft = 0 , right = 1 //  tempLeft = 2  right = 3 // tL=0 ri=3
		//最后一次 tempLeft = 0  right = 7
		while(tempLeft <= right) { 
			arr[tempLeft] = temp[t];
			t += 1;
			tempLeft += 1;
		}	
	}
}

3.4 详细解释下上述mergeSort方法:

public static void mergeSort(int[] arr, int left, int right, int[] temp) {
	if(left < right) {
		int mid = (left + right) / 2; //中间索引
		//向左递归进行分解
		// 方法1:
		mergeSort(arr, left, mid, temp);
		//向右递归进行分解
		// 方法2:
		mergeSort(arr, mid + 1, right, temp);
		//合并
		// 方法3:
		merge(arr, left, mid, right, temp);
		
	}
}

以数组[3,7,2,5,4,6,8,10]为例:

  1. 第一次进入mergeSort:left=0,right=7
    1. 判断生效,mid=3,进入递归
  2. 第二次进入mergeSort,此时为方法1调用:left=0,right=3
    1. 判断生效,mid=1,进入递归
  3. 第三次进入mergeSort,此时为方法1调用:left=0,right=1
    1. 判断生效,mid=0,进入递归
  4. 第四次进入mergeSort,此时为方法1调用:left=0,right=0
    1. 判断失效,回退到步骤3,往下走调用方法2
  5. 第五次进入mergeSort,此时为方法2调用:left=1,right=1
    1. 判断失效,回退到步骤3,往下走调用方法3
  6. 第一次进入merge方法,此时left=0,mid=0,right=1
    1. merge执行完成后得到顺序的3和7,回退到步骤2
  7. 第六次进入mergeSort,此时为方法2调用,left=2,right=3
    1. 判断生效,mid=2,进入递归
  8. 第七次进入mergeSort,此时left=3,right=3
    1. 判断失效,回退到步骤7,往下走
  9. 第二次进入merge方法,此时left=2,mid=2,right=3
    1. merge执行完成后得到顺序的2和5,回退到步骤2
  10. 第三次进入merge方法,此时left=0,mid=1,right=3
    1. merge执行完成后得到顺序的2,3,5,7,回退到步骤1
  11. 第八次进入mergeSort方法,此时left=4,right=7
  12. 接下来基本上重复,不再过多演示

在这里插入图片描述

4. 基数(桶)排序

4.1 简介

  1. 基数排序(radix sort)属于“分配式排序”,又称“桶子法”或bin sort,顾名思义,它就是通过键值的各个位的值,将要排序的元素分配至某些“桶”中,达到排序的目的
  2. 基数排序法是属于稳定性的排序,基数排序是效率高的稳定性排序法
  3. 基数排序时桶排序的扩展
  4. 实现方式:将整数按位数切割成不同的数字,然后按每个位数分别比较

4.2 基本思想

  1. 将所有待比较数值统一为同样的数位长度,数位较短的数前面补领。然后从最低位开始,依次进行一次排序。这样从最低位排序一直到最高位排序完成后,数列就变成一个有序序列
  2. 图文解释:将数组[53,3,542,748,14,214]排序:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4.3 代码实现:

public class RadixSort {

	public static void main(String[] args) {
		int arr[] = { 53, 3, 542, 748, 14, 214};
		
		// 80000000 * 11 * 4 / 1024 / 1024 / 1024 =3.3G 
//		int[] arr = new int[8000000];
//		for (int i = 0; i < 8000000; i++) {
//			arr[i] = (int) (Math.random() * 8000000); // 生成一个[0, 8000000) 数
//		}
		System.out.println("排序前");
		Date data1 = new Date();
		SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String date1Str = simpleDateFormat.format(data1);
		System.out.println("排序前的时间是=" + date1Str);
		
		radixSort(arr);
		
		Date data2 = new Date();
		String date2Str = simpleDateFormat.format(data2);
		System.out.println("排序前的时间是=" + date2Str);
		
		System.out.println("基数排序后 " + Arrays.toString(arr));
		
	}

	//基数排序方法
	public static void radixSort(int[] arr) {
		
		//根据前面的推导过程,我们可以得到最终的基数排序代码
		
		//1. 得到数组中最大的数的位数
		int max = arr[0]; //假设第一数就是最大数
		for(int i = 1; i < arr.length; i++) {
			if (arr[i] > max) {
				max = arr[i];
			}
		}
		//得到最大数是几位数
		int maxLength = (max + "").length();
		
		
		//定义一个二维数组,表示10个桶, 每个桶就是一个一维数组
		//说明
		//1. 二维数组包含10个一维数组
		//2. 为了防止在放入数的时候,数据溢出,则每个一维数组(桶),大小定为arr.length
		//3. 名明确,基数排序是使用空间换时间的经典算法
		int[][] bucket = new int[10][arr.length];
		
		//为了记录每个桶中,实际存放了多少个数据,我们定义一个一维数组来记录各个桶的每次放入的数据个数
		//可以这里理解
		//比如:bucketElementCounts[0] , 记录的就是  bucket[0] 桶的放入数据个数
		int[] bucketElementCounts = new int[10];
		
		
		//这里我们使用循环将代码处理
		
		for(int i = 0 , n = 1; i < maxLength; i++, n *= 10) {
			//(针对每个元素的对应位进行排序处理), 第一次是个位,第二次是十位,第三次是百位..
			for(int j = 0; j < arr.length; j++) {
				//取出每个元素的对应位的值
				int digitOfElement = arr[j] / n % 10;
				//放入到对应的桶中
				bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
				bucketElementCounts[digitOfElement]++;
			}
			//按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
			int index = 0;
			//遍历每一桶,并将桶中是数据,放入到原数组
			for(int k = 0; k < bucketElementCounts.length; k++) {
				//如果桶中,有数据,我们才放入到原数组
				if(bucketElementCounts[k] != 0) {
					//循环该桶即第k个桶(即第k个一维数组), 放入
					for(int l = 0; l < bucketElementCounts[k]; l++) {
						//取出元素放入到arr
						arr[index++] = bucket[k][l];
					}
				}
				//第i+1轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
				bucketElementCounts[k] = 0;
				
			}
			//System.out.println("第"+(i+1)+"轮,对个位的排序处理 arr =" + Arrays.toString(arr));
			
		}
		
		/*
		
		//第1轮(针对每个元素的个位进行排序处理)
		for(int j = 0; j < arr.length; j++) {
			//取出每个元素的个位的值
			int digitOfElement = arr[j] / 1 % 10;
			//放入到对应的桶中
			bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
			bucketElementCounts[digitOfElement]++;
		}
		//按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
		int index = 0;
		//遍历每一桶,并将桶中是数据,放入到原数组
		for(int k = 0; k < bucketElementCounts.length; k++) {
			//如果桶中,有数据,我们才放入到原数组
			if(bucketElementCounts[k] != 0) {
				//循环该桶即第k个桶(即第k个一维数组), 放入
				for(int l = 0; l < bucketElementCounts[k]; l++) {
					//取出元素放入到arr
					arr[index++] = bucket[k][l];
				}
			}
			//第l轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
			bucketElementCounts[k] = 0;
			
		}
		System.out.println("第1轮,对个位的排序处理 arr =" + Arrays.toString(arr));
		
		
		//==========================================
		
		//第2轮(针对每个元素的十位进行排序处理)
		for (int j = 0; j < arr.length; j++) {
			// 取出每个元素的十位的值
			int digitOfElement = arr[j] / 10  % 10; //748 / 10 => 74 % 10 => 4
			// 放入到对应的桶中
			bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
			bucketElementCounts[digitOfElement]++;
		}
		// 按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
		index = 0;
		// 遍历每一桶,并将桶中是数据,放入到原数组
		for (int k = 0; k < bucketElementCounts.length; k++) {
			// 如果桶中,有数据,我们才放入到原数组
			if (bucketElementCounts[k] != 0) {
				// 循环该桶即第k个桶(即第k个一维数组), 放入
				for (int l = 0; l < bucketElementCounts[k]; l++) {
					// 取出元素放入到arr
					arr[index++] = bucket[k][l];
				}
			}
			//第2轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
			bucketElementCounts[k] = 0;
		}
		System.out.println("第2轮,对个位的排序处理 arr =" + Arrays.toString(arr));
		
		
		//第3轮(针对每个元素的百位进行排序处理)
		for (int j = 0; j < arr.length; j++) {
			// 取出每个元素的百位的值
			int digitOfElement = arr[j] / 100 % 10; // 748 / 100 => 7 % 10 = 7
			// 放入到对应的桶中
			bucket[digitOfElement][bucketElementCounts[digitOfElement]] = arr[j];
			bucketElementCounts[digitOfElement]++;
		}
		// 按照这个桶的顺序(一维数组的下标依次取出数据,放入原来数组)
		index = 0;
		// 遍历每一桶,并将桶中是数据,放入到原数组
		for (int k = 0; k < bucketElementCounts.length; k++) {
			// 如果桶中,有数据,我们才放入到原数组
			if (bucketElementCounts[k] != 0) {
				// 循环该桶即第k个桶(即第k个一维数组), 放入
				for (int l = 0; l < bucketElementCounts[k]; l++) {
					// 取出元素放入到arr
					arr[index++] = bucket[k][l];
				}
			}
			//第3轮处理后,需要将每个 bucketElementCounts[k] = 0 !!!!
			bucketElementCounts[k] = 0;
		}
		System.out.println("第3轮,对个位的排序处理 arr =" + Arrays.toString(arr)); */
		
	}
}

4.4 基数排序的说明

  1. 基数排序是对传统桶排序的扩展,速度更快
  2. 基数排序是经典的空间换时间的方式,占用内存很大,对海量数据排序时,容易造成OOM
  3. 技术排序是稳定的。
    1. 假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列汇总:r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的,否则成为不稳定的
  4. 有负数的数组,我们不用基数排序来进行排序,如果要支持负数,可以分成2块,大于0和小于0,使用绝对值排序最后合并

5. 排序算法总结

首先,从算法的平均时间复杂度、最坏时间复杂度和算法所需的辅助控件三个方面,对各种排序方法进行比较如下:
在这里插入图片描述

其次,从排序方法的稳定性角度对各种排序方法加以比较。插入排序、冒泡排序、归并排序是稳定的,而选择排序、快速排序、堆排序是不稳定的。

排序算法在计算机程序设计中非常重要,根据上面比较的各种排序方法的特点,根据上面比较的各种排序方法的特点,其适用的场合也不同。在选择哪种排序方法时需要考虑如下因素:待排序的记录数目n的大小;记录本身数据量的大小,也就是记录中除关键码外的其他信息量的大小;关键码的结构及其分布情况;对排序稳定性的要求。依据这些条件,可得出结论如下:

(1)若数目n较小(n<50),可采用插入、冒泡、选择排序。如果数目较多且移动费时,应采用选择排序;

(2)若记录的初始状态依据按关键码基本有序,则选用直接插入或冒泡排序法。

(3)若数目n较大,则应采用快速排序、堆排序、归并排序法。这三个时间复杂度一样,但就平均性能而言,快速排序被认为是目前基于比较记录关键码的内部排序中最好的排序方法,但遗憾的是,快速排序在最坏情况下的时间复杂度是O(n²)(情况不常见),堆排序与归并排序在最坏情况下的时间复杂度仍保持不变。堆排序和快速排序法都是不稳定的,若要求稳则选归并。

(4)前面讨论的排序算法都是顺序存储实现的。当记录本身的信息量很大时,为避免大量时间用在移动数据上,可以用链表作为存储结构。插入、归并都容易在链表上实现。

综上所述,每一种排序方法各有特点,没有绝对最优的,应根据具体情况选择合适的排序方法,当然也可以结合使用。

摘自:
https://www.jianshu.com/p/51dffaaf7023
https://blog.csdn.net/csdn_aiyang/article/details/73108606
https://blog.csdn.net/qq_40941722/article/details/94396010

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值