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
posted @ 2018-06-29 17:41  听雨危楼  阅读(641)  评论(0编辑  收藏  举报