数据结构与算法四(快速排序、堆排序、归并排序)

(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)
一般情况下,就运行时间而言:快速排序<归并排序<堆排序
三种排序算法的缺点:
快速排序:极端情况下排序效率低
归并排序:需要额外的内存开始
堆排序:在快的排序算法中相对较慢
在这里插入图片描述
稳定性:挨个移动位置的都是稳定的,不是挨个换的都是不稳定的。

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