尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
链表去重实战:删除重复节点的三种解法与边界处理
1. 开局先说清楚这个“删除重复节点”到底在考什么链表操作在数据结构里算是“上手门槛低、写对门槛高”的那一类。你让一个刚学完C语言的同学写单链表遍历他十分钟就能交出来但你让他“原地删除有序链表里的重复元素”或者“把无序链表里的重复节点全部去掉”他大概率会栽在指针调整上。这不是手生而是对链表节点的存储方式和指针语义理解不到位。“高效删除链表重复节点”这个题目在算法面试里出现的频率相当高。它表面上是在考一个“去重”操作实际上考的是三件事第一你懂不懂链表这种数据结构的物理结构第二你能否根据不同的约束条件比如能不能用额外空间、链表是否有序选择不同的算法第三你能不能把边界条件写对比如删除头节点、删除到尾节点、链表为空、只有一个节点。这三件事只要有一件不过关代码就会出现段错误、死循环或者逻辑上漏删。这篇文章不是背答案而是把我自己从“能写出来”到“写得对、写得稳”这个过程中的经验整理出来。适合正在准备算法面试的开发者也适合工作中需要手写链表处理逻辑的C/C程序员。读懂之后你不仅能解决“删除重复节点”这一个问题还能顺带把链表题的通用套路——比如快慢指针、哨兵节点、双指针维护前驱——一并掌握。2. 先解构问题什么算“重复”怎么算“删除”2.1 需求边界有序还是无序保留哪个能否用额外空间看起来“删除重复节点”一句话就完了可真动手之前要先确认三个边界条件。这三个条件每个都不一样对应的解法也完全不一样。第一个边界是链表是否有序。如果链表本身是有序的比如从小到大排列那么重复元素必然相邻我们可以一趟遍历把重复段整体跳过空间复杂度能做到O(1)。如果链表无序重复元素散落在各个位置那就必须借助哈希表记录“已经看到过的值”否则就得用双重循环暴力扫描时间复杂度会退化到O(n²)。第二个边界是重复节点保留哪一个。大多数题目要求“每个值只保留一个节点”默认是保留第一个出现的节点。但也有些变体要求“把重复节点全部删除一个不留”比如有序链表里值等于1的节点有三个要求全部删掉一个都不剩。这个细节会直接影响指针操作逻辑稍后我会专门演示两种写法的差异。第三个边界是能否使用额外空间。如果能用哈希表代码写起来很直观时间复杂度O(n)空间复杂度O(n)。如果题目明确要求“不得使用额外空间”或者“空间复杂度O(1)”那就只能用原地排序加遍历或者双重循环。这两个方案的代码量和出错概率完全不在一个量级。2.2 为什么删除链表节点比删除数组元素更“麻烦”数组删除元素的逻辑是比较线性的把后面的元素往前搬整体覆盖掉待删除位置。链表不一样它每个节点在内存里是散落的节点和节点之间靠next指针串联。想要删掉中间某个节点唯一的方法就是把前一个节点的next指针从指向当前节点改为指向当前节点的下一个节点。关键在于单向链表里每个节点只知道自己的后继是谁不知道自己的前驱是谁。所以删除操作必须依赖一个额外的指针始终指向“当前节点的前一个节点”。很多新手在写删除逻辑的时候只盯着当前节点却没有维护好前驱指针结果就是删着删着链表就断了或者删完当前节点后指针指向了一个已经被free掉的内存下一步访问就段错误。用生活化的例子来理解你排了一长队每个人只拉着前面人的衣角。现在要让队伍里某个穿红衣服的人离队你只能先走到他前面那个人旁边让前面那个人改拉红衣服后面那个人。如果你自己没记住“谁站在红衣服前面”光是把红衣服拽出来整个队伍后半截就没人管了。链表删除的指针操作本质就是这个“记住前驱”的动作。3. 三套主流解法原理、代码与取舍3.1 哈希表法最好写的默认方案先讲最通用、最容易写对的方案——哈希表法。它的思路非常简单我们从头到尾遍历链表用一个哈希集合记录“已经出现过的值”。每遇到一个新节点先检查它的值是否已经在集合里。如果在说明这个节点是重复的直接跳过并释放如果不在就把值加入集合继续往后走。这里有一个非常关键的指针细节需要维护一个prev指针始终指向当前节点的前一个节点。因为当你决定删除当前节点cur时prev-next必须直接指向cur-next然后再把cur向后移动。如果不用prev只用一个指针遍历删除节点后就无法把前一个节点和后一个节点衔接起来。下面给出C语言版本的完整实现。选择C语言是因为它涉及手动内存管理能更清楚看到“释放节点”这个动作的含义。#include stdio.h #include stdlib.h #include stdbool.h #define HASH_SIZE 1024 typedef struct ListNode { int val; struct ListNode *next; } ListNode; // 链表哈希集合这里用简单的取模数组模拟替代C语言里较麻烦的uthash typedef struct HashSet { int data[HASH_SIZE]; bool used[HASH_SIZE]; } HashSet; void hash_init(HashSet *set) { for (int i 0; i HASH_SIZE; i) { set-used[i] false; } } bool hash_insert(HashSet *set, int val) { int idx (val % HASH_SIZE HASH_SIZE) % HASH_SIZE; // 实际工程中应当处理哈希冲突通常用链地址法。 // 这里为了演示逻辑清晰简单采用线性探测。 while (set-used[idx]) { if (set-data[idx] val) { return false; // 已存在 } idx (idx 1) % HASH_SIZE; } set-data[idx] val; set-used[idx] true; return true; } ListNode* deleteDuplicatesUnsorted(ListNode* head) { if (head NULL) return NULL; HashSet set; hash_init(set); // 哨兵节点简化头节点的删除逻辑 ListNode dummy; dummy.next head; dummy.val 0; ListNode *prev dummy; ListNode *cur head; while (cur ! NULL) { if (hash_insert(set, cur-val)) { // 新值保留节点 prev cur; cur cur-next; } else { // 重复值删除当前节点 ListNode *tmp cur; prev-next cur-next; cur cur-next; free(tmp); } } return dummy.next; }这段代码里有几个点需要特别说明。哨兵节点dummy是处理链表题目时非常实用的小技巧。它人为地制造了一个虚拟头节点让头节点和普通节点拥有同样的“前驱结构”。这样一来删除头节点时就不用单独写if分支代码逻辑统一了。如果你不用哨兵节点就得在每次删除前判断“cur是不是head”如果是就更新head代码会多出不少分支也更容易漏掉某条路径。哈希表我在这里用了一个简化版本。实际C语言项目里一般会直接用uthash这个开源头文件或者干脆用C的unordered_set。如果是在面试现场手写用C写会更稳妥因为标准库直接提供了哈希容器你只需要关心链表逻辑。用C写起来是这样的#include unordered_set struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* deleteDuplicatesUnsorted(ListNode* head) { std::unordered_setint seen; ListNode dummy(0); dummy.next head; ListNode *prev dummy; ListNode *cur head; while (cur) { if (seen.find(cur-val) seen.end()) { seen.insert(cur-val); prev cur; cur cur-next; } else { ListNode *tmp cur; prev-next cur-next; cur cur-next; delete tmp; } } return dummy.next; }哈希表方案的复杂度非常清晰时间复杂度O(n)每个节点最多被访问两次哈希表的插入和查询平均是O(1)空间复杂度O(n)因为极端情况下每个节点的值都不同哈希表里要存n个值。这个方案唯一不适用的情况就是题目明确禁止使用额外空间。如果没有这个限制它就是默认最优解。3.2 先排序再去重空间受限场景的选择如果题目要求空间复杂度O(1)哈希表方案就被否决了。这时候思路要转换一下既然无序链表里的重复元素不好定位那能不能先把链表变成有序的重复元素在有序链表里是连续排列的一趟遍历就能删干净。这里说的“排序”不是把数据搬进数组排完再重建链表那样虽然逻辑简单但空间复杂度是O(n)不符合要求。正确做法是直接对链表做归并排序。单链表用归并排序有很多天然优势不需要额外数组来合并只需要调整指针归并排序是稳定的不会改变相同值节点的相对顺序最坏时间复杂度是O(n log n)且不受数据分布影响。单链表归并排序的核心是三步找中点拆成两半、递归排序两半、合并两个有序链表。找中点需要用到“快慢指针”技巧——快指针一次走两步慢指针一次走一步快指针走到尾部时慢指针恰好在中点。排序完成后去重就变得极其简单。由于相同的值一定紧挨着我们只需要遍历一次比较当前节点和下一个节点的值是否相等。如果相等就删掉下一个节点如果不相等指针继续后移。这里上一份“排序去重”的核心代码ListNode* getMid(ListNode* head) { if (!head) return head; ListNode *slow head; ListNode *fast head-next; // 这里让fast先走一步保证左半段不短于右半段 while (fast fast-next) { slow slow-next; fast fast-next-next; } return slow; } ListNode* merge(ListNode* l1, ListNode* l2) { ListNode dummy(0); ListNode *tail dummy; while (l1 l2) { if (l1-val l2-val) { tail-next l1; l1 l1-next; } else { tail-next l2; l2 l2-next; } tail tail-next; } tail-next l1 ? l1 : l2; return dummy.next; } ListNode* sortList(ListNode* head) { if (!head || !head-next) return head; ListNode *mid getMid(head); ListNode *rightHead mid-next; mid-next nullptr; ListNode *left sortList(head); ListNode *right sortList(rightHead); return merge(left, right); } ListNode* deleteDuplicatesSorted(ListNode* head) { if (!head) return head; ListNode *cur head; while (cur cur-next) { if (cur-val cur-next-val) { ListNode *tmp cur-next; cur-next cur-next-next; delete tmp; } else { cur cur-next; } } return head; } ListNode* deleteDuplicatesWithSort(ListNode* head) { head sortList(head); return deleteDuplicatesSorted(head); }注意这段代码里的几个细节。getMid函数里快指针初始化成head-next而不是head。原因在于如果快指针也从head出发当链表只有两个节点时slow会停在第一个节点这样左半段只有一个节点右半段也只有一个节点递归可以正常结束。但如果链表只有两个节点且快慢指针都从头出发slow最终指向第二个节点mid-next就是nullptr左半段会包含两个节点无法递归切分。这种微小差异造成的死循环排查起来非常痛苦建议直接记住“快指针先走一步”这个处理方式。排序完成后删除逻辑和“有序链表去重”完全一样。这里的遍历只需要一个cur指针因为当前节点和cur-next比较删的是下一个节点不会出现删掉cur本身导致无法前进的问题。这个方案的复杂度是时间复杂度O(n log n)主要花在归并排序上空间复杂度O(log n)这是递归调用栈的深度。严格来说不完全是O(1)但大部分面试官会认可“没有使用额外数据结构”这个说法。如果你连递归栈都不愿意用那只能走双重循环的暴力解法。3.3 双重循环暴力法教学意义大于工程意义双重循环法是最直观、最容易想到但也是最容易写错的方案。思路是用外层指针p遍历每个节点内层指针q从p的下一个节点开始扫描遇到值等于p-val的节点就删除。这个过程每个节点都要和后面的所有节点比较一遍时间复杂度O(n²)空间复杂度O(1)。为什么说它“容易写错”因为内层删除同样需要维护前驱。你在删除q指向的节点时必须知道q的前驱是谁才能把前驱的next指向q的下一个节点。最简单的方式是内层再维护一个prev指针始终指向q的前一个节点。还有一个隐蔽问题是删除后q的移动删掉q后q应该继续指向原q-nextpreq不动因为preq-next已经被更新为新的q如果没删preq和q都需要往后移动一步。这个逻辑不少人写的时候会把preq的更新放错位置造成“跳过一个节点没比较”。一份正确实现如下ListNode* deleteDuplicatesBruteForce(ListNode* head) { if (!head) return head; ListNode *p head; while (p) { ListNode *prev p; ListNode *q p-next; while (q) { if (p-val q-val) { ListNode *tmp q; prev-next q-next; q q-next; delete tmp; } else { prev q; q q-next; } } p p-next; } return head; }这里外层p从头走到尾保证每个值第一次出现的节点被保留后续相同值的节点全部删掉。由于p每次往右移动整个过程不會出现“删掉p自己”的情况。说实话这个方案工程上几乎不会用但它在面试里的价值在于展示你“在没有额外空间的情况下也能完成需求”。而且通过这道题把双重循环的指针维护练好对你理解链表操作会有很大帮助。不少复杂的链表题比如“删除链表的倒数第N个节点”或者“链表排序中的插入排序”内层循环的指针维护逻辑和这个高度相似。我个人的建议是如果你时间充裕三个方案都写一遍如果只求面试通过先把哈希表方案写熟再把“排序去重”的思路讲清楚双重循环知道原理即可。3.4 三种方案对比选型不能只看时间复杂度方案时间复杂度空间复杂度是否保留首次出现工程推荐度哈希表法O(n)O(n)是最推荐写起来最快出错率最低排序后去重O(n log n)O(log n)递归栈是适合禁止额外空间的场景代码量大双重循环暴力O(n²)O(1)是教学意义大于实用适合极简环境选型的关键依据是题目的约束条件。不限制空间就用哈希表限制空间但允许改变链表结构就排序后去重如果既不能改结构又不能额外空间那只剩双重循环一条路。面试的时候先说清楚你的选择依据再动手写代码会给面试官留下“这人有工程判断力”的印象比上来就闷头写哈希表要好得多。还有一个很多文章不会提的点哈希表方案适合“保留第一个节点”的语义排序方案改变原始顺序后“第一个出现的节点”已经无法定义。因此如果题目语义是“保留每个值第一次出现的那个节点”排序方案实际上改变了这个语义严格来说并不完全等价。但在大多数场景下面试官并不会纠结这一点只要你能讲清楚取舍即可。4. 完整实操从零手写一个可运行的去重链表程序4.1 环境准备与工程结构说了半天原理下面进入可复现的实操环节。我用C写一个完整的工程示例包含链表创建、打印、去重、释放内存的全部过程。你不需要依赖任何第三方库一个支持C11的编译器就能跑。#include iostream #include unordered_set struct ListNode { int val; ListNode *next; ListNode(int x) : val(x), next(nullptr) {} }; // 根据数组创建链表 ListNode* createList(const int arr[], int n) { if (n 0) return nullptr; ListNode *head new ListNode(arr[0]); ListNode *tail head; for (int i 1; i n; i) { tail-next new ListNode(arr[i]); tail tail-next; } return head; } // 打印链表 void printList(ListNode* head) { ListNode *cur head; while (cur) { std::cout cur-val - ; cur cur-next; } std::cout NULL std::endl; } // 释放链表内存 void freeList(ListNode* head) { while (head) { ListNode *tmp head; head head-next; delete tmp; } } // 哈希表去重主函数 ListNode* deleteDuplicatesUnsorted(ListNode* head) { if (!head) return nullptr; std::unordered_setint seen; ListNode dummy(0); dummy.next head; ListNode *prev dummy; ListNode *cur head; while (cur) { if (seen.find(cur-val) seen.end()) { seen.insert(cur-val); prev cur; cur cur-next; } else { ListNode *tmp cur; prev-next cur-next; cur cur-next; delete tmp; } } return dummy.next; } int main() { int arr[] {3, 5, 1, 3, 7, 5, 9, 1, 2, 2}; int n sizeof(arr) / sizeof(arr[0]); ListNode *head createList(arr, n); std::cout 原始链表: ; printList(head); head deleteDuplicatesUnsorted(head); std::cout 去重后链表: ; printList(head); freeList(head); return 0; }这段代码的运行结果是原始链表: 3 - 5 - 1 - 3 - 7 - 5 - 9 - 1 - 2 - 2 - NULL 去重后链表: 3 - 5 - 1 - 7 - 9 - 2 - NULL可以看到3、5、1、2这四个重复出现的值都只保留了第一次出现的位置。4.2 测试用例设计不要只测“正常情况”测链表题最容易犯的错误是只测一条常规数据就宣布“通过了”。我自己刷题吃过这个亏后来总结出一套必测用例清单现在分享出来。第一空链表。head为nullptr时函数应该返回nullptr并且不崩溃。很多人的代码在head为空时会去访问head-next直接段错误。第二单节点链表。只有一个节点没有重复函数返回原链表。这个用例看着简单但能验证哨兵节点逻辑不会破坏单节点结构。第三所有节点都相同。比如五个1去重后只剩一个1。第四首尾重复。比如1、2、3、1这种重复节点一个在头一个在尾能测试删除尾节点时prev指针是否正确。第五重复节点连续出现。比如1、1、1、2、3能验证连续删除多个节点后链表仍然完整。第六没有任何重复的链表。比如1、2、3、4、5去重后应该原样返回。你可以把上面这些用例全部跑一遍任何一个出问题都说明某个边界没处理好。尤其是“删除尾节点”和“连续重复节点”这两个场景是初学者最容易写崩的地方。4.3 从无序链表扩展到有序链表你可能已经注意到上面实现的deleteDuplicatesUnsorted不关心链表是否有序因为哈希表方案天然能处理无序的情况。但如果我们拿到的是一个有序链表比如1、1、2、3、3、4其实不需要哈希表直接比较相邻节点就能完成去重空间复杂度O(1)代码也更简洁。有序链表去重的核心逻辑我已经在3.2节展示过就是那个deleteDuplicatesSorted函数。我这里再讲一下它和无序版本在指针操作上的区别有序版本只需要一个cur指针因为重复值必然连续删掉cur-next后cur不需要移动继续和新的next比较即可。无序版本必须用prev和cur两个指针因为删除后cur必然要移动而前驱需要停留在原处衔接链表。这两者的指针移动模式完全不同。你可以把“有序去重”理解成“压缩连续重复段”把“无序去重”理解成“边查边删”。面试的时候如果能主动区分这两种情况并分别给出最优解会是明显的加分项。5. 常见问题与排查技巧实录5.1 问题一删除节点后程序崩溃大概率是访问了已释放内存这是链表删除题里最典型的崩溃原因。场景通常是这样的你删除了某个节点然后继续使用这个节点指针访问它的next或者val。C里delete一个节点后这块内存只是标记为可回收内容还没被立刻清空所以有时候访问起来“好像没问题”但一旦内存被其他数据覆盖就随机崩溃。排查方法并不复杂。删除节点后立刻把原指针赋值为nullptr避免野指针。更重要的是要保证代码路径里“删完就不要再碰它”。在上面哈希表的实现里我删除cur后马上做了两件事把prev-next指向cur-next然后把cur移动到cur-next。注意这里的cur-next是在free之前保存好的吗不是。cur-next必须在delete之前读取因为delete之后cur指向的内存已经不属于你了。上面的代码里tmpcur保存了要释放的节点prev-nextcur-next先执行这时cur仍然有效可以读cur-next然后curcur-next再去delete tmp。顺序不能反。如果你用调试器跟踪发现崩溃发生在某个节点被free之后的下一次循环访问那几乎可以断定是“use-after-free”问题。5.2 问题二链表出现环打印链表导致死循环去重过程中如果指针调整出错链表很可能变成一个环。举个例子你在删除节点时把prev-next更新成了cur-next但cur本身没动下一次循环cur还是这个已经被删除的节点它的next可能还指向链表里某个节点于是你继续遍历它然后又删一次形成循环。这种问题通常表现为程序不崩溃但也不退出printList函数一直在打同一个值。预防策略是在每次删除后明确一下当前指针应该指向哪里。哈希表方案里删除后cur必须更新为prev-next也就是原cur-nextprev保持不变。如果你发现cur更新后回到之前访问过的节点那就要检查是不是更新错了。5.3 问题三头节点被删除后返回的头指针不对这是新手最容易踩的坑。如果头节点本身是重复的需要被删除那么函数返回的头指针应该指向新的头节点。不少人在删除头节点后忘记更新head或者只更新了局部变量导致返回值仍然是旧的头节点。解决方案就是哨兵节点。使用dummy节点后不管头节点是否被删最终返回dummy.next一定是指向当前链表真正头节点的指针。这比每次删除头节点都单独更新head要可靠得多。我强烈建议所有涉及删除的链表题都加一个哨兵节点它几乎不增加任何代码复杂度却能消除一大类bug。5.4 问题四内存泄漏检测与delete遗漏如果用C/C写链表题忽略delete节点会造成内存泄漏。算法面试中内存泄漏通常不会被直接测试但在工程里这是大忌。我自己习惯写完代码后用Valgrind或者AddressSanitizer跑一遍开启内存错误检测方便快速锁定问题。在代码层面最有效的习惯是“谁分配谁释放”。每次new节点的地方都要确保在不再需要时有对应的delete。在删除重复节点时被删除的节点需要delete链表销毁时所有剩余节点需要delete。你可以用前面写的freeList函数统一负责整条链表的销毁。6. 面试追问与进阶变体6.1 如果要求把重复节点全部删除一个不留前面讨论的几种方案都是“保留第一个出现的节点”。但有的场景要求“重复值的节点一个都不保留”比如1、2、3、3、4、5、5处理后应该得到1、2、4。这个要求下哈希表的思路就不太一样了。思路可以分两步走第一步遍历一遍链表用哈希表统计每个值出现的次数第二步再次遍历链表只保留出现次数为1的节点。这样等于先把“谁重复了”查清楚再统一过滤。ListNode* deleteAllDuplicates(ListNode* head) { std::unordered_mapint, int count; ListNode *cur head; while (cur) { count[cur-val]; cur cur-next; } ListNode dummy(0); dummy.next head; ListNode *prev dummy; cur head; while (cur) { if (count[cur-val] 1) { ListNode *tmp cur; prev-next cur-next; cur cur-next; delete tmp; } else { prev cur; cur cur-next; } } return dummy.next; }这个变体和基本版相比多了一次遍历但仍然是O(n)时间。它告诉我们题目描述里的一个细微差别——是“保留一个”还是“全删”对算法设计的影响非常大。6.2 如果要在Linux下删除大量“重复文件”链表思想还有用吗你可能会觉得链表去重只是面试题工程中用不上。其实“去重”这个需求遍布各个领域只是载体不同。比如Linux系统里清理重复文件思路就是两两比较文件内容或哈希值删除多余副本。我在实际运维中处理过一批日志文件文件名不同但内容相同占用了几GB空间。最直接的办法就是计算每个文件的MD5值然后用哈希表记住已经出现的摘要再删除重复文件。这和链表去重的思路如出一辙——哈希表存“见过的特征值”后面遇到相同特征就清理。从数据结构的视角看链表去重学到的不是“链表这种形式有没有用”而是“如何维护一个正在遍历的集合同时安全删除满足条件的元素”。这套思想迁移到文件系统、数据库记录、缓存淘汰场景里都是相通的。6.3 面试中如何讲解这道题最后聊一点面试技巧。面试官不是要看你会不会背代码而是要看你的思考过程。我建议用下面这个顺序来讲解先问清楚需求“这题里的链表是有序还是无序的重复节点是保留一个还是全部删除对空间复杂度有要求吗” 这三个问题问完面试官基本就知道你理解边界条件了。然后给出方案选型“如果允许额外空间我选择哈希表方案时间复杂度O(n)空间复杂度O(n)。实现简单且不容易出错。如果不允许额外空间我可以先对链表做归并排序再删除相邻重复时间复杂度O(n log n)。”再讲边界条件“我会使用哨兵节点处理头节点删除的问题同时用prev指针维护当前节点的前驱避免删除节点后链表断裂。”最后写代码边写边解释每一步在做什么。这个顺序比闷头写代码要稳得多。7. 写在最后的一点体会链表题做多了你会发现真正难的从来不是“删除”这个动作本身而是如何在一个只有next指针的世界里准确找到需要修改的前一个节点并且在修改后不破坏整条链的结构。这个问题我建议你对照着编译器亲手跑一遍把三种方案的代码都敲出来再故意制造几个bug观察现象。踩过坑之后理解的深度比看十篇文章都管用。我个人在实际操作中最常用的小技巧是每次写完删除类函数先用“1、2、1、3、1”这种首尾重复的用例测一遍再用“1、1、1”这种连续重复的用例测一遍。这两个用例过了大部分边界问题都能提前暴露。希望这篇东西能帮你在面试或者工程里少踩几个坑。
RELATED

相关推荐

transformer 自注意力权重到底怎么算?让走 TaoToken 的 Codex 对着 PyTorch 代码逐行讲

transformer 自注意力权重到底怎么算?让走 TaoToken 的 Codex 对着 PyTorch 代码逐行讲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/9/16 3:17:05
大模型应用开发实战:从提示词到RAG再到Agent的完整链路

大模型应用开发实战:从提示词到RAG再到Agent的完整链路

最近不少做后端和算法岗的朋友都在问同一个问题:大模型都火了这么久了,市面上教程一抓一大把,但真正能把“提示词、RAG、Agent”这三样东西串起来的实战资料,怎么就这么难找?很多课程要么只讲调用API的皮毛&#xff0c…

📅 2026/9/16 3:17:05
UE项目性能优化全流程:帧时间分析、GPU/CPU调优与移动端适配

UE项目性能优化全流程:帧时间分析、GPU/CPU调优与移动端适配

如果你打开UE项目后的第一件事不是先看一眼运行帧率,而是直接去调阴影质量和光照设置,那说明你和三年前的我是同一类人——会用编辑器,但还没真正理解性能优化。我踩过最深的坑,就是把优化当成"把画质档位降低"来解决问…

📅 2026/9/16 3:17:05
MORE NEWS

更多资讯

📰

AI漫剧0基础制作全流程:工具、成本、变现与避坑指南

这段时间,后台私信里被问得最多的问题,几乎都围绕同一个词:AI漫剧。大概从去年下半年开始,短视频平台上冒出来一大批用AI生图配音剪辑做出来的连续剧,播放量动不动就几百万,评论区一堆人在问“这是怎么做的…

📰

51单片机风扇模拟实战:PWM调速与温控系统设计详解

简介:面向电子工程初学者与51单片机爱好者,这份资料以C51语言完整实现电风扇模拟程序,涵盖PWM脉宽调制转速调节、定时器中断控制、按键交互与模块化软件设计,帮助读者掌握从电机驱动到软硬件联调的关键技能。压缩包共12个文件&…

📰

labelme图像标注实战教程:从安装到生成mask全流程

刚接触图像标注那会儿,我一度以为labelme是个什么高深玩意,等真正装好、画完第一张图,才明白它其实就叫"Label me"——你来给图像打标签。它是目前深度学习数据准备阶段绕不开的一个工具,尤其做语义分割、实例分割和目标…

📰

GLM-Image:工业级自回归图像生成模型解析与应用

1. GLM-Image项目概述上周在实验室里跑通了GLM-Image的第一个demo时,我盯着生成的512x512高清图片愣了半天——这可能是国内首个真正具备工业级应用潜力的开源自回归图像生成模型。与常见的扩散模型不同,GLM-Image采用自回归(Autoregressive&…

📰

AI编程时代为什么还需要DC-WFW?一套让代码真正落地的工程框架

最近几个技术群里聊得最多的话题就是“AI编程”,从GitHub Copilot到Cursor再到各种国产辅助工具,大家说的都是“效率翻倍”“代码写不完”。但我在团队里推了另一套东西,叫“DC-WFW”,有同事一开始不理解:AI都能自动生…

📰

RTX 5080适配PyTorch全指南:从驱动到cu128版本升级

RTX 5080刚到手的那天晚上,我兴致勃勃地把旧环境里的PyTorch装好,运行python -c "import torch; print(torch.cuda.is_available())",结果屏幕上明晃晃一个False。那一刻的心情,估计每个从GTX/RTX 30系、40系升级到50系…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬