點擊上方“五分鐘學算法”,選擇“星標”公衆號
重磅乾貨,第一時間送達
轉自景禹
冒泡排序乍看最爲簡單,但請你問自己下面幾個問題:
冒泡排序如何判斷數組是否有序了呢?
冒泡排序數組
[3,1,2,4,5,6,7,8,9]
是否有優化方式呢?冒泡排序最好的時間複雜度,最壞的時間複雜度,還有空間複雜度清楚嗎?
如何用遞歸的形式實現冒泡排序?
如何使用兩個棧來實現冒泡排序?
對單鏈表又該如何進行冒泡排序?
最後一個簡單的,如何使用冒泡排序對字符串數組進行排序?
如果你看着每一個問題,心中都很明朗,就此打住;若不是,希望你從這篇文章中收穫到你想要的東西,也會對這些問題進行分析和講解。
還有製作動畫的福利奧~
冒泡排序
冒泡排序是最簡單的排序算法了,簡單到景禹不知如何更清晰地呈現(哈哈,開個玩笑)。冒泡排序通過不斷地比較兩個相鄰元素,將較大的元素交換到右邊(升序),從而實現排序。
話不多說,上例子:
我們對數組 [5,1,4,2,8,4]
,採用冒泡排序進行排序,注意這裏的兩個 4 的紋理是不同的,主要是爲了區分兩個不同的 4 ,進而解釋冒泡排序算法的穩定性問題。
第一輪冒泡排序
第一步:比較 5 和 1 ,5 > 1,則交換 5 和 1 的位置:
比較大小:
交換位置:
第二步,比較 5 和 4,5 > 4,交換 5 和 4 的位置:
第三步:比較 5 和 2 ,5 > 2,交換 5 和 2 的位置:
第四步:比較 5 和 8 ,5 < 8 ,不交換
第五步:比較 8 和 4 , 8 > 4,交換 8 和 4 :
此刻我們獲得數組當中最大的元素 8 ,使用橘黃色進行標記:
景禹寫的圖文中的動畫如何製作?(插曲)
上面給各位盆友講冒泡排序的 11 張圖都是景禹在 PowerPoint (俗稱PPT)中一張一張畫的,不信你看:
具體如何繪製,我就不給大家教學了,矩形框,文本框等等之類,大家在網上稍微學一下就能夠學會。關鍵是有了這 11 張圖如何製作一個 GIF動圖呢?
給大家推薦一款軟件 Easy GIF Animator
,至少我是用這個軟件每一次給大家制作動圖的,感覺很舒服(哈哈),需要的朋友後臺回覆 EasyGif。
首先要把 PPT 中的每一頁都保存成 jpg 或者 png 格式的圖片,然後打開 Easy GIF Animator
點擊 創建新的動畫
:
然後點擊 添加圖像
,將 PPT 中保存的 11 張圖片添加進來:
然後下一步,會看到下面的嚮導:
通常景禹是將這個切換速度改爲 100,也就是 1秒,之後就是下一步,下一步,完成。
然後保存成 gif 就可以了,如下所示。
第一輪冒泡動畫演示
第二輪冒泡動畫演示
事實上第二階段結束,整個數組已經有序了,但是對於冒泡排序而言並不知道,她還需要通過第三階段的比較操作進行判斷。
第三輪冒泡動畫演示
對於冒泡排序算法而言,她是通過判斷整個第三階段的比較過程中是否發生了交換來確定數組是否有序的,顯然上面的過程中沒有交換操作,冒泡排序也就知道了數組有序,整個算法執行結束。
冒泡排序的實現
不考慮優化的實現方式
void swap(int *xp, int *yp)
{
int temp = *xp;
*xp = *yp;
*yp = temp;
}
// 冒泡排序
void bubbleSort(int arr[], int n)
{
int i, j;
for (i = 0; i < n-1; i++)
// 每一次找出一個元素的合適位置。
for (j = 0; j < n-i-1; j++)
if (arr[j] > arr[j+1])
swap(&arr[j], &arr[j+1]);
}
這種實現方式有一個明顯的弊端,就是不論數組是否有序,兩層 for 循環都要執行一遍,而聰明的小禹希望數組有序的時候,僅進行一輪判斷,或者一輪都不進行(當然不判斷,排序算法是不能知道數組是否有序的)。所以我們一起來看看數組有序的情況下,僅判斷一輪的情況如何實現。
一次優化的實現方式
void swap(int *xp, int *yp)
{
int temp = *xp;
*xp = *yp;
*yp = temp;
}
// 冒泡排序的優化版本
void bubbleSort(int arr[], int n)
{
int i, j;
bool swapped; //用於標記數組是否有序
for (i = 0; i < n-1; i++)
{
swapped = false; //初始化爲 false
for (j = 0; j < n-i-1; j++)
{
if (arr[j] > arr[j+1])
{
swap(&arr[j], &arr[j+1]);
swapped = true;
}
}
// 如果swapped 爲 false ,說明沒有交換,數組有序,退出排序
if (swapped == false)
break;
}
}
這裏我們增加了一個標識數組是否有序的布爾變量 swapped
,當冒泡排序過程中沒有交換操作時,swapped = false
,也意味着數組有序;否則數組無序繼續進行冒泡排序。不要小看這個變量奧,因爲這個變量,當數組有序的時候,冒泡排序的時間複雜度將降至 (因爲其只需要執行一遍內層的 for 循環就可以結束冒泡排序),沒有這個變量,數組有序也需要 的時間複雜度。
二次優化的實現方式
一次優化是爲了避免數組有序的情況下,繼續進行判斷操作的。那麼二次優化又爲了什麼呢?
我們看下面的例子。
輸入數組:
一次優化的冒泡排序執行動畫演示:
但是我們注意到,數組數組中的 [5,6,8]
本身已經有序,而對於有序的部分進行比較是沒有意義的,相當於在白白浪費資源,有沒有什麼辦法減少這樣的比較次數呢?
換句話說,是否能夠確定出已經有序部分和無序部分的邊界呢?
答案當然是肯定的,這個邊界就是第一趟冒泡排序的過程中最後一次發生交換的位置 j
:
也就是 1 和 4 發生交換之後,4 和 5 沒有發生交換,此時 1 之後的元素爲有序。
還不清晰,我們分步驟看一下:
第一步:4 和 2比較,4 > 2 ,交換 4 和 2 ,將 LastSwappedIndex = 0
;
第二步:4 和 1 比較,4 > 1,交換 4 和 1, LastSwappedIndex = 1
;
第三步:比較 4 和 5 , 4 < 5,不交換, lastSwappedIndex
也不更新;
第四步:比較 5 和 6 ,不交換, lastSwappedIndex
也不更新;
第五步:比較 6 和 8 ,不交換, lastSwappedIndex
也不更新;
第一趟冒泡排序結束了,這裏似乎看不出和之前有什麼區別,但是來看第二趟冒泡排序就不一樣了,此時 j 的 取值將從 j = 0
到 j = lastSwappedIndex
,
第一步:比較 2 和 1 ,2 > 1,交換,lastSwappedIndex = 0
,並且第二趟冒泡也就結束了,也就說我們節省了 從 2 到 6的比較操作;
最後再來一趟冒泡排序,發現沒有任何交換,所以冒泡排序結束。
相比於一次優化的實現方式,二次優化的實現方式進一步減少了不必要的執行次數,兩種優化後的實現方式需要冒泡排序的趟數是一樣的,本質上沒有什麼區別。所以即使對於一個有序的數組,兩種方式的時間複雜度都是 .
動畫演示:
實現代碼:
void swap(int *xp, int *yp)
{
int temp = *xp;
*xp = *yp;
*yp = temp;
}
// 冒泡排序的優化版本
void bubbleSort(int arr[], int n)
{
int i, j;
bool swapped; //用於標記數組是否有序
int lastSwappedIndex = 0; //記錄最後一次交換的位置
int sortBorder = array.length - 1; //將有序和無序部分的邊界初始化爲最後一個元素
for (i = 0; i < n-1; i++)
{
swapped = false; //初始化爲 false
for (j = 0; j < sortBorder; j++)
{
if (arr[j] > arr[j+1])
{
swap(&arr[j], &arr[j+1]);
swapped = true;
lastSwappedIndex = j;
}
}
sortBorder = lastSwappedIndex;
// 如果swapped 爲 false ,說明沒有交換,數組有序,退出排序
if (swapped == false)
break;
}
}
複雜度分析
時間複雜度
最壞情況下(數組逆序):
當 i == 0 的時候,j 的取值範圍是從 0 到 n -1,內循環的執行判斷和交換操作 n - 1 次;
當 i == 1 的時候,j 的取值範圍是從 0 到 n -2,內循環的執行判斷和交換操作 n - 2 次;
以此類推......
當 i 取最大值 n - 2 時,j 的取值爲 1,內循環的執行判斷操作 1 次;
所以,整體內循環的判斷語句執行次數就是:1 + 2 + 3 + ... + (n - 2) + (n - 1) 。
則最壞情況下的時間複雜度爲 量級。
最好情況下(數組有序):
當 i == 0 的時候,swapped = false
,j 的取值範圍是從 0 到 n -1,內循環的執行判斷操作 n - 1 次,但是沒有發生交換操作,冒泡排序算法直到數組已經有序,所以執行結束。
則最好情況下的時間複雜度爲
空間複雜度
冒泡排序沒有使用任何額外的空間,空間複雜度爲 ,是典型的 原地排序 算法(In-place Sorting Algorithm)。
穩定性分析
原始的數組序列:
冒泡排序之後的數組序列:
我們可以發現,兩個 4 的相對位置沒有發生變化,也就是說冒泡排序是穩定的。但這僅相當於實驗驗證,而在理論上冒泡排序爲什麼是穩定的呢?
本質原因在於冒泡排序比較和交換的是兩個相鄰元素,對於鍵值相同的關鍵字是不交換位置的,所以排序前後鍵值相同的關鍵字的相對位置才保持不變的。
實戰演練
如何使用遞歸實現冒泡排序?
使用遞歸來實現冒泡排序在性能和實現方式上並無優勢,但是用來檢查你對於冒泡排序和遞歸卻是一個再好不過的方式。
如果我們仔細研究冒泡排序算法,就會注意到在第一趟冒泡排序中,將最大的元素移到末尾(假設進行升序排列)。在第二趟中,將第二大元素移至倒數第二個位置,依此類推,每一趟冒泡排序的過程是一樣的,可以採用遞歸實現。
這裏推薦之前一篇寫遞歸的文章:數據結構與算法之遞歸 + 分治
遞歸三要素:
明確你這個函數想要幹什麼
尋找遞歸結束條件
找出函數的等價關係式
public class JingYuSorting
{
// 冒泡排序的遞歸實現
// 1.明確你這個函數想要幹什麼
// 函數功能:進行一趟冒泡排序
static void bubbleSort(int arr[], int n)
{
// 2.尋找遞歸結束條件
// 如果數組只有一個一個元素時,有序,返回
if (n == 1)
return;
// 3.找出函數的等價關係式
// 進行一趟冒泡排序
for (int i=0; i<n-1; i++)
if (arr[i] > arr[i+1])
{
// 交換 arr[i], arr[i+1]
int temp = arr[i];
arr[i] = arr[i+1];
arr[i+1] = temp;
}
// 找到了數組最大的元素
// 遞歸對除最大元素之外的數組進行冒泡排序
bubbleSort(arr, n-1);
}
}
如何使用兩個棧實現冒泡排序?
對於這個問題本身,你可能會覺得沒有任何意義,但是當你去努力實現的時候,就會發現,你對於棧和冒泡排序的理解有了新的見解。
問題本身不難理解,就是利用兩個棧,然後每一次選擇出數組中最大的元素,並存入數組對應的位置。但是當你自己去實現時,還是會發現好多問題,比如如何互換着使用兩個棧?如何對棧中相鄰的兩個元素比較大小,並交換位置?
記得自己嘗試着實現一下,一定對你的學習、面試或考試有幫助。
下面是參考的思路:
給定兩個棧 s1
和 s2
,以及一個長度爲 n 的數組 arr
:
將數組
arr
中的所有元素壓入棧s1
當中;執行 for 循環
n
次(每一次選擇出一個最大的元素):
情況一:
s1
不爲空,s2
爲空,則嘗試將棧s1
當中的所有元素壓入棧s2
,並保證s2
的棧頂元素爲最大值;當s1
爲空時,s2
中的棧頂元素即爲棧中元素的最大值,插入數組相應位置。情況二:
s2
不爲空,s1
爲空,則嘗試將棧s2
當中的所有元素壓入棧s1
,並保證s1
的棧頂元素爲最大值;當s2
爲空時,s1
中的棧頂元素即爲棧中元素的最大值,插入數組相應位置。
詳細解析
初始時兩個棧 s1
和 s2
都爲空棧,數組 arr[] = [5,1,4,2,8]
。
第一步:將數組 arr
中的所有元素都壓入棧 s1
當中:
第二步:棧 s2
爲空,直接將 s1
的棧頂元素 8 壓入棧 s2
第三步:棧 s1
不爲空,嘗試將 s1
的棧頂元素 2 壓入棧 s2
,但是此時 s2
的棧頂元素 8 > 2,所以利用一個臨時變量 tmp
交換兩個元素在棧中的位置,先將 s2
的棧頂 8 保存到 tmp
並彈出,然後壓入元素 2
,最後再將 8 重新入棧。(其實就是交換操作)
第四步:棧 s1
不爲空,同第三步一樣將 s1
的棧頂元素壓入棧 s2
當中:
第五步:棧 s1
不爲空,同上將s1
的棧頂元素 1 壓入棧 s2
當中:
第五步:棧 s1
不爲空,同上將s1
的棧頂元素 5 壓入棧 s2
當中:
第六步:棧 s1
爲空,彈出 s2
的棧頂元素,並將其放到數組 arr[n - i - 1]
的位置:
之後的過程和前面講的類似,將棧 s2
中的元素壓入棧 s1
當中,並找到次大元素 5 ,以此類推,實現對數組的冒泡排序。
動畫演示
實現代碼
public class BubbleSort
{
// 使用棧進行冒泡排序
static void bubbleSortStack(int arr[], int n)
{
Stack<Integer> s1 = new Stack<>();
// 將 arr 中的所有元素壓入棧 s1
for (int num : arr)
s1.push(num);
Stack<Integer> s2 = new Stack<>();
for (int i = 0; i < n; i++)
{
// 初始時 s1 不爲空,使用i 的奇偶來決定將哪一個棧中的元素轉移到另外一個棧
if (i % 2 == 0)
{
while (!s1.isEmpty())
{
int t = s1.pop();
if (s2.isEmpty())
s2.push(t);
else
{
if (s2.peek() > t)
{
// 交換操作
int temp = s2.pop();
s2.push(t);
s2.push(temp);
}
else
{
s2.push(t);
}
}
}
// 將找到的最大元素放到正確的位置 n-i-1
arr[n-1-i] = s2.pop();
}
else
{
while(!s2.isEmpty())
{
int t = s2.pop();
if (s1.isEmpty())
s1.push(t);
else
{
if (s1.peek() > t)
{
int temp = s1.pop();
s1.push(t);
s1.push(temp);
}
else
s1.push(t);
}
}
arr[n-1-i] = s1.pop();
}
}
System.out.println(Arrays.toString(arr));
}
// 主方法
public static void main(String[] args)
{
int arr[] = {5, 1, 4, 2, 8};
bubbleSortStack(arr, arr.length);
}
}
如何使用冒泡排序對單鏈表進行排序?
這道題目本身並不難,主要是考察一下各位單鏈表的知識點,還有對冒泡排序進行鞏固。單鏈表的文章推薦看:線性錶鏈式存儲結構之單鏈表 。
最自然的實現方式就是比較相鄰的兩個結點,如果前面結點的值大於 next 結點的值,則交換兩個結點的值,具體如下。
第一趟冒泡排序(僅交換結點的值)
第一步:指針 ptr1
指向頭結點 head
,ptr1->next
則指向了值爲 1 的結點;
比較 ptr1
指向的結點的值 5 > ptr1->next
的值 1,交換兩個結點的值,ptr1 = ptr1 ->next
:
第二步:比較 5 和 4,5 > 4,交換 5 和 4 的值,ptr1 = ptr1 ->next
:
第四步:比較 5 和 2,5 > 2,交換 5 和 2 的值,ptr1 = ptr1 ->next
:
第五步:比較 5 和 8,5 < 8,不交換,ptr1 = ptr1 ->next
,然後用指針 lptr
指向 ptr1
,再將 ptr1 = head
。
通過一趟冒泡排序找到單鏈表中最大的值 8 ,並用指針 lptr
標識當前最大的元素。第二趟冒泡以同樣的方式找到次大元素 5, lptr
指向 5 ,以此類推,得到最終的有序單鏈表。
實現代碼
/* 單鏈表上的冒泡排序(交換值的方式) */
void bubbleSort(struct Node *start)
{
int swapped, i;
struct Node *ptr1;
struct Node *lptr = NULL;
/* 檢查單鏈表是否爲空 */
if (start == NULL)
return;
//數組有序時退出循環
do
{
swapped = 0; //標識單鏈表是否已經有序,0有序,1無序
ptr1 = start;
while (ptr1->next != lptr)
{
if (ptr1->data > ptr1->next->data)
{
swap(ptr1, ptr1->next);
swapped = 1;
}
ptr1 = ptr1->next;
}
lptr = ptr1;
} while (swapped);
}
/* 交換單鏈表兩個結點的值*/
void swap(struct Node *a, struct Node *b)
{
int temp = a->data;
a->data = b->data;
b->data = temp;
}
上面這種方式的確實現了單鏈表的冒泡排序(通過值的方式),但是如果面試官或考官問你,我們通過 交換結點本身 的方式對單鏈表進行排序,又該如何實現呢?
交換結點本身比交換結點的值稍微複雜一些,但是隻要細心一點,也沒有問題,我們還是以上面的例子說明。
第一趟冒泡排序(交換結點本身)
第一步:定義指向頭結點的指針 h
,並將 p1
指向與 h
相同的位置,p2
指向 p1->next
;
比較 p1
指向的結點和 p2
指向的兩個結點的大小,然後將 p1
和 p2
進行交換:
將指針
p2->next
指向p1
p1->next = p2
h = &(*h)->next
head = p2
即得到如下形式:
第二步:p1 = h
,p2 = p1->next
,比較 p1
和 p2
:
整個步驟最關鍵的就是結點交換後指針的修改,可以參考代碼再理解理解!裏面涉及的指針操作比較多,但是冒泡排序的整個代碼框架沒有變化。
/*交換結點 */
struct Node* swap(struct Node* ptr1, struct Node* ptr2)
{
struct Node* tmp = ptr2->next;
ptr2->next = ptr1;
ptr1->next = tmp;
return ptr2;
}
/* 對單鏈表進行冒泡排序 */
int bubbleSort(struct Node** head, int count)
{
struct Node** h;
int i, j, swapped;
for (i = 0; i <= count; i++)
{
h = head;
swapped = 0;
for (j = 0; j < count - i - 1; j++)
{
struct Node* p1 = *h;
struct Node* p2 = p1->next;
if (p1->data > p2->data)
{
/* 交換結點之後修改鏈接 */
*h = swap(p1, p2);
swapped = 1;
}
h = &(*h)->next;
}
/* 如果沒有任何交換操作,鏈表有序 */
if (swapped == 0)
break;
}
}
請使用冒泡排序對字符串數組進行排序
最後來個簡單的,讓大家獲得一定的成就感,這樣才能繼續前行~~
冒泡排序爲什麼叫冒泡呢?
大家一定都喝過可口可樂之類的碳酸飲料,碳酸類飲料中常常有許多小小的氣泡,嘩啦嘩啦向上冒。這是因爲組成小氣泡的二氧化碳比水要 輕 ,所以小氣泡都向上冒。冒泡排序也是一樣,每一趟冒泡排序都會讓最小的元素浮出水面(降序排列),所以很形象地命名爲冒泡,也歡迎大家評論區冒泡呀!讓景禹見見你~
字符串排序相當簡單就不給大家解釋了,我想你心裏都寫出了代碼,僅供參考:
public static void sortStrings(String[] arr, int n)
{
String temp;
for (int j = 0; j < n - 1; j++)
{
for (int i = j + 1; i < n; i++)
{
if (arr[j].compareTo(arr[i]) > 0)
{
temp = arr[j];
arr[j] = arr[i];
arr[i] = temp;
}
}
}
}
好好學習,天天向上呢~~
推薦閱讀
• 吳師兄實名吐槽 LeetCode 上的一道題目。。。• 面試字節跳動時,我竟然遇到了原題……• Leetcode 驚現馬化騰每天刷題 ? 爲啥大佬都這麼努力!• 爲什麼 MySQL 使用 B+ 樹• 一道簡簡單單的字節跳動算法面試題• 新手使用 GitHub 必備的兩個神器• 臥槽!紅警代碼竟然開源了!!!
歡迎關注我的公衆號“五分鐘學算法”,如果喜歡,麻煩點一下“在看”~