对于前K个问题,描述很简单,即有一个相对较大的数据,求其中前K个大的数据,比如:
问题:当前有10w个数据,求出其中前100大的数据,或者求其中第100大的数据
常见解决前K个问题的主要有以下的解决方案:
注意:此问题的通用解法是使用分治法,如果数据重复性高的话可以使用bigmap
我们首先生成10w条数据,求其中前100个:
def create_test_data():
number = 100000
return [random.randint(1, number) for i in range(number)]
对于堆排序,堆的调整,在以下的文章中已经详细说明了,这个地方不再细说,直接使用:堆排序说明传送门
因为需要求取前k个最大或者最小的值,那么按照堆的性质,如果需要求找前k个最大的数据,则我们需要构建一个小顶堆,堆顶元素就是这第k个数的最小值.,只要这个数据比堆顶元素大,堆顶元素就应该弹出,对当前数据进行自顶向下操作,得到的新的堆顶元素就是这第k个数的新最小值。
构建一个长度为k的堆,然后使用data里面的数据遍历的与这个堆的堆顶元素进行比较,如果大于堆顶元素,则弹出堆顶,当前值换到堆顶元素上,并且进行自顶向下的堆调整,即进行一次堆的插入操作
然后求解前k个数据的问题:
def top_k_to_heap(data, k):
"""
前k个问题
问题描述:当前有100000个数据,求出其中前100大的数据,或者求其中第100大的数据
1. 如果求前k大,则构造小顶堆
2. 如果求前k小,则构建大顶堆
3. 比较堆顶元素,因为堆顶元素就是第k个元素
本题求解的是第K个大的问题
:param data: 数据集合
:param k: k的长度
:return:
"""
if not data:
data = create_test_data()
# 构建一个长度为k的堆
heap_data = make_heap(data[:k])
# 对剩余数据,与堆顶元素比较
for i in range(k, len(data)):
num = data[i]
if num < heap_data[0]: # 不能等于,因为前k个可能有重复的
continue
heap_data[0] = num
min_adjust_heap_top2down(heap_data, 0)
return heap_data
我们可以使用优先队列解决这个问题,优先队列本质上也是堆,优先级队列是不同于先进先出队列的另一种队列。每次从队列中取出的是具有最高优先权的元素。
在我们说堆的存储当中,一般使用数组来存储堆,即对于小顶堆:
1
/ \
2 3
/ \ / \
4 5 6 7
我们可以表示为:
min_heap = [1, 2, 3, 4, 5, 6, 7]
而对于优先队列,它的最高权值即小顶堆的堆顶元素,可以认为优先队列的权值是按照小顶堆的下标决定的。,所以如果我们往队列中存入:[1, 3, 5, 10, 2],自动会被排列[1, 2, 3, 5, 10]
不同语言使用了一些包可以直接调度用优先队列,比如java的import java.util.PriorityQueue,python的import heapq。这里使用heapq。
def top_k_to_priority_queue(data, k):
"""
优先队列
:param data:
:param k:
:return:
"""
if not data:
data = create_test_data()
queue = []
for i in range(k):
heapq.heappush(queue, data[i])
for i in range(k, len(data)):
num = data[i]
if num < queue[0]:
continue
heapq.heappop(queue)
heapq.heappush(queue, num)
return queue
使用桶排序完成前K个问题,只是在数据比较分散相对能够提高一定速度,数据内容主要集中在某一个范围内,那么效率则跟排序算法差不多。使用桶排序的求前K个问题步骤:
代码:
def top_k_to_bucket_sort(data, k):
"""
桶排序求Top K
:param data: len(data) > 10000
:param k:
:return:
"""
if not data:
data = create_test_data()
data_max = max(data)
data_len = len(data)
limit_num = data_len // 100 # 一个可控范围的值
# 生成桶
key_list = list(range(((data_max + 1) // k) + 1)) # 划定桶的范围,我们取最大值除以k
bucket = {
i: [] for i in key_list}
# 数据入桶
for i in data:
key = i // k
bucket[key].append(i)
now_len = k # 距离k个数据的剩余值
ret_data = []
for i in range(len(key_list) - 1, -1, -1):
now_bucket = bucket[key_list[i]]
bucket_len = len(now_bucket)
if now_len >= bucket_len:
ret_data.extend(now_bucket)
now_len -= bucket_len
if now_len == 0:
return ret_data
# 如果这个数据在一个可接受的范围内,到下一个bucket数据中去取剩余值
if now_len < limit_num:
pre = 0 if i-1 == 0 else i-1
now_bucket = bucket[key_list[pre]]
extend_data = sort_cut_top_k(now_bucket, now_len)
ret_data.extend(extend_data)
return ret_data
continue
# now_len < bucket_len
extend_data = sort_cut_top_k(now_bucket, now_len)
ret_data.extend(extend_data)
return ret_data
分治法求解Top k问题是最常用的方法,与快速排序不一样的是,我们只需要求一半的快速排序,主要步骤:
代码:
def top_k_to_divide(data, k):
"""
分治法
:param data:
:param k:
:return:
"""
if not data:
data = create_test_data()
return divide_function(data, 0, len(data) - 1, k)
def divide_function(data, left, right, k):
"""
实际调用的分治法方法
:param data:
:param left:
:param right: 一直是len(data) - 1
:param k:
:return:
"""
if left < right and k < right - left:
pos = divide_position(data, left, right)
# 当发现当前的数据已经小于k的时候,已经没有办法分治,此时数据量接近k,直接排序截取即可
if k >= (right - pos):
cut_data = data[left:]
return sort_cut_top_k(cut_data, k)
return divide_function(data, pos + 1, right, k)
def divide_position(data, left, right):
index = data[right] # 取最后一个当做主元
i = left - 1 # 第一个数据项,由于left加进来之后+1了,这里-1
for j in range(left, right):
if index > data[j]: # 如果比主元小,那么放到左边
i += 1
data[i], data[j] = data[j], data[i]
# 最后调换主元与第i+1个的数据,此时的第i+1个数据肯定比主元大,放在最后面也就没啥问题了
data[i + 1], data[right] = data[right], data[i + 1]
return i + 1
bigmap处理大数据的方法,适用的场景更大一些,比如有1亿数据,需要找出其中的前1000个数据,这时候就需要考虑到进程能够分配多少内存的问题了
题目:当我有1亿个浮点型数据,大概是0.913GB的数据,只给你1MB的内存空间,求前1000个数据
这个数据量在我们硬盘中是一个非常大的数据集合
我们只给进程分配1MB的内存,是肯定无法一次把所有数据内容都读取到内存中的
因此第一个需要做的,应该是把这个数据先拆分成一个个小的文件,步骤如下:
这个代码示例相对来说比较复杂,这里就不提供了。
def sort_cut_top_k(data, k):
"""
排序截取前top个
:param data: len(data) > k
:param k:
:return:
"""
data = sorted(data)
return data[len(data) - k:]
def cmp_list(list1, list2):
"""
比对两个乱序的list内的数是否是一样的
:param list1:
:param list2:
:return:
"""
count_map1 = {
}
count_map2 = {
}
data_len = len(list1)
if data_len != len(list2):
return False
for i in range(data_len):
append_count_map(count_map1, list1[i])
append_count_map(count_map2, list2[i])
return operator.eq(count_map1, count_map2)
if __name__ == '__main__':
data = create_test_data(number=30)
print(data)
k = 8
heap_res = top_k_to_heap(copy.deepcopy(data), k)
priority_res = top_k_to_priority_queue(copy.deepcopy(data), k)
bucket_res = top_k_to_bucket_sort(copy.deepcopy(data), k)
divide_res = top_k_to_divide(copy.deepcopy(data), k)
print("heap_res", heap_res)
print("priority_res", priority_res)
print("bucket_res", bucket_res)
print("divide_res", divide_res)
print("heap_res is", cmp_list(priority_res, heap_res))
print("bucket_res is", cmp_list(priority_res, bucket_res))
print("divide_res is", cmp_list(priority_res, divide_res))