前言:本篇文章只是記錄王爭的數(shù)據(jù)結(jié)構(gòu)與算法之美的學(xué)習(xí)筆記,寫下來能強(qiáng)迫自己系統(tǒng)的再過一遍,加深理解。這門課
以實(shí)際開發(fā)中遇到的問題為例
,引入解決問題涉及到的的數(shù)據(jù)結(jié)構(gòu)和算法,但不會(huì)講的太細(xì),最好結(jié)合一本實(shí)體書進(jìn)行學(xué)習(xí)。
1. 排序算法
1.1 介紹
下面是我們常用的 8 種排序算法,按照時(shí)間復(fù)雜度分成了三類,如下圖
1.2 排序算法的執(zhí)行效率
最好情況、最壞情況、平均情況時(shí)間復(fù)雜度
以及對應(yīng)的要排序的原始數(shù)據(jù),有序度不同的數(shù)據(jù),對于排序的執(zhí)行時(shí)間是有影響的時(shí)間復(fù)雜度的系數(shù)、常數(shù)、低階
時(shí)間復(fù)雜度反映的是數(shù)據(jù)規(guī)模 n 很大時(shí)的一個(gè)增長趨勢,會(huì)忽略系數(shù)、常數(shù)、低階,但實(shí)際開發(fā)中,排序的可能是 10 個(gè)、100 個(gè)、1000 個(gè)數(shù)據(jù),在對同一階時(shí)間復(fù)雜度的排序算法性能對比時(shí),需要把系數(shù)、常數(shù)、低階考慮進(jìn)來。比較次數(shù)和交換(移動(dòng))次數(shù)
基于比較的排序算法的執(zhí)行過程,會(huì)涉及兩種操作:比較大小和元素交換或移動(dòng)
,也需要考慮進(jìn)去
1.3 排序算法的內(nèi)存消耗
算法的內(nèi)存消耗可以通過空間復(fù)雜度
來衡量,針對排序算法的空間復(fù)雜度,還引入了一個(gè)新的概念:原地排序
。原地排序算法,就是特指空間復(fù)雜度是 O(1)
的排序算法。
1.4 排序算法的穩(wěn)定性
什么是穩(wěn)定的排序算法?就是如果待排序序列中存在值相等的元素,經(jīng)過排序后,相等元素之間原有的先后順序
不變,否則就是不穩(wěn)定的排序算法。
舉個(gè)例子,比如有一組數(shù)據(jù) 2 9 3 4 8 3,按照大小排序之后就是 2 3 3 4 8 9,經(jīng)過某種排序算法排序之后,如果兩個(gè) 3 的前后順序沒有改變,那這種排序算就是穩(wěn)定的排序算法,否則就是不穩(wěn)定的排序算法。
2. 有序度&逆序度
-
有序度是數(shù)組中具有有序關(guān)系的元素對的個(gè)數(shù)
image.png
對于一個(gè)倒序排列的數(shù)組,比如 6 5 4 3 2 1,有序度為 0;
對于一個(gè)有序排列的數(shù)組,比如 1 2 3 4 5 6,有序度就是n*(n-1)/2
,就是 15,完全有序,為滿有序度。
逆序度的定義和有序度相反,并且逆序度 = 滿有序度 - 有序度
。我們排序的過程就是一種增加有序度,減少逆序度的過程,最后達(dá)到滿有序度,說明排序完成。
3. 冒泡排序
冒泡排序只會(huì)操作相鄰的兩個(gè)數(shù)據(jù)
,每次冒泡操作都會(huì)對相鄰的兩個(gè)元素進(jìn)行比較,看是否滿足大小關(guān)系需求。如果不滿足就讓它倆互換,一次冒泡會(huì)讓至少一個(gè)元素移動(dòng)到它應(yīng)該在的位置,重復(fù) n 次,就完成了 n 個(gè)數(shù)據(jù)的排序工作。
對一組數(shù)據(jù) 4,5,6,3,2,1,從小到大進(jìn)行排序,第一次冒泡操作示例如下:
經(jīng)過一次冒泡操作之后,6 這個(gè)元素已經(jīng)存儲(chǔ)在正確的位置上了,要想完成所有數(shù)據(jù)的排序,我們只要進(jìn)行 6 次冒泡操作就行了:
我們可以對相面的冒泡過程進(jìn)行優(yōu)化,當(dāng)某次冒泡操作沒有數(shù)據(jù)交換時(shí),說明已經(jīng)達(dá)到完全有序,不需要在進(jìn)行后續(xù)的冒泡操作,如下圖:
代碼如下:
// 冒泡排序,a表示數(shù)組,n表示數(shù)組大小
public void bubbleSort(int[] a, int n) {
if (n <= 1) return;
for (int i = 0; i < n; ++i) {
// 提前退出冒泡循環(huán)的標(biāo)志位
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; // 表示有數(shù)據(jù)交換
}
}
if (!flag) break; // 沒有數(shù)據(jù)交換,提前退出
}
}
- 冒泡排序是原地排序算法,只涉及相鄰數(shù)據(jù)的交換操作,空間復(fù)雜度為 O(1)
- 冒泡排序是穩(wěn)定的排序算法,當(dāng)有兩個(gè)相鄰元素大小相等的時(shí)候,可以不做交換
-
最壞情況時(shí)間復(fù)雜度為 O(n^2)
image.png
4. 插入排序
主要思想就是在有序的數(shù)組
中,通過遍歷數(shù)組,將新的數(shù)據(jù)插入到合適的位置,繼續(xù)保持?jǐn)?shù)組有序:
就是將數(shù)組中的數(shù)據(jù)分為已排序區(qū)間
和未排序區(qū)間
,初始時(shí)已排序區(qū)間只有數(shù)組的第一個(gè)元素。插入算法的核心思想就是取未排序區(qū)間中的元素,在已排序區(qū)間中找到合適的插入位置將其插入,并保證已排序區(qū)間數(shù)據(jù)一直有序。重復(fù)這個(gè)過程,直到未排序區(qū)間中元素為空,算法結(jié)束。
代碼如下:
// 插入排序,a表示數(shù)組,n表示數(shù)組大小
public 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]; // 數(shù)據(jù)移動(dòng)
} else {
break;
}
}
a[j+1] = value; // 插入數(shù)據(jù)
}
}
- 插入排序并不需要額外的空間,空間復(fù)雜度為 O(1),為原地排序算法
- 是穩(wěn)定的排序算法,對于值相同的元素,我們可以選擇將后面出現(xiàn)的元素,插入到前面元素的后面,保持原有的前后順序不變
- 平均時(shí)間復(fù)雜度為 O(n^2)
5. 選擇排序
選擇排序?qū)崿F(xiàn)思路類似于插入排序,也分為已排序區(qū)間
和未排序區(qū)間
。但是選擇排序每次會(huì)從未排序區(qū)間中找到最小的元素,將其放到已排序區(qū)間的末尾(交換):
- 選擇排序空間復(fù)雜度為 O(1),也是一種原地排序算法
- 時(shí)間復(fù)雜度為 O(n^2)
- 選擇排序是一種不穩(wěn)定的排序算法,相對于冒泡排序和插入排序,選擇排序稍微遜色
6. 總結(jié)
冒泡排序不管怎么優(yōu)化,元素交換的次數(shù)總是原始數(shù)據(jù)的逆序度;
插入排序不管怎么優(yōu)化,元素移動(dòng)的次數(shù)也是原始數(shù)據(jù)的逆序度。
從代碼實(shí)現(xiàn)上看,冒泡排序的數(shù)據(jù)交換要比插入排序的數(shù)據(jù)移動(dòng)復(fù)雜,冒泡排序需要 3 個(gè)賦值操作,插入排序只需要 1 個(gè),所以插入排序要優(yōu)于冒泡排序:
冒泡排序中數(shù)據(jù)的交換操作:
if (a[j] > a[j+1]) { // 交換
int tmp = a[j];
a[j] = a[j+1];
a[j+1] = tmp;
flag = true;
}
插入排序中數(shù)據(jù)的移動(dòng)操作:
if (a[j] > value) {
a[j+1] = a[j]; // 數(shù)據(jù)移動(dòng)
} else {
break;
}
- 分析&評價(jià)一個(gè)排序算法,要從執(zhí)行效率、內(nèi)存消耗和穩(wěn)定性三個(gè)方面來看