剑指offer系列-面试题40-最小的k个数(python)

文章目录

  • 1. 题目
  • 2. 解题思路
  • 3. 代码实现
    • 3.1 思路1
    • 3.2 思路2
    • 3.3 思路3
  • 4. 总结
  • 5. 参考文献

1. 题目

输入n个整数,找出其中最小的k个数。例如,输入4, 5, 1, 6, 2, 7, 3, 8这8个数字,则最小的4个数字是1, 2, 3, 4。

2. 解题思路

我的思路

看到这个题目之后,我有两个思路:
思路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。

3. 代码实现

3.1 思路1

让我们回想下,快排的快就快在双指针。
时间复杂度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)

3.2 思路2

这个思路时间复杂度太高了,而且还使用了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)

3.3 思路3

此处代码引用 窥探算法之美妙——寻找数组中最小的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)

4. 总结

pass

5. 参考文献

[1] 剑指offer丛书
[2] 窥探算法之美妙——寻找数组中最小的K个数&python中巧用最大堆

你可能感兴趣的:(算法)