算法與數據結構之美-排序
本篇博客將會總結一下冒泡、插入、選擇排序,思考一個問題就是,插入排序和冒泡排序的時間複雜度都是O(n^2),但是爲何在實際軟件開發中更傾向於選擇插入排序?
如何分析一個排序算法?
分析排序算法的執行效率,主要是從以下幾個方面:
算法的執行效率
- 最好情況、最壞情況、平均情況複雜度;
- 時間複雜度的係數、常數、低階;
- 比較次數和交換次數;
排序算法的內存消耗
對於排序算法的空間複雜度,引入一個原地排序(Sorted in place)的概念,原地排序算法就是空間複雜度爲O(1)的排序算法;
排序算法的穩定性
穩定性,是指待排序的序列中存在值相等的元素,經過排序之後,想等元素之間原有的先後順序不變;
冒泡排序(Bubble Sort)
冒泡排序每次只會操作相鄰的兩個數據,看是否滿足大小關係,不滿足就呼喚,一次冒泡排序就會至少讓一個元素移動到它應該在的位置上,重複n次,就完成了n個數據的排序工作。
上圖爲一次冒泡排序的過程,其實冒泡排序還是可以優化的。當某次冒泡操作沒有數據可以交換時,說明已經達到完全有序,不用在執行後序的冒泡操作。
代碼如下:
public void bubbleSort(int[ ] a,int n){
if(n<=1) return;
for(int i=0;i<n;i++){
//提前退出冒泡排序的標誌flag
boolean flag = false;
for(int j = 0;j<n-i-1;j++){
if(a[j]>a[j+1]){
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true; //有數據進行交換
}
}
if(!flag) break; //沒有數據交換,提前退出
}
}
- 冒泡排序算法是原地排序算法,冒泡的過程只涉及相鄰數據的交換,空間複雜度是O(1);
- 冒泡排序算法是穩定排序算法,當相鄰元素相等時,不做交換;
- 冒泡排序最好的時間複雜度是O(n),平均和最壞時間複雜度都是O(n^2);
插入排序(Insertion Sort)
首先將數組中的數據分爲兩個區間,已排序區間和未排序區間。初始已排序區間只有一個元素,就是數組的第一個元素;插入排序算法的核心是從未排序區間中選取元素,在已排序區間中找到合適的插入位置將其插入,並保證已排序區間中的數據一直有序,直到未排序的區間中的元素爲空;下圖是插入排序的過程;
//插入排序,a表示數組,n表示數組大小
public static void insertionSort(int[ ] a ,int n){
if(n<=1) return;
for(int i =1;i<n;i++){
int value = a[i];
int j = i-1;
//查找要插入的位置並移動數據
for(;j>=0;--j){
if(a[j]>value){
a[j+1]=a[j];
}else{
break;
}
}
a[j+1] = value;
}
}
- 插入排序是原地排序算法;
- 插入排序是穩定的排序算法;
- 插入排序的最好時間複雜度是O(n),每次在數組的第一個位置插入新的數據,就需要移動大量的數據,所以最壞的時間複雜度是O(n^2);
選擇排序(Selection Sort)
選擇排序算法類似插入排序,將數組分爲已排序區間和未排序區間,但是每次是從未排序區間中找到最小的元素,將其放到已排序的區間的末尾;
//選擇排序,a表示數組,n表示數組大小
public static void selectionSort(int[ ] a,int n){
if(n<=1) return;
for(int i =0;i< n-1;i++){
//查找最小值
int minIndex = i;
for(int j = i+!;j<n;j++){
if(a[j]<a[minIndex]){
minIndex = j;
}
}
//交換
int tmp = a[i];
a[i] = a[minIndex];
a[minIndex] = tmp;
}
}
- 選擇排序是一種原地排序算法,空間複雜度是O(1);
- 最壞和平均時間複雜度都是O(n^2);
- 選擇排序是一種不穩定的排序算法,每次都是從未排序元素中取出最小值和前面的元素交換位置,破壞了穩定性;
解答開篇
從代碼實現上來看,冒泡排序的數據交換要比插入排序的數據移動要複雜,冒泡排序需要3個賦值操作,而插入排序只需要1個。我們來看這段操作:
冒泡排序中數據的交換操作:
if (a[j] > a[j+1]) { // 交換
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true;
}
插入排序中數據的移動操作:
if (a[j] > value) {
a[j+1] = a[j]; // 數據移動
} else {
break;
}
我們把執行一個賦值語句的時間粗略地計爲單位時間(unit_time),然後分別用冒泡排序和插入排序對同一個逆序度是K的數組進行排序。用冒泡排序,需要K次交換操作,每次需要3個賦值語句,所以交換操作總耗時就是3*K單位時間。而插入排序中數據移動操作只需要K個單位時間雖然冒泡排序和插入排序在時間複雜度上是一樣的,都是O(n2),但是如果我們希望把性能優化做到極致,那肯定首選插入排序。