【数据结构与算法】之排序全家桶(十大排序详解及其Java实现)---第七篇

数据结构与算法 同时被 2 个专栏收录
35 篇文章 8 订阅

博主秋招提前批已拿百度、字节跳动、拼多多、顺丰等公司的offer,可加微信:pcwl_Java 一起交流秋招面试经验,可获得博主的秋招简历和复习笔记。 

本篇文章汇总了10种场常见的排序算法,篇幅较长,可以通过下面的索引目录进行定位查阅:

一、排序的基本概念

二、十大经典排序算法

1、冒泡排序

2、插入排序

3、希尔排序

4、选择排序

5、归并排序

6、快速排序

7、桶排序

8、计数排序

9、基数排序

10、堆排序

对于学习排序算法的个人经验:

参考及推荐:


一、排序的基本概念

1、排序的定义

排序:就是使一串记录,按照其中的某个或者某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域都得到很大的重视,尤其在大量数据的处理方面。一个优秀的算法可以节省大量的资源。

其它相关概念:

稳定:如果A=B,且A原本在B的前面,排序之后A仍然在B的前面,则说明这种排序算法是稳定的;

不稳定:如果A=B,且A原本在B的前面,排序之后A排在了B的后面,则说明这种排序算法是稳定的。

2、排序的分类

因为排序算法应用非常广泛,所以对应得排序算法非常之多,这里我们只挑其中常用的八大经典排序算法【冒泡排序、插入排序、选择排序、快速排序、归并排序、桶排、计数排序以及基数排序】,下面对它们进行分类:

(1)按时间复杂度分类:

排序算法时间复杂度是否基于比较
冒泡排序、插入排序、选择排序O(n^{2})
快速排序、归并排序O(n\log n)
桶排序、计数排序、基数排序O(n)

(2)按线性时间非比较类排序和非线性时间比较类排序分类:

非线性时间比较排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(n\log n),因此称为非线性时间比较类排序。

线性时间非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此称为线性时间非比较类排序。

3、排序的时间复杂度和空间复杂度

排序方法平均时间复杂度最坏时间复杂度最好时间复杂度空间复杂度稳定性
桶排序O(n+k)O(n^{2})O(n+k)O(n+k)稳定
计数排序O(n+k)O(n+k)O(n+k)O(n+k)稳定
基数排序O(n)O(n * k)O(n * k)O(n * k)稳定
插入排序O(n^{2})O(n)O(n^{2})O(1)稳定
希尔排序O(nlogn)O(n^{2})O(n^{1.3})O(1)不稳定
选择排序O(n^{2})O(n^{2})O(n^{2})O(1)不稳定
冒泡排序O(n^{2})O(n)O(n^{2})O(1)稳定
快速排序O(nlogn)O(n^{2})O(nlogn)O(logn)不稳定
堆排序O(nlogn)O(nlogn)O(nlogn)O(1)不稳定
归并排序O(nlogn)O(nlogn)O(nlogn)O(n)稳定

4、如何衡量排序算法的执行效率?

对于排序算法,我们一般会从以下几个方面衡量它们的执行效率:

(1)最好情况、最坏情况以及平均情况时间复杂度

(2)时间复杂度的系数、常数、低阶

(3)比较次数和交换(或移动)次数


二、十大经典排序算法

1、冒泡排序

【ps:本部分内容参考及推荐阅读:冒泡排序算法详解

1.1  定义

冒泡排序(Bubble  Sort):是一种典型的交换排序算法,通过交换数据元素的位置进行排序。

关键的两步操作:比较和交换

1.2  基本思想

冒泡排序的基本思想就是:从无序序列头开始,进行两两比较,根据大小交换位置,直到最后将最大(或最小)的数据元素交换到了无序队列的队尾,从而成为有序序列的一部分;然后依次进行这个过程,直到所有数据元素都排好序为止。

1.3  算法描述

(1)比较相邻的元素,如果第一个比第二个大,就交换它们的位置;

(2)对每一对相邻元素作同样的工作,从开始第一对进行到结尾的最后一对,这样在最后的元素将是最大的数;

(3)针对所有位置的元素重复以上的步骤,除了最后一个;

(4)持续每次对越来越少的元素(无序元素)重复上面的步骤,直到没有任何一对相邻的数组需要比较,则序列最终有序。

1.4  图示例

如下图所示:对[3, 6, 4, 2, 11, 10, 5]这个数组进行排序的过程:每次都从头开始

1.5  代码实现

public class BubbleSort {
	
	public static int[] bubbleSort(int[] arr){
		// i用来控制需要排序多少趟,j用来控制当前元素需要比较的次数
		int i, j, temp, len = arr.length;
		// 共计多少趟排序,最后一个元素不用,前面的排好了,最后一个自动有序
		for(i = 0; i < len - 1; i++){
			// 比较次数,对剩下的无序元素进行排序
			for(j = 0; j < len - i - 1; j++){
				// 如果arr[j] > arr[j + 1],就交换它们的位置
				if(arr[j] > arr[j + 1]){  
					temp = arr[j];   
					arr[j] = arr[j + 1];
					arr[j + 1] = temp;
				}
			}
		}
		return arr;
	}
	
	
	// 测试案例
	public static void main(String[] args) {
		int[] arr = {3, 6, 4, 2, 11, 10, 5};
		int[] bubbleArray = bubbleSort(arr);
		for(int i = 0; i < bubbleArray.length; i++){
			System.out.print(bubbleArray[i] + ", ");
		}
	}
}

1.6 冒泡排序的改进

改进一:

如果你手写过上面冒泡排序的代码,你会发现,它的实现过程优点傻傻的感觉,它的想法就是:不管你本身有序还是无序,每个位置上的元素都会和相邻元素相互比较一次,再决定要不要进行位置交换。很明显这样的一个排序过程中有很多次的比较都是多余的,比如数组A = {4 ,3,5,7,8,9},很明显只需要将4和3的位置交换一次,再用4和后面的元素进行比较,就会发现这个数组已经有序了,就没有必要再进行剩下的4趟冒泡了。

针对上面的问题,我们可以在外层循环里面加入一个标志变量change,如果在一次冒泡过程中发现没有发送数据交换,则认为当前数组已经有序,则改变change变量的值,循环终止。实现代码如下:

public class BubbleSortImproved {

	public static int[] bubbleSort(int[] arr) {
		int i, j, temp, len = arr.length, changed = 1, count = 0;
		
		for (i = 0; i < len - 1 && changed != 0; i++) {
			count++;    // 记录冒泡的次数
			changed = 0;
			for (j = 0; j < len - i - 1; j++) {
				if (arr[j] > arr[j + 1]) {
					temp = arr[j];
					arr[j] = arr[j + 1];
					arr[j + 1] = temp;
					changed = 1;    // 发生位置交换,说明当前数组在本次位置交换前还是无序的
				}
			}
		}
		System.out.println(count);   // 4,如果是未改进的冒泡排序则需要6次冒泡(7-1)
		return arr;
	}

	// 测试案例
	public static void main(String[] args) {
		int[] arr = { 3, 6, 4, 2, 9, 10, 11 };
		int[] bubbleArray = bubbleSort(arr);
		for (int i = 0; i < bubbleArray.length; i++) {
			System.out.print(bubbleArray[i] + ", ");
		}
	}

}

可以看到上面代码种的测试案例,只是数组种的后三个元素有序,改进后的冒泡排序算法,只需要4次冒泡就可以完成整个数组的排序过程,而未改进的冒泡排序则需要6次冒泡才能完成最终的数组排序,实际上最后两次的冒泡排序都是多余的,增加了3次比较操作。

改进二:鸡尾酒排序/搅拌排序/来回排序

传统的冒泡排序中的每一趟排序操作都只能找到一个最大值或者最小值,第一次冒泡都是首先从左边开始一直比较到数组的最右边,然后再从最左边开始比较到无序数组的最右边(右边的最大有序数组不再参与比较)。考虑这个过程,我们可以在冒泡到无序数组中的最右边时,再进行一次往左边的冒泡操作,这样就可以在左边形成一个最小的有序数组。

简言之,在每趟排序过程中进行正向和反向两次冒泡的方法,这样一次排序可以得到两个最终值(最大值和最小值),从而使排序趟数几乎减少了一半。但是实际上,如果数组在乱序的状态下,鸡尾酒排序和传统的冒泡排序效率都很差劲。

public class BubbleSortImpoved2 {

	public static void cocktailSort(int arr[]){
		
		int i, tpme, left = 0, right = arr.length - 1;
		while(left < right){
			// 正向冒泡:从左往右找,找到最大的
			for(i = left; i < right; i++){
				if(arr[i] > arr[i + 1]){
					tpme = arr[i];
					arr[i] = arr[i + 1];
					arr[i + 1] = tpme;   // 把大的值给arr[i + 1]
				}
			}
			right--;   // 向左移动一位
			// 反向冒泡:从右往左找,找到最小的
			for(i = right; i > left; i--){
				if(arr[i - 1] > arr[i]){
					tpme = arr[i];
					arr[i] = arr[i - 1];
					arr[i - 1] = tpme;   // 把小的值给arr[i - 1]
				}
			}
			left++;   // 向右移动一位
		}
	}
		
	// 测试案例
	public static void main(String[] args) {
		
		int[] arr = {2, 3, 4, 5, 1};
		cocktailSort(arr);
	}
}

1.7 冒泡排序的性能分析

(1) 时间复杂度:(设置标志变量之后)

当原始数据元素正序排列时,冒泡排序的比较次数为n-1,移动次数为0,即最好情况时间复杂度为:O(n)

当原始数据元素逆序排列时,冒泡排序的比较次数为n(n-1)/2,移动次数为3n(n-1)/2,所以最坏时间复杂度为:O(n^{2})

当原始数据元素杂乱无序时,冒泡排序的平均时间复杂度为:O(n^{2})

(2) 空间复杂度

冒泡排序过程中只需要一个临时变量进行两两交换,所需要的额外空间为1,所以空间复杂度为O(1)

(3)稳定性

冒泡排序过程中,元素两两交换时,相同元素的前后顺序并没有发生改变,所以冒泡排序是一种稳定排序算法


2、插入排序

2.1 定义

插入排序(Insertion  Sort):是一种简单直观的排序算法。它的工作原理是通过构件有序序列,对于未排序的数据元素,在已排序序列中从后向前扫描,找到相应的位置并插入。

2.2 基本思想

顺序地把待排序的序列中的各个元素按照其关键字的大小,插入到已排序的序列中的适当位置。

2.3 算法描述

整个排序算法的执行过程建议查看:十大经典排序算法中插入排序的动态图,能够更加直观的理解。

(1)从第一个元素开始,该元素可以认为已经是被排序的了;

(2)取出下一个新元素,在已经排序的元素序列中从后向前扫描;

(3)如果该元素(已排序的)大于新元素,将该元素移动到下一个位置;

(4)重复步骤(3),直到找到已排序中小于等于新元素的位置;

(5)将新元素插入到该位置;

(6)重复步骤(2)~(5)。

2.4 图示例

2.5 代码实现

public class InsertionSort {
	
	public static int[] insertionSort(int[] arr){
		
		int i, j, temp, len = arr.length;
		
		// 从第2个元素开始,和前面有序的数列中的元素依次进行比较,直到找到小于它的位置
		for(i = 1; i < len; i++){
			temp = arr[i];
			// 最多和前面的i-1个数进行比较
			for(j = i - 1; j >= 0 && arr[j] > temp; j--){
				arr[j + 1] = arr[j];  // 如果arr[j]比arr[i]大的话,则后移一个位置
			}
			// 如果arr[j] <= arr[i]的话,则将arr[i]插入到j+1的位置,当前这个位置正好有空位,因为后面比arr[i]大的元素都后移了一个位置
			arr[j + 1] = temp;   	
		}
		return arr;
	}
	
	// 测试
	public static void main(String[] args) {
		int[] arr = {3, 6, 4, 2, 9, 10, 11 };
		int[] insertionArray = insertionSort(arr);
		for(int i = 0; i < insertionArray.length; i++){
			System.out.print(insertionArray[i] + ", ");
		}
	}
}

2.6 插入排序的性能分析

(1)时间复杂度

当原始序列正序时,直接插入排序效果最好,所有元素只需要进行一次比较(不包含第一个元素),所以共计n-1次比较,并且无需进行位置交换操作,所以直接插入排序最好情况复杂度为O(n);

当原始序列逆序时,直接插入排序效果最差,所以需要进行1+2+3+...+(n-1)次比较以及n-1次位置交换,所以最坏情况时间复杂度为O(n^{2});

当原始数据元素杂乱无序时,相当于在数组中插入一个数据(时间复杂度为O(n)),循环执行n次操作,所以直接插入排序的平均时间复杂度为:O(n^{2})

(2)空间复杂度

插入排序过程中只需要一个临时变量进行两两交换,所需要的额外空间为1,所以空间复杂度为O(1)

(3)稳定性

插入排序过程中,元素两两交换时,相同元素的前后顺序并没有发生改变,所以冒泡排序是一种稳定排序算法


3、希尔排序

3.1  定义

希尔排序是简单插入排序的改进版。它与插入排序的不同之处在于,他会优先比较距离较远的元素。希尔排序又叫缩小增量排序。

3.2 基本思想

插入排序每次只能将数据移动一位,效率是比较低的,那么希尔排序其实改进了插入排序的这个缺点。

希尔排序的基本思想:先将整个待排序的数据元素分割成若干子序列分别进行直接插入排序,待将序列中的元素基本有序时,再进行依次的插入排序。

3.3  算法描述

(1)选择一个增量序列T1,T2,.....Tk,其中对于Ti > Tj (i > j),Tk = 1;增量因子有很多种取法:最简单的就是T(i + 1) = Ti / 2;

(2)按增量序列个数k,对序列进行k趟排序;

(3)每趟排序,根据对应的增量Ti,将待排序列分割为若干长度为m的子序列,分别对各子序列进行插入排序。仅当增量因子为1时,整个序列作为一整个序列来处理。

3.4  图示例

3.5  代码实现

public class ShellSort {

	public static void shellSort(int[] arr){
		
		int incrementNum = arr.length / 3;
		while(incrementNum >= 1){
			for(int i = 0; i < arr.length; i++){
				// 进行插入排序
				for(int j = i; j < arr.length - incrementNum; j = j + incrementNum){
					if(arr[j] > arr[j + incrementNum]){
						int temp = arr[j];
						arr[j] = arr[j + incrementNum];
						arr[j + incrementNum] = temp;
					}
				}
			}
			// 设置新的增量
			incrementNum /= 3;
		}
	}
	
	// 测试
	public static void main(String[] args) {
			
		int[] arr = {70, 30, 40, 10, 80, 20, 90, 100, 75, 60, 45};
		shellSort(arr);
		System.out.println(Arrays.toString(arr));
	}
}

3.6  希尔排序的性能分析

(1)时间复杂度分析:

最好情况时间复杂度:O(n^{2})

最坏情况时间复杂度:O(n^{1.3})

平均时间复杂度为:O(nlog2n)

(2)空间复杂度分析:

空间复杂度为:O(1)

(3)稳定性分析:

插入排序过程中,元素两两交换时,相同元素的前后顺序发生了改变,所以冒泡排序是一种非稳定排序算法


4、选择排序

4.1 定义

选择排序(Select  Sort):是一种简单直观的排序算法,通过不断选择序列中最大(或最小)的元素完成排序。

4.2 基本思想

在要排序的一组数中,选出最小(或者最大)的一个数与第1个位置上的元素交换位置,然后再在剩下的数中找最小(或者最大)的数与第2个位置上的元素交换位置,依次类推,直到第n-1个元素(倒数第二个元素)和第n个元素(最后一个元素)比较为止。

4.3 算法描述

(1)在原始序列中找到最小(或最大)元素,将其和原始序列的第一个位置上的元素进行位置交换;

(2)再从剩下的未排序元素中找到最小(或最大)元素,然后放到已排序序列的末尾(即第二个元素位置);

(3)重复步骤(2),直到所有元素均有序为止。

4.4 图示例

4.5 代码实现

public class SelectionSort {

	public static int[] selectionSort(int[] arr){
		
		int i, j, temp, min, len = arr.length;
		// 共进行i-1次大循环,最后一个元素自动有序
		for(i = 0; i < len - 1; i++){
			// 在剩下的无序序列中,找出最小的元素放在位置i处
			min = i;
			for(j = i + 1; j < len; j++){
				if(arr[min] > arr[j]){
					min = j;   // 从剩下的无序序列中找到最小的元素位置
				}
			}
			temp = arr[min];
			arr[min] = arr[i];
			arr[i] = temp;
		}
		return arr;
	}
	
	// 测试
	public static void main(String[] args) {
		
		int[] arr = {49, 38, 65, 97, 76, 13, 27, 49};
		int[] selectionArray = selectionSort(arr);
		for(int i = 0; i < selectionArray.length; i++){
			System.out.print(selectionArray[i] + ", ");
		}
	}
}

4.6 选择排序的性能分析

(1)时间复杂度分析:

当原始序列正序时,也需要进行:(n-1) + (n-2) + 2 + 1次比较和0次位置交换,所以最好情况复杂度为:(O(n^{2}))

当原始序列逆序时,需要进行:(n-1) + (n-2) + 2 + 1次比较和n-1次位置交换,所以最坏情况时间复杂度为:(O(n^{2}))

当原始序列无序时,其平均时间复杂度为:(O(n^{2}))

(2)空间复杂度分析:

选择排序过程中只需要两个个临时变量temp和min,所需要的额外空间为2,所以空间复杂度为O(1)

(3)稳定性分析:

排序过程中,元素两两交换时,相同元素的前后顺序发生了改变,所以冒泡排序是一种非稳定排序算法。

因为选择排序会每次在无序序列里面选择最大或者最小的元素放到已经有序的序列最后,如果出现值相等的两个元素,选择排序会将后面的元素拿走排到有序序列的最后面,这样两个相同元素的位置就发生了改变。例如下面这个序列:

原始序列:【49, 38, 65, 97, 76, 13, 27, 49

排完序后的序列:【13,27,38,4949,65,76,97】

可以发现两个49的前后位置发生了变化,所以选择排序不是稳定排序算法。


5、归并排序

5.1  定义

归并排序(MERGE-SORT):是建立在归并操作上的一种有效的排序算法,该算法是采用分治思想(Divided  and  Conquer)的一个非常典型的应用。将已经有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。

5.2 基本思想

把待排序列分为若干个子序列,先使每个子序列是有序的,然后再把有序子序列合并为整体有序序列。

归并排序使用的就是分治思想。分治,顾名思义就是分而治之,将一个大问题分解成小的问题来解决,小的问题解决了,大的问题自然也就解决了。分治思想和递归思想很像,实际上分治算法一般都是用递归来实现的。分治是一种解决问题的处理思想,递归是一种编程技巧。归并排序采用的就是分治思想,可以用递归代码实现。

递推公式:mergeSort(p...r)  =  merge(mergeSort(p...q),  mergeSort(q...r))  

终止条件:p >= r 时不用再继续分解

5.3  算法描述

(1)把长度为n的输入序列分为两个长度为n/2的子序列;

(2)对这两个子序列分别采用归并排序;

              (2.1)申请空间,使其大小为两个已经排序序列之和,该空间用来存放合并后的序列;

              (2.2)设定两个指针,最初位置分别为两个已经排序序列的起始位置;

              (2.3)比较两个指针所指向的元素,选择相对小的元素放入合并空间,并移动指针到下一个位置;

              (2.4)重复步骤(3),直到某一指针超出序列尾;

              (2.5)将另一序列剩下的所有元素直接复制到合并序列尾部。

(3)将排序好的序列再拷贝回原数组。

5.4  图示例

5.5  代码实现

public class MergeSort {

	public static int[] mergeSort(int[] array, int low, int high){
		
		int mid = low + (high - low) / 2;    // 将当前序列分为两个子序列
		
		if(low < high){
			// 左边
			mergeSort(array, low, mid);
			// 右边
			mergeSort(array, mid + 1, high);
			// 左右归并排序
			merge(array, low, mid, high);
		}
		return array;
	}

	public static void merge(int[] array, int low, int mid, int high) {
		int[] temp = new int[high - low + 1];   // 临时数组
		int i = low;      // 左指针
		int j = mid + 1;  // 右指针
		int k = 0;        // 临时数组中的指针
		
		// 把较小的数放到临时数组中存放
		while(i <= mid && j <= high){
			if(array[i] <= array[j]){    // 注意:这里必须是<=,如果不加=,则不能保证稳定性,左边的值小,相同时,应该是左边的元素先插入
				temp[k++] = array[i++];  // 将array[i]放到temp[k]处
			}else{
				temp[k++] = array[j++];  // 将array[j]放到temp[k]处
			}
		}
		
		// 当i <= mid时,说明左边序列中元素有剩余,则把剩余的全部元素移动到临时数组
		while(i <= mid){    
			temp[k++] = array[i++];
		}
		
		// 当j <= high时,说明右边序列中元素有剩余,则把剩余的全部元素移动到临时数组
		while(j <= high){    
			temp[k++] = array[j++];	
		}
		
		// 将temp数组覆盖掉array数组,m + low是原array数组的开始下标即为low
		for(int m = 0; m < temp.length; m++){
			array[m + low] = temp[m];
		}
	}
	
	// 测试
	public static void main(String[] args) {
		int[] array = {32, 12, 56, 78, 76, 45, 36};
		
		mergeSort(array, 0, array.length - 1);
		System.out.println(Arrays.toString(array));
	}
}

5.6  归并排序的性能分析

(1)时间复杂度分析:

因为归并排序采用的是递归的实现方式,所以时间复杂度分析起来相对来说复杂一些,为了搞清楚整个过程,在这里做下详细的分析。

首先回想下递归的适用场景,一个问题A可以分解为多个子问题B和C,那么求解问题A就可以分解成求解问题B和问题C,待问题B和C解决后,我们再将B和C的求解结果合并起来,就是A的结果了。

那么下面我们定义求解问题A的时间函数是:T(A),求解问题B和C的时间函数分别为:T(B)和T(C),那么就有:

T(A)  =  T(B)  +  T(C)  +  K                                  (K:将B和C合并成问题A的结果时所消耗的时间)

由上面的分析我们可以得出这样的一个结论:不仅递归求解的问题可以写成递推公式,递归代码的复杂度也可以写成递推公式。

那么下面我们就用这个公式来分析下归并排序的时间复杂度。我们假设对n个元素进行归并排序所需要的时间是T(n),那么拆解为两个子数组排序的时间都是T(n/2)。而merge()函数合并两个子数组的时间复杂度为O(n)【n次插入和n/2次比较】,所以归并排序的时间复杂度的计算公式为:

T(1)   =   C;                                   n = 1时,只需要常量级的执行时间

T(n)   =  2  *  T(n  /  2)  +  n;     n > 1

下面分解下这个过程:

T(n)  =  2 * T(n / 2) + n

         =  2 * (2 * T(n / 4) + n / 2) + n  = 4 * T(n / 4) + 2 * n

         =  4 * (2 * T(n / 8)) + n / 4) + 2 * n  =  8 * T(n / 8) + 3 * n

         .......................

         = 2^k  *  T(n / 2^k)  +  k  *  n

则:T(n)  = 2^k  *  T(n / 2^k)  +  k  *  n;当T(n / 2^k) = T(1)的时候,即:n / 2^k  =  1,所以k  =  log2n,再将k代入T(n)中,可以得到:T(n)  =  Cn  +  nlog2n。则规定排序的时间复杂度为:O(nlog2n)。

从整个分析过程可以看出来,归并排序的执行效率与要排序的原始数组中元素的有序程度无关,所以其时间复杂度是非常稳定的,则其:

             最好情况时间复杂度为:O(nlog2n);

             最坏情况时间复杂度为:O(nlog2n)

             平均时间复杂度为:O(nlog2n)

(2)空间复杂度分析:

从分析过程可以看出来,归并排序不是原地排序。再合并两个有序子数组时,需要借助额外的存储空间。但是需要注意的是尽管每次合并操作都需要申请额外的内存空间,但在合并完成之后,临时开辟的内存空间就被释放掉了,在任意时刻,CPU只会有一个函数在执行,也只会有一个临时的内存空间在使用,临时内存空间最大也不会超过n个数据的大小,所以:

归并排序的空间复杂度为:O(n)

(3)稳定性分析:

排序过程中,元素两两交换时,相同元素的前后顺序没有发生改变,所以归并排序是一种稳定排序算法。


6、快速排序

【本部分内容引用及推荐阅读:快速排序原理及Java实现

6.1  定义

快速排序(Quick  Sort):是一种典型的交换排序算法,其通过不断的比较和移动来实现排序。其排序过程速度快、效率高。

6.2 基本思想

快速排序将关键字大的元素从前面移动到后面,关键字小的元素从后面直接移动到前面,从而减少了总的比较次数和移动次数,同时采用”分而治之“的思想,把大的拆分为小的,小的再拆分为更小的,其原理如下:

对于给定的一组元素,选择一个基准元素,通常选择第一个或者最后一个,通过一趟扫描,将序列分为两个部分,一部分比基准元素小,一部分比基准元素大,此时基准元素的的位置就是在整个序列有序后它的位置,然后用同样的方法递归地将划分的两个部分再进行上面的操作进行排序,直到序列中所有的元素都有序了为止。

快速排序的伪代码:

//  快速排序,A是数组,n表示数组的大小

quickSort(A,  0,  n-1){

           //  快速排序递归函数,p,r为下标           

          if  p  >=  r        return;

          q  =  partition(A,  p,  r) ;         // 获取基准点

          quickSort(A,  p,  q-1);           // 前半部分排序

          quickSort(A,  q+1,  r);           //  后半部分排序

}

6.3  算法描述

(1)选择一个基准点元素,通常选择第一个或者最后一个;

(2)然后分别从数组的两端扫描数组,设两个指示标志(low指向起始为止,high指向末尾),首先从后半部分开始扫描,扫描时high指针跟着移动,如果发现有元素比该基准点的值小,则就交换low和high位置上的元素。然后再从前半部分开始扫描,扫描时low指针跟着移动,如果发现有元素比基准点的值大时,则交换high和low位置上的元素

(3)循环(2),直到lo >= hi,然后把基准点的值放到high这个位置,一次排序就完成了;

(4)采用递归的方式分别对前半部分和后半部分进行排序,当前半部分和后半部分均有序时,整个数组自然也就有序了。

6.4  图示例

6.5  代码实现

public class QuickSort {

	public static void quickSort(int[] arr, int low, int high){
		if(low >= high){
			return;
		}
		
		// 进行第一轮排序获取分割点
		int index = partition(arr, low, high);
		// 排序前半部分
		quickSort(arr, low, index - 1);
		// 排序后半部分
		quickSort(arr, index + 1, high);
	}
	
	/**
	 * 一次快速排序
	 * @param arr   数组
	 * @param low   数组的前下标
	 * @param high  数组的后下标
	 * @return      key的下标index,也就是分片的间隔点
	 */
	public static int partition(int[] arr, int low, int high){
		
		// 固定的切分方式
		int key = arr[low];   // 选区数组的前下标上的元素为基准点
		
		while(low < high){
			// 从后半部分往前扫描
			while(high > low && arr[high] >= key){
				high--;
			}
			arr[low] = arr[high];    // 交换位置,把后半部分比基准点位置元素值小的元素交换到前半部分的low位置处
			
			// 从前半部分往后扫描
			while(high > low && arr[low] < key){
				low++;
			}
			arr[high] = arr[low];  // 交换位置,把前半部分比基准点位置元素值大的元素交换到后半部分的high位置处
		}
		arr[high] = key;   // 最后把基准存入
		return high;
	}
	
	// 测试案例
	public static void main(String[] args) {
	    int[] arr = {49, 38, 65, 97, 76, 13, 27, 49};

	    quickSort(arr, 0, arr.length-1);

	    for(int i:arr){
	        System.out.print(i+",");
	    }
	}
}

6.6  快速排序的性能分析

(1)时间复杂度分析:

快速排序采用的也是递归的方式,所有也可以使用时间复杂度的递推公式:

T(1)   =   C;                                   n = 1时,只需要常量级的执行时间

T(n)   =  2  *  T(n  /  2)  +  n;     n > 1

但是,公式成立的前提是:每次分区操作,我们选择的分区基准点pivot都很合适,正好能将大区间对等地一分为二,所以快速排序的最好情况时间复杂度为:O(nlogn)

然而实际上,每次分区都能将其一分为二是很难实现的。如果数组中的原始数据已经有序了,比如数组A = {1,3,5,7,9},如果我们每次选择第一个元素或者最后一个元素作为pivot,那么每次分区得到的两个区间都是不平等的。我们需要进行大约n次分区操作,才能完成整个快排过程。每次分区我们平均要扫描大约n/2个元素,所以快速排序的最坏情况时间复杂度为:O(n^{2}),其实这个时候快速排序就退化成了冒泡排序;

我们可以利用递归树求解出:快速排序的平均时间复杂度为:O(nlogn),只有在极端情况下会退化到O(n^{2})

(2)空间复杂度分析:

空间复杂度为:O(n)

(3)稳定性分析:

排序过程中,元素两两交换时,相同元素的前后顺序发生了改变,所以归并排序是一种非稳定排序算法。

6.7  快速排序的改进

【参考及推荐阅读:快速排序算法Java详解

综合上面的分析,我们总结下快速排序的优缺点:

优点:(1)对于当数据量很大的时候,快速排序很容易将某个元素放到对应的位置;

缺点:(1)如果原始数组就是有序的,那么快速排序过程中对序列的划分会十分的不均匀,将序列划分为:1和n-1大小(时间复杂度为:O(n^{2})),但是我们理想状态下是二分(时间复杂度为:O(nlogn))

                         (2)对于小数组进行排序时,也需要递归好几次才能将数据放到正确的位置上;

                         (3)快速排序不是稳定的排序算法,当重复数据比较多时,效率比较低。

那么下面就分别针对上面所列的三个缺点提出三种改进版的快速排序算法:

【1】三数取中法:优化分区时选取基准点pivot

由于快速排序在原始数据有序时,将退化为冒泡排序,其事件复杂度为O(n^{2})。解决的办法就是找一个可能在文件的中间位置的元素作为pivot。则可以选取数组中最左边元素、最右边元素以及中间元素中中间大小的元素作为pivot,这样使得最坏情况几乎不可能再发生,其次它减少了观察哨的需要。

// 三数取中
// 下面两步保证了array[high]是最大的
int mid = low + (high - low) / 2;
if(array[mid] > array[hi]){
    swap(array[mid], array[high]);
}
if(array[low] > array[high]){
    swap(array[low], array[high])
}

// 下面这一步只用比较array[low]和array[mid],让两者较大的在array[low]位置上
if(array[mid] > array[low]){
    swap(array[mid], array[low]);
}

int key = array[low];


public void swap(arr[a], arr[b]){
    int t;
    t = arr[a];
    arr[a] = arr[b];
    arr[b] = t;
}

【2】序列较小时,使用插入排序代替快速排序

快速排序在针对大文件(数组length比较大的)有很大的优势,但是对于小文件其优势将被削弱。对于基本的快速排序中,当递归到后面时【分区越来越小】,程序会调用自身的许多小文件,需要递归好几次才能将数据放入到正确的位置,因而在遇到子文件时需要我们对传统的快速排序算法进行改进。一种方法就是:每次递归开始之前对文件的大小进行测试,如果小于设定值,则将调用插入排序【插入排序对小文件的排序比较好】

private static final int M = 10;
public void quickSort(int[] arr, int low, int high){
    if(low >= high)   return; 
    if(high - low <= M)  return;   // 小数组不用排序

    int i = partition(arr, low, high);
    quickSort(arr, low, i - 1);  // 左边排序
    quickSort(arr, i + 1, high); // 右边排序
}

public void sort(int[] arr, int low, int high){
    quickSort(arr, low, high);
    insertionSort(arr, low, high);   // 小数据时使用插入排序
}

public void partition(int[] arr, int low, int high){
    // ...省略
}

【3】重复元素较多时,使用三分区法

通过划分让相等的元素连续地摆放,然后只对左侧小于V的序列和右侧大于V的序列进行排序。

如上图所示,从左至右扫描数组,维护一个指针lt使得[ lo...lt - 1]中的元素都比V小,一个指针gt使得所有[ gt + 1 ... hi ]的元素都大于V,以及一个指针i,使得所有[ lt ... i - 1 ]的元素都和V相等。元素[i...gt]之间是还没处理到的元素,从lo开始,从左至右开始扫描:

1、如果a[ i ] < V:交换a[ lt ]和a[ i ],lt和i自增;

2、如果a[ i ] > V:交换a[ i ]和a[ gt ],gt自减;

3、如果a[ i ] = V:i自增


下面的7、8、9三个小节分别对同桶排序、计数排序以及基数排序进行讲解。这些排序算法的时间复杂度都是线性的,所以把这类排序算法叫做线性排序(Linear  Sort)。之所以能够做到线性的时间复杂度,主要原因是,这三个排序算法是非基于比较的排序算法,都不涉及到元素之间的比较操作。

这几种排序算法理解起来不难,时间和空间复杂度也很简单,但是对要排序的数据要求很苛刻,所以重点学习这些排序算法的适用场景

7、桶排序

7.1  定义

桶排序(Bucket  Sort):又被称为:箱排序,是非基于比较的排序算法,因此其时间复杂度是线性的,为O(n)。

7.2 基本思想

桶排序的基本思想:将要排序的数据分到几个有序的桶里,每个桶里的数据再单独进行排序,有可能再使用其他排序算法或是递归的方式继续使用桶排序进行排序。桶内排完序后,再把每个桶里的数据按照顺序依次取出,组成的序列就是有序的了。当要被排序的数组内数值是均匀分配的时候,桶排序使用线性时间O(n)。

7.3  算法描述

(1)设置一个定量的数组作为空桶;

(2)遍历数列,并且把数据元素挨个放到对应的桶中;

(3)对每个不是空的桶子进行排序;

(4)从不是空的桶子里把项目再放回原来的序列里。

7.4  图示例

7.5  代码实现

public class BucketSort {

	/**
	 * @param arr:待排序数组
	 * @param max:数组中的最大值的范围
	 */
	public static void bucketSort(int[] arr, int max) {

		int[] buckets;

		if (arr.length == 0) {
			return;
		}

		// 创建一个容量为max的数组buckets,并且将buckets中的所有元素初始化为0
		buckets = new int[max];

		// 1.计数    统计数组中相同值大小的个数,并将这个数放入到对应的桶中,值是多少,就放入到几号桶中
		for (int i = 0; i < arr.length; i++) {
			buckets[arr[i]]++;
		}

		// 2.排序,这里是每个桶仅仅对应一个数,所以无需在单个桶中进行排序了,但是如果是一个桶中分了多个数据,那么还要继续在单个桶中进行排序
		for (int i = 0, j = 0; i < arr.length; i++) {
			while ((buckets[i]--) > 0) {
				arr[j++] = i;
			}
		}

		buckets = null;
	}

	public static void main(String[] args) {
		
		int arr[] = { 8, 2, 3, 4, 3, 6, 6, 3, 9 };
		bucketSort(arr, 10); // 桶排序

		System.out.println(Arrays.toString(arr));
	}

}

7.6  桶排序的性能分析

(1)时间复杂度分析:

我们把n个数据均匀地划分到m个桶内,每个桶里就有k=n/m个元素。每个桶内部再使用快速排序,时间复杂度为O(k * logk)。m个桶排序的时间复杂度就是O(m * k * logk),因为k = n / m,所以整个桶排序的时间复杂度为O(n * log(n / m))。当桶的个数m接近数据个数n的时候,log(n / m)就是一个非常小的常量,这个时候桶排序的时间复杂度接近O(n)

(2)空间复杂度分析:

在分析时间复杂度的时候可以发现,桶排序的时间复杂度取决于对各个桶之间数据进行排序的时间复杂度,很明显,桶划分的越小,各个桶之间的数据越少,排序所有的时间也会很少,但是相应的空间消耗就会比较大,典型的空间换时间的做法。

桶排序要有两个数组的空间开销,一个存放待排序数组,还需要一个额外的,就是所谓的桶,比如待排序的值是0到m-1,那就需要m个桶,这个桶数组至少需要m个空间,所以桶排序的空间复杂度为:O(n)

(3)稳定性分析:

排序过程中,元素两两交换时,相同元素的前后顺序没有发生改变,所以归并排序是一种稳定排序算法。

7.7  桶排序的适用场景

可以看到我们上面在对时间复杂度分析的时候,做了很多的假设,最后才得出时间复杂度为O(n),可以看出来桶排序对要排序的数据的要求还是十分苛刻的。

首先,要排序的n个数据要很容易的划分到m个桶里,并且桶与桶之间有着天然的大小顺序。这样每个桶内的数据都排序完成后,桶与桶之间的数据不需要再进行排序。

其次,数据在各个桶之间的分布是比较均匀的。如果数据经过桶划分之后,有些桶里面的数据非常多,有些非常少,很不平均,那桶内数据的时间复杂度就不再是常量级了。在极端情况下,如果数据都被划分到一个桶里,那就退化为O(nlogn)的排序算法了。

从上面的分析可以看出来,桶排序比较适合在外部排序中,所谓的外部排序就是数据存储在外部磁盘中,数据量比较大,内存有限,无法将数据全部加载到内存中。

比如说我们有10GB的订单数据,我们希望按订单金额进行排序,但是我们的内存有限,只有几百MB,没有办法一次性将10GB的数据全都加载到内存中,这个时候我们就可以借助桶排序来解决这个问题了。我们可以先扫描一遍文件,看订单金额所处的数据范围。假设经过扫描之后我们得到订单金额最小是1元,最大是10万元。我们将所有订单根据金额划分到100个桶里,第一个桶我们存储金额在1元到1000元之间的订单,第二个桶存储金额在1001元到2000元之内的订单,以此类推。每一个桶对应一个文件,并且按照金额范围的大小顺序编号命名(00,01,02 .... 99)。

理想的情况下,如果订单金额在1到10万之间均匀分布,那订单会被均匀划分到100个文件中,每个小文件中大约存储100MB的订单数据,我们就可以将这100个小文件依次放到内存中,再用快速排序来排序,等所有文件都排好序之后,我们只需要按照文件编号,从小到大依次读取每个文件中的订单数据,并将其写入一个文件中,那这个文件中存储的就是按照金额从小到大的订单数据了。

不过你可能也发现,订单按照金额在1元到10万元之间并不一定是均匀的,所以10GB订单数据是无法均匀地划分到100个文件中的。有可能某个金额区间的数据特别多,划分之后对应的文件就会很大,没法一次性读入内存。那么针对这些划分之后还是比较大的文件,我们可以继续划分,比如:订单金额在1到1000元的数据比较多,我们就将这个区间继续划分为10个小区间,1~100,101~200 ... 901~1000元。如果划分之后,101~200元之间的订单还是太多,无法一次读入内存,那就再继续进行划分,直到所有的文件都能读入到内存为止。

【ps:桶排序适用场景内容参考自极客时间的《数据结构与算法之美》专栏】


8、计数排序

8.1  定义

计数排序:不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

8.2 基本思想

下面用极客时间的《数据结构与算法之美》专栏里的例子对计数排序的基本思想进行解释:

假如xxx省2018年参加高考的考生为50w,考试的满分为700分,最低分为0分,这个数据范围比较小,所以我们可以将其分成901个桶,1分对应一个桶。根据考生的成绩将这50w的考生划分到901个桶里。桶内的数据都是分数相同的考生,所以并不需要再进行排序了,我们只需要依次扫描每个桶,将桶内的考生依次输出到一个数组中,就实现50w考生的排序。因为只涉及扫描遍历操作,所以时间复杂度为O(n)。

计数排序的算法思想就是这么简单,和桶排序十分类似,只是桶的大小粒度不同而已。那么”计数“的含义从何谈起呢?下面举个例子:

假设现在只有8个考生,分数在0到5分之间。这8个考生的成绩我们放在一个数组A[8]中,它们分别是:2,5,3,0,2,3,0,3。考生成绩从0分到5分,我们使用大小为6的数组C[6]表示桶,其中下标对应分数,值对应该分数为该下标的考生个数。我们只需要遍历一遍考生分数就可以得到C[6]的值,如图1所示:

图1

从图1中可以看出,分数为3的考生有3个,小于3分的考生有4个,所以成绩为3分的考生在排序之后的有序数组R[8]中,会保存下标为4,5,6的位置。

图2

那么我们如何快速计算出每个分数的考生在有序数组中对应的存储位置呢?这个处理方法非常巧妙,很不容易想到。思路是这样的:我们对C[6]数组顺序求和,C[6]存储的数据就变成了图3所示的样子。C[k]里存储小于等于分数k的考生个数。

图3

有了前面的准备之后,我们从后向前依次扫描数组A,比如扫描到3的时候,我们可以从数组C中取出下标为3的值7,也就是说,到目前为止,包括自己在内,分数小于等于3的考生有7个,也就是说3是数组R中的第7个元素(也就是数组R中下标为6的位置)。当3放入到数组R中后,小于等于3的元素只剩下6个了,所以相应的C[3]要减1,变成6.

以此类推,当我们扫描到第2个分数为3的考生的时候,就会把它放入数组R中的第6个元素位置(也就是数组R中下标为5的位置)。当我们扫描完整个数组A后,数组R内的数据就是按照分数从小到大有序排列的了。

图4

这种利用另外一个数组来计数的实现方式是非常巧妙的,这也是为什么我们把这种排序算法叫做计数排序的原因。

总结一下:

计数排序只能用在数据范围不大的场景中,如果数据范围k要比排序的数据n大很多,就不适合用计数排序了。而且计数排序只能给非负整数排序,如果排序的数据是其他数据类型,要将其在不改变相对大小的情况下,转化为非负整数。

比如:如果考生的成绩精确到小数点后一位,我们就需要将所有分数都先乘以10,转化为整数,然后再放到9010个桶内。再比如,如果要排序的数据中有分数,数据范围是[-1000, 1000],那么我们就需要先对每个数据都加1000,转化为非负整数。

8.3  算法描述

(1)找出待排序的数组中最大和最小的元素;

(2)统计数组中每个值为i的元素出现的次数,存入数组的C的第i项;

(3)对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);

(4)反向填充目标数组,将每个元素i放在新数组的第C[ i ]项,每放一个元素就将c[ i ]减去1。

8.4  图示例

如8.2 算法思想中的图片分析过程。

8.5  代码实现

public class CountSort {

	public static void countSort(int[] a, int n){
		
		if(n <= 1){
			return;
		}
		
		// 查找数组中数据的范围
		int max = a[0];
		for(int i = 1; i < n; i++){
			if(max < a[i]){
				max = a[i];
			}
		}
		
		int[] c = new int[max + 1];   // 申请一个计数数组C,下标大小为[0, max]
		for(int i = 0; i <= max; i++){
			c[i] = 0;     // 将数组c中的元素都初始化为0
		}
		
		// 计算每个元素的个数,放入c中
		for(int i = 0; i < n; i++){
			c[a[i]]++;    
		}
		
		// 依次累加
		for(int i = 1; i <= max; i++){
			c[i] = c[i - 1] + c[i];   // c数组当前下标的值等于当前下标对应值的数目和前面所有值数目之和
		}
		
		// 临时数组r,存储排序之和的结果
		int[] r = new int[n];
		// 计算排序的关键步骤
		for(int i = n - 1; i >= 0; i--){
			int index = c[a[i]] - 1;
			r[index] = a[i];
			c[a[i]]--;
		}
		
		// 将结果拷贝给数组a
		for(int i = 0; i < n; i++){
			a[i] = r[i];
		}	
	}
	
	// 测试
	public static void main(String[] args) {
		
		int[] a = {2, 5, 3, 0, 2, 3, 0, 3};
		countSort(a, 8);
		System.out.println(Arrays.toString(a));
	}
}

8.6  计数序的性能分析

(1)时间复杂符分析:

计数排序的时间复杂度为:O(n+k)

(2)空间复杂度分析:

计数排序的空间复杂度为:O(n+k)

(3)稳定性分析:

排序过程中,元素两两交换时,相同元素的前后顺序没有发生改变,所以归并排序是一种稳定排序算法。


9、基数排序

【推荐阅读:不基于比较的基数排序原理图解

9.1  定义

基数排序(Radix  Sort):是一种非比较型整数排序算法,其原理是将字符串按位数切割成不同的单个字符,然后按每个位数分别比较。

9.2 基本思想

基本思想:基数排序是按照低位优先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高为。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级的在前,高优先级相同的低优先级高的在前。

对于基数排序,我们更多的还是需要掌握它的思想,所以这里看一个应用场景【来自:极客时间《数据结构与算法之美》专栏】:

假设我们要对10w个手机号进行从小到大的排序,我们可以使用时间复杂度为O(nlogn)。但是桶排序和计数排序就不太适用了,因为手机号11位,范围太大了。那么另外一种时间复杂度也为O(n)的基数排序就可以很好的解决这个问题。

手机号从小到大排序问题的规律:假设要比较连个手机号Num1和Num2的大小,如果在前面几位中,Num1的手机号码已经比Num2的号码大了,那么后面的几位就没有必要再进行比较了。

因此,我们可以先按照最后一位来排序手机号码,然后,再按照倒数第二位重新排序,依次类推,最后按照第一位重新排序,经过11次排序之和,手机号码就有序了。需要注意的是,这里按照每位来排序的算法必须是稳定的,因为最后一次排序只会考虑最高位的大小顺序,完全不管其他位的大小关系,那么低位排序就没有意义了。

根据每一位来排序,我们可以借助桶排序或者计数排序,因为它们都是稳定的,它们的时间复杂度都为O(n)。如果要排序的数据有k位,那我们就需要k次桶排序或者计数排序,总的时间复杂度为O(k*n)。当k不大的时候,比如手机号11位,所以基数排序的时间复杂度就近似于O(n)。

实际上,有时候要排序的数据并不是都等长的,比如我们排序牛津字典中的20w个英文单词,最短的只有1个字母,最长的是尘肺病,45个字母。对于这种不等长的数据,我们可以把所有的单词补齐到相同的长度,位数不够的可以在后面补“0”,因为补0不会影响到原有的大小顺序。

总结:基数排序对要排序的数据是有要求的,需要可以分割出独立的“位”来比较,而且位之间有递进关系,如果a数据的高位比b数据大,那剩下的低位就不用再比较了。除此之外,每一位的数据范围都不能太大,要可以用线性排序算法来排序,否则,基数排序的时间复杂度就无法做到O(n)了。

9.3  算法描述

(1)、根据待排序整数序列的进制d(十进制为10,十六进制为16...)设置d个桶,编号分别为0,1,...,d-1;
            (2)、各个记录按照其关键字最低位的值的大小放入到相应的桶中;
            (3)、按照桶编号从小到大的顺序收集各个桶中的数据,对于同一桶中的数据按照先后次序收集,先进桶先收集;
            (4)、按照关键字的次低位,重复上述步骤...(没有高位的数据则高位补0 )按增量序列个数k,对序列进行k 趟排序; 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

9.4  图示例

用五个字符串简化排序过程

9.5  代码实现

public class RadixSort {

	/**
	 * @param arr  待排序的数组
	 * @param d    最大数的位数
	 */
	public static void radixSort(int[] arr, int d){
		
		int k = 0, n = 1, m = 1;
		// 数组的第一位表示可能的余数0-9
		int[][] temp = new int[10][arr.length];
		int[] order = new int[10];
		while(m <= d){
			for(int i = 0; i < arr.length; i++)
            {
                int lsd = ((arr[i] / n) % 10);
                temp[lsd][order[lsd]] = arr[i];
                order[lsd]++;
            }
            for(int i = 0; i < 10; i++)
            {
                if(order[i] != 0)
                    for(int j = 0; j < order[i]; j++)
                    {
                        arr[k] = temp[i][j];
                        k++;
                    }
                order[i] = 0;
            }
            n *= 10;
            k = 0;
            m++;
        }
    }
	
	// 测试
    public static void main(String[] args){
        
    	int[] data = {73, 22, 93, 43, 55, 14, 28, 65, 39, 81, 33, 100};
        radixSort(data, 3);
        System.out.println(Arrays.toString(data));
    }
}

9.6  基数排序的性能分析

(1)时间复杂度分析:

 基数排序的时间复杂度是O(2*k*n),其中n是排序元素个数,k是数字位数

(2)空间复杂度分析:

空间复杂度采用数组是O(n*d),采用链表是O(d+n),d是进制(关键字的取值范围)

(3)稳定性分析:

排序过程中,元素两两交换时,相同元素的前后顺序没有发生改变,所以归并排序是一种稳定排序算法。


【ps:还没有复习到堆这一块,先空着,后面再补上...】

10、堆排序

10.1  定义

 

10.2 基本思想

 

10.3  算法描述

 

10.4  图示例

 

10.5  代码实现

 

10.6  堆排序的性能分析

 


对于学习排序算法的个人经验:

1、最好先找找到每个排序算法的动态图,结合其排序思想,这样就可以比较直观的理解。但是这样还是不够的,下一步要做的就是代码实现,并且编写对应的测试案例,debug模式,跟着循环走,可以加深你对排序过程的理解。

2、对于每种排序的Java代码实现,建议多看几篇浏览量多的博客中的实现以及评论区中提出来的问题,因为每个人的代码风格不一样,而且每一种排序算法都有很多种实现方式,你需要找到属于你最容易理解的那种,当然也要膜拜一些大佬的代码,可能你需要写很多行,人家几行就搞定了。这个过程能够体会出算法之美!虽然有种被吊打的感觉.......

3、常用的算法(比如:快速排序、插入排序、堆排序等)既要了解它们的基本思想还要掌握代码的实现过程,但是对于不常用的算法(比如:桶排序、计数排序和基数排序)只需要掌握其思想,代码不用过多的去记忆。


参考及推荐:

1、十大经典排序算法(动图演示)【推荐阅读,内含排序过程的动态图演示过程】

2、九大排序算法再总结

3、八大排序算法

4、常见排序算法总结【推荐】

学习不是单打独斗,如果你也是做Java开发,可以加我微信:pcwl_Java,一起分享学习经验!

  • 0
    点赞
  • 1
    评论
  • 10
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

©️2022 CSDN 皮肤主题:大白 设计师:CSDN官方博客 返回首页

打赏作者

pcwl1206

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值