1.快速排序(原地排序)
思路:取一个元素p(第一个元素),使p归位;列表被p分为两部分,左边都比p小,右边都比p大;递归完成排序。
关键代码:归位的实现。
def partition(li,left,right):
tmp=li[left]
while left<right:#列表中至少两个值
while left<right and li[right]>=tmp:#一旦left和right重合,就退出
right-=1
li[left]=li[right]#把右边比第一个值小的数放到左边空位
while left<right and li[left]<=tmp:
left+=1
li[right]=li[left]#把左边比第一个值大的数放到右边空位
li[left]=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)
时间复杂度:O(nlogn)
快速排序的问题:递归:递归最大深度;最坏情况:时间复杂度O(n^2)
修改最大深度代码:
import sys
sys.setrecurtionlimit(10000)
2.推排序(原地排序)
·堆:一种特殊的完全二叉树结构
·大根堆:一个完全二叉树,满足任一节点都比其他孩子节点大,根节点为最大元素
·小根堆:一个完全二叉树,满足任一节点都比其他孩子节点小,根节点为最小元素
·堆的向下调整:假设节点的左右子树都是堆,但本身不是堆,通过向下调整根节点使其变成堆。
·堆排序过程:
(1)建立堆;(农村包围城市,从最下面开始调整,直到整个堆完成)
(2)得到堆顶元素,为最大元素;
(3)去掉堆顶,将堆最后一个元素放到堆顶,此时可通过一次调整重新使堆有序;
(4)堆顶元素为第二大元素;
(5)重复步骤(3),直到堆变空。
关键代码:向下调整和出数
向下调整代码:
def sift(li,low,high):
i=low#i开始指向根节点
j=2*i+1#j开始是左孩子
tmp=li[i]
while j<=high:#只要j位置有效
if j+1<=high and li[j+1]>li[j]:#如果右孩子有并且比较大
j=j+1
if li[j]>tmp:
li[i]=li[j]
i=j#往下看一层
j=2*i+1
else:#tmp更大,把tmp放在i的位置上
li[i]=temp#把tmp放在某一级领导位置上
break
else:
li[i]=temp#把tmp放到叶子节点上
#大根堆
时间复杂度:O(logn)
构造堆+出数:
def heap_sort(li):
n=len(li)
for i in range((n-1-1)//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
堆排序时间复杂度:O(nlogn)
堆的内置模块:
import heapq
import random
li=list(range(100))
random.shuffle(li)
heapq.heapify(li)#建堆(小根堆)
n=len(li)
for i in range(n):
print(heapq.heappop(li),end=',')#每次弹出一个最小值
堆排序-topk问题:现在有n个数,设计算法得到前k大的数(k<n)
解决方法:
(1)排序后切片:O(nlogn)
(2)简单排序三人组:O(kn)
(3)堆排序:O(nlogk)
解决思路:
(1)取列表前k个元素建立一个小根堆,堆顶就是目前第k个大的数;
(2)依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整;
(3)遍历列表所有元素之后,倒序弹出堆顶。
代码实现:
def sift(li,low,high):
i=low#i开始指向根节点
j=2*i+1#j开始是左孩子
tmp=li[i]
while j<=high:#只要j位置有效
if j+1<=high and li[j+1]<li[j]:#如果右孩子有并且比较小
j=j+1
if li[j]<tmp:
li[i]=li[j]
i=j#往下看一层
j=2*i+1
else:#tmp更小,把tmp放在i的位置上
li[i]=temp#把tmp放在某一级领导位置上
break
else:
li[i]=temp#把tmp放到叶子节点上
#小根堆
def topk(li,k):
heap=li[0:k]#取前k个数
sift(heap,0,k-1)#建堆
for i in range(k,len(li)-1):
if li[i]>heap[0]:
heap[0]=li[i]
sift(heap,0,k-1)
for i in range(k-1,-1,-1):#出数
heap[0],heap[i]=heap[i],heap[0]#将根节点的最小值与堆的最后一个节点交换
sift(heap,0,i-1)
return heap
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
归并排序的思路:(递归)
分解:将列表越分越小,直至分成一个元素
终止条件:一个元素是有序的
合并:将两个有序列表归并,列表越来越大
代码实现:
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)
时间复杂度:O(nlogn)
空间复杂度:O(n)
总结:
(1)快速排序、堆排序、归并排序的时间复杂度都是O(nlogn);
(2)一般情况下,就运行时间而言:快速排序<归并排序<堆排序;
(3)三种排序算法缺点:
快速排序:极端情况下排序效率低
归并排序:需要额外的内存开销
堆排序:在快的排序算法中相对较慢
(4)递归有空间开销;
(5)稳定的排序能保证相等元素的相对位置不变(相邻元素依次比较就是稳定排序)。
说明:
本篇博客只是作为个人的听课笔记使用。具体课程为b站上清华大学博士讲解的数据结构与算法,课程讲的非常详细易懂,有需要的可以移步观看。