目录
一、剑指 Offer 40. 最小的 k 个数
1.1 题求
1.2 求解
法一:堆排序
# 52ms - 91.70% - 使用了 heapq 模块 API - 不推荐
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
import heapq
return heapq.nsmallest(k, arr)
# 40ms - 99.45%
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
if not k:
return []
hp = arr[:k] # 最大堆大小/容量
hp = [-x for x in hp] # 最小 -> 最大
heapq.heapify(hp) # list -> heap
for i in range(k, len(arr)):
if -arr[i] > hp[0]: # 更大 (原本更小)
heapq.heappop(hp) # 弹出
heapq.heappush(hp, -arr[i]) # 压入
return [-x for x in hp] # 最大 -> 最小
法二:插入排序
# 超出时间限制
class Solution:
''' https://blog.csdn.net/qq_39478403/article/details/107838299 '''
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
for i in range(1, len(arr)):
cur_val = arr[i] # 当前待比较并确定位置的值
cur_idx = i # 比较起点
while cur_idx > 0 and cur_val < arr[cur_idx-1]:
arr[cur_idx] = arr[cur_idx-1] # 平移
cur_idx -= 1 # 回滚
arr[cur_idx] = cur_val # 固定
return arr[:k]
法三:选择排序
# 超出时间限制
class Solution:
''' https://blog.csdn.net/qq_39478403/article/details/106980383 '''
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
n = len(arr)
for i in range(n-1):
min_idx = i # 当前待确定的最小值位置索引
for j in range(i+1, n):
if arr[j] < arr[min_idx]: # 找到未排序序列的最小值索引
min_idx = j
arr[i], arr[min_idx] = arr[min_idx], arr[i]
return arr[:k]
法四:归并排序
# 364ms - 7.06%
class Solution:
''' https://blog.csdn.net/qq_39478403/article/details/107020558 '''
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
def merge_sort(array, left_index, right_index):
''' 归并排序函数 - 递归实现 '''
if left_index >= right_index: # 基本情况, 此时子数组长度为 1
return
mid = (left_index + right_index) // 2 # 中间元素索引
merge_sort(array, left_index, mid) # 左半区间子数组
merge_sort(array, mid+1, right_index) # 右半区间子数组
merge(array, left_index, right_index, mid) # 左、右子数组归并
def merge(array, left_index, right_index, mid):
''' 归并函数 - 对两个子数组排序+组合 '''
left_copy = array[left_index: mid+1] # 左子数组 拷贝
right_copy = array[mid+1: right_index+1] # 右子数组 拷贝
left_copy_index = 0 # 左拷贝子数组 元素索引
right_copy_index = 0 # 右拷贝子数组 元素索引
sorted_index = left_index # 待修改 元素索引
# 两个数组同时参与排序
while (left_copy_index < len(left_copy)) and (right_copy_index < len(right_copy)):
# 左小右大
if left_copy[left_copy_index] <= right_copy[right_copy_index]:
array[sorted_index] = left_copy[left_copy_index] # in-place
left_copy_index += 1 # 左进
# 左大右小
else:
array[sorted_index] = right_copy[right_copy_index] # in-place
right_copy_index += 1 # 右进
# 待修改位置索引+1
sorted_index += 1
# 右子数组提前耗尽, 左子数组剩余依次加入
while left_copy_index < len(left_copy):
array[sorted_index] = left_copy[left_copy_index]
left_copy_index += 1
sorted_index += 1
# 左子数组提前耗尽, 右子数组剩余依次加入
while right_copy_index < len(right_copy):
array[sorted_index] = right_copy[right_copy_index]
right_copy_index += 1
sorted_index += 1
merge_sort(arr, 0, len(arr))
return arr[:k]
官方说明
# 252ms - 22.43%
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
def quick_sort(arr, l, r):
# 子数组长度为 1 时终止递归
if l >= r:
return
# 哨兵划分操作(以 arr[l] 作为基准数)以数组某个元素(一般选取首元素)为 基准数
# 将所有小于基准数的元素移动至其左边,大于基准数的元素移动至其右边。
i, j = l, r
while i < j:
while i < j and arr[j] >= arr[l]: # 从右向左查找首个小于基准数的索引 j
j -= 1
while i < j and arr[i] <= arr[l]: # 从左向右查找首个大于基准数的索引 i
i += 1
arr[i], arr[j] = arr[j], arr[i] # 交换, 使小于基准数的在左边, 大于基准数的在右边
# i, j 两哨兵相遇时跳出, 此时令 i=j 处于 l 交换
arr[l], arr[i] = arr[i], arr[l]
# 递归左、右子数组执行哨兵划分
quick_sort(arr, l, i-1)
quick_sort(arr, i+1, r)
quick_sort(arr, 0, len(arr)-1)
return arr[:k]
# 128ms - 37.08%
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
if k >= len(arr):
return arr
def quick_sort(l, r):
# 快速排序
i, j = l, r
while i < j:
while i < j and arr[j] >= arr[l]:
j -= 1
while i < j and arr[i] <= arr[l]:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[l], arr[i] = arr[i], arr[l]
# 根据题求优化
if k < i:
return quick_sort(l, i-1) # 代表第 k+1 小的数字在左子数组中,则递归左子数组
if k > i:
return quick_sort(i+1, r) # 代表第 k+1 小的数字在右子数组中,则递归右子数组
# 未排序的部分顺序不重要, 在前 k 个即可
return arr[:k] # k == i 代表此时 arr[k] 即为第 k+1 小的数字,则直接返回数组前 k 个数
return quick_sort(0, len(arr)-1)
# 优化 - 108ms - 43.08%
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
if not k:
return []
def quick_sort(arr, l, r):
if l >= r:
return
i, j = l, r
while i < j:
while i < j and arr[l] <= arr[j]:
j -= 1
while i < j and arr[l] >= arr[i]:
i += 1
arr[i], arr[j] = arr[j], arr[i]
arr[i], arr[l] = arr[l], arr[i] # 首个元素/参考元素 与 中间元素交换, 此时 i=j
if i > k:
quick_sort(arr, l, i-1) # 仅需排序左半部分, 因为右半部分不会取到
elif i < k:
quick_sort(arr, i+1, r) # 仅需排序右半部分, 因为左半部分全都取到
quick_sort(arr, 0, len(arr)-1) # 快速排序
return arr[:k]
1.3 解答
参考资料:
《剑指 Offer 第二版》
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/ohvl0d/
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/ohwddh/
https://www.jianshu.com/p/801318c77ab5
https://www.cnblogs.com/wangbin2188/p/13094033.html
二、剑指 Offer 41. 数据流中的中位数 ☆
2.1 题求
2.2 求解
法一:单链表
# 超出时间限制
class Node:
def __init__(self, _val=None, _next=None):
"""
node of single-linked list
"""
self.val = _val
self.next = _next
class MedianFinder:
def __init__(self):
"""
initialize your data structure here.
"""
self.dummy = Node() # 哨兵节点
self.num = 0 # 节点计数
def addNum(self, num: int) -> None:
# 新增节点
new_node = Node(num)
self.num += 1
# 哨兵节点
cur_node = self.dummy
# 寻找新节点插入位置
while cur_node.next and cur_node.next.val < num:
cur_node = cur_node.next
# 插入新节点
new_node.next = cur_node.next
cur_node.next = new_node
return
def findMedian(self) -> float:
# 哨兵节点
cur_node = self.dummy
# 商及余数
div, mod = divmod(self.num+1, 2)
# 商决定必走步数
for _ in range(div):
cur_node = cur_node.next
res = cur_node.val
# 余数决定是否再走一步 (偶数个节点)
if mod == 1:
res = (res + cur_node.next.val) / 2
return res
# Your MedianFinder object will be instantiated and called as such:
# obj = MedianFinder()
# obj.addNum(num)
# param_2 = obj.findMedian()
官方说明
# 176ms - 89.22% - 最佳方案
from heapq import *
class MedianFinder:
def __init__(self):
self.A = [] # 小顶堆,保存较大的一半 + (如 6, 5, 4)
self.B = [] # 大顶堆,保存较小的一半 (如 -1, -2, -3)
def addNum(self, num: int) -> None:
# 若 小顶堆A 和 大顶堆B 个数不等, 则新值 num 加入 A, 再从 A 顶弹出取反加入 B, 从而 A B 个数相等
if len(self.A) != len(self.B):
heappush(self.A, num)
heappush(self.B, -heappop(self.A))
# 若 小顶堆A 和 大顶堆B 个数相等, 则新值 num 取反加入 B, 再从 B 顶弹出取反加入 A, 从而 A 多1个
else:
heappush(self.B, -num) # 注意符号!!
heappush(self.A, -heappop(self.B))
def findMedian(self) -> float:
# 若 小顶堆A 和 大顶堆B 个数不等, 仅取 A 顶
# 若 小顶堆A 和 大顶堆B 个数相等, 则取 A 顶 和 B 顶反 的均值
return self.A[0] if len(self.A) != len(self.B) else (self.A[0] - self.B[0]) / 2.0
2.3 解答
参考资料:
《剑指 Offer 第二版》
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/5vd1j2/
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/5v0zcc/
三、剑指 Offer 45. 把数组排成最小的数 ☆
3.1 题求
3.2 求解
官方说明
# 40ms - 83.11%
class Solution:
def minNumber(self, nums: List[int]) -> str:
def quick_sort(l , r):
# 索引越界, 完成子集排序
if l >= r:
return
# 左、右指针起点
i, j = l, r
while i < j:
# 小的放左边, 大的放右边
while strs[j] + strs[l] >= strs[l] + strs[j] and i < j: # 自定义排序规则
j -= 1
while strs[i] + strs[l] <= strs[l] + strs[i] and i < j: # 自定义排序规则
i += 1
# 交换, 使左小右大
strs[i], strs[j] = strs[j], strs[i]
# 跳出, 中间位置与首个位置元素交换
strs[i], strs[l] = strs[l], strs[i]
# 左、右子集快排
quick_sort(l, i-1)
quick_sort(i+1, r)
# int -> str
strs = [str(num) for num in nums]
# 基于自定义规则的快排
quick_sort(0, len(strs)-1)
return ''.join(strs)
# 36ms - 92.48%
class Solution:
def minNumber(self, nums: List[int]) -> str:
def sort_rule(x, y):
a, b = x + y, y + x
if a > b:
return 1
elif a < b:
return -1
else:
return 0
strs = [str(num) for num in nums]
strs.sort(key = functools.cmp_to_key(sort_rule)) # 根据自定义规则排序
return ''.join(strs)
3.3 解答
参考资料:
《剑指 Offer 第二版》
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/59ypcj/
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/59ceyt/
四、剑指 Offer 61. 扑克牌中的顺子
4.1 题求
4.2 求解
法一:排序 + 规则
# 20ms - 99.84%
class Solution:
def isStraight(self, nums: List[int]) -> bool:
# 排序
nums.sort()
# 相邻数值之差 (>1) 计数器
diff_sum = 0
for i in range(4, 0, -1): # i = 4, 3, 2, 1
# 若减数不为 0
if nums[i-1] != 0:
# 若被减数和减数均非 0, 则因重复不是顺子
if nums[i] == nums[i-1]:
return False
# 若相邻数值之差大于 1, 则非顺子, 累积差值
diff = nums[i] - nums[i-1]
if diff > 1:
diff_sum += diff
# 若减数为 0
else:
# 若顺子差的牌数 (diff_sum//2) 小于大/小王 (0) 的个数, 则可以构成顺子
return (diff_sum // 2) <= i
# 牌中未出现大/小王 (0), 是否构成顺子取决于是否存在相邻数值之差大于 1
return diff_sum == 0
官方说明
# 28ms - 95.53%
class Solution:
def isStraight(self, nums: List[int]) -> bool:
repeat = set()
ma, mi = 0, 14
for num in nums:
if num == 0: # 跳过大小王
continue
ma = max(ma, num) # 最大牌
mi = min(mi, num) # 最小牌
if num in repeat: # 若有重复,提前返回 false
return False
repeat.add(num) # 添加牌至 Set
return ma - mi < 5 # 最大牌 - 最小牌 < 5 则可构成顺子
# 32ms - 86.18%
class Solution:
def isStraight(self, nums: List[int]) -> bool:
joker = 0 # 最小牌起点 / joker 数
nums.sort() # 数组排序
for i in range(4):
if nums[i] == 0:
joker += 1 # 统计大小王数量
elif nums[i] == nums[i+1]:
return False # 若有重复,提前返回 false
return nums[4] - nums[joker] < 5 # 最大牌 - 最小牌 < 5 则可构成顺子
4.3 解答
参考资料:
《剑指 Offer 第二版》
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/57mpoj/
https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/572x9r/