本文总结一下冒泡排序,选择排序,插入排序,快速排序,归并排序,堆排序,主要从原理,代码实现,稳定性,时间复杂度,空间复杂度这几方面来介绍。
冒泡排序
原理
每轮排序,从第一个数开始,依次和后一个位置的数比较,如果左边的数大于右边的,交换两个数。这样,第i轮排序,就会挑选出一个第i大的值。
代码
public static void bubbleSort(int[] a, int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - 1 - i; j++) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}
}
}
}
稳定性
属于稳定排序,因为只有前面一个数大于后面时,才会交换,所以相等的两个数的相对位置不会改变。
时间复杂度
最好:O(n),原因是当第一次遍历时没有发生任何交换时,可以认为数组本身有序,不再执行后面的操作。代码稍作修改如下:
public static void bubbleSort(int[] a, int n) {
for (int i = 0; i < n - 1; i++) {
boolean swapped = false;
for (int j = 0; j < n - 1 - i; j++) {
if (a[j] > a[j + 1]) {
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
swapped = true;
}
}
if (!swapped) {
break;
}
}
}
最坏:O(n2)
平均:O(n2)
空间复杂度
O(1)
选择排序
原理
每次选择无序序列中最小的,然后将这个数和位置i的数进行交换。
代码
public static void selectSort(int[] a, int n) {
for (int i = 0; i < n - 1; i++) {
int index = i;
for (int j = i + 1; j < n; j++) {
if (a[j] < a[index]) {
index = j;
}
}
if (i != index) {
int temp = a[i];
a[i] = a[index];
a[index] = temp;
}
}
}
稳定性
不稳定,交换的时候可能改变两个相等元素的相对位置
例如
9 2 3 9 4 7 1
第一轮排序之后变为
1 2 3 9 4 7 9
时间复杂度
最好,最坏,平均都是O(n2)
空间复杂度
O(1)
插入排序
原理
每次将无序数组中的第一个元素插入到有序数组的合适的位置,这里定位合适的位置时既可以从前往后,也可以从后往前。如果当前元素比较大的话,从后往前定位合适,反之,从前往后合适。这里,两种写法都给出来。
代码
从前往后
public static void insertSort(int[] a, int n) {
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (a[i] < a[j]) {//找到要插入的位置,即为j
int temp = a[i];
for (int k = i - 1; k >= j; k--) {//把位置处于[j,i-1]的元素后移一位
a[k + 1] = a[k];
}
a[j] = temp;//插入当前元素
break;
}
}
}
}
从后往前:
public static void insertSort(int[] a, int n) {
for (int i = 1; i < n; i++) {
int j = i - 1;
for (; j >= 0; j--) {
if (a[i] >= a[j]) {//找到要插入的位置,即为j+1
break;
}
}
int temp = a[i];
for (int k = i - 1; k > j; k--) {//把位置处于[j+1,i-1]的元素后移一位
a[k + 1] = a[k];
}
a[j + 1] = temp;//插入当前元素
}
}
可以看出从前往后的代码比较好理解,而从后往前稍微复杂一些。从前往后:第二个for循环中没有找到位置,其实就相当于当前元素最大,就什么也不做就可以了。从后往前,第二个for循环中没有找到位置,说明当前元素最小,需要后移所有元素,所以比较麻烦,需要把移动元素的这个操作单独出来,不能写在for循环之内。本人更喜欢写从前往后定位的方法。
稳定性
属于稳定排序,从前往后时,当元素相等时,会继续往后走,保证,相等的元素的相对位置不变。从后往前时,如果元素相等,直接插入。
时间复杂度
最好,最坏,平均都是O(n2)
快速排序
原理
快速排序,每次以待排序数组的第一个元素为切分点,把数组分为两部分,大于等于这个元素的在后面,反之在前面,依次递归,直至整个数组有序。
代码
public static void quickSort(int[] a, int begin, int end) {
if (begin >= end) {
return;
}
int temp = a[begin];
int i = begin;
int j = end;
while (i < j) {
while (i < j && a[j] >= temp)
j--;
a[i] = a[j];
while (i < j && a[i] <= temp)
i++;
a[j] = a[i];
}
a[i] = temp;
quickSort(a, begin, i - 1);
quickSort(a, i + 1, end);
}
稳定性
属于不稳定排序,因为基于交换,且不同于冒泡,它不是相邻元素的交换,所以会改变相等元素的相对位置。
举个例子:
例如:
6 1 2 3 8 5 9 2
经过第一轮快排之后变为:
2 1 2 3 5 6 9 8
时间复杂度
最好:O(nlogn) 最坏:O(n2) 平均:O(nlogn)
最坏的情况是数组本身有序的情况,这样一轮快排并不能把数组分为元素数量基本相等的两部分
空间复杂度
最好:O(logn) 最坏:O(n) 平均:O(logn)
最坏的情况同上
归并排序
原理
使用递归,把两个有序数组合并为一个有序数组,注意部分有序的时候要记得复制回原数组。
代码
public static void mergeSort(int[] a, int begin, int end) {
if (begin >= end) {
return;
}
int mid = begin + (end - begin) / 2;//从中间将数组一分为二,递归调用归并排序使得左右两个数组有序
mergeSort(a, begin, mid);
mergeSort(a, mid + 1, end);
merge(a, begin, mid, end);//合并两个有序数组
}
public static void merge(int[] a, int begin, int mid, int end) {
int[] b = new int[a.length];
int k = begin;
int i = begin;
int j = mid + 1;
while (i <= mid && j <= end) {
if (a[i] <= a[j]) {
b[k++] = a[i++];
} else {
b[k++] = a[j++];
}
}
while (i <= mid) {
b[k++] = a[i++];
}
while (j <= end) {
b[k++] = a[j++];
}
for (k = begin; k <= end; k++) {//注意一定要复制回原数组
a[k] = b[k];
}
}
稳定性
属于稳定排序,在两个数组合并时,如果两个元素相等,取左边数组中的元素,保证相等元素的相对位置不会改变。
时间复杂度
最好,最坏,平均都是O(nlogn)
空间复杂度
因为需要一个辅助数组b,所以空间复杂度为O(n)。
堆排序
原理
如果要从小到大排序,那么使用大顶堆,堆用数组来存储。
构造大顶堆的方式是自底向上,从第一个非叶子节点开始,调整元素位置,使得每一个非叶子节点都大于等于它的左右子节点。所以,建堆的时间复杂度为O(n),因为自底向上,只需往下走一层即可,即使有元素交换,也不会打乱下面的关系,因为即使有元素交换,也是把大的换到下面,当然满足大顶堆的性质了。
下面开始真正的排序,排序就是把位置为0的元素和位置为i的元素交换,然后调整堆(0到i-1),使其重新满足大顶堆的性质。这样,每执行一轮,就会把当前最大的元素放到后面。这个过程的时间复杂度为O(nlogn)。
代码
public static void heapSort(int[] a) {
int len = a.length;
for (int i = (len - 2) / 2; i >= 0; i--) {//构造大顶堆
adjustHeap(a, i, len - 1);
}
for (int i = len - 1; i > 0; i--) {
//交换第一个和最后一个元素的位置
int temp = a[i];
a[i] = a[0];
a[0] = temp;
adjustHeap(a, 0, i - 1);//调整堆
}
}
public static void adjustHeap(int[] a, int parent, int end) {
int temp = a[parent];//目的就是把temp放到合适的位置
for (int i = parent * 2 + 1; i <= end; i = i * 2 + 1) {//i是左节点
if (i < end && a[i] < a[i + 1]) {//取左右节点的最大值
i++;
}
if (temp >= a[i]) {//如果temp大于等于最大值,满足大顶堆,找到位置,即parent,跳出循环
break;
}
a[parent] = a[i];//反之,把最大值放到parent的位置
parent = i;//parent指向i,继续向下遍历
}
a[parent] = temp;//找到位置或者遍历到最后,那么把temp放到parent的位置
}
稳定性
属于不稳定排序,举例,左面的是构造大顶堆之后的状态,右面的是经过第一轮排序之后的状态,两个2的相对位置改变了:
时间复杂度
最好,最坏,平均都是O(nlogn)
空间复杂度
O(1)
总结
最后,总结一下上面6种排序算法。
排序算法 | 稳定性 | 最好时间复杂度 | 最坏时间复杂度 | 平均时间复杂度 | 空间复杂度 |
---|---|---|---|---|---|
冒泡 | 是 | O(n) | O(n2) | O(n2) | O(1) |
选择 | 否 | O(n2) | O(n2) | O(n2) | O(1) |
插入 | 是 | O(n2) | O(n2) | O(n2) | O(1) |
快排 | 否 | O(nlogn) | O(n2) | O(nlogn) | O(logn) |
归并 | 是 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) |
堆排 | 否 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) |