经典排序算法原理解析与优劣对比

衡量一个排序算法的优劣,我们主要会从以下 3 个角度进行分析:

1时间复杂度,具体包括,最好时间复杂度、最坏时间复杂度以及平均时间复杂度。

2空间复杂度,如果空间复杂度为 1,也叫作原地排序。

3稳定性,排序的稳定性是指相等的数据对象,在排序之后,顺序是否能保证不变

   

【冒泡排序】

冒泡排序最好时间复杂度是 O(n)

冒泡排序最坏时间复杂度会比较惨,是 O(n*n)

平均时间复杂度也是 O(n*n)

冒泡排序不需要额外的空间,所以空间复杂度是 O(1)

冒泡排序过程中,当元素相同时不做交换,所以冒泡排序是稳定的排序算法

实现如下:

public static void bubbleSort(int[] nums) {

int size = nums.length;

for (int i=0; i<size; i++) {

for (int j=i; j<size; j++) {

if (nums[i] > nums[j]) {

int temp = nums[i];

nums[i] = nums[j];

nums[j] = temp;

}

}

}

}

   

【插入排序】

插入排序最好时间复杂度是 O(n)

插入排序最坏时间复杂度则需要 O(n*n)

插入排序的平均时间复杂度是 O(n*n)

插入排序不需要开辟额外的空间,所以空间复杂度是 O(1)

插入排序是稳定的排序算法;

实现如下:

public static void sortInsert(int[] nums) {

int size = nums.length;

for (int i = 1; i<size; i++) {

int temp = nums[i];

int j=i-1;

for (; j>=0; j--) {

if (temp < nums[j]) {

nums[j+1] = nums[j];

} else {

break;

}

}

nums[j+1] = temp;

}

}

   

相同点

   

插入排序和冒泡排序的平均时间复杂度都是 O(n*n),且都是稳定的排序算法,都属于原地排序

   

差异点

   

冒泡排序每轮的交换操作是动态的,所以需要三个赋值操作才能完成;

而插入排序每轮的交换动作会固定待插入的数据,因此只需要一步赋值操作。

   

【归并排序】

归并排序的原理其实就是我们上一课时讲的分治法。

它首先将数组不断地二分,直到最后每个部分只包含 1 个数据。然后再对每个部分分别进行排序,最后将排序好的相邻的两部分合并在一起,这样整个数组就有序了。

public static void mergeSort(int[] arr) {

int len = arr.length;

int[] result = new int[len];

merge(arr, result, 0, len - 1);

}

   

public static void merge(int[] arr, int[] result, int start, int end) {

if (start >= end) {

return;

}

int len = end - start;

int mid = (len >> 1) + start;

int start1 = start, end1 = mid;

int start2 = mid + 1, end2 = end;

merge(arr, result, start1, end1);

merge(arr, result, start2, end2);

int k = start;

while (start1 <= end1 && start2 <= end2) {

result[k++] = arr[start1] < arr[start2] ? arr[start1++] : arr[start2++];

}

while (start1 <= end1){

result[k++] = arr[start1++];

}

while (start2 <= end2){

result[k++] = arr[start2++];

}

for (int i = start; i <= end; i++) {

arr[i] = result[i];

}

}

   

对于归并排序,它采用了二分的迭代方式,复杂度是 logn

每次的迭代,需要对两个有序数组进行合并,这样的动作在 O(n) 的时间复杂度下就可以完成。

因此,归并排序的复杂度就是二者的乘积 O(nlogn)

同时,它的执行频次与输入序列无关,因此,归并排序最好、最坏、平均时间复杂度都是 O(nlogn)

   

空间复杂度方面,由于每次合并的操作都需要开辟基于数组的临时内存空间,所以空间复杂度为 O(n)

归并排序合并的时候,相同元素的前后顺序不变,所以归并是稳定的排序算法

   

【快速排序】

快速排序法的原理也是分治法。

它的每轮迭代,会选取数组中任意一个数据作为分区点,将小于它的元素放在它的左侧,大于它的放在它的右侧。

再利用分治思想,继续分别对左右两侧进行同样的操作,直至每个区间缩小为 1,则完成排序。

public static void fastSort(int[] nums, int start, int end) {

int low = start;

int high = end;

if (low >= high) {

return;

}

   

int standData = nums[start];

while (low < high) {

while (low < high && nums[high] >= standData) {

high--;

}

while (low < high && nums[low] <= standData) {

low++;

}

int temp = nums[low];

nums[low] = nums[high];

nums[high] = temp;

}

nums[start] = nums[low];

nums[low] = standData;

   

if(low - 1 > start) {

fastSort(nums, start, low - 1);

}

if(high + 1 < end) {

fastSort(nums, high + 1, end);

}

}

在快排的最好时间的复杂度下,如果每次选取分区点时,都能选中中位数,把数组等分成两个,那么此时的时间复杂度和归并一样,都是 O(n*logn)

   

而在最坏的时间复杂度下,也就是如果每次分区都选中了最小值或最大值,得到不均等的两组。那么就需要 n 次的分区操作,每次分区平均扫描 n / 2 个元素,此时时间复杂度就退化为 O(n*n)

   

快速排序法在大部分情况下,统计上是很难选到极端情况的。因此它平均的时间复杂度是 O(n*logn)

   

快速排序法的空间方面,使用了交换法,因此空间复杂度为 O(1)

   

很显然,快速排序的分区过程涉及交换操作,所以快排是不稳定的排序算法

   

【总结】

如果对数据规模比较小的数据进行排序,可以选择时间复杂度为 O(n*n) 的排序算法。

但对数据规模比较大的数据进行排序,就需要选择时间复杂度为 O(nlogn) 的排序算法了。

归并排序的空间复杂度为 O(n),也就意味着当排序 100M 的数据,就需要 200M 的空间,所以对空间资源消耗会很多

   

快速排序在平均时间复杂度为 O(nlogn)但是如果分区点选择不好的话,最坏的时间复杂度也有可能逼近 O(n*n)。而且快速排序不具备稳定性,这也需要看你所面对的问题是否有稳定性的需求。

   

   

posted @ 2020-06-30 15:19  流年的夏天  阅读(581)  评论(0编辑  收藏  举报