1-算法 - Top K问题
about
Python3.6 + win10 + x1 carbon 7th i7 10710U
现在有n个数(有序当然最好),设计算法得到前k大的数(k<n)。
所谓top k的问题,其实就是各种排行榜问题,比如热搜榜、新歌榜.....
怎么实现,这里别扯什么Redis,咱们这里只考虑算法实现。解决思路:
- 排序后切片,时间复杂度是:O(nlogn)
- 使用Low B三人组,它们的时间复杂度是:O(kn)
- 堆排序,时间复杂度是:O(nlogk)
另外,n越大,堆排序越快(相对的,没有对比就没有伤害!),low B三人组就越慢;当然,n超级大,谁都搁不住。
冒泡排序实现
冒泡实现的套路就是,利用冒泡的特性,每趟都能选出一个最大的数,取前k大,就循环k趟,就行了,所以,只需要改下冒泡的算法即可。
原版冒泡算法,这是升序排序的:
def bubble_sort(li):
""" 冒泡排序"""
for i in range(len(li) - 1): # 从0开始的第i趟
for j in range(len(li) - i - 1): # 要循环的趟数
exchange = False
if li[j] > li[j + 1]: # 后一个数比当前数大,就交换位置
# if li[j] < li[j+1]: # 降序排序, 大于是升序排序
li[j], li[j + 1] = li[j + 1], li[j]
exchange = True # 说明有交换,此时列表还需要进行排序
# print('每一趟排序后的列表: ', li)
if not exchange: # 如果这一趟结束,没有发生交换,说明列表已经有序,可以结束算法了
return
li = [6, 8, 1, 9, 3, 0, 7, 2, 4, 5]
print('before: ', li)
bubble_sort(li)
print('after: ', li)
"""
before: [6, 8, 1, 9, 3, 0, 7, 2, 4, 5]
after: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
"""
为了美观,我们搞成降序的:
def desc_bubble_sort(li):
""" 冒泡排序,降序版 """
for i in range(len(li) - 1):
exchange = False
for j in range(len(li) - 1, i, -1): # 从右往左循环列表
if li[j] > li[j - 1]: # 如果当前元素比前一个元素大,就交换它们的位置
li[j], li[j - 1] = li[j - 1], li[j]
exchange = True # 说明有交换,此时列表还需要进行排序
if not exchange: # 如果这一趟结束,没有发生交换,说明列表已经有序,可以结束算法了
return
li = [6, 8, 1, 9, 3, 0, 7, 2, 4, 5]
print('before: ', li)
desc_bubble_sort(li)
print('after: ', li)
"""
before: [6, 8, 1, 9, 3, 0, 7, 2, 4, 5]
after: [9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
"""
其实,降序版的已经能够处理top k的问题了。但性能.....enmmmmmm......极差!下面有对比。
现在来处理top k问题:
import time
import random
import copy
def cal_time(func):
def wrapper(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)
print('{} running: {}, result: {}'.format(func.__name__, time.time() - start, res))
return res
return wrapper
@cal_time
def desc_bubble_sort(li, k):
""" 冒泡排序,降序版,处理top k问题 """
for i in range(len(li) - 1):
exchange = False
for j in range(len(li) - 1, i, -1): # 从右往左循环列表
if li[j] > li[j - 1]: # 如果当前元素比前一个元素大,就交换它们的位置
li[j], li[j - 1] = li[j - 1], li[j]
exchange = True # 说明有交换,此时列表还需要进行排序
if not exchange: # 如果这一趟结束,没有发生交换,说明列表已经有序,可以结束算法了
return li[0:k]
return li[0:k]
@cal_time
def desc_bubble_sort_topk(li, k):
""" 冒泡排序,降序top k 版 """
for i in range(k):
for j in range(len(li) - 1, i, -1): # 从右往左循环列表
exchange = False
if li[j] > li[j - 1]: # 如果当前元素比前一个元素大,就交换它们的位置
li[j], li[j - 1] = li[j - 1], li[j]
exchange = True # 说明有交换,此时列表还需要进行排序
if not exchange: # 如果这一趟结束,没有发生交换,说明列表已经有序,可以结束算法了
return li[0:k]
return li[0:k]
li = list(range(10000))
random.shuffle(li)
li1 = copy.deepcopy(li)
li2 = copy.deepcopy(li)
k = 10
desc_bubble_sort(li, k)
desc_bubble_sort_topk(li, k)
"""
desc_bubble_sort running: 9.295966148376465, result: [9999, 9998, 9997, 9996, 9995, 9994, 9993, 9992, 9991, 9990]
desc_bubble_sort_topk running: 0.00102996826171875, result: [9999, 9998, 9997, 9996, 9995, 9994, 9993, 9992, 9991, 9990]
"""
性能差的不是一个量级,如果列表长度超级大,使用desc_bubble_sort
来处理,真是超级慢,因为它的套路简单呐,先把列表降序排序,完事取前k个就行了,但慢就慢在除了前k个之外,后面是否有序我们不在意,但这个函数依然兢兢业业的排序完了,所以比较慢。
而desc_bubble_sort_topk
完全解决了desc_bubble_sort
存在的问题,你要前k大,我就循环k次就行了,后面的我也不管了。
但无论如何,冒泡处理top k的问题,总是不太适合,时间复杂度太高,因为列表长度是n,取前k大,它的复杂度就是:O(kn)。
插入排序实现
插入排序解决top k问题的时间复杂度也是:O(kn)。所以就.....没动力写了,等抽空练习插入排序算法的时候再补吧。
选择排序实现
同插入排序.......
堆排序实现
堆排解决top k问题无疑是个好办法!
来看思路:
- 取列表前k个元素建立一个小根堆,堆顶就是目前第k大的数。
- 依次向后遍历原列表,对于列表中的元素,如果小于堆顶,则忽略该元素;如果大于堆顶,则将堆顶更换为该元素,并且对堆进行一次调整。
- 遍历列表所有元素后,倒叙弹出堆顶。
来看实现:
import time
import random
import copy
def cal_time(func):
def wrapper(*args, **kwargs):
start = time.time()
res = func(*args, **kwargs)
print('{} running: {}, result: {}'.format(func.__name__, time.time() - start, res))
return res
return wrapper
def small_sift(li, low, high):
"""
小根堆调整函数
:param li: 列表
:param low: 最开始指向的是堆顶,也就是根节点
:param high: 指向的堆的最后一个元素,作用就是判断son是否越界
:return:
"""
i = low # 最开始指向堆顶,而后续 i 随着循环在改变
son = 2 * i + 1 # 最开始指向左孩子,后续 son 也随着循环和判断在改变
tmp = li[low] # 把堆顶存起来,此时堆顶位置为空,即 i 位置为空
while son <= high: # 只要 son 位置有值,就需要对比和调整,否则意味着 i 的空位可以放 tmp 了
if son + 1 <= high and li[son + 1] < li[son]: # 如果右孩子存在,且该右孩子比左孩子小,就进行调整
son += 1 # son从原本的左孩子指向右孩子
# 经过上面if之后,son现在指向的是左右两个孩子中较大的那个孩子,接下来就是要看较大的孩子和tmp比,是否需要上去
if li[son] < tmp: # 如果孩子节点(左右孩子都有可能)比父节点小,儿子节点就向上调整,补上父节点的空位
li[i] = li[son] # 孩子节点补上父节点的空位,注意,此时的孩子节点空了
i = son # 将 i 再次指向空位节点,这个节点也成为了新的父节点
son = 2 * i + 1 # 孩子节点重新指向新的 i 节点的左孩子节点,如果i是最下层了,那么它没有下级节点了,即son越界了
else: # 如果孩子节点没有父节点大,退出循环,将临时存储起来的原堆顶补上 i 所在的空位
break
li[i] = tmp # 临时存储起来的原堆顶补上 i 所在的空位,无论怎么调整,最后都需要补空位,所以,放到了循环外面
@cal_time
def heap_sort_topk(li, k):
"""
:param li: 列表n
:param k: k的长度
:return: 返回前k大的列表
"""
tmp_heap = li[0:k]
# 1. 建堆,建立小根堆
for i in range((k - 2) // 2, -1, -1):
small_sift(tmp_heap, i, k - 1)
# 2. 遍历整个列表,找出前k大的数
for i in range(k, len(li) - 1):
if li[i] > tmp_heap[0]: # 表示原列表中有值比堆顶大,就替换到堆顶,然后再调整
tmp_heap[0] = li[i]
small_sift(tmp_heap, 0, k - 1)
# 3. 挨个出数,小根堆此时就是所有符合条件的前k大的数
for i in range(k - 1, -1, -1):
tmp_heap[0], tmp_heap[i] = tmp_heap[i], tmp_heap[0]
small_sift(tmp_heap, 0, i - 1)
return tmp_heap
li = list(range(10000)) #
random.shuffle(li)
li1 = copy.deepcopy(li)
li2 = copy.deepcopy(li)
k = 10
desc_bubble_sort_topk(li, k)
heap_sort_topk(li, k)
"""
# n = 10000,冒泡还行,而堆排耗时太短了,时间装饰器都没统计到
desc_bubble_sort_topk running: 0.027262210845947266, result: [9999, 9998, 9997, 9996, 9995, 9994, 9993, 9992, 9991, 9990]
heap_sort_topk running: 0.0, result: [9999, 9998, 9997, 9996, 9995, 9994, 9993, 9992, 9991, 9990]
# n = 10000000 可以看到冒泡跟堆排还是差了18条街
desc_bubble_sort_topk running: 39.840404987335205, result: [9999999, 9999998, 9999997, 9999996, 9999995, 9999994, 9999993, 9999992, 9999991, 9999990]
heap_sort_topk running: 1.6941826343536377, result: [9999999, 9999998, 9999997, 9999996, 9999995, 9999994, 9999993, 9999992, 9999991, 9999990]
"""
that's all