-
序言
这里说的六种比较排序算法分别是:冒泡排序、选择排序、插入排序、快速排序、归并排序、堆排序。如果只记忆代码,我感觉这几种排序很容易就会搞混,然后忘记,毕竟我们关心的是能否出现正确的排序结果,而如何实现的——不好意思,等我有时间再看吧。所以,这里还是希望能够记住排序的思想,那么今后无论使用何种语言,比较排序就不是问题了。
代码之后的时间复杂度、空间复杂度及稳定性分析可能有出入,请批判的的去阅读。
-
冒泡排序
冒泡排序应该是我们接触的最简单的排序,如果说世界上还是一种适用于记忆的排序,那么我会选择冒泡。算法的原理只是就是构造一种“上浮”机制。我们以将一个无序数组按从小到大排序为例进行说明。那么我们的“上浮”机制是什么呢?就是将数组中最大的数排到数组最后,那么我们就实现了所有比这个数小的数组元素的整体“上浮”。这就相当于正常情况下将木块和铁块一起丢到水里,然后结果可想而知。算法的具体实现如下:
package chapter1.sort;
/**
* 冒泡排序,是通过嵌套循环,达到数字按大小顺序依次沉淀,使符合比较规则的元素逐渐上浮
*/
public class BubbleSort {
/**
* 元素交换的过程
*/
public void swap(int[] A, int i, int j){
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
/**
* 元素排序的过程
*/
public void sort(int[] A, int n){
for(int j = 0; j < n - 1; j++){
for(int i = 0; i < n - j - 1; i++){
if(A[i] > A[i + 1]){
swap(A, i, i+1);
}
}
}
}
/**
* 测试用例
*/
public static void main(String[] args){
int[] A = {5, 7, 8, 2, 1, 9, 4, 3, 6};
BubbleSort bs = new BubbleSort();
bs.sort(A, A.length);
for (int i = 0; i < A.length; i++){
System.out.print(A[i]);
}
}
}
我们看到了,冒泡排序的关键步骤就是比较排序。元素的排序过程中,外层循环起控制作用,负责决定内层排序执行排序的范围(i < n - j -1),而内层循环是则执行“比较交换”动作 ,达到元素上浮的目的。
空间复杂度计算:因为中swap中的temp是额外空间,所以其空间复杂度为O(1)。
时间复杂度计算:首先明确比较排序的原子操作为比较。外层循环执行了n-1次,内层循环最多执行 n-1次,最少执行1次,那么其时间复杂度函数为(n-1)*[(n-1 + 1)/2] = (n^2 - n)/2, 按照时间复杂度计算原则,去掉常数,去掉最高此项系数,结果为O(n^2)。
算法稳定性:相对稳定。之所以说的是相对稳定,是因为如果将sort函数中的比较规则纳入了"相等元素也需要互换"的情况,那么这个算法就不是稳定的了。
-
选择排序
我一直想用一种想思考方式去理解选择排序和插入排序的区别,因为不知道是不是我语文不好的原因,总将这两种排序混淆。如何记忆选择排序,我们来回顾下小学生站队。首先确定排队的规则身高由低到高,那么选择排序的方式是什么呢?我先选择站在第一位的学生,和后面的同学比较身高,如果发现了身高比这名学生还要低的学生,那么让原来的学生回去,比较身高的换成这名身高更低的学生,如此进行直到结束,然后将身高最低的学生与第一名学生互换位置,之后第二名学生进行比较,直到所有位置的学生都比较了身高。算法的具体实现如下:
package sort;
/**
* 简单选择排序 寻找最大(小)的数,然后在之后的数列中重复该操作
*/
public class SelectionSort {
/**
* 元素交换的过程
*/
public void swap(int[] A, int i, int j){
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
/**
* 元素排序的过程
*/
public void sort(int[] A, int n){
for (int i = 0; i < n-1; i++){
int min = i;
for (int j = i+1; j < n; j++){
if(A[j] < A[min]){
min = j;
}
}
if(min != i){
swap(A, i, min);
}
}
}
/**
* 测试用例
*/
public static void main(String[] args){
int[] A = {5, 7, 8, 2, 1, 9, 4, 3, 6};
SelectionSort ss = new SelectionSort();
ss.sort(A, A.length);
for (int i = 0; i < A.length; i++){
System.out.print(A[i]);
}
}
}
空间复杂度计算:因为中swap中的temp是额外空间,所以其空间复杂度为O(1)。
时间复杂度计算:O(n^2)。
算法稳定性:相对不稳定。不稳定的情况出现在选择的过程中,如果出现较小的元素,在两个相同的较大元素之后,那么排序的时候,两个较大的元素可能出现相对位置互换,例如{6, 7, 8, 7, 3}。
-
插入排序
之前说了选择排序和插入排序容易混淆,那么我再说下我怎么记忆插入排序的吧。还是小学生排队的问题,这次的比较是先第一名和第二名比较身高,排出高低顺序,之后第三名和前两名比较,这时,如果第三名比前两名身高都低,那么前两名各向后移动一位,然后第三名站到前两名之前。之后的排序方式都是新的学生与前面的已经排好序的学生比较身高,当找到合适位置的时候,这个位置及其后面的学生向后移动一位,然后比较的学生站到这个位置,直到所有位置的学生都比较了身高。算法的具体实现如下:
package sort;
/**
* 简单插入排序 新元素与之前的有序元素进行比较,找到位置后,该位置及其后面元素向后移一位
*/
public class InsertionSort {
/**
* 元素排序的过程
*/
public void sort(int[] A, int n){
for (int i = 1; i < n; i++){
int temp = i;
int j = i - 1;
while (j >= 0 && A[j] > temp){
A[j+1] = A[j];
j --;
}
A[j+1] = temp;
}
}
/**
* 测试用例
*/
public static void main(String[] args){
int[] A = {5, 7, 8, 2, 1, 9, 4, 3, 6};
InsertionSort is = new InsertionSort();
is.sort(A, A.length);
for (int i = 0; i < A.length; i++){
System.out.print(A[i]);
}
}
}
空间复杂度计算:因为中比较中需要的temp是额外空间,所以其空间复杂度为O(1)。
时间复杂度计算:O(n^2)。
稳定性:相对稳定。之所以说的是相对稳定,是因为如果将sort函数中的比较规则纳入了"相等元素也需要互换"的情况,那么这个算法就不是稳定的了。
-
快速排序
快速排序应该是面试中提问频率最高的的排序算法了,而JAVA的Arrays类中的排序方法,也是以快速排序为基础的双轴快速排序。所以掌握快速排序是很有必要的。排序思想呢,我们还是用上面的小学生排队来举例。首先,我们选出一名学生,原队列中比这名学生高的学生站在他的后面,而比他矮的站在他的前面,之后对前后两个队列分别重复上面的操作,直到整个队列身高有序。算法的具体实现如下:
package sort;
/**
* 快速排序,以二分的思想不停的比较并细化队列,直到队列有序
*/
public class QuickSort {
/**
* 元素交换的过程
*/
public void swap(int[] A, int i, int j){
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
/**
* 快排分队过程
*/
private int partition(int[] A, int left, int right){
int pivot = A[right];
int tail = left - 1;
for (int i = left; i < right; i++){
if(A[i] <= pivot){
swap(A, ++tail, i);
}
}
swap(A, tail + 1, right);
return tail + 1;
}
/**
* 元素排序的过程
*/
public void sort(int[] A, int left, int right){
if (left >= right) return;
int pivot_index = partition(A, left, right);
sort(A, left, pivot_index - 1);
sort(A, pivot_index + 1, right);
}
/**
* 测试用例
*/
public static void main(String[] args){
int[] A = {5, 7, 8, 2, 1, 9, 4, 3, 6};
QuickSort qs = new QuickSort();
qs.sort(A, 0, A.length - 1);
System.out.println("这是快速排序:");
for (int i = 0; i < A.length; i++){
System.out.print(A[i]);
}
System.out.println();
}
}
空间复杂度计算:取决于递归过程中递归树的深度,最差为O(n),最优为O(logn)。
时间复杂度计算:介于O(n^2)与O(logn)之间。
稳定性:相对不稳定。不稳定的情况出现在快速分队的过程中,如果出现较小的元素,在两个相同的较大元素之后,那么排序的时候,如果以这个较小的元素为基准(或者说轴),那么两个较大的元素可能出现相对位置互换,例如{6, 7, 8, 7, 3},以3为基准。
-
归并排序
归并排序也是一种以二分法为思想基础去理解的排序,其最终的目的是先将数组分为最小的二元组或一元组进行分别排序,然后在对各个元组进行排序,最后递归直至所有队列有序。还是以小学生排队为例,先将队列从前到后两两分队,如果是单数,那么单人是一个队,然后这些分队内部先排序,之后相邻队的学生两两进行一次排序,如此重复,直至最后是两个大队的学生排序完成,整个队列就排序完成。算法的具体实现如下:
package sort;
/**
* 归并排序,将排序先局部,再整体
*/
public class MergeSort {
/**
* 元素交换的过程
*/
public void merge(int A[], int left, int mid, int right){
int tempLength = right - left + 1;
int[] temp = new int[tempLength];
int index = 0;
int i = left;
int j = mid + 1;
while (i <= mid && j <= right){
temp[index++] = A[i] <= A[j] ? A[i++] : A[j++];
}
while (i <= mid){
temp[index++] = A[i++];
}
while (j <= right){
temp[index++] = A[j++];
}
for (int k = 0; k < tempLength; k++){
A[left++] = temp[k];
}
}
/**
* 元素排序的过程
*/
public void sortRecursion(int[] A, int left, int right){
if(left >= right){
return;
}
int mid = (left + right) / 2;
sortRecursion(A, left, mid);
sortRecursion(A, mid + 1, right);
merge(A, left, mid, right);
}
/**
* 测试用例
*/
public static void main(String[] args){
int[] A = {5, 7, 8, 2, 1, 9, 4, 3, 6};
int[] B = {5, 7, 8, 2, 1, 9, 4, 3, 6};
MergeSort ms = new MergeSort();
ms.sortRecursion(A,0, A.length - 1);
System.out.println("这是递归的归并排序:");
for (int i = 0; i < A.length; i++){
System.out.print(A[i]);
}
}
}
空间复杂度计算:归并需要额外的空间存放排序结果,即需要数组等长的空间,所以空间复杂度为O(n)。
时间复杂度计算:O(nlogn)。
稳定性:相对稳定。之所以说的是相对稳定,是因为如果将merge函数中的比较规则纳入了"相等元素也需要互换"的情况,那么这个算法就不是稳定的了。
-
堆排序
堆排序是用“堆”这种数据结构来实现排序的过程。下面我们用大根堆来讨论。先用数组生成大根堆,我们知道大根堆的最大特点是将数组中的最大数作为堆的堆顶,这样堆顶(即数组首项)为最大,那么我们想要由小到大排列,只需将数组的首项和尾项互换即可,然后将数据的前n-1项继续重复上述操作,直到堆内只有一个元素,那么这个就是数组的最小项,排序完成。算法的具体实现如下:
package sort;
/**
* 堆排序,下面为大根堆排序,堆顶(首项)与数组尾项互换,数据前n-1项继续重复上述操作
*/
public class HeapSort {
/**
* 元素交换的过程
*/
public void swap(int[] A, int i, int j){
int temp = A[i];
A[i] = A[j];
A[j] = temp;
}
/**
* 大元素前移的过程
*/
private void heapify(int[] A, int i, int size){
int left_child = 2 * i + 1;
int right_child = 2 * i + 2;
int max = i;
if(left_child < size && A[left_child] > A[max]){
max = left_child;
}
if(right_child < size && A[right_child] > A[max]){
max = right_child;
}
if(max != i){
swap(A, max, i);
heapify(A, max, size);
}
}
/**
* 建堆的过程,从堆底开始上浮大元素,注意初始值和i--
*/
private int buildHeap(int[] A, int n){
int heap_size = n;
for (int i = heap_size /2 - 1; i >= 0; i--){
heapify(A, i, heap_size);
}
return heap_size;
}
/**
* 元素排序的过程
*/
public void sort(int[] A, int n){
int heap_size = buildHeap(A, n);
while (heap_size > 1){
swap(A, 0, --heap_size);
heapify(A, 0, heap_size);
}
}
/**
* 测试用例
*/
public static void main(String[] args){
int[] A = {5, 7, 8, 2, 1, 9, 4, 3, 6};
HeapSort hs = new HeapSort();
hs.sort(A, A.length);
System.out.println("这是堆排序:");
for (int i = 0; i < A.length; i++){
System.out.print(A[i]);
}
System.out.println();
}
}
空间复杂度计算:因为中swap中的temp是额外空间,所以其空间复杂度为O(1)。
时间复杂度计算:O(nlogn)。
稳定性:不稳定。不稳定发生在堆顶和数组尾项交换的时候,这次可能会打乱数组中等值元素的相对位置,例如{9, 7, 6, 5, 7}。
-
结束语
以上就是我对这几种排序算法的理解,上面的例子可能不是很好,但是希望对大家有所帮助,如果想了解更详细的,请查看这篇文章。