输入n个整数,找出其中最小的k个数。例如,输入4, 5, 1, 6, 2, 7, 3, 8这8个数字,则最小的4个数字是1, 2, 3, 4。
我的思路
看到这个题目之后,我有两个思路:
思路1:(若要求数字不重复的话,才用的上这一步)对这n个整数先去重,
然后对去重后的数字升序排序,获取前k个数字。
但是这种算法的时间复杂度最小(快排排序)也是O(nlogn),时间复杂度有点高了。思路2:
每遍历一次获得一个最小值,遍历k次之后获得前k个最小的数字。
但是好像时间复杂度也挺高的O(kn)思路3:
能不能之遍历一遍就找出最小的前k个数字呢?
书中的思路
使用一个额外的长度为k的数据容器,使用最大堆作为容器来存储这个k个元素。最大堆的特点就是,根节点的值总是大于它子树中任意节点的值。这样就可以在O(1)时间内获得这k个元素中的最小值,但是需要O(logk)时间完成删除及插入操作;
当容器中元素数量小于k时,直接将当前元素加入容器中;
当容器中元素满了之后,遍历外部数组,比较此元素和容器中最大元素的大小。若小于其最大元素,则替换容器的最大元素为此元素,否则,不改变容器。
python中没有现成最大堆。但是在heapq包中有最小堆。
然后有人就想了个很聪明的法子实现最大堆。
push(e)改为push(-e),pop(e)为-pop(e),也就是说存入和取出的数都是相反数,其他逻辑和TopK相同。
实现用户自定义的比较函数,允许elem是一个tuple,按照tuple的第一个元素进行比较,所以可以把tuple的第一个元素作为我们的比较的key。
让我们回想下,快排的快就快在双指针。
时间复杂度O(nlogn),空间复杂度S(1)
def quick_sorted(array, left, right):
"""
快排
"""
low = left
high = right
key = array[low]
# 两个指针
while left < right:
while left < right and array[left] < key:
left += 1
array[right] = array[left]
while left < right and array[right] > key:
right -= 1
array[left] = array[right]
array[right] = key
# 这里不用分片,分片的话就不是原地排序了
quick_sorted(array, low, left-1)
quick_sorted(array, left+1, high)
def quick_sort(array, l, r):
if l < r:
q = partition(array, l, r)
quick_sort(array, l, q - 1)
quick_sort(array, q + 1, r)
def partition(array, l, r):
x = array[r]
i = l - 1
for j in range(l, r):
if array[j] <= x:
i += 1
array[i], array[j] = array[j], array[i]
array[i + 1], array[r] = array[r], array[i+1]
return i + 1
def get_least_numbers_1(array, k):
"""
思路1:时间复杂度O(nlogn),空间复杂度S(n)
获取最小的k个数字
:param array: 数组
:param k: 整数
"""
if not array or k > len(array) or k < 1:
return
# 若要求数字不重复的话
# array_c = list(set(array))
# 快排,时间复杂度O(nlogn)
quick_sort(array) # 升序排序,原地修改
return array[:k]
if __name__ == '__main__':
seq = [1, 4, 5, 1, 6, 2, 7, 3, 8, 3, 2, 3]
res1 = get_least_numbers_1(seq, 4)
print(res1)
这个思路时间复杂度太高了,而且还使用了python列表的方法(当然可以用一个循环实现)。
时间复杂度O(n^2),空间复杂度O(1)
def get_least_numbers_2(array, k):
"""
思路2:时间复杂度O(n^2),空间复杂度S(k)
遍历依次获得一个最小值,直到获得了k个最小值
获取最小的k个数字
:param array: 数组
:param k: 整数
"""
results = {} # 记录已经成为最小值的数字
if not array or k > len(array) or k < 1:
return
i = 0 # 遍历次数
# 遍历1次获得一个最小值
while i < k:
min_value = max(array)
# 遍历一遍数组,获得一个最小值,记录在字典中,防止获得重复的最小值
for val in array:
if val < min_value and results.get(val) != 1:
min_value = val
results[min_value] = 1
# 从数组中删除等于当前最小值的这些元素(如何删除所有?)
i += 1
return results.keys()
if __name__ == '__main__':
seq = [1, 4, 5, 1, 6, 2, 7, 3, 8, 3, 2, 3]
res2 = get_least_numbers_2(seq, 4)
print(res2)
此处代码引用 窥探算法之美妙——寻找数组中最小的K个数&python中巧用最大堆
时间复杂度O(nlogk),空间复杂度S(k)
import heapq
def get_least_numbers_3(array, k):
"""
获取最小的k个数字
因为heapq只有最小堆,没有最大堆,所以在入堆的时候,存入原始数据的负数。
在出堆之后,再取出存取的数据的负数。
:param array: 数组
:param k: 整数
"""
# 最大堆
max_heap = []
# 边界条件
if not array or k < 1 or k > len(array):
return
# 遍历数组
for ele in array:
# 当最大堆中元素数量小于k时,直接插入元素到堆中。
if len(max_heap) < k:
heapq.heappush(max_heap, -ele) # push元素到堆中
else:
# 堆中的最小值
max_value = -heapq.heappushpop(max_heap) # pop最小元素出堆, 然后取负数
if ele < max_value:
heapq.heappush(max_heap, -ele) # push元素到堆中
return map(lambda x:-x, max_heap)
if __name__ == '__main__':
seq = [1, 4, 5, 1, 6, 2, 7, 3, 8, 3, 2, 3]
res3 = get_least_numbers_3(seq, 4)
print(res3)
pass
[1] 剑指offer丛书
[2] 窥探算法之美妙——寻找数组中最小的K个数&python中巧用最大堆