【5】資料結構與演算法--- 演算法 進階
第 4 章 演算法 進階
4.1 排序
4.1.1 排序演算法簡介
排序:把無序的佇列變成有序的佇列
排序演算法:排序演算法是一種將一串
無規律資料
依照特定順序
進行排列的一種方法或思路
。排序演算法的穩定性:佇列中有相同的元素,排序前後,這兩個相同元素的順序有沒有發生變化。
特點:
輸入:無序佇列 輸出:有序佇列
應用場景:
各種排行榜 - 服不服排行榜
各種表格 - 座位表
排序演算法的關鍵點:
有序佇列:有序區剛開始沒有任何資料,逐漸變多
無序佇列:有序區剛開始存放素所有資料,逐漸變空
排序演算法的穩定性
佇列中相同元素排序前後
- 沒有發生變化,這表示演算法有穩定性 發生變化,表示沒有穩定性
常見的排序演算法:
三基冒插選,中快高堆歸,其基希爾桶,冒快會歸併
常見排序演算法 | 演算法 |
---|---|
基礎 | 冒泡、插入、選擇 |
中級 | 快速 |
高階 | 堆、歸併 |
其他 | 基數、希爾、桶 |
4.1.2 氣泡排序
氣泡排序
: 相鄰的元素兩兩比較,升序的話:
大的在右,小的在左
,降序的話,反之。經過數次比較迴圈,最終達到一個從小到大(升序)
或者從大到小(降序)
的有序序列.這個演算法由於類似於氣泡從水底冒出,所以叫“冒泡”排序。
過程跟蹤
: 在整個氣泡排序過程中,有一個標識指向兩個元素的最大值,當這個最大值移動的時候,標識也會隨之移動,這就叫做:
過程跟蹤
。
特點:
-
- 元素替換:相鄰元素從小到大: 左比右大,資料先交換位置,大的和右側的元素繼續比較 左比右小,資料不交換位置,大的和右側的元素繼續比較 左右相等,資料不交換位置,大的和右側的元素繼續比較 - 比較次數:無序佇列元素個數 - 1 - 冒泡次數:無序佇列元素個數 - 1 - 冒泡次數和比較次數關係: - 冒泡次數:1 ===> n - 比較次數:n ===> 1 - 過程跟蹤:alist[i]=最大值
氣泡排序的分析:
>- 元素替換:最基本元素比較(列表的下表)
>
>- 內層比較迴圈:每次氣泡排序,內層的元素比較次數
>
> 比較次數 + 元素範圍(下標)
>
>- 外層冒泡迴圈:執行多少次氣泡排序
>
> 冒泡次數、冒泡次數和元素比較次數關係
>
>- 不替換情況:特殊情況
>
> 計數器
氣泡排序的實踐:
def bubble_sort(alist):
"""氣泡排序"""
# 獲取列表元素的總數量
n = len(alist)
# 3.氣泡排序迴圈範圍
# 氣泡排序只關注排序次數
for j in range(n - 1, 0, -1):
# 開始比較前,定義計數器 count 的初始值為 0
count = 0
# 2.內層的資料比較迴圈範圍
for i in range(j):
# 1.相鄰元素替換
if alist[i] > alist[i + 1]:
alist[i], alist[i + 1] = alist[i + 1], alist[i]
# 資料替換完畢,計數器加 1
count += 1
# 4.不替換情況
# 如果計數器的值為 0,表示沒有發生任何替換,那麼就退出當前迴圈
if count == 0:
break
if __name__ == '__main__':
li = [54, 26, 93, 17, 77, 31, 44, 55, 20]
print(li)
bubble_sort(li)
print(li)
時間複雜度
最優時間複雜度:O(n)
最壞時間複雜度:O(n^2)
穩定性:穩定
拓展:降序<
4.1.3 選擇排序
簡單直觀
從無序佇列裡面挑選最小的元素,和無序佇列頭部元素替換(放到有序佇列中),最終全部元素形成一個有序的佇列。
選擇排序的原理:
選擇排序的主要特點與
元素替換
有關。每次移動一個元素,就有一個元素放到有序佇列,n個元素的無序佇列最多進行(n-1)次交換,就可以形成有序佇列。
如果整個佇列已排序,那麼它不會被移動。
選擇排序的分析:
比較迴圈:無序佇列查詢最小元素
mix表示同意最小元素
cur標識用於遍歷所有元素
⚠️注意:過程跟蹤:mix永遠指向最小的,cur指向的元素負責
對比
。mix 和 cur 標籤的初始化地址是相鄰的:
mix標籤所在元素的下標是
j
,那麼cur標籤所在元素的下標是j+1
元素替換:最小元素和無序佇列第一個元素替換位置
**選擇迴圈:**需要多少次替換,才能形成有序佇列
選擇排序的實踐
def selection_sort(alist):
"""選擇排序"""
# 獲取列表元素的總數量
n = len(alist)
# 3. 選擇迴圈
# 定義一個外圍迴圈次數
# 排序次數範圍的確定
for j in range(n - 1):
# 定義min_index 初始值
min_index = j
# 1.比較迴圈
# cur 標籤元素下標移動範圍(1,n-1)
for i in range(j + 1, n):
# 找到最小的元素
if alist[i] < alist[min_index]:
min_index = i
# 2.元素替換
# 保證最新的 min_index 不在無序佇列首位,那麼就將它和無序佇列的首個元素進行替換
if min_index != j:
# mix 標籤元素和無序佇列首位置元素替換
alist[j], alist[min_index] = alist[min_index], alist[j]
if __name__ == '__main__':
li = [11, 3, 6, 33, 5, 8, 2, 88]
print(li)
selection_sort(li)
print(li)
關鍵點:
1、mix標籤初始化:min_index = j
2、比較迴圈的範圍:for i in range(j+1,n)
3、元素替換的條件:if min_index != j
4、排序次數範圍的確定:for j in range(n-1)
時間複雜度
最優時間複雜度:O(n^2)
最壞時間複雜度:O(n^2)
穩定性:看情況
4.1.4 插入排序
先定義一個
有序佇列
,然後把無序佇列中的第一個元素
放到有序佇列的合適位置
,重複操作,直至形成一個完整的有序佇列
插入排序原理
1、構建有序序列
2、選擇無序佇列的第一個元素,先放在有序佇列末尾,然後進行氣泡排序,放到指定的位置
3、迴圈2步,直到無序佇列中所有元素全部進入有序佇列的合適位置
特點:
(1)插入 ?冒泡
-
聯絡:插入排序的有序佇列用到了冒泡演算法
-
區別:升序情況下,
冒泡:有序佇列在後,無序佇列在前(無序佇列 + 有序佇列);
插入:有序佇列在前,無序佇列在後(有序佇列 + 無序佇列)
(2)插入?選擇
- 選擇:遍歷未排序佇列,將最小的元素移動到有序佇列的末尾;
- 插入:把無序佇列的
第一個元素
放到有序佇列,通過使用冒泡演算法,移動到合適的位置。
插入排序分析
元素替換:有序佇列中元素比較替換
比較迴圈:每次排序,有序佇列元素比較替換的次數
排序迴圈:需要進行多少次排序
插入排序實踐
def insert_sort(alist):
"""插入排序"""
# 無序佇列元素數量
n = len(alist)
# 3.插入排序迴圈
# 有序佇列迴圈的次數
for i in range(n):
# 2.比較迴圈次數的確定
# 有序佇列末尾元素下標為i,範圍(0,i]
for j in range(i, 0, -1):
# 1.元素替換
# 有序列表的兩個元素進行比較
if alist[j] < alist[j - 1]:
# 大小值元素替換
alist[j], alist[j - 1] = alist[j - 1], alist[j]
# 條件不滿足,大小元素不替換
else:
break
if __name__ == '__main__':
li = [11, 3, 6, 33, 5, 8, 2, 88]
print(li)
insert_sort(li)
print(li)
關鍵點:
1、元素替換:if alist[j] < alist[j-1]
2、比較迴圈:for j in range(i,0,-1)
3、插入迴圈:for i in range(n):
時間複雜度
最優時間複雜度:O(n)
最壞時間複雜度:O(n^2)
穩定性:穩定
4.1.5 希爾排序
希爾排序(Shell Sort)是
插入排序
的一種。也稱縮小增量排序
,是插入排序演算法的一種高效的改進版本。
希爾排序原理:
兩兩一組、四四一組、八八一組…,直到所有元素為一組,進行排序
特點:
下標增量分組,對小組元素進行插入排序
下標增量的特點:
第一次分組,gap=n/2 ,
從第二次分組,gap=gap/2,
最後一次分組gap=1
整個分組過程就是:遞迴
希爾排序分析:
元素替換:分組佇列中元素比較替換
下標的範圍必須大於0
比較次數:每次分組後,同時有幾組在進行比較
插入排序
分組次數:需要進行多少次分組
希爾排序實踐:
def shell_sort(alist):
"""希爾排序"""
# 獲取列表的長度
n = len(alist)
# 3.遞迴分組迴圈
# 獲取下標偏移量gap(取整)
gap = n // 2
# 只要gap在合理範圍內,就一直分組下去
while gap >= 1:
# 2.比較迴圈(多少組進行插入排序)
# 指定i下標的取值範圍
for i in range(gap, n):
# 1.元素替換
# 對移動元素的下標進行條件判斷
# 下標的範圍必須大於0
while (i - gap) >= 0:
# 組內大小元素進行替換
if alist[i] < alist[i - gap]:
alist[i], alist[i - gap] = alist[i - gap], alist[i]
# 修改i 的屬性重新指向原始元素(過程跟蹤)
i = i - gap
# 否則的話,不進行替換
else:
break
# 沒執行完一次分組內的插入排序,對gap進行/2細分
gap = gap // 2
if __name__ == '__main__':
li = [11, 3, 6, 33, 5, 8, 2, 88]
print(li)
shell_sort(li)
print(li)
1、元素替換:
下標範圍:while (i - gap) >= 0:
替換條件:if alist[i] < alist[i-gap]:
過程跟蹤:i = i - gap
2、比較迴圈:
元素的範圍:for i in range(gap,n):
3、遞迴分組迴圈
偏移量初始值:gap = n // 2
遞迴迴圈的退出條件:while gap >= 1
gap偏移量規律:gap = gap // 2
時間複雜度
最優時間複雜度:O(nlogn)~O(n^2)
最壞時間複雜度:O(n^2)
穩定性:不穩定
4.1.6 快速排序
快速排序
,又稱劃分交換排序
,從無序佇列中挑取一個元素,把無序佇列分割成獨立的兩部分
,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序,
整個排序過程可以遞迴進行
,以此達到整個資料變成有序序列。挑元素、劃分組、分組重複前兩步
快速排序原理
挑元素劃分組,整體遞迴分組
特點:
1、因為是無序佇列,所以位置可以隨機挑
2、臨時劃分一個
空間
,存放我們挑選出來的中間元素3、左標籤位置空,移動右標籤,反之一樣
4、重複3,直到左右側標籤指向同一個位置,
5、把臨時存放的中間元素,
歸位
左手右手一個慢動作,右手左手慢動作重播
整體劃分特點:
1、遞迴拆分
2、拆分到最後,所有小組內的元素個數都是1
遞迴拆分到不能再拆
快速排序分析
序列切割
三個基本標籤:
mid:指定要切割的臨時中間數字
left:從佇列左側推進的標籤
right:從佇列右側推進的標籤
left永遠小於right
右側推進
左側推進
停止推進 (即元素歸位)
遞迴切分
遞迴拆分:小組邊界的確定 和 遞迴功能實現
左側邊界start:
0
右側邊界end:left-1
左側邊界start:
left+1
右側邊界end:len(alist)-1
遞迴退出條件
快速排序實踐
# 2.遞迴切分
# 2.1 小組邊界確定
# 增加兩個引數,左邊界 start,右邊界 end
def quick_sort(alist, start, end):
"""快速排序"""
# 2.3定義遞迴退出條件
if start < end:
# 1.序列切割
# 1.1定義三個基本標籤
# 因為 mid 指定的是傳入列表的左邊界元素
mid = alist[start]
left = start
right = end
# 定義拆分條件
while left < right:
# 1.2 右側推進
# 如果right元素 > mid值,right標籤左移
while right > left and alist[right] >= mid:
right -= 1
# 如果right元素 < mid值,left標籤元素設定為right標籤元素
alist[left] = alist[right]
# 1.3 左側推進
# 如果left元素 < mid值,left標籤右移
while left < right and alist[left] < mid:
left += 1
# 如果left元素 > mid值,right標籤元素設定為left標籤元素
alist[right] = alist[left]
# 1.4 停止標籤(元素歸位)
# 退出迴圈,表示 left和right 標籤合併在一起了
# 獲取中間值
alist[left] = mid
# 2.2 遞迴功能的實現
# 函式自呼叫
# 對切割後左邊的子部分進行快速排序
quick_sort(alist, start, left - 1)
# 對切割後右邊的子部分進行快速排序
quick_sort(alist, left + 1, end)
if __name__ == "__main__":
li = [54, 26, 93, 17, 77, 31, 44, 77, 20]
print(li)
quick_sort(li, 0, len(li) - 1)
print(li)
序列切割:
1、挑中間元素:mid = alist[start]
2、右推進:while right > left and alist[right] >= mid:
3、左推進:while left < right and alist[left] < mid:
4、推進迴圈:while left < right:
5、元素歸位:alist[left] = mid
遞迴拆分:
1、小組邊界確定:left = start、right = end
2、遞迴退出條件:if start < end:
3、函式自呼叫:quick_sort(alist, start, end)
時間複雜度
最優時間複雜度:O(nlogn)
最壞時間複雜度:O(n^2)
穩定性:不穩定
4.1.7 歸併排序
歸併排序是採用
分治法
的一個非常典型的應用。將無序佇列
拆成
兩個小組,組內
元素排序
,然後組間
元素逐個比較
,把小元素依次放到新佇列
中。關鍵字:拆分、排序、組間、小、新佇列
分組排序,合併新佇列
歸併排序原理
分組排序階段:
1、將無序佇列alist,拆分成成兩個小組A和B,
2、分別對兩個小組進行同樣的
氣泡排序
3、用標籤left和right,分別對小組A和B進行管理
合併新佇列階段:
4、兩個標籤所在的元素比較大小,
5、將小的元素放到一個新佇列中,然後小元素所在的標籤向
右移
6、多次執行4和5,最終肯定有一個小組先為空 7、把不為空的小組元素,按順序全部移到新佇列的末尾
8、無序佇列中的所有元素就在新佇列中形成有序隊列了
特點:
兩個階段:分組排序 + 合併
合併策略:組間比較、新增小,小移標
兩種情況:
分兩組合並排序
遞迴分組合並排序:層級分組、排序、層級合併
歸併排序分析
分組實現
首次分組
正常分組
不能分組:空佇列/佇列只有一個元素
遞迴分組
合併分組
合併分組的排序
準備工作
組標籤:定義兩個標籤l和r,分別是分組列表的首 位下標值0
組長度
空佇列
空列表增加資料
空列表新增資料
一組空,另一組剩餘元素按順序一次性新增到空列
歸併排序實踐
def fen_zu(alist):
"""分組"""
# 獲取當前序列的長度
n = len(alist)
# 1.首次分組
# 1.2 不能分組
# 佇列異常情況
if n <= 1:
return alist
# 1.1 正常分組
# 把當前列表分成兩部分,使用切片方式h獲取兩部分內容
mid = n // 2
# 左半部分
# left = alist[:mid]
# 右半部分
# right = alist[mid:]
# 2.遞迴分組
# 左半邊資料
zuo = fen_zu(alist[:mid])
# 右半邊資料
you = fen_zu(alist[mid:])
# 3.合併分組
# 將分組後的資料交個一個合併資料的函式去處理
return merge(zuo, you)
def merge(zuo, you):
"""歸併排序"""
# 3.1 準備工作
# (1)組標籤
# 定義標籤 l 和 r 在兩組的位置
l, r = 0, 0
# (3)空佇列
# 定義一個空列表
result = []
# (2)組長度
# 獲取兩個分組的長度
zuo_len = len(zuo)
you_len = len(you)
# 指定標籤的有效範圍
while l < zuo_len and r < you_len:
# 3.2 空列表新增資料
# 判斷兩側標籤指定的資料大小
if zuo[l] <= you[r]:
# 將左側小資料追加到新佇列
result.append(zuo[l])
# left標籤右移一位
l += 1
else:
# 將右側小資料追加到新佇列
result.append(you[r])
# right標籤右移一位
r += 1
# 3.3 一組為空,另一組剩餘元素按順序新增到新佇列
# 將左側的剩餘內容,一次性新增到 result 表中
result += zuo[l:]
# 將右側的剩餘內容,一次性新增到 result 表中
result += you[r:]
# 返回 result 表
return result
if __name__ == "__main__":
li = [54, 26, 93, 17, 77, 31, 44, 77, 20]
print("處理前: %s" % li)
sort_list = fen_zu(li)
print("處理後: %s" % li)
print("新列表: %s" % sort_list)
關鍵點:
1、異常分組:if n <= 1:
2、遞迴分組:fen_zu(alist[:mid])
3、分組合並:merge(zuo, you)
1、資料比較條件:while l < zuo_len and r < you_len:
2、小元素移動:result.append(zuo[l])
3、小元素標籤處理:l += 1
4、異常情況:result += zuo[l:]
5、最終效果:return result
時間複雜度
最優時間複雜度:O(nlogn)
最壞時間複雜度:O(nlogn)
穩定性:穩定
4.1.8 堆排序
堆是採用
順序表
儲存的一種近似完全二叉樹
的結構。父節點和子結點關係(父找子):
左子節點位置:
2i + 1
右子節點位置:2i + 2
堆分類
堆分類 | |
---|---|
大頂堆 | 任一節點都比其孩子節點大最大值 堆頂元素 alist[0] |
小頂堆 | 任一節點都比其孩子節點小 |
它是指利用堆這種樹結構所設計的一種排序演算法。
將無序列表先構造一個有特點的堆,然後利用列表的特點快速定位最大/小的元素,將其放到一個佇列中。
特點:
無序佇列構建一個堆,堆頂和堆尾元素替換位置
重新構建堆,堆頂和堆尾元素替換位置,…
頭尾替換,恢復堆後再繼續
堆排序原理
構建一個堆:從
最後一個有子節點的節點
開始構建,下標為[n/2-1]堆的調整:移除堆頂元素,用
佇列中最後一個元素
填補,自上而下進行調整
1 將無序列表構造
為一個標準的大頂堆
2 將堆頂元素和堆尾元素進行替換
相當於將最大元素放到了有序序列
剩餘的無序序列少了一個
3 將剩餘的無序序列重新調整為標準的大頂堆
4 重複2-3 最終形成一個有序序列
堆排序分析
堆的調整:
準備工作
佇列引數:data
堆頂元素的確定:傳入一個堆頂佇列的下標low
堆頂元素的臨時存放空間:tmp
無序佇列中元素的最大下標值:high
選擇新的堆頂節點
選大的子節點
最大子節點跟移除的堆頂元素進行比較
堆頂元素排序
堆的構造
堆頂元素輸出到有序佇列
步驟
1 無序序列構造標準大頂堆
2 堆頂和堆尾元素替換
3 剩餘無序序列調整大頂堆
4 重複2-3
堆排序實踐
關鍵點
1 無序序列構造標準大頂堆
從最後一個包含子節點的父節點開始構造
n/2 -1
順序:從下向上來構造的
n/2 -1 n/2 -2 n/2 -3 。。。0
表示式:
range(int(n/2)-1,-1,-1)
2 堆頂和堆尾元素替換
無序列表是alist
堆頂位置 0
假設堆尾下標是z
替換:
alist[0],alist[z] = alist[z],alist[0]
3 剩餘無序序列調整大頂堆
3.1 把臨時堆頂元素放到臨時空間tmp
tmp = alist[0]
3.2 選擇一個最大的子節點
3.3 最大子節點和臨時堆頂元素比較
如果臨時堆頂元素大,那麼歸位
如果子節點大,那麼最大子節點放到堆頂位置
假設子節點下標為j alist[0] = alist[j]
3.4 沿著破壞的路徑繼續調整下去
i = j
j = 2*i + 1
原則:
從上到下
調整的時候,:
物件 alist
堆頂位置 low
調整的範圍 high
4 重複2-3
在替換的過程中,堆尾元素的下標z變化是:
n-1 n-2 n-3 n-4 ..... 0
表示式:
range(n-1,-1,-1)
def sift(alist, low, high):
"""堆的調整"""
# 1.準備工作:堆頂元素的標籤 + 臨時佇列
# 指定移除的堆頂位置元素下標為 i
i = low
# 將移除的堆頂元素存放到一個臨時佇列tmp
tmp = alist[i]
# 2.選擇新的堆頂節點
# 假設左子節點是大節點,下標是 j
j = 2 * i + 1
# j 下標的操作範圍
# 無序佇列中元素的最大下標值
# 左側子節點小於堆的最大範圍值
while j <= high:
# 2.1 選取最大子節點
# 左右子節點進行比較
if j + 1 <= high and alist[j] < alist[j + 1]:
# 過程跟蹤,保證通過 j 找到最大元素
# 上移節點標號 j 指向右側節點
j += 1
# 2.2 最大子節點 和 原堆頂節點比較
# 子結點 > 原堆頂節點
if alist[j] > tmp:
# 將子節點元素移動到堆頂元素
alist[i] = alist[j]
# 因為子節點位置空了,相當於堆頂節點移除了,又要重複操作,所以需要更新 i 和 j 的值
i = j
j = 2 * i + 1
# 如果最大的子節點小於移除的堆頂元素,終止該操作即可
else:
break
# 臨時堆頂元素歸位
# 設定堆頂節點為原來的內容
alist[i] = tmp
def heap_sort(alist):
"""堆排序"""
# 獲取當前列表長度
n = len(alist)
# 1.無序列表構造標準大堆頂
# 對所有父節點進行堆的調整,而且是降序排列(從下往上構造)
# 最後一個元素是n,其父元素尾n/2-1
for i in range(int(n / 2) - 1, -1, -1):
sift(alist, i, n - 1)
# 堆頂元素排序
# 指定最小元素的範圍
for z in range(n - 1, -1, -1):
# 2.堆頂元素與堆尾元素替換
# 佇列中最大元素和最小元素進行替換
alist[0], alist[z] = alist[z], alist[0]
# 3.剩餘無序列表調整大頂堆(從上到下)
# 調整新佇列
# 替換完畢後,重新調整堆結構,新的堆結構元素個數變成了 i-1 個
sift(alist, 0, z - 1)
# 返回最終的有序佇列
return alist
if __name__ == '__main__':
a = [0, 2, 6, 98, 34, 5, 23, 11, 89, 100, 7]
print("排序之前:%s" % a)
c = heap_sort(a)
print("排序之後:%s" % c)
時間複雜度
成本:
最優: O(nlogn)
最壞: O(nlogn)
穩定性:不穩定
4.1.9 排序總結
技術:
冒小左移,選追加
插入合適,分希爾
快速兩半,歸新列
順表構造首尾堆
成本:
冒泡 插入 選擇 希爾 堆 歸併 快速 系統
技術總結
氣泡排序 | 在無序佇列中選擇最小的移動到最左側 |
選擇排序 | 定一個有序佇列,從無序佇列中選擇最小的元素追加到有序佇列的末尾 |
插入排序 | 定一個有序佇列,從無序佇列中選擇第一個元素,插入到到有序佇列的合適位置 |
希爾排序 | 通過對無序佇列進行分組,然後再採用插入的排序方法 |
快速排序 | 指定一個元素,將無序佇列拆分為大小兩部分,然後層級遞進,最終實現有序佇列 |
歸併排序 | 是將無序佇列拆分,然後小組內排序,組間元素比較後在新佇列中進行排序 |
堆 排 序 | 順序表方式構造堆,首尾替換調整堆 |
冒小左移 選追加,插入合適 分希爾,快速兩半 歸新列,順表構造首尾堆
成本總結
排序方法 | 時間複雜度 | 穩定性 | 程式碼複雜度 |
---|---|---|---|
最壞情 |