堆与TopK · 从零到通透 —— 9 节全系列复盘

本博客是 GP_堆与TopK/ 01--09 节的总复习 :以三板斧模板 + TopK 必答对照表 为总纲,串起手写堆、大顶堆模拟、快选 vs 堆、多路归并、双堆中位数。

学完整册后用它串一遍;面试前刷它收尾。

对应代码练习在 01_认识堆/ ... 09_数据流的中位数/ 各目录的 practice.py

每个知识点总结后面附完整实现 (纯函数,不含测试代码)。

05、06、09 三节的练习都不止一个解法------本文对它们做了专门的对比总结(含我在 practice 里真实踩过的坑)。


0. 先立骨架:9 个文件夹是怎么互相长出来的

复制代码
01 认识堆(数组即树 / 下标公式)
   └── 02 堆操作心法(两板斧 sift_up / sift_down + heapify)
          ├── 03 手写小顶堆(MinHeap 类,防身)
          ├── 04 最后一块石头(heapq 存负弹正 = 大顶)
          │      ├── 05 数组第K大(K守门员堆 vs 快选 vs 全排序)──┐
          │      └── 06 前K高频(Counter+K堆 vs 桶排序)        ├ TopK 主线
          ├── 07 合并K个有序链表(多路归并 + tuple 垫下标)       │
          ├── 08 丑数II(候选池堆 + seen vs DP 三指针)           │
          └── 09 数据流中位数(双堆分工,压轴) ──────────────────┘

三条贯穿全册的主线:

主线 一句话 用在哪
三步心法 定堆型(谁在堆顶)→ 算下标(父子公式)→ 调堆序(sift 两板斧) 每题
K 大小的堆方向 求"最大 K 个"用小顶(堆顶=第 K 大守门员);求"最小 K 个/反复取最大"用大顶 04--06
TopK 取舍 静态数组快选平均 O(n);流式/海量数据 K 堆 O(n log k) 05 的必答

★ 总纲:三板斧模板、heapq 速记与 TopK 必答表

这一节是全篇核心。堆题的一切代码都从两板斧 长出来;堆题的一切选择题都查 TopK 必答表

三板斧(小顶堆版,手写必备)

python 复制代码
def sift_up(arr, i):
    """尾部新元素上浮:和父比,比父小就换。O(log n)。"""
    while i > 0:
        p = (i - 1) // 2
        if arr[i] < arr[p]:
            arr[i], arr[p] = arr[p], arr[i]
            i = p
        else:
            break                       # 父已经 <= 我,上面更小,停


def sift_down(arr, i, size):
    """i 位置元素下沉:找两个孩子里较小的 m,比孩子大就换。O(log n)。"""
    while True:
        l, r, m = 2 * i + 1, 2 * i + 2, i
        if l < size and arr[l] < arr[m]:
            m = l
        if r < size and arr[r] < arr[m]:
            m = r
        if m == i:
            break                       # 我 <= 两个孩子,堆序已恢复
        arr[i], arr[m] = arr[m], arr[i]
        i = m                           # ★ 下沉到新位置,继续


def heapify(arr):
    """自底向上建堆:从最后一个非叶子倒着 sift_down。O(n)。"""
    for i in range(len(arr) // 2 - 1, -1, -1):
        sift_down(arr, i, len(arr))

三个"为什么"(面试必问):

  • sift_down 认"较小的孩子":跟大的换完可能还比另一个孩子大,堆序没恢复;
  • 弹堆顶 = 换尾 + pop + sift_down(0)pop(0) 是 O(n) 且断序;
  • heapify O(n):一半节点是叶子(0 次操作),1/4 只沉 1 层...... Σ(高度×节点数) 收敛到 O(n),不是 O(n log n)。

heapq 速记(Python 刷题用)

python 复制代码
# heapq.heapify(a)        原地变小顶堆
# heapq.heappush(a, x)    O(log n)
# heapq.heappop(a)        弹最小,O(log n)
# heapq.nlargest(k, a)    TopK 一行版(内部就是 K 堆)
# heapq.nsmallest(k, a)   前 k 小
#
# 大顶堆:存负弹正      heap = [-x for x in nums]; 堆顶 = -heap[0]
# tuple 防比较崩:值相等时 heapq 逐位比 → 垫一个下标 (val, i, obj)

TopK 必答对照表(背下来,05 必考)

解法 时间 空间 场景
全排序 O(n log n) O(1) 保底,无亮点
K 大小的堆 O(n log k) O(k) 数据流 / 海量数据(全集进不了内存时唯一可行)
快速选择 平均 O(n),最坏 O(n²) O(1) 静态数组、一次性查询

一句话答法:"静态数组用快选平均 O(n);数据流或数据大到内存放不下,用 K 大小的堆 O(n log k)。"


01 · 认识堆

一句话本质 :数组看成完全二叉树,下标就是路标------堆顶永远是最值,拿最值 O(1),进出 O(log n)。

复制代码
数组 [1, 3, 2, 6, 5, 4]
下标  0  1  2  3  4  5

             0:1
           /      \
        1:3        2:2
       /    \     /
    3:6    4:5  5:4

要点

  • 下标公式(地基,先默写):parent = (i-1)//2,children = 2i+1 / 2i+2
  • 堆序只管纵向(父 <= 子),兄弟无关------第 2 小不一定是 arr1,可能在 arr2
  • 堆 vs BST:BST 横向有序(查找快);堆只承诺堆顶最值------所以堆不能查找任意值,扬长避短全是 TopK 主场。

公式:peek O(1) / push·pop O(log n) / heapify O(n)。

完整实现

python 复制代码
def parent(i):
    return (i - 1) // 2


def children(i, size):
    return [c for c in (2 * i + 1, 2 * i + 2) if c < size]


def is_min_heap(arr):
    """体检:每个父 <= 自己的孩子(兄弟之间不管)。"""
    for i in range(1, len(arr)):
        if arr[parent(i)] > arr[i]:
            return False
    return True

02 · 堆操作心法

一句话本质 :堆只有两个基本动作------插入从尾巴进再上浮 (sift_up),弹顶先换尾再下沉(sift_down);push/pop/heapify 全是组装。

要点

  • sift_up 和父比:新元素必须加在数组末尾(保持完全二叉树形状),一路换到"父 <= 我"。
  • sift_down 和较小的孩子比:弹顶后新堆顶可能两头都大,跟较小的换才保证换完两边都服。
  • heapify 从 n//2 - 1 倒着来:先把每棵子树摆好,父下沉时落到的已是合法堆。
  • 弹空序列 = 升序------堆每次弹最小,弹完就是堆排序(验证手写堆的标准手法)。

公式:两板斧各 O(log n);heapify O(n)。

完整实现

python 复制代码
def heappush(arr, x):
    """push = append + sift_up。"""
    arr.append(x)
    sift_up(arr, len(arr) - 1)


def heappop(arr):
    """pop = 换尾 + pop + sift_down(0)。绝不 pop(0)。"""
    arr[0], arr[-1] = arr[-1], arr[0]
    v = arr.pop()
    sift_down(arr, 0, len(arr))
    return v

03 · 手写小顶堆

一句话本质 :02 节三板斧 + 一个 self.data 装进类------没有新知识,练的是"不许用 heapq"时的肌肉记忆

要点

  • 手写自查三连:① 单元素 push/pop;② 乱序逐个 push 后堆顶必最小;③ 弹空序列升序。
  • 大小顶只差比较器 :所有 < 反过来就是大顶堆(或值取负存小顶)------这句话面试说出来很加分。
  • peek 是 O(1) 的"只看不拿"。

公式:push/pop O(log n),peek O(1),空间 O(n)。

完整实现

python 复制代码
class MinHeap:
    def __init__(self):
        self.data = []

    def _sift_up(self, i):
        while i > 0:
            p = (i - 1) // 2
            if self.data[i] < self.data[p]:
                self.data[i], self.data[p] = self.data[p], self.data[i]
                i = p
            else:
                break

    def _sift_down(self, i):
        size = len(self.data)
        while True:
            l, r, m = 2 * i + 1, 2 * i + 2, i
            if l < size and self.data[l] < self.data[m]:
                m = l
            if r < size and self.data[r] < self.data[m]:
                m = r
            if m == i:
                break
            self.data[i], self.data[m] = self.data[m], self.data[i]
            i = m

    def push(self, x):
        self.data.append(x)
        self._sift_up(len(self.data) - 1)

    def pop(self):
        if not self.data:
            return None
        self.data[0], self.data[-1] = self.data[-1], self.data[0]
        v = self.data.pop()
        self._sift_down(0)
        return v

    def peek(self):
        return self.data[0] if self.data else None

    def __len__(self):
        return len(self.data)

04 · 最后一块石头(LC 1046)

一句话本质 :"反复取最大、处理完放回" = 堆模拟 ;Python heapq 只有小顶 → 存负弹正造大顶。

要点

  • 存的时候 -x,取的时候 -heap[0]------"存负弹正"四字口诀。
  • 收尾一行 return -heap[0] if heap else 0,覆盖"剩一块 / 全碎"两种结局。
  • 02/03 的手写堆随时可以替换 heapq------本题代码不变,只换 API。

公式 :n-1 轮相撞 × O(log n) → O(n log n)

完整实现

python 复制代码
import heapq


def last_stone_weight(stones):
    heap = [-x for x in stones]
    heapq.heapify(heap)                      # 小顶存负 = 大顶堆
    while len(heap) > 1:
        x = -heapq.heappop(heap)             # 最大(存负弹正)
        y = -heapq.heappop(heap)             # 次大
        if x != y:
            heapq.heappush(heap, -(x - y))
    return -heap[0] if heap else 0

05 · 数组第 K 大(LC 215,必考)------ 三种解法对比

一句话本质 :第 K 大 = 升序第 n−k 位 ;三种解法不是"会一种就行",是讲清取舍------面试官的标准追问就是这张表。

解法一:K 大小的"小顶"堆(本模块主角)

方向是命门:求第 K 大,维护小顶堆------堆里装"目前的前 K 大",堆顶是它们中最小的 = 第 K 大守门员:

复制代码
nums = [3, 2, 1, 5, 6, 4]   k = 2

进 3 → 堆 {3}        堆顶 3
进 2 → 堆 {2,3}      堆顶 2(目前的第 2 大)
进 1 → 1 < 堆顶 2?不配进前 2 → 无事发生
进 5 → 5 > 2 → 进堆踢掉 2 → {3,5}
进 6 → 6 > 3 → {5,6}
进 4 → 4 < 5 → 无事发生
答案 = 堆顶 5 ✓
python 复制代码
def find_kth_largest_heap(nums, k):
    heap = []
    for x in nums:
        heapq.heappush(heap, x)
        if len(heap) > k:
            heapq.heappop(heap)      # 踢掉最小的,前 K 之外的不要
    return heap[0]

解法二:快速选择(静态数组理论最优)

partition 把 pivot 放到最终位置 ;pivot 恰在 n−k 就收工,否则只递归包含 target 的那一侧------这就是它和快排的全部区别(只走一条路)。

python 复制代码
def find_kth_largest_quick(nums, k):
    arr = list(nums)
    target = len(arr) - k              # 第 k 大 = 升序第 n-k 位
    lo, hi = 0, len(arr) - 1
    while lo < hi:
        r = random.randint(lo, hi)     # 随机 pivot 防退化
        arr[r], arr[hi] = arr[hi], arr[r]
        pivot, i = arr[hi], lo
        for j in range(lo, hi):        # Lomuto:小的甩左边
            if arr[j] < pivot:
                arr[i], arr[j] = arr[j], arr[i]
                i += 1
        arr[i], arr[hi] = arr[hi], arr[i]     # pivot 归位 i
        if i == target:
            return arr[i]
        if i < target:
            lo = i + 1                 # ★ 跳过整段(i 左侧全部出局)
        else:
            hi = i - 1                 # ★ 跳过整段
    return arr[lo]

平均 O(n) 的论证(必背):随机选 pivot 每次期望扔掉一半,n + n/2 + n/4 + ... = 2n → O(n);最坏(每次只扔一个)O(n²),随机化后概率极低。

解法三:全排序(保底)

python 复制代码
def find_kth_largest_sort(nums, k):
    return sorted(nums, reverse=True)[k - 1]

三解法对比表(面试照这张说)

K 堆 快选 全排序
时间 O(n log k) 平均 O(n) / 最坏 O(n²) O(n log n)
空间 O(k) O(1) O(1)~O(n)
数据流/海量 ✓ 唯一可行 ✗(需要随机访问全量)
静态数组 n·log k(k 小≈n) 理论最优 平庸

进阶版 :LC 719(找出第 k 小的距离对)= 快选 + 答案空间二分(二分"距离"而非下标,check 用双指针滑动窗口);LC 410(分割数组最大值)同模子。


06 · 前 K 个高频元素(LC 347)------ 两种解法对比

一句话本质 :比较的键从"数值"换成"频率"------先数数(Counter),再 TopK(05 的守门员堆原样搬)

解法一:Counter + K 守门员堆

堆里放 (频率, 元素) 的 tuple,堆顶 = "第 K 高频率"守门员------和 05 唯一的区别是比较键:

python 复制代码
def top_k_frequent(nums, k):
    counter = Counter(nums)
    heap = []
    for num, cnt in counter.items():
        heapq.heappush(heap, (cnt, num))    # tuple 先比频率
        if len(heap) > k:
            heapq.heappop(heap)             # 踢掉频率最低的
    return [num for _, num in heap]

命门预告 :tuple 按字典序比较,cnt 相同会去比 num(都是数字不崩);等堆里放不可比较对象(ListNode)时必须垫下标------07 节的著名坑。

解法二:桶排序(静态最优 O(n))

频率的上界是 n → 开 n+1 个桶,桶 i 装"出现 i 次"的元素,从后往前收集:

python 复制代码
def top_k_frequent_bucket(nums, k):
    counter = Counter(nums)
    buckets = [[] for _ in range(len(nums) + 1)]
    for num, cnt in counter.items():
        buckets[cnt].append(num)
    out = []
    for cnt in range(len(buckets) - 1, 0, -1):
        for num in buckets[cnt]:
            out.append(num)
            if len(out) == k:
                return out
    return out

两解法对比

K 堆 桶排序
时间 O(n + m log k) O(n)(m = 不同元素数)
前提 频率上界已知(≤ n)
数据流 ✗(要开满桶)

并列频率注意{A:3, B:2, C:2} 取 k=2,B、C 并列第 2------两解法可能选不同的(都合法,LC 347 允许任意顺序)。互验时别拿两版结果直接比,要拿"频率集合"比

进阶版:LC 692(前 K 个高频单词)加一个"频率相同按字典序"的比较键;数据流版(频率动态变化)要堆 + 懒删除。


07 · 合并 K 个有序链表(LC 23,Hard · 联动题)

一句话本质 :K 条链各自有序 → 全局最小必在某条链的头部 ------K 个链头进小顶堆,弹全局最小接上结果,该链下一个顶进堆。多路归并

复制代码
[[1,4,5], [1,3,4], [2,6]]
堆 {1(L1), 1(L2), 2(L3)} → 弹 1(L1),4 顶上 → ... 每个节点进出堆各一次

要点

  • tuple 必须垫下标 (val, i, node):val 相等时 heapq 会去比 node------ListNode 不可比较直接 TypeError。垫个整数 i,永远轮不到 node。
  • 链表手法(dummy、尾接)全部来自 GP_链表------本题是链表手法 × 堆调度的合体。
  • 替代解:两两分治合并也是 O(N log k)(复用两链 merge);顺序两两合并 O(N·k) 是反面教材

公式 :N 个节点各进出一次 → O(N log k),k 为链数。

完整实现

python 复制代码
class ListNode:                          # 节点定义同 GP_链表
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next


def merge_k_lists(lists):
    heap = []
    for i, node in enumerate(lists):
        if node:
            heapq.heappush(heap, (node.val, i, node))   # 垫 i 防 val 相等比 node
    dummy = ListNode()
    tail = dummy
    while heap:
        _, i, node = heapq.heappop(heap)
        tail.next = node
        tail = node
        if node.next:
            heapq.heappush(heap, (node.next.val, i, node.next))
    return dummy.next

08 · 丑数 II(LC 264)------ 两种解法对比

一句话本质 :下一个丑数 = 已有丑数 ×2/×3/×5 里还没出过的最小值------07 多路归并的"数轴版"。

解法一:候选池小顶堆 + seen

python 复制代码
def nth_ugly_heap(n):
    heap = [1]
    seen = {1}                      # ★ 入堆即登记:每个数最多进堆一次
    out = 0
    for _ in range(n):              # 弹 n 次堆顶就是第 n 个
        out = heapq.heappop(heap)
        for f in (2, 3, 5):
            if out * f not in seen:
                seen.add(out * f)   # 登记在 push 时!
                heapq.heappush(heap, out * f)
    return out

命门是 seen 去重的登记时机 :6 = 2×3 = 3×2 有多条生成路径,必须在入堆时登记;登记在"弹出时"会指数爆炸(见下方复盘)。

解法二:DP 三指针(O(n) 更优解)

三条"乘 2/3/5 的生产线",dp[i] = min(dp[p2]*2, dp[p3]*3, dp[p5]*5)谁的产品被选中谁的指针前进

python 复制代码
def nth_ugly_dp(n):
    dp = [0] * n
    dp[0] = 1
    p2 = p3 = p5 = 0
    for i in range(1, n):
        dp[i] = min(dp[p2] * 2, dp[p3] * 3, dp[p5] * 5)
        if dp[i] == dp[p2] * 2:     # 三个 if 不用 elif:
            p2 += 1                 # 同时命中(6=2×3=3×2)都前进,重复自动跳过
        if dp[i] == dp[p3] * 3:
            p3 += 1
        if dp[i] == dp[p5] * 5:
            p5 += 1
    return dp[n - 1]

两解法对比

候选池堆 DP 三指针
时间 O(n log n) O(n)
空间 O(n)(seen + heap) O(n)(dp 数组)
泛化性 ✓ 换质因子集合零改动(LC 313 超级丑数) ✗ 换因子要加指针

翻车复盘:seen 登记时机(我踩的最狠的一个)

我在 practice 里把 seen.add 写在了弹出时------n=10/30/60 结果全对,n=1690 直接超时。根因链条:

  1. 丑数 6 由 2×3 和 3×2 两条路径生成;弹 2 时 push 了 6,此时 6 还没弹过、不在 pool 里
  2. 弹 3 时 6 not in pool 仍成立 → 6 第二次入堆
  3. 重复弹出的 6 又生成一遍 12、18、30......指数级膨胀
n 登记在弹出时(我的错版) 登记在入堆时(正确)
10 12 次弹出 10 次
30 88 次 30 次
60 458 次 60 次
1690 超时 1690 次,0.0013s

通用纪律:seen 的登记时机 = 元素第一次进入数据结构的时机(入堆/入队/入候选集),不是它被取出的时机。小数据全对、大数据爆炸------这类 bug 只有压力测试才现形。


09 · 数据流的中位数(LC 295,压轴)------ addNum 两个版本对比

一句话本质 :中位数把数分成低半和高半,各自只需要"边界值"------低半大顶堆(存负)+ 高半小顶堆,中位数 = 两堆顶。

复制代码
        低半(大顶堆 lo)          高半(小顶堆 hi)
        1, 2, 3, [5]              [7], 8, 9, 10
                   ↑ 堆顶=低半最大    ↑ 堆顶=高半最小

总数奇数:中位数 = lo 的堆顶(约定 lo 多放一个)
总数偶数:中位数 = (lo 堆顶 + hi 堆顶) / 2

两个不变量(所有代码都在守护它们):

  1. 平衡len(lo) == len(hi)len(lo) == len(hi) + 1(lo 最多多 1);
  2. 有序max(lo) <= min(hi)

版本一:三步走"先进门,再分工"(首选,白板不易错)

python 复制代码
def add_num(x):                              # 版本一:三步走
    heapq.heappush(lo, -x)                   # 1. 一律先进低半
    heapq.heappush(hi, -heapq.heappop(lo))   # 2. 低半的最大搬去高半(保"有序")
    if len(hi) > len(lo):                    # 3. 高半超编,搬回一个(保"平衡")
        heapq.heappush(lo, -heapq.heappop(hi))

为什么无脑进再搬好?它不关心 x 属于哪半------三步走完两个不变量自动恢复,代码零 if 分支、白板不易写错。

版本二:if 版"先看门牌再进门"(省一次搬动)

python 复制代码
def add_num_if(x):                           # 版本二:if 版
    if len(lo) == len(hi):                   # 偶数个 → 放完 lo 多 1
        if hi and x > hi[0]:                 # x 属于高半:进高半,旧边界挤回低半
            heapq.heappush(hi, x)
            heapq.heappush(lo, -heapq.heappop(hi))
        else:                                # x 属于低半:直接进
            heapq.heappush(lo, -x)
    else:                                    # 奇数个 → lo 已多 1,补 hi
        if x < -lo[0]:                       # x 属于低半:进低半,旧边界挤到高半
            heapq.heappush(lo, -x)
            heapq.heappush(hi, -heapq.heappop(lo))
        else:                                # x 属于高半:直接进
            heapq.heappush(hi, x)

两版本对比

场景 三步走(堆操作数) if 版(堆操作数)
x 恰好属于该进的半 3(进 lo → 搬 hi → 可能搬回) 1
x 要跨界 3~5 3

三步走不管 x 多大都"先进 lo 再搬"------x 明明属于 hi 也要绕道;if 版先比大小该进哪半直接进,只有跨界才挤一次旧边界。复杂度都是 O(log n),差别在常数。

面试表演顺序 :先写三步走(保正确)→ 口头提 if 版(显深度)。findMedian 两版完全一样:

python 复制代码
def find_median():
    if len(lo) > len(hi):
        return float(-lo[0])                 # 奇数个:低半最大
    return (-lo[0] + hi[0]) / 2              # 偶数个:两堆顶平均

完整实现(三步走版整装)

python 复制代码
class MedianFinder:
    def __init__(self):
        self.lo = []     # 大顶堆(存负数):较小的一半
        self.hi = []     # 小顶堆:较大的一半

    def add_num(self, x):
        heapq.heappush(self.lo, -x)
        heapq.heappush(self.hi, -heapq.heappop(self.lo))
        if len(self.hi) > len(self.lo):
            heapq.heappush(self.lo, -heapq.heappop(self.hi))

    def find_median(self):
        if len(self.lo) > len(self.hi):
            return float(-self.lo[0])
        return (-self.lo[0] + self.hi[0]) / 2

进阶版:数据流第 K 大(LC 703)= 05 的 K 堆原样搬;滑动窗口中位数(LC 480)= 双堆 + 懒删除(窗口滑出时堆顶作废不急删,取值时校验下标)。


尾声 · 一页纸收束

概念 一句话
下标公式 parent=(i-1)//2,children=2i+1/2i+2------数组即树
两板斧 sift_up 和父比(插入);sift_down 和较小的孩子 比(弹顶),别忘 i = m
弹堆顶 换尾 + pop + sift_down(0),绝不 pop(0)
heapify 从 n//2-1 倒着 sift_down,O(n) 不是 O(n log n)
大顶堆 heapq 存负弹正;手写版所有 < 反过来
TopK 必答 静态快选平均 O(n);流式/海量 K 堆 O(n log k);全排序保底
K 堆方向 第 K 大小顶(堆顶=守门员)------方向反了退化 O(n log n)
tuple 垫下标 (val, i, obj):值相等时 heapq 逐位比,obj 不可比较就崩
seen 时机 登记在入堆时不是弹出时------否则重复入堆指数爆炸
双堆中位数 大顶存低半(负数)+ 小顶存高半;三步走零分支最稳,if 版省一次搬动
多路归并 K 个链头进堆,弹全局最小,该链下一个顶上------O(N log k)
桶排序 频率上界 n → O(n) 静态最优;数据流还得靠堆

一句话讲复杂度:"堆的 push/pop 是 O(log n)------每层最多换一次,树高 log n;heapify 是 O(n)------越往下的节点越多、沉得越浅,级数求和收敛。"

相关阅读(同系列):

  • GP_链表/博客.md ------ 单链表 10 题 · 从反转到归并排序
  • GP_双向链表/博客.md ------ 双向链表 · 从两句口诀到 LFU 缓存(含踩坑复盘)
  • GP_二分/博客.md ------ 二分 · A/B 两套模板总纲
  • GP_滑动窗口/博客.md ------ 滑动窗口 · 三套模板与镜像口诀
相关推荐
明月_清风1 小时前
Hash 表从入门到精通:Go 实战与工程细节
前端·后端·算法
nbsaas-boot1 小时前
多智能体系统的架构边界:任务编排、共享状态与故障隔离设计
人工智能·算法·动态规划
wuyk5552 小时前
12.归并排序:分治思想的稳定排序算法
开发语言·数据结构·算法·排序算法
吴声子夜歌2 小时前
ApacheCommons——commons-text(模板替换与文本算法)
java·开发语言·算法·apache
2601_962294612 小时前
Python接口自动化测试实战:使用requests库
python·接口自动化测试·异常处理·requests库·api测试
砚底藏山河2 小时前
【量化纯GET实战 #23】多股票相关性:用收益率看板块联动
java·数据库·python·金融·数据分析
阿童木写作2 小时前
跨境电商翻译工具推荐:批量图片翻译+视频字幕实时翻译
人工智能·python·音视频
晶捷软件2 小时前
晶捷智能:集团型制造企业ERP如何实现多工厂统一管控
大数据·人工智能·python·制造
JiNan.YouQuan.Soft2 小时前
数值计算引擎:计算固体力学
人工智能·算法·机器学习