news 2026/10/9 8:25:26

比较排序通关攻略:冒泡到堆排,复杂度与稳定性全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
比较排序通关攻略:冒泡到堆排,复杂度与稳定性全解析

提到数据结构排序算法,最让大家头疼的往往不是背代码,而是代码能写出来、但说不清背后的取舍。排序算法里又有一大类叫比较排序:冒泡、选择、插入、希尔、归并、快排、堆排,全都靠元素之间的两两比较来决定顺序。这篇《排序算法通关攻略:比较排序篇》就是把这条线从青铜到王者重新走一遍,适合刚学完基础概念但总写错边界条件的同学,也适合打算突击面试、想一次性把复杂度、稳定性和工程优化串起来的开发者。示例代码我统一用C语言写,因为C语言没有太多库函数帮忙,数组下标、递归栈、临时内存分配这些坑全都暴露出来,你用Java、Python、Go去对照也完全没有障碍。后边每一级我都会讲清楚原理、代码、复杂度以及我实际踩过的问题。

1. 青铜起步:选择排序与冒泡排序,先搞懂比较与交换

1.1 冒泡排序:相邻比较与交换,先把最大的“顶”上去

只要写过排序,十个里有八个第一版是冒泡。思路很直白:每一轮从头往后扫描,相邻两个元素如果前一个比后一个大,就交换它们,一轮下来最大的数就像气泡一样被顶到数组末尾。下一轮再把剩下 n-1 个元素里最大的顶到倒数第二个位置,n-1 轮之后整个数组有序。

C语言实现:

void bubble_sort(int a[], int n) { for (int i = 0; i < n - 1; i++) { int swapped = 0; for (int j = 0; j < n - 1 - i; j++) { if (a[j] > a[j + 1]) { int tmp = a[j]; a[j] = a[j + 1]; a[j + 1] = tmp; swapped = 1; } } if (!swapped) break; } }

很多人觉得冒泡是最没用的算法,但它其实是理解“比较排序”最好的起点。这里的核心动作只有两个:比较和交换。每一轮都确定一个元素的最终位置,这是选择排序同样使用的思想。带 swapped 标记之后,如果某轮没有发生交换,说明数组已经有序,直接结束,这就是冒泡最好情况能做到 O(n) 的原因。

需要注意的点:外层循环只需要到 n-1,最后一个元素在只剩它一个时自然归位;内层循环上界是 n-1-i,因为每轮已经排好了 i 个最大值;用大于号而不是大于等于,是为了保证相等元素的相对顺序不乱,这是稳定排序的基本要求。我之前见过不少同学写成a[j] >= a[j+1],虽然结果也是升序,但相等元素被交换过,稳定性丢了,后边遇到需要稳定排序的场景就会出问题。

复杂度上,平均和最坏都是 O(n^2),空间只有 O(1)。你要是拿冒泡去排十万个随机数,基本会被秒杀,但排基本有序的小数组时它其实不慢,因为一旦发现没有交换就退出了。所以冒泡不是一无是处,而是要看输入数据长什么样。

1.2 选择排序:每次挑一个最小值,和冒泡死磕同一复杂度

选择排序的思路比冒泡更“暴力”:从左到右扫描一遍,找到最小的元素,把它放到下标 0;再从剩下元素里找最小,放到下标 1;以此类推。它的核心是“选择”而不是“交换”,一轮只交换一次,但依然要做完 n(n-1)/2 次比较。

C语言实现:

void selection_sort(int a[], int n) { for (int i = 0; i < n - 1; i++) { int min_idx = i; for (int j = i + 1; j < n; j++) { if (a[j] < a[min_idx]) { min_idx = j; } } if (min_idx != i) { int tmp = a[i]; a[i] = a[min_idx]; a[min_idx] = tmp; } } }

选择排序比冒泡有优势的地方在于交换次数少,最多 n-1 次交换;但它的比较次数是固定的,不管原始数据是否有序,都是 O(n^2)。这意味着面对“已经排好序”的数组,它不会像冒泡那样提前退出,性能反而更差。这一点在算法面试里经常被拿来考:问你冒泡、选择、插入三个 O(n^2) 排序哪个更适合近乎有序的数据,答案是插入,不是选择。

关于稳定性,选择排序是不稳定的。经典例子是5 5 3:第一轮找到最小值 3,把 3 和第一个 5 交换,数字顺序就变成了3 5 5,原本在前面的 5 跑到了另一个 5 的后面。如果你在业务代码里依赖稳定性,用选择排序就得谨慎。其实工程里我们几乎不手写这两种 O(n^2) 排序,但理解它们能帮你打下“比较交换”和“原地排序”的直觉,这是后边所有高级算法的基础。

2. 白银进阶:插入排序与希尔排序,让数据局部有序

2.1 插入排序:像打扑克一样整理数组,小数组王者

插入排序的直觉我特别喜欢:摸牌的时候,把新摸到的牌从右往左比较,找到合适位置插进去。实现的时候,数组左边永远是已排序的部分,右边是待处理部分。对于下标 i,先把值存到 key,然后不断向左移动,找到第一个不大于 key 的位置,把 key 插进去。

C语言实现:

void insertion_sort(int a[], int n) { for (int i = 1; i < n; i++) { int key = a[i]; int j = i - 1; while (j >= 0 && a[j] > key) { a[j + 1] = a[j]; j--; } a[j + 1] = key; } }

插入排序最值得记的性能特征是“对近乎有序的数据接近 O(n)”。如果数据已经有序,每次循环里 while 判断a[j] > key第一个就不成立,整轮只做一次比较和一次赋值。所以它非常适合:数据量小、局部有序、以及作为高级排序的递归退出条件。像 C++ 的std::sort在快排递归深度到小数组时切到插入排序,Python 的 TimSort 也会对小块用插入排序。

它也是最稳的 O(n^2) 排序,因为相等元素不会被移动到前边,排序是稳定的。空间 O(1)。缺点就是逆序数据时和冒泡一样悲催,最坏 O(n^2)。

常见错误是 while 里忘了j >= 0的边界判断,或者把a[j] > key写成>=,后者会让排序变得不稳定。尤其要注意,插入排序中“寻找插入位置”的过程就是不断右移记录,移动次数比想象中多,这个行为在后续的希尔排序中会直接影响效率。

2.2 希尔排序:让元素先跳到远处,再慢慢归位

希尔排序可以理解成“分组的插入排序”。直接插入排序的问题是每次只能交换相邻元素,逆序多的时候要搬很多次家。希尔排序先选一个较大的间隔 gap,把相隔 gap 的元素看成一组,对每组做插入排序;然后缩小 gap,再排;直到 gap=1,再做一次全量插入排序。因为大间隔让元素能快速跨越大范围移动,最后一次普通的插入排序工作量就小很多。

C语言实现(简单的希尔增量):

void shell_sort(int a[], int n) { for (int gap = n / 2; gap > 0; gap /= 2) { for (int i = gap; i < n; i++) { int key = a[i]; int j = i - gap; while (j >= 0 && a[j] > key) { a[j + gap] = a[j]; j -= gap; } a[j + gap] = key; } } }

注意这里内层不是严格的“分组排序”写法,而是从 gap 位置开始往后遍历,每次把 a[i] 当成新元素,和同一组里前面的元素做插入。这比写多重分组循环更容易理解,效率也一样。gap 序列的选择直接影响复杂度,希尔增量是 n/2 每次减半,最坏 O(n^2);Hibbard 增量或者 Sedgewick 增量能到 O(n^1.5) 甚至更好。工程上已经不太用希尔排,但很多教材和考试还爱考,你得知道 gap 最后一步必须是 1,否则数组不会整体有序。

希尔排序是不稳定的,因为相等的元素可能被分到不同的组里,跨组移动会破坏相对顺序。另外我在写希尔的时候踩过的坑是 gap 缩到 0 之后的死循环,以及内层把j -= gap写成了j--,这会导致分组逻辑错乱。验证时用一个简单的[5, 2, 3, 1, 4]手跑一遍插入过程,比Debug还快。

3. 黄金分治:归并排序,稳定与确定性的权衡

3.1 归并排序的灵魂:把两个有序数组合到一起

归并排序是第一个真正意义上的 O(n log n) 排序,也是学习分治思想的经典案例。它的做法很清晰:把数组从中间分成两半,分别排序,再把两个有序子数组合并成一个整体。递归拆到只剩一个元素时,它天然有序,合并过程就能向上回溯。

C语言实现:

void merge_sort(int a[], int left, int right, int tmp[]) { if (left >= right) return; int mid = left + (right - left) / 2; merge_sort(a, left, mid, tmp); merge_sort(a, mid + 1, right, tmp); merge(a, left, mid, right, tmp); } void merge(int a[], int left, int mid, int right, int tmp[]) { int i = left, j = mid + 1, k = 0; while (i <= mid && j <= right) { if (a[i] <= a[j]) tmp[k++] = a[i++]; else tmp[k++] = a[j++]; } while (i <= mid) tmp[k++] = a[i++]; while (j <= right) tmp[k++] = a[j++]; for (int p = 0; p < k; p++) { a[left + p] = tmp[p]; } }

合并函数是核心:两个有序区间从头开始比,谁小谁先进临时数组。必须用<=而不是<,这样当左右两半出现相等元素时,左边先进来,排序就是稳定的。时间复杂度永远 O(n log n),和输入顺序无关,空间 O(n) 需要一块临时数组,稳定。

归并排序的应用场景很明确:外部排序。当数据量大到放不进内存,比如几十 GB 的日志,没法用快排或堆排直接原地搞,只能按块读入内存排序,写到磁盘,再通过多路归并合并。因为归并排序是顺序读写,对磁盘友好。链表排序也通常用归并,因为它不需要随机访问,只要改指针。

3.2 递归写法与小段优化,稳定性的代价

用递归写归并最容易犯的错误是mid的边界。我习惯写int mid = left + (right - left) / 2;,而不是(left + right) / 2,后者在数组很大时可能整数溢出。虽然日常不会遇到几亿长度的数组,但面试官喜欢问这种细节,改一下又不用花钱。

性能上归并排序有明显的优化空间。第一个优化是当区间足够小(比如小于等于 16 或 32 个元素)时,直接改用插入排序,而不是继续递归切分。递归本身有函数调用开销,插入排序在小数组上更快,这一步几乎能让整体性能提升百分之二三十。第二个优化是临时数组一次性分配,不要每次递归都 malloc,避免反复申请内存。第三个优化是在合并前判断if (a[mid] <= a[mid + 1]),如果两个子数组的最高位小于等于后一个的最低位,说明它们已经有序,可以直接返回,不用合并。这个判断对局部有序数据特别有效。

稳定性是归并排序最大的卖点,但也意味着合并时要多花一点内存和拷贝时间。如果你写完一个归并发现输出不对,先检查临时数组的索引k和回写位置left + p是不是对上了,这是我见过最多的问题。另一个常见的坑是递归终止条件只写left == right,万一传入的区间奇怪(比如 left > right),就会越界,所以写left >= right更安全。

4. 铂金策略:快速排序,平均最快的通用算法

4.1 快排的partition:选一个基准,把数组分成两半

快排的流行程度不用多说,大多数语言内置排序的核心都是它或者它的变体。核心概念是 partition:选一个元素作为基准 pivot,把数组重新整理,让 pivot 左边都小于等于它,右边都大于等于它,然后递归处理左右两边。分完一次,pivot 就到了它最终的位置,和归并的“先拆再合”不同,快排是“边分边排”。

C语言实现(霍尔分区法):

int partition(int a[], int low, int high) { int pivot = a[low]; int i = low, j = high; while (i < j) { while (i < j && a[j] >= pivot) j--; a[i] = a[j]; while (i < j && a[i] <= pivot) i++; a[j] = a[i]; } a[i] = pivot; return i; } void quick_sort(int a[], int low, int high) { if (low < high) { int p = partition(a, low, high); quick_sort(a, low, p - 1); quick_sort(a, p + 1, high); } }

这里的 partition 每次从右边找一个小于 pivot 的元素填到左边,从左边找一个大于 pivot 的元素填到右边,最后把 pivot 放回 i 的位置。循环条件是重点:while (i < j && a[j] >= pivot)必须用大于等于,否则遇到和 pivot 相等的元素时会一直交换,造成死循环。退出后low < high才能递归,等于的情况不需要处理。

平均复杂度是 O(n log n),最坏 O(n^2)。最坏发生在每次划分都极度不均衡,比如数组本身有序时如果每次选到的是最小或最大值作为 pivot。空间上递归栈平均 O(log n),最坏 O(n),不稳定。所以工程上必须对 pivot 的选取做手脚,这也就是下一节的内容。

4.2 优化三板斧:随机化、三数取中、小数组切插入

快排的优化值得单独聊,因为“实现一个能用的快排”和“实现一个生产级快排”差的真的太远了。第一板斧是随机选取 pivot,让最坏情况变成概率事件;第二板斧是三数取中,取 low、mid、high 三个位置的中位数当 pivot,对抗基本有序数组;第三板斧是小数组切到插入排序,减少递归到底的调用开销。

我一般会在 partition 开始之前先选 pivot 并交换到首位:

void swap(int *a, int *b) { int tmp = *a; *a = *b; *b = tmp; } int pick_pivot(int a[], int low, int high) { int mid = low + (high - low) / 2; if (a[mid] < a[low]) swap(&a[mid], &a[low]); if (a[high] < a[low]) swap(&a[high], &a[low]); if (a[high] < a[mid]) swap(&a[high], &a[mid]); swap(&a[mid], &a[low]); return a[low]; }

选完之后把 pivot 交换到a[low],再走统一的霍尔分区。三数取中本质上降低了最坏情况的触发概率,比纯随机更稳定。

还有一个进阶点是三路快排:当数据里大量重复元素时,标准快排会退化成 O(n^2)。三路快排把数组分成小于 pivot、等于 pivot、大于 pivot 三段,等于 pivot 的部分不参与递归,这样重复多的数据也能保持 O(n log n)。Java 的基本类型排序在双轴快排里也用到了类似思想。

快排在业务里的下场其实很矛盾:很多同学面试时能写出标准快排,但一放大数组就爆栈,因为递归深度无法控制。后面我会单独聊爆栈问题,这里先记住一点:任何时候递归版都要用随机化或中位数优化,并且给递归加一个深度上限,一旦超过上限就切换堆排序。这就是工业级内省排序的做法,第六节和第七节会说清楚。

5. 钻石进阶:堆排序,用二叉树思维排序

5.1 堆化细节:为什么从最后一个非叶节点开始

堆排序依赖完全二叉树,但不额外建树,直接在数组上操作。大顶堆要求父节点不小于子节点,堆顶就是最大值。排序过程分两步:先把数组堆化,让最大值到堆顶;然后把堆顶和最后一个元素交换,堆大小减一,再对根节点做一次下沉调整,就能反复取出剩余最大值。

C语言实现:

void sift_down(int a[], int n, int i) { while (i < n) { int largest = i; int left = 2 * i + 1; int right = 2 * i + 2; if (left < n && a[left] > a[largest]) largest = left; if (right < n && a[right] > a[largest]) largest = right; if (largest == i) return; int tmp = a[i]; a[i] = a[largest]; a[largest] = tmp; i = largest; } } void heap_sort(int a[], int n) { for (int i = n / 2 - 1; i >= 0; i--) { sift_down(a, n, i); } for (int i = n - 1; i > 0; i--) { int tmp = a[0]; a[0] = a[i]; a[i] = tmp; sift_down(a, i, 0); } }

细节都藏在索引里。数组下标从 0 开始,那么节点 i 的左孩子是2*i+1,右孩子是2*i+2,最后一个非叶节点是n/2 - 1。从它开始往前循环,逐个下沉,才能保证每一个子树都满足大顶堆。如果你从 0 开始正着做,叶子节点没有孩子,不处理是可以的,但分组调整的顺序就不对了。

建堆的时间复杂度是 O(n),不是 O(n log n)。推导起来不复杂:高度越低的节点数量越少,综合下来每层工作量是 O(height),总数趋近于 O(n)。排序阶段每次把堆顶放到底部,交换后再下沉 O(log n),所以总复杂度 O(n log n)。空间 O(1),原地排序,但不稳定,比如相同值的元素可能因为下沉交换位置。

5.2 堆排序没快排快,但TopK离不开它

为什么实际排序不用堆排?因为堆排序的访问模式是跳跃式的,对 CPU 缓存不友好,而快排是顺序访问,缓存命中率高得多。堆排的常数比较大,在大多数输入下跑不过快排。但它有一个优点:最坏情况就是 O(n log n),不存在退化到 O(n^2) 的风险。因此很多异常对抗场景或者实时系统里,宁可选择堆排或内省排序中的堆排兜底。

堆真正的用武之地是优先队列和 TopK 问题。你要在几亿个数据里找最大的一百个,没必要把所有数据排一遍,建一个小顶堆,不断用新元素替换堆顶并下沉,堆大小维持 100,最后堆里的就是 Top100。这个过程时间复杂度 O(n log k),k 是 100 的话几乎就是线性的。理解堆排以后,写这些工具函数就是顺手的事。

常见的堆排错误有两个。一个是在 heapify 循环里只传 n 而忽略当前子树的范围,下沉时把已经排定位置的后缀也算进去,破坏了排序阶段的结果;另一个是递归版 sift_down 在小堆上没问题,但在大数组中递归深度其实只有 log n,用递归还行,不过迭代版更稳,能避免不必要的函数调用。我建议把这套代码背到滚瓜烂熟,因为它不仅是排序,还是很多高级数据结构的根。

6. 王者升华:工业级混合排序与比较排序的边界

6.1 工业级排序:std::sort、Arrays.sort、TimSort各自怎么混

到了这个段位,你已经知道没有银弹。纯快排最坏 O(n^2),纯归并需要 O(n) 空间,纯堆排常数太大,所以工业级排序基本都是混合体。C++ 的std::sort用的是内省排序:初始走快速排序,但设置一个递归深度限制,一旦递归太深就不再快排,改成堆排序兜底;当区间缩小到一定阈值(通常是 16 或 24)时,切换到插入排序。这样既有快排的平均速度,又避免了最坏情况的灾难。

Java 的Arrays.sort()分成两条线:对基本类型用双轴快速排序,对对象用 TimSort。为什么?因为对对象排序,用户可能依赖稳定性,所以用稳定排序;基本类型相等元素没有身份区分,稳定性没有意义,就用更快的双轴快排。双轴快排选两个 pivot,把数组分成三段,比单轴快排平均比较次数更少。Python 内置排序也是 TimSort,重点是它利用了数据中天然存在的“run”(连续递增或递减段),先把 run 识别出来,再用归并思路合并,所以对真实世界的有序片段特别友好。

从这些实现能看到一个共同规律:算法基础不是让你背 API,而是让你理解“快排+插入排序兜底”、“归并+稳定性”、“堆排+最坏保证”这三个组合的动机。面试里被问到std::sort原理,如果能讲出这些细节,基本就稳稳过关了。

6.2 比较排序的极限:Ω(n log n)是怎么来的

既然有这么多技巧,那么基于比较的排序能不能突破 O(n log n)?答案是不能。这里有一个经典的决策树证明:n 个元素的排列一共有 n! 种可能,而一次比较最多把可能性分成两个分支。如果决策树高度是 h,最多能区分 2^h 种结果,所以要满足 2^h ≥ n!。两边取对数,h ≥ log2(n!),用斯特林公式展开就是 n log2 n 的数量级。也就是说,不管怎么设计,最坏情况至少需要 n log n 次比较。

这个结论的意义不是让你放弃优化,而是帮你划定边界:通用比较排序,最好平均也就是 O(n log n)。所以当有人说发明了 O(n) 的通用比较排序时,第一反应应该是质疑。但非比较排序不在此列,计数排序、基数排序、桶排序通过利用数据范围或分布的假设,可以做到 O(n+k),代价是对输入有额外限制。比如手机号排序可以用基数排序,成绩排序可以用桶排序。实际选型时就按这个逻辑来:数据分布有规律、取值范围有限,优先非比较;数据通用、不确定,交给比较排序中的工业混合实现。

这一章的最后,我想再说一个实际体会:工业级排序的很多优化,都是对“最坏情况”和“常数因子”的反复较量。你光会用sort当然可以,但在分布式热点数据、实时流 TopK、外部归并这类场景里,只有把比较排序的思想吃透,才能在出问题时快速定位到底是算法问题还是数据问题。

7. 常见问题与排查技巧实录

7.1 越界死循环:手写排序最常见的翻车现场

手写排序,百分之六十的 bug 都在边界上。冒泡容易把内层j < n - i写成j < n,导致每轮多比较一个已经排好的尾部;选择排序容易在找最小值后忘记min_idx != i判断,原地自交换也行,但没必要;快排的 partition 一旦用了a[j] > pivot而不是>=,遇到大量等于 pivot 的元素就可能来回交换死循环。排查这类问题别只靠眼睛,拿一个 10 个元素的乱序数组,把每轮数组状态打印出来,哪儿越界、哪儿没交换,一眼就能看到。

还有一个隐藏很深的越界:归并排序的临时数组。如果你在递归里分配int tmp[n],区间长度计算错就会读超出栈;如果合并时回写位置写错,数组会变得乱七八糟。我的做法是统一在排序入口分配一块长度为 n 的临时数组,递归函数只传指针和左右边界,这样既不反复分配内存,也减少了越界概率。

7.2 递归深度爆栈:怎么定位和改写成非递归

快排和归并都有递归版本,递归深度和区间划分相关。快排最坏情况下递归深度等于数组长度,10 万元素有序且每次选到边界 pivot 时,直接栈溢出。定位它非常简单:在递归函数入口打印 low 和 high,如果看到区间长度下降得非常慢,说明 partition 几乎没把数组分开,pivot 选得有问题。解法优先用随机化或三数取中;如果还不够,就设置深度阈值,超过阈值改用堆排序,这就是内省排序。

真要彻底避免递归,可以改成显式栈。快排用一个栈存[low, high]区间,每次弹出一个区间 partition,把左右子区间再压栈。注意压栈顺序无所谓,但别把新区间和旧区间搞混。归并改成迭代也容易:从长度为 1 的子数组开始,不断按 2 倍长度合并,这就是自底向上的归并排序。它不需要递归栈,但需要处理好最后一个不完整区间的边界。工程上,我建议先优化递归而不是一上来就非递归,因为非递归的边界错误更难调。

7.3 稳定性错乱:工程里翻车最隐晦的一次

稳定性是最容易被忽略的比较排序属性。我见过一个真实案例:线上按时间排序用到一个自写的插入排序,后来数据源加了优先级字段,需求变成“先按优先级排,同优先级按时间排”,同事直接把插入排序换成了快速排序,结果大量数据的相对时间顺序乱了,排查了很久才发现是稳定性问题。这个教训非常典型:当排序关键字是复合的,而且存在“主关键字 + 次关键字”时,稳定排序能直接用单次排序搞定;不稳定排序要么额外处理倒序,要么就翻车。

所以面试和实际写代码时,先问自己一句:这次排序需要稳定吗?需要的话老老实实用归并、插入或 TimSort;不需要的话随便用快速排序。很多语言内置排序已经帮你做了决定,但 C 语言里你自己手写时,必须对每个算法的稳定性如同条件反射一样清楚。我把这张常用的复杂度对照表背得很熟,关键时候能救命:

算法最好时间平均时间最坏时间空间稳定性
冒泡排序O(n)O(n²)O(n²)O(1)稳定
选择排序O(n²)O(n²)O(n²)O(1)不稳定
插入排序O(n)O(n²)O(n²)O(1)稳定
希尔排序依赖增量约 O(n^1.5)O(n²)O(1)不稳定
归并排序O(n log n)O(n log n)O(n log n)O(n)稳定
快速排序O(n log n)O(n log n)O(n²)O(log n)不稳定
堆排序O(n log n)O(n log n)O(n log n)O(1)不稳定

最后再分享一个从我踩坑里长出来的习惯:写完任何手写排序,先跑三组数据,一组乱序、一组基本有序、一组全部相同。乱序验证正确性,基本有序验证有没有利用局部有序的能力,全部相同则能暴露>=和<=这些边界条件写得对不对。这三组数据一过,绝大多数排序实现的隐藏问题都会现原形。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 8:24:38

从手写连接池到DBUtils:Flask生产环境连接池配置与调优

2. 手写一个最小连接池&#xff1a;从零理解核心机制 先别急着用库&#xff0c;我们从最底层的逻辑看一遍。连接池说白了就三件事&#xff1a; 预先创建一批连接存起来 、 每次要用的时候借一个 、 用完还回去 。这个模型用 queue.Queue 二十行代码就能搭起来。 impo…

作者头像 李华
网站建设 2026/10/9 8:24:20

PySpark JAVA_GATEWAY_EXITED报错排查:从JVM到内存的根本解法

凌晨一点四十七分&#xff0c;我盯着终端里一行红色的报错发呆&#xff1a;pyspark.errors.exceptions.base.PySparkRuntimeError [JAVA_GATEWAY_EXITED] Java gateway process exited before sending its port number。老实说&#xff0c;刚接触PySpark的人看到这个错&#xf…

作者头像 李华
网站建设 2026/10/9 8:22:39

Linux文本处理实战:grep、sed、awk与日志分析流水线

如果你在Linux下干活&#xff0c;无论是运维、后端开发还是数据分析&#xff0c;早晚都会撞上这么一类需求&#xff1a;手里的数据不是数据库里规规矩矩的表&#xff0c;而是横七竖八的日志、配置、接口返回和CSV导出。上班第一件事打开终端&#xff0c;要批量改几十个配置文件…

作者头像 李华
网站建设 2026/10/9 8:21:35

网鼎杯2020 AreUSerialz:PHP反序列化入门与绕过技巧详解

网鼎杯 2020 青龙组的这道 AreUSerialz&#xff0c;我愿称之为 PHP 反序列化的入门必修课。题目名字本身就是谐音梗&#xff1a;AreUSerialz 读起来就是 Are you serial?&#xff0c;出题人等于在说&#xff1a;不会序列化就别来了。事实也确实如此&#xff0c;整道题的核心就…

作者头像 李华
网站建设 2026/10/9 8:21:34

本地RAG实践:用Ollama和ChromaDB构建私人文档问答助手

2026年1月25日&#xff0c;第29天。这是我给自己定的“100天实践计划”走到第29天的一个节点&#xff0c;今天干了一件之前一直想做但没敢动手的事&#xff1a;在本地电脑上把一堆散乱的PDF、Word和Markdown文档&#xff0c;变成一个能针对内容直接提问的“私人资料问答助手”。…

作者头像 李华
网站建设 2026/10/9 8:21:00

SpringBoot+Vue+MySQL个人理财系统开发实战与部署指南

个人理财系统这个题目标题我看了很多遍——SpringBoot后端Vue前端MySQL&#xff0c;标注“可直接运行”。说实话&#xff0c;这类项目最大的价值恰恰就在“可直接运行”这四个字上&#xff0c;但也是最大的坑。很多同学下载完源码对着 springboot 版本太高、vue安装及环境配置、…

作者头像 李华