(1)快速排序
快速排序:快
快速排序思路:
- 取一個元素p(第一個元素),使元素p歸位
- 列表被p分成兩部分,左邊都比p小,右邊都比p大
- 遞歸完成排序
快速排序的代碼實現
#歸位函數
def partition(li,left,right):
tmp=li[left] #將最開始的數字存在temp
while left<right:
while left<right and li[right]>=tmp:#從右邊開始找比tmp小的數
right-=1 #往左移一步
li[left]=li[right]#把右邊的值寫在左邊的空位
#print(li,'right')
while left<right and li[left]<=tmp:
left+=1
li[right]=li[left]#把左邊的值寫到右邊空位上
#print(li,'left')
li[left]=tmp#把tmp歸位
return left
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)
li=[5,7,4,6,3,1,2,9,8]
quick_sort(li,0,len(li)-1)
print(li)
快速排序的時間複雜度:O(nlogn)
快速排序的問題:
(1)最壞情況
(2)遞歸
(2)堆排序
堆排序前傳-樹與二叉樹
樹是一種數據結構,比如:目錄結構
樹是一種可以遞歸定義的數據結構
樹是由n個節點組成的集合:
- 如果n=0,那這是一棵空樹;
- 如果n>0,那存在1個節點作爲樹的根節點,其他節點可以分爲m個集合,每個集合本身又是一棵樹。
樹相關的一些概念
根節點:例如A
葉子節點:沒有再分叉的節點,例如B、C、H、I、P、Q、K、L、M、N
樹的深度:看最深有幾層。上圖所示的樹有4層
節點的度:看往下分了幾個叉。例如E節點的度爲2,F節點的度爲3
樹的度:整個樹中節點的度最大的那個數。例如該結構中A分了6個叉,所以該樹的度爲6
孩子節點/父節點:例如E是I的父節點,I是E的孩子節點
子樹:樹中的部分節點構成的樹
二叉樹
二叉樹:度不超過2的樹。每個節點最多有兩個孩子節點,兩個孩子節點被區分爲左孩子節點和右孩子節點。
滿二叉樹:一個二叉樹,如果每一個層的結點數都達到最大值,則這個二叉樹就是滿二叉樹
完全二叉樹:葉節點只能出現在最下層和此次下層,並且最下面一層的結點都幾種在該層最左邊的若干位置的二叉樹。
二叉樹的存儲方式(表示方式): - 鏈式存儲方式
- 順序存儲方式(以列表的形式存儲)
父節點和左孩子節點的編號下標有什麼關係? - 0-1 1-3 2-5 3-7 4-9
- i——>2i+1
父節點和右孩子節點的編號下標有什麼關係? - 0-2 1-4 2-6 3-8 4-10
- i——>2i+2
什麼是堆?
堆:一種特殊的完全二叉樹結構 - 大根堆:一棵完全二叉樹,滿足任一節點都比其孩子節點大
- 小根堆:一棵完全二叉樹,滿足任一節點都比其孩子節點小
假設:節點的左右子樹都是堆,但自身不是堆。如下圖所示
可以通過一次向下調整將其變換成一個堆
堆排序過程
(1)建立堆
(2)得到堆頂元素,爲最大元素
(3)去掉堆頂,將堆最後一個元素放到堆頂,此時可通過一次調整重新是堆有序
(4)堆頂元素爲第二大元素
(5)重複步驟3,直到堆變空
#向下調整函數
def sift(li,low,high):
'''li列表
high堆的最後一個元素的下標
low堆的根節點位置'''
i=low#i最開始指向根節點
j=2*i+1#j開始是左孩子
tmp=li[low]#把堆頂存起來
while j<=high:#只要j位置有數
if j+1<high and li[j+1]>li[j]:#如果右孩子有,並且右孩子大於左孩子
j=j+1#j指向右孩子
if li[j]>tmp:
li[i]=li[j]
i=j#往下看一層
j=2*i+1
else:#tmp更大,把tmp放到i的位置
li[i]=tmp#把tmp放到某一級領導的位置
break
else:
li[i]=tmp#把tmp放到葉子節點上
#堆排序的實現
#最後一個非葉子節點的下標爲(n-2)/2
def heap_sort(li):
n=len(li)
for i in range((n-2)//2,-1,-1):
#i表示建堆時調整的部分的根的下標
sift(li,i,n-1)
#建堆完成
#挨個出數
for i in range(n-1,-1,-1):
#i一直指向當前堆的最後一個元素
li[0],li[i]=li[i],li[0]
sift(li,0,i-1)#i-1是新的high
li=[i for i in range(20)]
import random
random.shuffle(li)
print(li)
heap_sort(li)
print(li)
堆排序的時間複雜度:O(nlogn)
堆的內置模塊——heapq
常用函數
heapify(x):建堆,建立的是小根堆
heappush(heap,item):將item元素插入堆
heappop(heap):每次彈出一個最小的值
hrapreplace(heap,x):將heap中最小元素彈出,同時將x元素寫入堆
hlargest(n,iter):返回iter中第n大的元素
hsmallest(n,iter):返回iter中第n小的元素
import heapq
import random
li=list(range(100))
random.shuffle(li)
print(li)
heapq.heapify(li)#建堆,建立的是小根堆
print(li)
n=len(li)
for i in range(n):
print(heapq.heappop(li),end=',')#每次彈出一個最小的值
堆排序——topk問題
現在有n個數,設計算法得到前k大的數(k<n)。
解決思路:
- 排序後切片 O(nlogn)
- 用冒泡排序,排k趟就可以得到前k大的數 O(kn)
- 堆排序思路:取列表前k個元素建立一個小根堆。堆頂就是目前第k大的數;依次向後遍歷原列表,對於列表中的元素,如果小於堆頂,則忽略該元素;如果大於堆頂,則將堆頂更換爲該元素,並且對堆進行依次調整;遍歷列表所有元素後,倒敘彈出堆頂。 O(nlogk)
利用堆排序解決topk問題
#向下調整函數
def sift(li,low,high):
'''li列表
high堆的最後一個元素的下標
low堆的根節點位置'''
i=low#i最開始指向根節點
j=2*i+1#j開始是左孩子
tmp=li[low]#把堆頂存起來
while j<=high:#只要j位置有數
if j+1<high and li[j+1]<li[j]:#如果右孩子有,並且右孩子小於左孩子
j=j+1#j指向右孩子
if li[j]<tmp:
li[i]=li[j]
i=j#往下看一層
j=2*i+1
else:#tmp更大,把tmp放到i的位置
li[i]=tmp#把tmp放到某一級領導的位置
break
else:
li[i]=tmp#把tmp放到葉子節點上
def topk(li,k):
#1.建堆
heap=li[0:k]
for i in range((k-2)//2,-1,-1):
sift(li,i,k-1)
#2.遍歷
for i in range(k,len(li)-1):
if li[i]>heap[0]:
heap[0]=li[i]
sift(heap,0,k-1)
#3.挨個出數
for i in range(k-1,-1,-1):
heap[0],heap[i]=heap[i],heap[0]
sift(heap,0,i-1)
return heap
import random
li=list(range(1000))
random.shuffle(li)
print(topk(li,10))
(3)歸併排序
假設現在的列表分兩段有序,將這兩段有序列表合成爲一個有序列表的操作就稱爲一次歸併
#歸併
def merge(li,low,mid,high):
i=low
j=mid+1
ltmp=[]
while i<=mid and j<=high:#只要左右兩邊都有數
if li[i]<li[j]:
ltmp.append(li[i])
i+=1
else:
ltmp.append(li[j])
j+=1
#while執行完,肯定有一部分沒數了
while i<=mid:#如果左邊有數
ltmp.append(li[i])
i+=1
while j<=high:#如果右邊有數
ltmp.append(li[j])
j+=1
li[low:high+1]=ltmp
li=[2,4,5,7,1,3,6,8]
merge(li,0,3,7)
print(li)
歸併排序——使用歸併
- 分解:將列表越分越小,直至分成一個元素
- 終止條件:一個元素是有序的
- 合併:將兩個有序列表歸併,列表越來越大
歸併排序代碼實現
#歸併
def merge(li,low,mid,high):
i=low
j=mid+1
ltmp=[]
while i<=mid and j<=high:#只要左右兩邊都有數
if li[i]<li[j]:
ltmp.append(li[i])
i+=1
else:
ltmp.append(li[j])
j+=1
#while執行完,肯定有一部分沒數了
while i<=mid:#如果左邊有數
ltmp.append(li[i])
i+=1
while j<=high:#如果右邊有數
ltmp.append(li[j])
j+=1
li[low:high+1]=ltmp
def merge_sort(li,low,high):
if low<high:#至少有兩個元素,遞歸
mid=(low+high)//2
merge_sort(li,low,mid)#遞歸排序左邊
merge_sort(li,mid+1,high)#遞歸排序右邊
merge(li,low,mid,high)
li=list(range(1000))
import random
random.shuffle(li)
print(li)
merge_sort(li,0,len(li)-1)
print(li)
歸併排序的時間複雜度:O(nlogn)
歸併排序的空間複雜度:O(n)
小結:
三種排序算法的時間複雜度都是O(nlogn)
一般情況下,就運行時間而言:快速排序<歸併排序<堆排序
三種排序算法的缺點:
快速排序:極端情況下排序效率低
歸併排序:需要額外的內存開始
堆排序:在快的排序算法中相對較慢
穩定性:挨個移動位置的都是穩定的,不是挨個換的都是不穩定的。