排序
歸併排序
歸併排序介紹與程式碼
大體思路:歸併排序總體思路是,先把一串待排序數列分為前後兩組,把這兩組分別排為順序陣列,再將兩組順序陣列合為一整個大的順序陣列。
objection1:分組後分別排好序?用選擇排序嗎?遞迴的思路是什麼?
- 並非選擇排序,而是遞迴的方式。可以看到,第一次“將一串待排序數列分為兩組”後,顯然不是有序的,這就輪到遞迴出場了:透過遞迴,將已經分好的再分為兩組,然後,再分為兩組……直到只剩兩個為一組
- 兩個為一組,再進行最後一次分組,然後遇到遞迴出口:返回小於等於單個數值的組。於是就返回了這兩個單個值的陣列。
- 再透過比較大小將這兩個值,按順序放到陣列裡,返回。這樣就返回了2個數的順序陣列。思考一下其他分支在幹什麼……也返回了兩個數的順序陣列,於是開始一層層返回,2到4,4到8,最終得到全部排好值的順序陣列。
objection2:具體怎麼操作?
- 假設此時已經有了兩個排好的數列left和right,為二者設定index分別為i和j。
①比較left【i和right【2
②迴圈以下兩步
③若left【i的數值較小,則將left【i寫入temp-list列表中,i自增1。若i超出索引範圍,跳出迴圈。
④若right【j的數值較小,則將right【j寫入temp-list列表中,j自增1。若j超出索引範圍,跳出迴圈。
⑤將未超出索引範圍的數列全部寫入temp-list中(因為是排好順序的,所以可以直接全部放入)。
def merge_sort(arr)
#出口
if len(arr)<=1:
return arr
#mid = len(arr)除以2後向下取整:若len(arr)=3,則mid = 【1.5】 = 1
mid = len(arr) // 2
#以下是將未排序數列分為兩組,使用切片。
#因為切片是前閉後開原則,所以使用[:mid]和[mid:]不會導致漏項
#所以如果要做到不重不漏,一定要兩句的結尾和開頭的index相同
left = merge_sort(arr[:mid])
right = merge_sort(arr[mid:])
return merge(left,right)
def merge(left,right):
temp = [] #提前做的列表,用於接收排序好的數
i = j = 0 #兩個數值表的索引index
#兩個數列left和right中,總會有一個先耗完
#與此同時兩者的index(i、j)也會隨之增加
#跳出迴圈後,將餘下的數列放入temp-list即可
while i<len(left) and j < len(right):
#單個進行比較,較小的放到temp-list中。
if left[i] < right[j]
temp.append(left[i])
#注意,i和j自增的條件要確定好
#只有放入temp-list的數的index(i、j)才可以自增
i += 1
else:
temp.append(right[j])
j += 1
#將餘下的陣列全部放入temp-list中
#無需設定條件,耗完的數列什麼都不會向temp-list中寫入
temp.extend(left[i:])
temp.extend(right[j:])
return temp
arr = [1,23,4,54,3,2,7,65,9]
print("排序後為:",merge_sort(arr))
歸併排序-練習-小和問題
題目:
在一個陣列中每一個數左邊比當前數小的數累加起來,叫做這個數的小和,給定陣列[1,3,8,2,6,3,9,1],求這個陣列的小和
例:[1,3,6,2,5]
1的小和=0:左邊沒有比他小的數
3的小和=1:左邊比他小的數1
6的小和=4:左邊比他小的數1,3
2的小和=1:左邊比他小的數1
5的小和=18:左邊比他小的數1,3,6,2,5
[!IMPORTANT]
我在思考中鑽了牛角尖,在遞迴中想要採用返回排序好的陣列作為小和計算載體,可是這樣也導致了需要兩個返回值的問題,給我的編碼帶來了很大阻礙。而用排序後的結果對原陣列更新,並只返回小和的做法更簡單。
這裡與原歸併演算法不同的是,排序操作透過尋找資料的index進行排序,排序後對原始陣列arr進行覆蓋更新。
舉個例子就是說,10個數的陣列甲(Y、D、A、B、C、H、R、T、L、P),對第3-5個數(A、B、C)進行了排序,結果為(C、A、B),則需要對原陣列甲進行覆蓋更新為(Y、D、C、A、B、H、R、T、L、P)
[!NOTE]
疑問:有時會出現
msum += arr[l] * (right - r + 1) if arr[l]<arr[r] else 0 ^^^^^^^^^^^^ IndexError: list index out of range
的情況,但是對餘下資料填入部分修改後(加=,或者方法二改成方法一),錯誤消失。不知為何。
# 資料清洗函式,防止錯誤資料被執行,可以透過呼叫merge函式再呼叫process函式
# def merge(arr):
# if arr is None or len(arr)<2:
# return 0
# return process(arr,0,len(arr)-1)
def process(arr,left,right):
if left == right: #此時說明到了單個資料為一組的步驟
return 0 #沒有小和,故返回0
#接下來要將整個陣列傳入,每進行一次排序都會對這整個陣列的部分進行修改
#所以這條語句是計算陣列的index來確定目的資料位置。
mid = left + ((right-left) // 2)
#一口氣全部計算相加並返回
#在每一次排序中都會產生小和,所以每一次呼叫process都要計算上其產生的小和
return process(arr,left,mid)+process(arr,mid+1,right)+sml_sum(arr,left,mid,right)
def sml_sum(arr,left,mid,right):
msum = 0
l = left
r = mid+1
temp = []
while l<=mid and r<=right:
#右組某數A比左組某數B大,所以一定有小和的值B
#A的右邊的數一定比A要大,且A的右邊有C個數(包括A)
#那麼B這個數產生的小和總值=B*C
#如果A比B小或者等於B,那麼小和值=0
msum += arr[l] * (right - r + 1) if arr[l]<arr[r] else 0
#正常的歸併步驟
if arr[l]<arr[r]:
temp.append(arr[l])
l+=1
else:
temp.append(arr[r])
r+=1
#方法一:將餘下的資料放入temp中
#與原始歸併中的程式碼不同的是,這裡的arr是全部的數,所以填入餘下部分需要設定界限
temp.extend(arr[l:mid+1])
temp.extend(arr[r:right+1])
#方法二:將餘下的資料放入temp中
# while l<=mid:
# temp.append(arr[l])
# l+=1
# while r<=right:
# temp.append(arr[r])
# r+=1
arr[left:right+1] = temp #一定要記得更新排序後整個陣列的內容
return msum #然後返回小和
arr = [1,3,8,2,6,3,9,1]
#不進行資料清理,直接開始遞迴
print(process(arr,0,len(arr)-1))
#資料清理後,再遞迴
print(merge(arr))
快速排序
快速排序介紹與程式碼
大體思路:①在陣列中,隨機挑選一個n,小於n的放在左邊,大於n的放在右邊,等於n的放在中間,再將n放在大於區最左邊的位置,由此便分出了三個區域,小於區、大於區和等於區。②在小於區和大於區分別挑選一個n,重複第①步的內容,直到所有資料都排好。
注意1:在quicksort函式中,partition函式返回的是(大於區、小於區)和等於區的邊緣
注意2:如果partition返回的是l_dom 和 r_dom-1的話,在下一次遞迴呼叫quicksort的時候edge[x]就不用加一或者減一了呢?答案是不能,根據我的經驗來說,會導致索引溢位
注意3:擴張小於區時,不能單純將小於區擴張,仍需要將小於區最後一個與下標為cur的互換。因為當arr[cur] == arr[std_idx]時,cur會加一,由此略過arr[cur],小於區再次擴張時,若不互換則會將與標準數相等的數引入小於區。
import random
def quicksort(arr,left,std_idx):
if left < std_idx:
#return two-edge of equal-domination
#注意1
edge = []
edge = partition(arr,left,std_idx)
#注意2
quicksort(arr,left,edge[0]-1)
quicksort(arr,edge[1]+1,std_idx)
return arr
def partition(arr,cur,std_idx):
# add random element,imporve the worst situation speed
piovt = random.randint(cur,std_idx)
arr[std_idx],arr[piovt] = arr[piovt],arr[std_idx]
l_dom = cur - 1 #l_dom是左邊界的界限,當arr[cur]<arr[std_idx]時,l++,小於區右擴
r_dom = std_idx #r_dom是右邊界的界限,當arr[cur]>arr[std_idx]時,r--,大於區左擴
#cur是當前數的current_index,傳入時,是陣列的左邊界
#與cur的左邊數字交換數值時,右移一位,其他情況不動
#std_idx是標準數n的下標(位於陣列最右端),永遠不動,作為標準
while cur < r_dom :
#小於區擴張
if arr[cur]<arr[std_idx]:
l_dom += 1
arr[cur],arr[l_dom] = arr[l_dom],arr[cur]
cur += 1
#大於區擴張
elif arr[cur] > arr[std_idx]:
r_dom -= 1
arr[cur],arr[r_dom] = arr[r_dom],arr[cur]
#等於區增加
else: cur += 1
#將標準數放到大於區與等於區臨近的位置
arr[r_dom],arr[std_idx] = arr[std_idx],arr[r_dom]
return l_dom + 1,r_dom #返回左邊界+1作為接下來的右邊界、和右邊界作為接下來的左邊界
#但是如果返回[l_dom,r_dom-1]的話,返回後呼叫quicksort函式,會在第30行提示arr[cur] out of range
# return l_dom,r_dom - 1
arr = [1,3,8,2,9,6,6,4,9,0,76254,73846,13498,4546,123423,6767,34352,32235468,46745,4,5,3,67,23,12,78,12,45,67,23,98,45,23,5,643,3,256]
print(quicksort(arr,0,len(arr)-1))
快速排序練習-第K個最大數
題目:
給定整數陣列 nums 和整數 k,請返回陣列中第 k 個最大的元素。
請注意,你需要找的是陣列排序後的第 k 個最大的元素,而不是第 k 個不同的元素。
你必須設計並實現時間複雜度為 O(n) 的演算法解決此問題。
例:若陣列為:num = [3,5,21,8,4,7,3,18], k = 2, 則第k個最大數為18
程式碼連結:https://zhuanlan.zhihu.com/p/91142297
思路:
- 此題非常簡單,唯二難點一是對partition部分性質把握;二是如何確定基準值左邊的數正好是k-1個
- partition性質:"比基準值大的在左邊,比基準值小的在右邊"
- 也就是說,當基準值左邊的數有k-1個時,num[index]就是要找到的數,根本不用把數排號
- partition返回index時,index > k-1說明大於區中數的個數比k-1個多,說明大於區中有小於咱們要找的數的數,所以邊界high要以index為基準 -1
- index < k-1,大於區中的數不夠k-1個,說明大於區之外有數比要找的數大,所以邊界high要以index為基準 +1
- index == k-1,此時正好有k-1個數比num[index]大,說明num[index]就是我們要找的數
程式碼中變數與陣列的對照與流程:
b_dom(大於區) | high+1 | |||||||
---|---|---|---|---|---|---|---|---|
3 | 10 | 345 | 2323 | 25 | 7 | 3 | 67 | |
cur(標準值) && i (索引) |
| | b_dom(大於區) | | | | | | | high+1 |
| :---------: | :-----------: | :------: | :--: | :--: | :--: | :--: | :--: | ------ | :: |
| 3 | 345 | 10 | 2323 | 25 | 7 | 3 | 67 | | |
| cur(標準值) | | i (索引) | | | | | | | |
b_dom(大於區) | high+1 | |||||||
---|---|---|---|---|---|---|---|---|
3 | 345 | 2323 | 10 | 25 | 7 | 3 | 67 | |
cur(標準值) | i (索引) |
b_dom(大於區) | high+1 | |||||||
---|---|---|---|---|---|---|---|---|
3 | 345 | 2323 | 25 | 10 | 7 | 3 | 67 | |
cur(標準值) | i (索引) |
b_dom(大於區) | high+1 | |||||||
---|---|---|---|---|---|---|---|---|
3 | 345 | 2323 | 25 | 10 | 7 | 3 | 67 | |
cur(標準值) | i (索引) |
b_dom(大於區) | high+1 | |||||||
---|---|---|---|---|---|---|---|---|
3 | 345 | 2323 | 25 | 10 | 7 | 3 | 67 | |
cur(標準值) | i (索引) |
b_dom(大於區) | high+1 | |||||||
---|---|---|---|---|---|---|---|---|
3 | 345 | 2323 | 25 | 10 | 7 | 67 | 3 | |
cur(標準值) | i (索引) |
b_dom(大於區) | high+1 | |||||||
---|---|---|---|---|---|---|---|---|
67 | 345 | 2323 | 25 | 10 | 7 | 3 | 3 | |
cur(標準值) | i (索引) |
def partition(num,cur,high):
b_dom = cur
pivot = num[cur]
for i in range(cur+1,high+1):
if num[i] > pivot:
b_dom += 1
num[b_dom],num[i] = num[i],num[b_dom]
num[cur],num[b_dom] = num[b_dom],num[cur]
return b_dom
def main():
num = [3,1,345,2323,25,7,3,67]
k = 4
high = len(num)-1
cur = 0
while True:
#把所有大於標準數的都放到左邊來,然後返回大於區的右邊界值
#也就是說,右邊界值+1就是大於區有幾個數
index = partition(num,cur,high)
#看看大於區中的數夠不夠k個
#為什麼當 index == k-1 時,num[index] 就是我們要找的數呢?此處涉及到快速排序的特點
#當 partition 函式返回時,基準值所在的位置 index 意味著在它左邊有 index 個元素是大於它的。
#當 index == k-1 時,這意味著在基準值左邊恰好有 k-1 個元素比它大,所以基準值是第 k 個最大的元素
if index == k - 1:
return num[index]
#比k個多,說明大於區中有小於咱們要找的數的數
elif index > k - 1:
#所以再執行partition的時候,去掉那個小的數
high = index - 1
else:
#其他情況下,就是不夠k個數
#有一個或多個大於或等於目標數的數再大於區外
#再次執行partition的時候,多給一個大於區名額
cur = index + 1
if __name__ == "__main__":
print(main())
堆排序
堆排序基本介紹與程式碼
大體思路:就是使用堆的基本操作“堆化”成大頂堆或小頂堆,再將最頂點的父節點和最右邊的子節點交換值,在執行“堆化”操作。
- 堆化?:堆化就是將堆整理成父節點大於子節點的形式的操作
- 操作流程:
- 拿到一個父節點,記錄父節點的索引為max
- 找到他的兩個(或一個)孩子
- 比較大小
- 若左孩子比父節點大,則交換值,更新父節點索引max為左孩子節點的索引值
- 再次比較交換後的父節點與其兩個孩子的值的大小並交換,一直到兩個孩子都比父節點大
- 若沒有發生交換,則跳出迴圈
- 操作流程:
def sift_down(num,dad,n):
while True:
#本迴圈中參與堆化的是:以索引i為父節點和他的兩個孩子
#n是陣列長度,防止溢位
left = dad * 2 + 1 #左孩left
right = dad * 2 + 2 #右孩right
max = dad #預設三者中最大節點是父節點dad
# 在不超出索引範圍之內,使max = 更大的子節點索引
if left < n and num[left]>num[dad]:
max = left
# num[left],num[dad] = num[dad],num[left]
if right < n and num[right]>num[n]:
max = right
# num[right],num[dad] = num[dad],num[right]
#若下方if成立,則說明初始父節點就是最大節點,無需繼續堆化
if max == dad:
break
#父子節點交換
num[max],num[dad] = num[dad],num[max]
dad = max #為什麼向下:因為執行一次本函式只對一個節點的大小、相對位置進行交換等操作
#所以在本迴圈中,迴圈一次就會讓選中的節點交換一次位置
#若在一次迴圈裡沒有交換位置,就會跳出迴圈,結束函式呼叫
'''
關於堆的性質:
假設完全二叉樹的節點數量為n,
則葉節點數量為(n+1)/2 ,
其中 // 為向下整除。
因此需要堆化的父節點數量為(n-1)//2
'''
def heap_sort(num):
#按照陣列索引來查詢對應堆的位置
#第一個要堆化的父節點是len(num) // 2,是最後一個父節點
#因為堆化是從下向上的,所以倒序堆化
#一次迴圈只對一個父節點進行位置確定(確保父比子大)
#執行完下方for迴圈後,該堆變為一個大頂堆
for i in range(len(num) // 2 - 1,-1,-1):
sift_down(num,i,len(num)-1)
#堆化完成,現在開始一步一步將頂點節點與最右葉子節點做交換
for i in range(len(num)-1,0,-1):
#交換最頂節點與最右葉子節點
num[i],num[0] = num[0],num[i]
#不符合堆的要求,進行堆化
sift_down(num,0,i)
def main():
num = [3,5,8,4,2,7,6]
heap_sort(num)
print(num)
if __name__ == "__main__":
main()
堆排序練習-出現頻率前K高的元素
描述:給定一個陣列num,和一個整數k,返回出現頻率前k高的元素
若給出num=[1,1,1,2,2,3,4,5], k=2
輸出為:[1, 2]
問題分析:
桶排序
大體思路:按照資料,分出幾個範圍,每一個範圍稱之為一個桶。在遍歷陣列時,按照不同範圍放到不同的桶裡。再對每一個桶內進行排序。最後進行整合。桶排序也是分治法的應用之一
分析:
- 平均時間複雜度為O(N+K),K is the number of bucket
- 最壞情況時間複雜度:O(N2) 所有元素都放到同一個桶裡的情況
- 空間複雜度:O(N+K),K is the number of bucket
'''
桶排序,也是分治法的應用
將數字按照範圍放到不同的桶裡
在桶中進行排序
再將所有桶合併
'''
def bucket_sort(num):
#這裡先進行一次遍歷,增加了時間複雜度,但是這樣比較簡單
max_num = max(num)
#設定一半陣列長度的桶數量
#無疑會增加空間複雜度
bucket = [[] for _ in range(len(num) // 2)]
for i in num:
bucket[int(i/max_num)].append(i)
#對各桶內資料進行排序
for buc in bucket:
#這裡使用python自帶的排序函式
buc.sort()
#重新寫入num陣列
i = 0
for buc in bucket:
for bu in buc:
num[i] = bu
i+=1
return num
print(bucket_sort([2,4,1,5,9]))