02.python實現排序算法
一、列表排序
將無序列表變為有序列表
應用場景: 榜單,表格, 給二分查找用,給其他算法用
二、python實現三種簡單排序算法
時間復雜度O(n^2), 空間O(1)
1、冒泡排序
思路:
列表每兩個相鄰的數,如果前面的比後面的大,那麽交換這兩個數
代碼實現:
# 冒泡排序 @cal_time # 測試執行時間 def bubble_sort(li): for i in range(len(li)-1): # i表示第i趟 # 第i趟無序區位置【0,n-i-1】 for j in range(len(li)-i-1):if li[j] > li[j+1]: li[j], li[j+1] = li[j+1], li[j] # 最好情況 O(n^2) # 平均情況 O(n^2) # 最壞情況 O(n^2) # 優化改進>>> # 思路:如果冒泡排序中執行一趟而沒有交換,則列表已經是有序狀態,可以直接結束算法。 @cal_time def bubble_sort2(li): for i in range(len(li)): # 表示第i趟 exchange = 0 # 第i趟無序區的位置【0,n-i-1】 n是列表長度for j in range(len(li)-i-1): if li[j] > li[j+1]: li[j], li[j+1] = li[j+1], li[j] exchange = 1 if not exchange: # 如果遍歷一遍沒有發生交換,則已經有序,直接返回 return # 最好情況 O(n) # 平均情況 O(n^2) # 最壞情況 O(n^2)
2、選擇排序
思路:
一趟遍歷記錄最小的數,放到第一個位置;
再一趟遍歷記錄剩余列表中最小的數,繼續放置;
...
問題:
怎麽選出最小的數
# 找最小值 def find_min(li): min_num = li[0] for i in range(1,len(li)): if li[i] < min_num: min_num = li[i] return min_num # 找最小值的下標 def find_min_pos(li): min_pos = 0 for j in range(1,len(li)): if li[j] < li[min_pos]: min_pos = j return min_pos li = [2,5,8,9,11,15,5,1] print(find_min(li)) # 1 print(find_min_pos(li)) # 7
選擇排序:
def select_sort(li): for i in range(len(li)-1): # 第i趟遍歷,從0開始 # 第i趟 無序區【i, len(li)-1】 # 找無序區最小數位置,和無序區第一個數交換 min_pos = i for j in range(i+1, len(li)): # 從無序區第二個開始找 if li[j] < li[min_pos]: min_pos = j li[min_pos], li[i] = li[i], li[min_pos] li = list(range(100)) random.shuffle(li) # 打亂列表次序 print(li) select_sort(li) print(li)
比冒泡排序快。
3、插入排序
思路:
列表被分為有序區和無序區兩個部分。最初有序區只有一個元素。每次從無序區選擇一個元素,插入到有序區的位置,直到無序區變空。
代碼:
def insert_sort(li): for i in range(1, len(li)): # i表示第i趟,還表示無序區第一個數的位置 tmp = li[i] j = i - 1 # j 從後往前遍歷有序區的指針 while j >= 0 and li[j] > tmp: # j遍歷結束或無序區第一位大於j指向的數時跳出循環 li[j + 1] = li[j] # 有序區的第j位往後挪一位 j -= 1 # j 向前指一位 li[j + 1] = tmp # 將tmp插入到有序區j後面一位
三、python實現三種較復雜排序算法
1、快速排序
時間復雜度: O(nlogn)
思路:
取一個元素p(第一個元素),使元素p歸位;
列表被p分成兩部分,左邊都比p小,右邊都比p大;
左右兩邊遞歸完成排序。
方法一(經典方法):
歸位思路:
將要歸位的數p存起來,此時左遊標left指向空
將遊標指向的數與p比較,大於放右邊,小於放左邊(詳細操作:
先將右遊標right指向的數與p比較,大於p,位置不變,右遊標往左移一位,繼續比較;小於p,放到left指向的空位置,此時right指向空,然後移left
再將left遊標往右移一位指向的數與p比較,小於p,位置不變,left往右移一位,繼續比較;大於p,放到right指向的空位置,此時left指向空,然後移right)
當左遊標等於右遊標時,遊標指向的位置就是p要歸的位置
註意處理最壞情況(列表倒序)導致遞歸達到最大深度,從列表中隨機取一個與第一個交換位置
代碼:
def quick_sort(li, left, right): if left < right: # 遞歸區域至少有兩個元素 mid = partition(li, left, right) # 歸位 quick_sort(li, left, mid-1) # 左邊 quick_sort(li, mid+1, right) # 右邊 def partition(li, left, right): i = random.randint(left, right) # 防止最壞情況(列表有序或倒序)導致遞歸達到最大深度,從列表中隨機取一個與第一個交換位置 li[i], li[left] = li[left], li[i] tmp = li[left] # 將要歸位的數存起來 while left < right: while left < right and li[right] >= tmp: right -= 1 # 右邊的數大於等於tmp就不動,right遊標往左走 li[left] = li[right] # 右邊的數小於tmp就往左放 while left < right and li[left] <= tmp: left += 1 # 左邊的數小於等於tmp就不動,left遊標往右走 li[right] = li[left] # 左邊的數大於tmp就往右放 li[left] = tmp # left=right 將tmp歸位 return left
方法二(算法導論中的歸位方法):
歸位思路:
取最後一個元素r歸位,
分兩個區域,將小於r的數都放到區域一, 剩下的就是大於r的區域二
然後將r與區域二的第一個數交換,就歸位成功了
與方法1一樣也有python遞歸最大深度的問題
代碼實現:
def partition2(li, left, right): # 區域1:[left, i] 區域2:[i+1, j-1] i = left - 1 # 初始區域1和區域2都空,i指向區域1最後一個數 for j in range(left, right): if li[j] < li[right]: # 放到區域1,i往後移一位 i += 1 li[i], li[j] = li[j], li[i] # 與區域2的第一個數(i+1)交換歸為區域1, li[right], li[i+1] = li[i+1], li[right] # 歸位 return i+1 # 返回mid
方法三(占用空間的方法):
思路:
每次都取中間的數為歸位,盡可能的避免最壞情況導致python遞歸達最大深度的問題
開三個列表,一個放大於歸位數的,一個放小於歸位數的,一個放等於歸位數的
然後將三個列表拼起來
遞歸結束條件,列表長度小於等於1,
代碼:
def quick_sort3(li): if len(li) <= 1: return li m = li[len(li)//2] # 防止列表本來就是有序或倒序的,導致遞歸達到最大深度,不取li[0] left = [item for item in li if item < m] right = [item for item in li if item > m] x = [i for i in li if i == m] return quick_sort3(left) + x + quick_sort3(right)
一行實現快速排序:
quick_sort4 = lambda li: li if len(li) <= 1 else quick_sort4([item for item in li[1:] if item <= li[0]]) + [li[0]] + quick_sort4([item for item in li[1:] if item > li[0]])
2、堆排序
堆的概念:
堆是完全二叉樹,完全二叉樹可以用列表來存儲,通過規律可以從父親找到孩子或從孩子找到父親,堆中某個節點的值總是不大於或不小於其父節點的值
大根堆:一棵完全二叉樹,滿足任一節點都比其孩子節點大
小根堆:一棵完全二叉樹,滿足任一節點都比其孩子節點小
堆排序利用了堆向下調整的特征:節點的左右子樹都是堆,但自身不是堆。
向下調整,挨個出數;
通過父節點找子節點:父節點下標為i
則:孩子節點為,2i+1 和 2i+2
通過孩子節點找父節點:孩子節點為j
子節點為左節點,父節點為:(j-1) / 2
子節點為右節點,父節點為:(j-2) / 2
不知道為左子節點還是右子節點: (j-1) // 2
代碼:
def sift(li, low, high): ‘‘‘ 向下調整 :param li: :param low: 堆頂下標 :param high: 堆中最後一個元素下標 :return: ‘‘‘ tmp = li[low] i = low j = 2 * i + 1 # i, j 兩個遊標,初始i指向堆頂,j指向堆頂的左孩子 while j <= high: # 第二個結束循環的條件,沒有孩子和tmp競爭i這個位置 if j+1 <= high and li[j+1] > li[j]: # 如果右孩子存在並且比左孩子大 j指向右孩子 j += 1 if li[j] > tmp: li[i] = li[j] # 大的數往上調整 i = j # i指向下一個要調整的堆的堆頂 j = 2 * i + 1 # j指向調整堆的堆頂的左孩子 else: break # 第一種循環退出情況,tmp比目前兩個孩子都大 li[i] = tmp # i就是tmp要調整到的位置 def heap_sort(li): ‘‘‘ 堆排序 :param li: :return: ‘‘‘ # 1. 從列表構造堆,low的值和high的值 n = len(li) # 子節點找父節點: low = (i-1)//2 --> (n-2)//2 --> n//2-1 for low in range(n//2-1, -1, -1): sift(li, low, n-1) # 2. 挨個出數 利用原來的空間存儲下來的值,但是這些值不屬於堆 for high in range(n-1, -1, -1): # 或range(n-1, 0, -1) li[high], li[0] = li[0], li[high] # 1.把最大的調下來 2.high對應的數調上去 sift(li, 0, high - 1) # 3.調整,high 往前移一個,low為0
3、歸並排序
思路:
假設現在的列表分兩段有序,如何將其合成一個有序列表, 這個操作稱為一次歸並。
有了歸並之後怎麽用?
分解 :將列表越分越小,直至分成一個元素
終止條件:一個元素是有序的。
合並:將兩個有序列表歸並,列表越來越大。
一次歸並:
def merge(li, low, mid, high): ‘‘‘ 歸並 :param li: :param low: :param mid: :param high: :return: ‘‘‘ i = low j = mid + 1 li_tmp = [] while i <= mid and j <= high: # 兩邊都有數 if li[i] <= li[j]: li_tmp.append(li[i]) i += 1 else: li_tmp.append(li[j]) j += 1 # i<=mid 和 j<=high 兩個條件 只能有一個滿足 while i <= mid: li_tmp.append(li[i]) i += 1 while j <= high: li_tmp.append(li[j]) j += 1 # li_tmp 0~high-low 復制回li low~high for i in range(len(li_tmp)): li[low + i] = li_tmp[i]
分解合並:
def merge_sort(li, low, high): if low < high: # 至少兩個元素 # print(li[low:high+1], ‘->‘, end=‘ ‘) mid = (low + high) // 2 # 分解 # print(li[low:mid+1], li[mid+1: high+1]) merge_sort(li, low, mid) # 遞歸排序左邊 merge_sort(li, mid + 1, high) # 遞歸排序右邊 # print(li[low:mid+1], li[mid+1: high+1], ‘->‘, end=‘ ‘) merge(li, low, mid, high) # 一次歸並 合並 # print(li[low:high+1])
小結:
02.python實現排序算法