剑指Offer 4000 最小的k个数(partition操作) (简单, 2021-12)
剑指Offer 4100 数据流中的中位数 (困难, 2021-12)
剑指Offer2 076 数组中的第K大的数字 (中等, 2022-02)
牛客 0051 合并k个已排序的链表 (较难, 2022-03)
输入整数数组 arr ,找出其中最小的 k 个数
详细描述
输入整数数组 arr ,找出其中最小的 k 个数。例如,输入4、5、1、6、2、7、3、8这8个数字,则最小的4个数字是1、2、3、4。
示例 1:
输入:arr = [3,2,1], k = 2
输出:[1,2] 或者 [2,1]
示例 2:
输入:arr = [0,1,2,1], k = 1
输出:[0]
限制:
0 <= k <= arr.length <= 10000
0 <= arr[i] <= 10000
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/zui-xiao-de-kge-shu-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
- 快排的过程:
- partition 过程:以数组某个元素(一般取首元素)为基准数,将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。
- 递归地对左右部分执行 partition 过程,直至区域内的元素数量为 1;
- 基于以上思想,当某次划分后基准数正好是第 k+1 小的数字,那么此时基准数左边的所有数字便是题目要求的最小的 k 个数。
Python
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
def partition(lo, hi): # [lo, hi]
if lo >= hi: return
p = arr[lo] # 选取第一个位置为基准点
l, r = lo, hi # l 的初始位置应该在 lo,而不是 lo + 1
# 假设初始化为 lo + 1,当 a[lo] 为最小值时,此时训处循环后 l == r == lo + 1,再交换 a[lo] 和 a[l] 就会出错
while l < r: # 退出循环时 l == r
# 先移动 r,在移动 l,此时退出循环后 l 和 r 同时指向一个小于 p 的值;反之,如果用 a[hi] 初始化 p,就要先移动 l,在移动 r;
# 从 r 开始,从右往左找到第一个 < p 的值,所以循环条件是 >=
while l < r and arr[r] >= p: r -= 1
# 从 l 开始,从左往右找到第一个 > p 的值,所以循环条件是 <=
while l < r and arr[l] <= p: l += 1
arr[l], arr[r] = arr[r], arr[l]
arr[lo], arr[l] = arr[l], arr[lo] # 将基准点移动到分界点
if l < k: partition(l + 1, hi)
if l > k: partition(lo, l - 1)
partition(0, len(arr) - 1)
return arr[:k]
- 写法1) 维护一个长度为 k 的大顶堆(第一个数最大),当下一个元素小于堆顶值,就更新堆(弹出堆顶,插入新值);
- 写法2) 直接对整个数组构建一个小顶堆,然后循环弹出前 k 个值;
- 注意写法1 的时间复杂度是
O(NlogK)
,而写法2 是O(NlogN)
;
Python:写法1(使用库函数)
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
if k < 1 or not arr: # 使用堆,要添加非空断言
return []
import heapq
# python 默认是小顶堆,且不支持自定义比较函数,所以要添加负号转成取前 k 大的数
ret = [-x for x in arr[:k]]
heapq.heapify(ret)
for i in range(k, len(arr)):
if -arr[i] > ret[0]:
heapq.heappop(ret)
heapq.heappush(ret, -arr[i])
return [-x for x in ret]
Python:写法2(使用库函数)
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
if k < 1 or not arr: # 使用堆,要添加非空断言
return []
import heapq
# python 默认是小顶堆
heapq.heapify(arr)
ret = []
for _ in range(k):
ret.append(heapq.heappop(arr))
return ret
- 因为题目限制了
arr[i]
的范围,所以还可以使用计数排序,时间复杂度O(N)
;
Python
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
if k >= len(arr): # 使用计数排序要加长度判断
return arr
dp = [0] * 10001
for x in arr:
dp[x] += 1
ret = []
cnt = 0
for i in range(len(dp)):
while dp[i] and cnt < k:
ret.append(i)
cnt += 1
dp[i] -= 1
if cnt == k:
return ret
设计一个支持以下两种操作的数据结构:
void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。
详细描述
如何得到一个数据流中的中位数?如果从数据流中读出奇数个数值,那么中位数就是所有数值排序之后位于中间的数值。如果从数据流中读出偶数个数值,那么中位数就是所有数值排序之后中间两个数的平均值。
例如,
[2,3,4] 的中位数是 3
[2,3] 的中位数是 (2 + 3) / 2 = 2.5
设计一个支持以下两种操作的数据结构:
void addNum(int num) - 从数据流中添加一个整数到数据结构中。
double findMedian() - 返回目前所有元素的中位数。
示例 1:
输入:
["MedianFinder","addNum","addNum","findMedian","addNum","findMedian"]
[[],[1],[2],[],[3],[]]
输出:[null,null,null,1.50000,null,2.00000]
示例 2:
输入:
["MedianFinder","addNum","findMedian","addNum","findMedian"]
[[],[2],[],[3],[]]
输出:[null,null,2.00000,null,2.50000]
限制:
最多会对 addNum、findMedian 进行 50000 次调用。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/shu-ju-liu-zhong-de-zhong-wei-shu-lcof
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
- 分别使用一个大顶堆存放较小的一半(堆顶为其中的最大值),和一个小顶堆存放较大的一半(堆顶为其中的最小值);
- 动态保持两个堆的元素数量相等或差1(为了减少判断,可以始终保持固定的堆数量多1)
Python:优化前
- 这份代码的逻辑非常直白,看上起也比较啰嗦;
import heapq
class MedianFinder:
def __init__(self):
"""
initialize your data structure here.
"""
self.lo = [] # 大顶堆,维护小于中位数的部分
self.hi = [] # 小顶堆,维护大于中位数的部分
self.cnt = 0 # 计数
def addNum(self, num: int) -> None:
if self.cnt == 0: # 初始化
heapq.heappush(self.hi, num)
self.cnt += 1
return
if num > self.findMedian(): # to hi
if self.cnt % 2:
heapq.heappush(self.hi, num)
tmp = heapq.heappop(self.hi)
heapq.heappush(self.lo, -tmp)
else:
heapq.heappush(self.hi, num)
else: # to lo
if self.cnt % 2:
heapq.heappush(self.lo, -num)
else:
heapq.heappush(self.lo, -num)
tmp = heapq.heappop(self.lo)
heapq.heappush(self.hi, -tmp)
self.cnt += 1
def findMedian(self) -> float:
if self.cnt % 2:
return self.hi[0]
else:
return (-self.lo[0] + self.hi[0]) / 2
Python:优化后
from heapq import *
class MedianFinder:
def __init__(self):
self.hi = [] # 小顶堆,保存较大的一半
self.lo = [] # 大顶堆,保存较小的一半
def addNum(self, num: int) -> None:
# 开始时,都为 0,先存入 self.lo,在转移到 self.hi
if len(self.hi) == len(self.lo):
heappush(self.lo, -num)
heappush(self.hi, -heappop(self.lo))
else:
heappush(self.hi, num)
heappush(self.lo, -heappop(self.hi))
def findMedian(self) -> float:
if len(self.hi) != len(self.lo):
return self.hi[0]
else:
return (-self.lo[0] + self.hi[0]) / 2
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
详细描述
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
示例 2:
输入: [3,2,3,1,2,4,5,5,6] 和 k = 4
输出: 4
提示:
1 <= k <= nums.length <= 10^4
-10^4 <= nums[i] <= 10^4
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/xx4gT2
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
思路1:partition操作(分治)
- partition操作描述:先随机确定一个锚点,然后将数组划分为小于锚点和大于锚点的两部分呢;
import random
class Solution:
""""""
def findKthLargest(self, nums: List[int], k: int) -> int: # noqa
""""""
lo, hi = 0, len(nums) - 1
while True: # 第 k 大,排序后期下标应该是 k - 1
idx = self.partition(nums, lo, hi)
if idx + 1 == k:
return nums[idx]
elif idx + 1 < k:
lo = idx + 1
else:
hi = idx - 1
def partition(self, nums: List[int], lo: int, hi: int) -> int:
""""""
# === 挑选锚点 ===
# 方式1)默认选 lo 作为锚点
# pivot = nums[lo]
# 方式2)随机选择一个锚点,并把锚点固定到首位或末位,这里交换到首位
flag = random.randint(lo, hi)
pivot = nums[flag]
nums[flag], nums[lo] = nums[lo], nums[flag]
# === partition 操作 ===
# 方式1)单向遍历
idx = lo # 记录锚点在数组中的升序顺位
for i in range(lo + 1, hi + 1):
if nums[i] > pivot: # 找到一个大于锚点的值
idx += 1
nums[idx], nums[i] = nums[i], nums[idx]
nums[idx], nums[lo] = nums[lo], nums[idx] # 把锚点交换到 idx 的位置
return idx
# 方式2)左右交换
# l, r = lo, hi
# while l < r:
# while l < r and nums[r] <= pivot:
# r -= 1
# while l < r and nums[l] >= pivot:
# l += 1
# if l < r:
# nums[l], nums[r] = nums[r], nums[l]
# nums[lo], nums[l] = nums[l], nums[lo]
#
# return l
思路2:大顶堆(Python,调库)
import heapq
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
""""""
heap = []
for x in nums:
heapq.heappush(heap, -x) # 默认是小顶堆,这里传入 -x,模拟大顶堆
for _ in range(k - 1):
heapq.heappop(heap)
return -heap[0]
合并 k 个升序的链表并将结果作为一个升序的链表返回其头节点。
写法1:不重载运算符,利用 tuple 排序
class Solution:
def mergeKLists(self , lists: List[ListNode]) -> ListNode:
import heapq
h = []
cnt = 0 # 一个自增变量,避免直接对 node 排序
# init
for node in lists:
if node:
heapq.heappush(h, (node.val, cnt, node))
cnt += 1
cur = dummy = ListNode(0)
while h:
_, _, node = heapq.heappop(h)
cur.next = node
cur = cur.next
if node.next:
node = node.next
heapq.heappush(h, (node.val, cnt, node))
cnt += 1
return dummy.next
写法2:重载运算符
class Solution:
def mergeKLists(self , lists: List[ListNode]) -> ListNode:
import heapq
h = []
cnt = 0
# 重载运算符
def lt(a, b):
return a.val < b.val
ListNode.__lt__ = lt
# 下面的写法也可以,但是不推荐,因为 lambda 表达式是不支持序列化的
# ListNode.__lt__ = lambda a, b: a.val < b.val
# init
for node in lists:
if node:
heapq.heappush(h, node)
cur = dummy = ListNode(0)
while h:
node = heapq.heappop(h)
cur.next = node
cur = cur.next
if node.next:
node = node.next
heapq.heappush(h, node)
return dummy.next