排序
- 快速排序、堆排序和归并排序都是常用的排序算法,它们各有其优点。
- 快速排序( Q u i c k S o r t QuickSort QuickSort):平均时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),对于大量数据的排序效率较高;它是原地排序算法,不需要额外的存储空间,节省内存资源;内存使用上有效率,它最差运行情况的复杂度为 O ( n 2 ) O(n^2) O(n2),会在数据接近有序的情况下发生。
- 堆排序( H e a p S o r t HeapSort HeapSort):时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn),性能稳定;原地排序算法,不需要额外的存储空间;堆排序不适合小数据集,因为它的常数因子相对较大。同时,它的缓存性能也不好,因为它需要大量的随机访问。
- 归并排序( M e r g e S o r t MergeSort MergeSort):时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn),性能稳定;归并排序在处理链表和在外存中的大数据集时很有用,因为归并排序只需要少量的随机访问内存;归并排序需要额外的存储空间,这可能在处理大量数据时成为问题。
快速排序
- 取一个元素 p p p(第一个元素),使元素 p p p归位。
- 列表被 p p p分成两部分,左边都比 p p p小,右边都比 p p p大。
- 递归完成排序
- 视频链接link
5 5 5将列表分成左右两个部分 [ 2 , 1 , 4 , 3 ] , [ 6 , 7 , 9 , 8 ] [2,1,4,3],[6,7,9,8] [2,1,4,3],[6,7,9,8],左边挑选第一位 2 2 2归位,于是 2 2 2将子列表再次分成左右两部分 [ 1 ] , [ 4 , 3 ] [1],[4,3] [1],[4,3], [ 1 ] [1] [1]一个元素达到有序, [ 4 , 3 ] [4,3] [4,3]让 4 4 4归位,变成 [ 3 , 4 ] [3,4] [3,4], 4 4 4左边为一个元素、右边没有元素均达到有序。
//QuickSort
def quick_sort(data, left, right):
if left < right: # 至少两个元素
mid = partition(data, left, right)
quick_sort(data, left, mid-1)
quick_sort(data, mid+1, right)
//partition function(归位操作)
def partition(list, left, right):
template = list[left]
while left < right:
while left < right and list[right] >= template: # 从右边找比template小的数
right -= 1 # 往左走一步
list[left] = list[right] # 把右边的值放到左边空位上
while left < right and list[left] <= template:
left += 1
list[right] = list[left]
list[left] = template
return left
此外,快速排序可能会有最坏的情况出现,比如列表
[
5
,
4
,
3
,
2
,
1
]
[5,4,3,2,1]
[5,4,3,2,1],在每次归位之后总会出现一侧没有元素,这样会导致每次归位截断之后,下次处理的数据量只比上一次少了一个,所以不会出现
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn)的复杂度,而是
O
(
n
2
)
O(n^2)
O(n2)。所以一旦出现这种情况,由于数据量过大,可能超过递归的最大默认深度1000,可以通过设置
s
y
s
.
s
e
t
r
e
c
u
r
s
i
o
n
l
i
m
i
t
sys.setrecursionlimit
sys.setrecursionlimit来更改。
为了解决这种最坏情况的发生,采用随机化版本的快速排序。先前归位操作选取的值都是第一个值,现在我们在待排序表种随机选取一个数,然后与表头的第一个元素交换,剩下的步骤同原先一样。或者同样推荐在排序之前将原表
s
h
u
f
f
l
e
shuffle
shuffle一次。
堆排序
下面的图表示完全二叉树:
- 将完全二叉树的节点值存放在 l i s t = [ a , b , c , d , e , f , g , h , i ] list=[a,b,c,d,e,f,g,h,i] list=[a,b,c,d,e,f,g,h,i]中。
- l i s t list list的父节点和左孩子节点的索引关系: ( 0 − 1 ) , ( 2 − 5 ) , ( 1 − 3 ) , ( 3 − 7 ) (0-1),(2-5),(1-3),(3-7) (0−1),(2−5),(1−3),(3−7),满足 i = 2 i + 1 i=2i+1 i=2i+1的关系。
- l i s t list list的父节点和右孩子节点的索引关系: ( 0 − 2 ) , ( 1 − 4 ) , ( 2 − 6 ) , ( 3 − 8 ) (0-2),(1-4),(2-6),(3-8) (0−2),(1−4),(2−6),(3−8),满足 i = 2 i + 2 i=2i+2 i=2i+2的关系。
- 堆是一种特殊的完全二叉树,当任一节点都比其孩子节点大,则称为大根堆。当满足任一节点都比其孩子节点小,则称为小根堆。
- 堆的向下调整:当根节点的左右子树都是堆时,可以通过一次向下调整来将其变换成一个堆。
堆排序步骤如下:
代码如下:
//向下调整函数的实现
def sift(list, low, high)
'''
list:列表
low:堆的根节点位置
high:堆的最后一个元素的位置
'''
i = low # i最开始指向根节点
j = 2*i+1 # j最开始是根节点的左孩子节点
template = list[low] # 把堆顶存起来
while j<=high: # 只要j的位置有数
if j+1<=high and list[j+1] > list[j]: # 如果有右孩子且右孩子节点更大
j = j+1 # j指向右孩子节点
if list[j] > template: # 假如大于根节点
list[i] = list[j] # 交换
i = j # 交换完成,更新一下i和j(往下看一层)
j = 2*i+1
else: # template更大,把template放到i的位置上
list[i] = template
break
else:
list[i] = template # 把template放到叶子节点上
//实现堆排序
def heap_sort(list):
n = len(list)
for i in range((n-2)//2, -1, -1):
# i表示建堆的时候调整的部分的根的下标索引
sift(list, 1, n-1) # 堆建立完成
for i in range(n-1, -1, -1) # 该循环执行完毕,堆空了,堆顶去除完毕
# i 指向当前堆的最后一个元素
list[0], list[i] = list[i], list[0]
sift(list, 0, i-1) # i-1是新的high
实际上在Python内部有内置模块“heapq”来实现堆排序:
import heapq
import random
liheapq = []
li = list(range(100))
random.shuffle(li)
heapq.heapify(li) # 建小根堆
for i in range(len(li)):
liheapq.append(heapq.heappop(li)) # 每次弹出最小元素
- (堆排序的应用)topk问题:现有
n
n
n个数,设计算法找到前
k
k
k大的数。
(
k
<
n
)
(k<n)
(k<n)
方法一:排序后切片的复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)(所有都排一遍)。方法二:简单三种排序的复杂度为 O ( k n ) O(kn) O(kn)(只排 k k k个)。方法三:堆排序的复杂度为 O ( n l o g k ) O(nlogk) O(nlogk)。
实现思路:
- 取列表前 k k k个元素建立一个小根堆。堆顶就是目前取出的这 k k k个数里面的第 k k k大的数。
- 从剩下的列表里向后依次遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;若大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整,将其再次调整为小根堆。
- 最后遍历完成,完全二叉树上的 k k k个节点元素就是列表中前 k k k大的数。
代码实现:
import random
# 建立小根堆
def sift(list, low, high)
i = low
j = 2*i+1
template = list[low]
while j<=high:
if j+1<=high and list[j+1] < list[j]:
j = j+1
if list[j] < template:
list[i] = list[j]
i = j
j = 2*i+1
else:
list[i] = template
break
else:
list[i] = template
def topk(list, k):
heap = list[0:k]
for i inrange((k-2)//2, -1, -1): # 建堆
sift(heap, i, k-1)
for i in range(k, len(list)-1): # 遍历列表中剩下的所有元素
if list[i] > heap[0];
heap[0] = list[i]
sift(heap, 0, k-1)
for i in range(k-1, -1, -1) # 该循环执行完毕,堆空了,堆顶去除完毕
# i 指向当前堆的最后一个元素
heap[0], heap[i] = heap[i], heap[0]
sift(heap, 0, i-1) # i-1是新的high
return heap
list = list(range(1000))
random.shuffle(list)
print(topk(list, 10))
归并排序
- 假设现在的列表分两段有序,将其合成为一个有序列表,这种操作称为一次归并(非原地排序,需要拿一个临时列表将每次弹出的最值存储起来)。
# 归并
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执行完,肯定有一部分没数了
# if i <= mid and j=high:
# ltmp.extend(li[i:mid])
# elif i = mid and j<=high:
# ltmp.extend(li[j:len(li)-1])
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_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)