文章目录
一、堆
1. 堆的定义
根据 维基百科 的定义,堆 是一种特别的二叉树,满足以下条件的二叉树,可以称之为 堆:(优先队列)
- 完全二叉树;(特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。需要注意的是,满二叉树肯定是完全二叉树,而完全二叉树不一定是满二叉树。)
- 每一个节点的值都必须 大于等于或者小于等于 其孩子节点的值。(即最大堆或最小堆)
堆 具有以下的特点: - 可以在 O ( l o g N ) O(logN) O(logN)的时间复杂度内向 堆 中插入元素;
- 可以在 O ( l o g N ) O(logN) O(logN)的时间复杂度内向 堆 中删除元素;
- 可以在
O
(
1
)
O(1)
O(1) 的时间复杂度内获取 堆 中的最大值或最小值。
2.堆的插入
-
最小堆的插入
插入的过程必须满足(堆的定义)
最后满足堆的定义,构造成新的最小堆。 -
最大堆的插入
此时就将15插入到最大堆中。 -
最小堆的删除
一般来说堆的删除都是删除堆顶元素。
将最后一个元素替换堆顶元素,并删除最后一个元素。
需要满足最小堆的特性。
这样就得到一个新的最小堆。 -
最大堆的删除
最后的元素替换第一个元素,并删除最后一个元素。
这样就得到一个新的最大堆。
二、题库
1.堆的实现(最大堆python)
# 「最大堆」的实现
import sys
class MaxHeap:
def __init__(self, heapSize):
# heapSize用于数组的大小,因为数组在创建的时候至少需要指明数组的元素个数
self.heapSize = heapSize
# 使用数组创建完全二叉树的结构,然后使用二叉树构建一个「堆」
self.maxheap = [0]*(heapSize+1)
# realSize用于记录「堆」的元素个数
self.realSize = 0
# 添加元素函数
def add(self, element):
self.realSize += 1
# 如果「堆」中元素的个数大于一开始设定的数组的个数,则返回「Add too many elements」
if self.realSize > self.heapSize:
print("Add too many elements!")
self.realSize -= 1
return
# 将添加的元素添加到数组中
self.maxheap[self.realSize] = element
# 新增元素的索引位置
index = self.realSize
# 新增元素的父节点的索引位置
# 注意,如果用数组表示完全二叉树,并且根结点存储在数组的索引1的位置的时候,任何一个节点的父节点索引位置为「该节点的索引位置/2」,任何一个节点的左孩子节点的索引位置为「该节点的索引位置*2」,任何一个节点的右孩子节点的索引位置为「该节点的索引位置*2+1」
parent = index // 2
# 当添加的元素大于父节点时,需要将父节点的值和新增元素的值交换
while (self.maxheap[index] > self.maxheap[parent] and index > 1):
self.maxheap[parent], self.maxheap[index] = self.maxheap[index], self.maxheap[parent]
index = parent
parent = index // 2
# 获取堆顶元素函数
def peek(self):
return self.maxheap[1]
# 删除堆顶元素函数
def pop(self):
# 如果当前「堆」的元素个数为0, 则返回「Don't have any element」
if self.realSize < 1:
print("Don't have any element!")
return sys.maxsize
else:
# 当前「堆」中含有元素
# self.realSize >= 1
removeElement = self.maxheap[1]
# 将「堆」中的最后一个元素赋值给堆顶元素
self.maxheap[1] = self.maxheap[self.realSize]
self.realSize -= 1
index = 1
# 当删除的元素不是孩子节点时
while (index < self.realSize and index <= self.realSize // 2):
# 被删除节点的左孩子节点
left = index * 2
# 被删除节点的右孩子节点
right = (index * 2) + 1
# 当删除节点的元素小于 左孩子节点或者右孩子节点,代表该元素的值小,此时需要将该元素与左、右孩子节点中最大的值进行交换
if (self.maxheap[index] < self.maxheap[left] or self.maxheap[index] < self.maxheap[right]):
if self.maxheap[left] > self.maxheap[right]:
self.maxheap[left], self.maxheap[index] = self.maxheap[index], self.maxheap[left]
index = left
else:
self.maxheap[right], self.maxheap[index] = self.maxheap[index], self.maxheap[right]
index = right
else:
break
return removeElement
# 返回「堆」的元素个数
def size(self):
return self.realSize
def toString(self):
print(self.maxheap[1 : self.realSize+1])
if __name__ == "__main__":
# 测试用例
maxHeap = MaxHeap(5)
maxHeap.add(1)
maxHeap.add(2)
maxHeap.add(3)
# [3,1,2]
maxHeap.toString()
# 3
print(maxHeap.peek())
# 3
print(maxHeap.pop())
# 2
print(maxHeap.pop())
# 1
print(maxHeap.pop())
maxHeap.add(4)
maxHeap.add(5)
# [5,4]
maxHeap.toString()
2.堆的常用方法
1.创建堆
创建堆指的是初始化一个堆实例(python库(heapq:只有最小堆无最大堆))
时间复杂度:
O
(
N
)
O(N)
O(N)
空间复杂度:
O
(
N
)
O(N)
O(N)
Python 最小堆转换城最大堆的逻辑:
就是将元素进行取反,如[1,2,3]。[-1,-2,-3],这时候建立最小堆即可,就满足最大堆的逻辑。
- 插入元素
插入操作指的是向 堆 中插入一个新元素。值的注意的是,新元素插入完毕后,堆 依旧需要维持它的特性。
- 时间复杂度: O ( l o g N ) O(logN) O(logN)。
- 空间复杂度: O ( 1 ) O(1) O(1)。
- 获取堆顶元素
最大堆 的堆顶元素是 堆 中的最大值,最小堆 的堆顶元素是 堆 中的最小值。因此,堆顶元素是 堆 中最重要的元素。
- 时间复杂度: O ( 1 ) O(1) O(1)。
- 空间复杂度: O ( 1 ) O(1) O(1)。
- 删除堆顶元素
删除堆顶元素后,值得注意的是,堆 依旧需要维持它的特性。此时,新的堆顶元素也是当前堆中的最大值(如果堆是 最大堆)或最小值(如果堆是 最小堆)。
- 时间复杂度: O ( l o g N ) O(log N) O(logN)。
- 空间复杂度: O ( 1 ) O(1) O(1)。
- 获取堆的长度
删除堆顶元素后,值得注意的是,堆 依旧需要维持它的特性。此时,新的堆顶元素也是当前堆中的最大值(如果堆是 最大堆)或最小值(如果堆是 最小堆)。
- 时间复杂度: O ( l o g N ) O(log N) O(logN)。
- 空间复杂度: O ( 1 ) O(1) O(1)。
- 获取堆的长度
堆 的长度可以用来判断当前堆的大小,也可以用来判断当前堆是否还有元素。如果当前堆中没有元素,则 堆 的长度为 0。
- 时间复杂度: O ( 1 ) O(1) O(1)
- 空间复杂度: O ( 1 ) O(1) O(1)
# 最小堆完整代码
import heapq
# 新建一个列表
minHeap = []
# 将列表堆化,即将列表转换为最小堆
heapq.heapify(minHeap)
# 分别往最小堆中添加3,1,2
heapq.heappush(minHeap, 3)
heapq.heappush(minHeap, 1)
heapq.heappush(minHeap, 2)
# 查看最小堆的所有元素,结果为:[1,3,2]
print("minHeap: ",minHeap)
# 获取最小堆的堆顶元素
peekNum = minHeap[0]
# 结果为:1
print("peek number: ", peekNum)
# 删除最小堆的堆顶元素
popNum = heapq.heappop(minHeap)
# 结果为:1
print("pop number: ", popNum)
# 查看删除1后最小堆的堆顶元素,结果为:2
print("peek number: ", minHeap[0])
# 查看最小堆的所有元素,结果为:[2,3]
print("minHeap: ",minHeap)
# 获取堆的元素个数,即堆的长度
size = len(minHeap)
# 结果为:2
print("minHeap size: ", size)
3.堆的应用
1.排序
思路:堆排序指的是利用堆的数据结构对一组无序元素进行排序。
最小堆排序算法步骤如下:
- 将所有元素堆化成一个最小堆;
- 去除并删除堆顶元素,并将堆顶元素放置在存储有序元素的数据集T中。
- 此时,堆会调整成新的最小堆。
- 重复3和4步骤,直到堆中没有元素;
- 此时得到一个新的数据集T,其中的元素按照从小到大的顺序排列。
最大堆的排序算法步骤如下:(其实可以利用最小堆来实现)
- 将所有元素堆化成一个 最大堆;
- 取出并删除堆顶元素,并将该堆顶元素放置在存储有序元素的数据集 T 中;
- 此时,堆 会调整成新的 最大堆;
- 重复 3 和 4 步骤,直到 堆 中没有元素;
- 此时得到一个新的数据集 T,其中的元素按照从大到小的顺序排列
时间复杂度: O ( N l o g N ) O(Nlog N) O(NlogN)。 N N N是 堆 中的元素个数。(每次获取一个最大/最小元素都需要logN的时间)
空间复杂度: O ( N ) O(N) O(N)。 N N N是 堆 中的元素个数。
2.Top K问题
1.解法1
思路:主要是利用堆的数据结构获取最大元素或者最小元素
【Top k小元素】解法步骤:
- 创建一个【最小堆】
- 将所有元素都加到【最小堆】中
- 通过【边删除边遍历】的方法,将堆顶元素删除,并将它保存到结果集T中
- 重复3步骤K次,直到去除前K个最小的元素。
时间复杂度: O ( K l o g N ) O(KlogN) O(KlogN)
空间复杂度: O ( N ) O(N) O(N),(堆的大小)
2.解法2(常用)
利用最大堆来获取最小的top k个元素。
【Top k 小元素】解法步骤:
- 创建一个大小为K的 【最大堆】
- 依次将元素添加到 【最大堆】 中
- 当 【最大堆】 的元素个数达到k时,将当前元素与堆顶元素进行对比;
如果当前元素 大于堆顶元素,则放弃当前元素,继续进行下一个元素;
如果当前元素 小于堆顶元素,则删除堆顶元素,将当前元素加入到【最小堆】中。 - 重复步骤2和步骤3,直到所有元素遍历完毕
- 此时【最大堆】中的k个元素就是前K个最小的元素。
时间复杂度: O ( N l o g K ) O(NlogK) O(NlogK)
空间复杂度: O ( K ) O(K) O(K),(堆的大小)
3.Top Kth问题
1.解法1(常用)
Top kth 大元素解法步骤:
- 创建一个大小为k的【最小堆】
- 一次将元素进行添加到【最小堆】中,
- 当【最小堆】的元素个数达到k时,将当前元素与堆顶元素进行比较:
如果当前元素小于堆顶元素,则放弃当前元素,继续进行下一个元素;
如果当前元素大于堆顶元素,则删除堆顶元素,将单签元素加入到【最小堆】中。 - 重复步骤2和步骤3,直到所有元素遍历完成。
- 此时【最小堆】中的堆顶元素就是第K个最大的元素。
时间复杂度: O ( N l o g K ) O(NlogK) O(NlogK)
空间复杂度: O ( K ) O(K) O(K),(堆的大小)
2.解法2
Top kth 大元素解法步骤:
- 创建一个「最大堆」;
- 将所有元素都加到「最大堆」中;
- 通过 「边删除边遍历」(大部分语言中为 pop() 或者 poll()) 方法,将堆顶元素删除;
- 重复 3 步骤 K 次,直到获取到第 K 个最大的元素;
时间复杂度:
O
(
K
l
o
g
N
)
O(KlogN)
O(KlogN)
空间复杂度:
O
(
N
)
O(N)
O(N),(堆的大小)
三、题
1.简单
剑指 Offer 40. 最小的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]
class Solution:
def getLeastNumbers(self, arr: List[int], k: int) -> List[int]:
# 思路1:排序,返回前k个数 时间:o(NlogN) 空间:o(Nlog N)
# arr.sort()
# return arr[:k]
# 思路2:利用最小堆来实现 时间:o(klogN) 空间:o(N)
ans=[]
heapq.heapify(arr)
for i in range(k):
ans.append(heapq.heappop(arr))
return ans
703. 数据流中的第 K 大元素
设计一个找到数据流中第 k 大元素的类(class)。注意是排序后的第 k 大元素,不是第 k 个不同的元素。
请实现 KthLargest 类:
KthLargest(int k, int[] nums) 使用整数 k 和整数流 nums 初始化对象。
int add(int val) 将 val 插入数据流 nums 后,返回当前数据流中第 k 大的元素。
示例:
输入:
[“KthLargest”, “add”, “add”, “add”, “add”, “add”]
[[3, [4, 5, 8, 2]], [3], [5], [10], [9], [4]]
输出:
[null, 4, 5, 5, 8, 8]
解释:
KthLargest kthLargest = new KthLargest(3, [4, 5, 8, 2]);
kthLargest.add(3); // return 4
kthLargest.add(5); // return 5
kthLargest.add(10); // return 5
kthLargest.add(9); // return 8
kthLargest.add(4); // return 8
class KthLargest:
# 解法1
def __init__(self, k: int, nums: List[int]):
self.k=k
self.num_copy=nums
heapq.heapify(self.num_copy)
def add(self, val: int) -> int:
heapq.heappush(self.num_copy,val)
while len(self.num_copy)>self.k:
heapq.heappop(self.num_copy)
return self.num_copy[0]
2. 中等
215. 数组中的第K个最大元素
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
示例 1:
输入: [3,2,1,5,6,4] 和 k = 2
输出: 5
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
# 思路1:排序
# nums.sort()
# return nums[-k]
# 思路2:最大堆
nums=[-i for i in nums]
heapq.heapify(nums)
while k-1>0: # 写1 的原因是获取第k个最大元素,则第k个是不必要删除的
heapq.heappop(nums)
k-=1
return -nums[0]
3.高频top10
347. 前 K 个高频元素
给你一个整数数组 nums 和一个整数 k ,请你返回其中出现频率前 k 高的元素。你可以按 任意顺序 返回答案。
示例 1:
输入: nums = [1,1,1,2,2,3], k = 2
输出: [1,2]
示例 2:
输入: nums = [1], k = 1
输出: [1]
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
# 思路1:哈希法,时间复杂度0(nlogn) 空间复杂度o(n)
ans=[]
dict_count=Counter(nums)
dict_count=sorted(dict_count.items(),key=lambda x:x[1],reverse=True)
for i in range(k):
ans.append(dict_count[i][0])
return ans
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
# 思路2:哈希统计,然后利用最小堆进行获取频数最高的k个数 时间复杂度 o(nlogk) 空间复杂度o(k)
ans=[]
dict_count=Counter(nums)
minheap=[]
# 将元素进行传入到最小堆中(大小只为k),超过K每次去除最小次数的元素
for key,val in dict_count.items():
heapq.heappush(minheap,(val,key)) # 元祖形式传入,默认是以第一位进行构建最小堆
if len(minheap)>k:
heapq.heappop(minheap)
# 取出前k个高频元素
ans=[]
for i in range(k):
ans.append(heapq.heappop(minheap)[1])
return ans
912. 排序数组
给你一个整数数组 nums,请你将该数组升序排列。
示例 1:
输入:nums = [5,2,3,1]
输出:[1,2,3,5]
示例 2:
输入:nums = [5,1,1,2,0,0]
输出:[0,0,1,1,2,5]
class Solution:
def sortArray(self, nums: List[int]) -> List[int]:
# nums.sort()
# return nums
# 思路:堆排序 时间复杂度 o(n log n) 空间复杂度 o(n)
ans=[]
heapq.heapify(nums)
while len(nums)>0:
ans.append(heapq.heappop(nums))
return ans
面试题 17.14. 最小K个数
设计一个算法,找出数组中最小的k个数。以任意顺序返回这k个数均可。
示例:
输入: arr = [1,3,5,7,2,4,6,8], k = 4
输出: [1,2,3,4]
class Solution:
def smallestK(self, arr: List[int], k: int) -> List[int]:
#思路1: top kth 问题(堆) 求k个最小数,可以转化成,用最大堆存储最小的k个数 时间复杂度o(nlogk) 空间复杂度o(k)
ans=[]
i=0
while i<len(arr):
heapq.heappush(ans,-arr[i])
if len(ans)>k:
heapq.heappop(ans)
i+=1
return [-i for i in ans]
23. 合并K个升序链表
给你一个链表数组,每个链表都已经按升序排列。
请你将所有链表合并到一个升序链表中,返回合并后的链表。
示例 1:
输入:lists = [[1,4,5],[1,3,4],[2,6]]
输出:[1,1,2,3,4,4,5,6]
解释:链表数组如下:
[
1->4->5,
1->3->4,
2->6
]
将它们合并到一个有序链表中得到。
1->1->2->3->4->4->5->6
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeKLists(self, lists: List[Optional[ListNode]]) -> Optional[ListNode]:
#思路1:暴力,添加全部值,然后排序,最后生成链接 时间复杂度 o(n*k log n*k) 空间复杂度 o(n)
ans=[]
for head in lists:
while head:
ans.append(head.val)
head=head.next
ans.sort()
head=ListNode(-1)
curr=head
for i in ans:
curr.next=ListNode(i)
curr=curr.next
return head.next
# 思路2: 利用最小堆 一直存储 lists中每个小list的最小值序列,然后依次添加 时间复杂度 o(n*k log k) 空间复杂度 o(k) 是k的原因在于,每一次只获取全部子链表中的最小值进行比较。
ans=[]
head=ListNode(-1)
curr=head
for i in range(len(lists)):
if lists[i]:
heapq.heappush(ans,(lists[i].val,i)) # 默认是对val进行最小堆的比较
lists[i]=lists[i].next # 子链表第一个元素已经添加到最小堆中,则下一次都是从第二个元素开始
while ans:
val,idx=heapq.heappop(ans) # 返回最小堆的最小的元素
curr.next=ListNode(val) # 添加最小节点
curr=curr.next
if lists[idx]: # 如果子链表还存在元素,则进行添加
heapq.heappush(ans,(lists[idx].val,idx))
lists[idx]=lists[idx].next #获取子链表的下一个元素
return head.next
239. 滑动窗口最大值
给你一个整数数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。
返回 滑动窗口中的最大值 。
示例 1:
输入:nums = [1,3,-1,-3,5,3,6,7], k = 3
输出:[3,3,5,5,6,7]
解释:
滑动窗口的位置 最大值
[1 3 -1] -3 5 3 6 7 3
1 [3 -1 -3] 5 3 6 7 3
1 3 [-1 -3 5] 3 6 7 5
1 3 -1 [-3 5 3] 6 7 5
1 3 -1 -3 [5 3 6] 7 6
1 3 -1 -3 5 [3 6 7] 7
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
# 最大堆,只管添加,知道最大值的索引小于i-k时,开始删除(将堆中索引小于i-k的都删除)
q=[(-nums[i],i) for i in range(k)]
heapq.heapify(q)
ans=[-q[0][0]]
for i in range(k,len(nums)):
heapq.heappush(q,(-nums[i],i))
while q[0][1]<=i-k: # 使用while原因在于 ,有可能子节点的索引也超过了当前的滑动窗口范围
heapq.heappop(q)
ans.append(-q[0][0])
return ans
复杂度分析
- 时间复杂度:O(nlogn),其中 n 是数组 nums 的长度。在最坏情况下,数组 nums 中的元素单调递增,那么最终优先队列中包含了所有元素,没有元素被移除。由于将一个元素放入优先队列的时间复杂度为 O(logn),因此总时间复杂度为 O(nlogn)。
- 空间复杂度:O(n),即为优先队列需要使用的空间。这里所有的空间复杂度分析都不考虑返回的答案需要的 O(n) 空间,只计算额外的空间使用。
剑指 Offer II 061. 和最小的 k 个数对
给定两个以升序排列的整数数组 nums1 和 nums2 , 以及一个整数 k 。
定义一对值 (u,v),其中第一个元素来自 nums1,第二个元素来自 nums2 。
请找到和最小的 k 个数对 (u1,v1), (u2,v2) … (uk,vk) 。
示例 1:
输入: nums1 = [1,7,11], nums2 = [2,4,6], k = 3
输出: [1,2],[1,4],[1,6]
解释: 返回序列中的前 3 对数:
[1,2],[1,4],[1,6],[7,2],[7,4],[11,2],[7,6],[11,4],[11,6]
class Solution:
def kSmallestPairs(self, nums1: List[int], nums2: List[int], k: int) -> List[List[int]]:
# s
# m, n = len(nums1), len(nums2)
# ans = []
# pq = [(nums1[i] + nums2[0], i, 0) for i in range(min(k, m))]
# while pq and len(ans) < k:
# _, i, j = heappop(pq)
# ans.append([nums1[i], nums2[j]])
# if j + 1 < n:
# heappush(pq, (nums1[i] + nums2[j + 1], i, j + 1))
# return ans
# 和最小的k个数对,那么使用最大堆进行实现 时间复杂度为o(n^2logk) 空间复杂度为o(n)
pq=[]
for i in range(len(nums1)):
for j in range(len(nums2)):
heapq.heappush(pq,(-nums1[i]-nums2[j],i,j))
if len(pq)>k:
heapq.heappop(pq)
ans=[]
for i in pq:
ans.append([nums1[i[1]],nums2[i[2]]])
return ans
264. 丑数 II
给你一个整数 n ,请你找出并返回第 n 个 丑数 。
丑数 就是只包含质因数 2、3 和/或 5 的正整数。
示例 1:
输入:n = 10
输出:12
解释:[1, 2, 3, 4, 5, 6, 8, 9, 10, 12] 是由前 10 个丑数组成的序列。
class Solution:
def nthUglyNumber(self, n: int) -> int:
# 丑数即是2,3,5的倍数
factors = [2, 3, 5]
seen = {1}
heap = [1]
for i in range(n - 1):
curr = heapq.heappop(heap)
for factor in factors:
if (nxt := curr * factor) not in seen:
seen.add(nxt)
heapq.heappush(heap, nxt)
return heap[0]
复杂度分析
-
时间复杂度:O(nlogn)。得到第 n 个丑数需要进行 n 次循环,每次循环都要从最小堆中取出 1 个元素以及向最小堆中加入最多 3 个元素,因此每次循环的时间复杂度是 O(log3n+3log3n)=O(logn),总时间复杂度是 O(nlogn)。
-
空间复杂度:O(n)。空间复杂度主要取决于最小堆和哈希集合的大小,最小堆和哈希集合的大小都不会超过 3n。
基于LLM实现金融领域数据分析agent
原因:(1)由于输入LLMs上下文的限制,直接读取和处理大量数据与处理文本一样具有挑战性LLMs。此外,在直接访问私有数据源时LLMs,还存在数据泄露的潜在风险。(2)数据处理非常复杂,涉及许多繁琐的数值计算和错综复杂的表格操作。LLMs不擅长执行这些任务。(3)只能LLM输出文本,而我们在分析数据时通常更喜欢图表或表格。
第一步,数据探索,将将每个数据源转换成解析文件,包括数据说明、访问方法、数据架构(每列的名称)和用法示例,然后将解析文件和一些请求通过prompt书写的形式输入到讯飞星火max模型中,进行生成更多样化的请求。对得到的请求可能存在幻觉请求,因此通过生成好的请求,通过反向转换调用api的形式是否有关键信息和报错内容,从而过滤掉幻觉请求。
第二部,根据大量的请求通过LLM生成通过接口代码。操作时,将数据解析文件和现有接口作为prompt输入,让llm为每个请求生成一个完整的代码,生成过程中,优先使用现有接口,如果不满足则进行重新生成新的接口。然后进行不断的迭代。
数据探索,将每个数据源转换成解析文件并加上请求,通过讯飞星火max模型生成更多多样化请求。