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