數據結構與算法四(快速排序、堆排序、歸併排序)

(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)
一般情況下,就運行時間而言:快速排序<歸併排序<堆排序
三種排序算法的缺點:
快速排序:極端情況下排序效率低
歸併排序:需要額外的內存開始
堆排序:在快的排序算法中相對較慢
在這裏插入圖片描述
穩定性:挨個移動位置的都是穩定的,不是挨個換的都是不穩定的。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章