ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

个人版伪代码规范:让算法设计更清晰、可复用

个人版伪代码规范:让算法设计更清晰、可复用 写代码之前我习惯先在编辑器里用伪代码把思路捋一遍确认逻辑没毛病再动手写正式实现。这个习惯帮我避开了不少写一半发现方向错了的返工。可时间一长问题也来了几个月前随手存的一份伪代码回看时居然要反复猜——那个temp变量存的到底是啥这个while的终止条件是还是为什么上一处还规规矩矩缩进着下一处突然顶格了说白了伪代码写得随意自己就是最大的受害者。今天想和你聊的就是一份我自己在用的【个人版】伪代码规范它不解决团队审查的问题只负责让你写的每一份伪代码下次打开时不用重新推理。这份规范适合谁算法学习者、准备面试手撕代码的、写技术方案或论文时需要描述算法的人。它能解决的核心痛点就一个让你的伪代码保持稳定、可读、可复用。我会把所有语法约定、结构模板、示例和常见坑都展开讲最后再送你一个简单的伪代码检查脚本思路帮你把规范固化下来。1. 为什么个人版伪代码也需要规范伪代码这东西定位很奇妙。它既不是正经编程语言也不是完全自由的自然语言。正是这种两头不靠的状态让很多人在写的时候完全放飞自我。但一旦放飞代价马上就来了——你写得爽的那一瞬间是在透支未来的自己的理解成本。个人版规范的意义恰恰是把这种透支降到最低。1.1 伪代码的用途边界什么场景真正需要它先说清楚伪代码不是每个场景都该写。我梳理了一下真正需要伪代码的场景其实就三类。第一类是算法设计阶段。这时候代码还没写你脑子里有一个大概的求解思路但细节还没完全确定。用伪代码把思路固化成结构化的步骤比画流程图快也比直接写正式代码轻。第二类是面试和算法学习。手撕代码前先在纸上写伪代码相当于给了自己一个缓冲能把思路理顺再落到真实语法上。第三类是写技术方案、论文、设计文档。这类文档面向读者伪代码能比自然语言更精确地表达算法又不至于像真实代码那样被语言细节淹没。反过来说如果代码逻辑已经很清晰、直接写正式代码就行或者需要给别人运行验证那就不该用伪代码。伪代码的价值区间是思路已经成型但还没固化到具体语言的那一段。1.2 伪代码失范的三个典型症状长期不约束伪代码写法通常会表现出三个典型症状我每一条都踩过。第一个症状是风格漂移。今天写的那份像 Cint a 0;这种尾巴都带上了明天那份像 Python缩进表达一切后天那份又像自然语言连把数组排个序这种话都直接写进去。风格漂移的麻烦在于每换一种风格你的大脑就要重新加载一套阅读规则。第二个症状是块结构不清晰。if的影响范围、for循环体从哪里开始到哪里结束全靠肉眼数缩进。一旦某个分支跨了好几行写到后面自己对不上号逻辑错误就这么混进去了。第三个症状是命名随心所欲。变量叫a、b、c函数叫f、g。写的时候觉得挺省事回看时完全不知道这些符号在说什么。伪代码最终是给人看的变量名承载的信息量至关重要。1.3 个人版和团队规范的区别团队级的伪代码规范比如一些教材或公司内部的标准往往追求两件事多人协作时的一致性以及形式化的可验证性。为了达到这两个目标规范会非常厚重甚至引入接近编程语言语法的形式化定义。个人版完全不用这么兴师动众。你的规范只需要服务一个读者——未来的你。所以判断标准很简单只要能让你在回看时以最低的认知成本还原当时的思路就是好规范。这意味着你可以砍掉一切形式主义只保留真正影响阅读效率的约定。我后面讲的所有规则本质上都是怎么让未来的自己读起来不费劲。注意个人版规范不等于没有规范。它反而更需要主动约束因为没有人替你把关全凭自觉。2. 伪代码语法元素先定一套最小关键字集要写一份能长期用的伪代码规范第一步是确定语法元素。我倾向于把它压缩成一套最小关键字集——够用、不啰嗦、不会让你在写的时候纠结这个情况该用哪个词。所有关键字写成全大写一眼就能和变量区分开。2.1 统一关键字的建议写法下面这组关键字是我实践后固定下来的你可以直接抄走也可以按自己习惯微调但核心原则是一旦选定全篇统一不要换。用途建议写法备选写法说明条件分支IF/ELSE IF/ELSEif/else if/elseELSE IF不建议写成ELSEIF保持空格更容易阅读计数循环FORfor配合TO或DOWNTO使用条件循环WHILEwhile循环体结束靠缩进不需要END WHILE直到型循环REPEAT ... UNTIL较少用到用于至少执行一次的场景遍历FOREACHfor each用于遍历集合或数组函数定义FUNCTIONdef函数名用驼峰比如quickSort返回RETURNreturn函数必须有明确的RETURN输出PRINT/WRITEOUTPUT按场景选一个就行输入READ/INPUT—从外部读取一个值逻辑AND/OR/NOT/ 空值NULLnull表示空引用或未定义取模MOD%避免和注释符号混淆表里这些足够覆盖绝大多数算法场景。真遇到需要别的关键字我的习惯是平时用什么就补什么但补完立刻记到规范文档里。2.2 赋值与比较用←还是这是伪代码里最值得认真定夺的一个设定。它直接影响你回看时的理解速度。我强烈建议赋值统一用左箭头←比较统一用。为什么因为伪代码里最隐蔽的逻辑错误恰恰来自赋值和比较的混淆。你写if a b的时候到底是判断 a 和 b 相等还是把 b 赋给 a用表示赋值再配合表示比较当然也行但只要哪天手滑写成单等号整个伪代码的语义就变了。用←后赋值这个动作在视觉上有一个明确的指向感判断则保留的对称感两者完全分家。举个例子sum ← 0 FOR i ← 1 TO n sum ← sum i ENDFOR这里sum ← sum i一读就知道是累加。如果写成sum sum i也能看懂但连续性不如箭头直观。箭头唯一的小麻烦是需要输入特殊字符不过现代编辑器的自动替换都能解决或者你干脆在输入法里加一个自定义短语。2.3 变量、常量和函数的命名约定命名规则是整个规范里性价比最高的一部分。我给自己定了三条硬规矩。变量名用小写驼峰。userCount、maxVal、curPos这种比a、b、c多敲几个字符但回看时省下的脑力远超那点输入成本。数组和集合用复数名词nums、edges、items一眼能看出这是多个元素。常量用全大写加下划线。MAX_SIZE、DEFAULT_TIMEOUT。它和变量在视觉上有截然不同的区分所以哪怕伪代码里不写类型也不会搞混。函数名用动词加宾语。computeAverage、findMaxIndex、mergeSortedArray。伪代码稿里函数少说也有三五个名字不清真不行。函数定义行要同时写出它的参数比如FUNCTION quickSort(arr, low, high)参数列表用逗号分隔。另外强烈建议在规范的头部约定数组下标。算法教材里经常从 1 开始很多编程语言从 0 开始。我自己的约定是如果这份伪代码最终要翻译成 C/Java/Python 这类从 0 开始的语言就用 0 起始如果只是抽象描述就用 1 起始。关键是每份伪代码开头注明避免歧义。2.4 注释规范写意图而不是过程注释这事的坑比大多数人以为的深。伪代码本身的抽象层次已经比真实代码高如果注释还在描述做了什么那基本是废话。我给自己定的规则是注释统一用//开头放在代码上方或行尾内容必须说明这一步在整个算法里扮演什么角色而不是翻译代码。// 在左半区间内继续寻找因为当前中间值比目标值小 low ← mid 1这一行注释如果只写low 赋值为 mid 加一那就是纯粹浪费眼睛。真需要这种级别的解释说明伪代码的变量名没起好。注释是最不该偷懒的地方因为它记录的是你当时脑子里的设计意图代码本身替代不了。3. 结构布局让每一份伪代码都长一个模样有了语法元素还得让整套伪代码在宏观结构上稳定。我的经验是把一份伪代码文件当成一篇小文章它应该有固定的骨架和清晰的段落感。3.1 五段式结构骨架一份规范的伪代码我建议固定包含五个部分。不要嫌事多这五部分加起来也就十几行却能让回看效率翻倍。算法头部说明。写在最上方用 3 到 5 行描述算法名称、输入参数、输出结果、时间复杂度简注。这是整个文件的目录一眼就能定位信息。辅助函数区。如果算法里有独立的子过程比如快排里的partition写在主体前面单独成块。初始化区。所有变量定义、初始赋值集中在这里用空行和主体隔开。主体逻辑区。算法的核心流程按步骤推进每一步都是一个可执行的块。收尾区。包括最终返回值的组装、必要的状态输出。下面是一个二分查找的骨架示例等下第 4 节还会展开。// 算法binarySearch // 输入有序数组 arr[]目标值 target // 输出目标值所在下标未找到返回 -1 // 复杂度O(log n) low ← 0 high ← len(arr) - 1 WHILE low high mid ← (low high) / 2 IF arr[mid] target RETURN mid ELSE IF arr[mid] target low ← mid 1 ELSE high ← mid - 1 RETURN -1这种固定结构最大的好处是将来任何一份伪代码你都知道去哪里找初始化、去哪里找返回值不用从头到尾通读一遍才能定位。3.2 块级缩进用缩进取代END IF块结构的表现方式是伪代码风格分化最严重的地方。有些教材喜欢类 Pascal 风格每个IF配一个END IF每个FOR配一个END FOR。这种写法非常严谨但写起来啰嗦而且个人使用的场景下忘记写 END这个额外错误反而更常见。我推荐的做法是彻底拥抱缩进所有块结构靠缩进表达不写显式的结束关键字。就像 Python 那样。缩进统一用 4 个空格不要用 Tab 和空格混用。一旦选定全篇不变。这种做法的风险也明显如果某次缩进乱了逻辑边界就模糊了。所以我在第 4 节的示例里会特别标注缩进第 6 节还会给一个自动检查缩进的小工具思路。3.3 块与块之间的过渡空行与分隔注释一份长得像流水账的伪代码读起来非常累。我的习惯是逻辑上相关的步骤放在同一个块里块与块之间用空行隔开。一个块通常完成一个阶段性的目标比如初始化数据、遍历所有元素、更新最优解。有些更复杂的算法我会在块与块之间加一行分隔注释类似这样// —— 第二阶段根据第一阶段的统计结果更新权重 ——这一步看似多余但当你回看一份几百行的伪代码时这些分隔注释就是锚点,能让你在很短时间内重新建立起对整体流程的认知。4. 实操示例三种算法一份规范理论说再多不如上手拆案例。我选了三个有代表性的算法二分查找、快速排序、二维动态规划。它们分别对应基础结构、递归与分治、状态转移这三种典型场景。每一步我都会讲清楚为什么这么写。4.1 示例一二分查找——基础结构的完整落地二分查找是伪代码格式的入门练习。它结构简单但能把变量初始化、条件循环、三分支、返回值这些基础元素全部覆盖。// 算法binarySearch // 输入有序数组 arr[]目标值 target // 输出目标值所在下标未找到返回 -1 // 复杂度O(log n)空间 O(1) low ← 0 high ← len(arr) - 1 WHILE low high mid ← low (high - low) / 2 IF arr[mid] target RETURN mid ELSE IF arr[mid] target // 目标在右半区缩小左边界 low ← mid 1 ELSE // 目标在左半区缩小右边界 high ← mid - 1 RETURN -1这里有几个设计点值得说明。mid用low (high - low) / 2而不是(low high) / 2是为了避免大数相加溢出。伪代码虽然不用关心具体语言实现但这种写法能直观表达取中间偏左的位置语义更准确。WHILE low high的条件用而非意味着搜索区间是闭区间[low, high]。写伪代码时最好顺手在注释里标注区间开闭因为这是后续翻译代码时最容易出错的地方。如果你喜欢左闭右开区间也行但这里的边界更新逻辑要同步调整。RETURN出现两次。一次在循环内命中时直接返回一次在循环结束后表示未找到返回-1。函数体里每个分支都要有明确的返回路径这一点在伪代码里就要检查清楚。4.2 示例二快速排序——递归与分区函数的组织方式快速排序涉及递归和辅助函数正好检验规范对模块拆分和递归终止的约定。先看完整伪代码// 算法quickSort // 输入待排序数组 arr[]区间 [low, high] // 输出原数组原地排序无返回值 // 复杂度平均 O(n log n)最坏 O(n^2) FUNCTION quickSort(arr, low, high) IF low high RETURN // 分区pivotIndex 是基准元素最终位置 pivotIndex ← partition(arr, low, high) // 递归排序左右两个子区间 quickSort(arr, low, pivotIndex - 1) quickSort(arr, pivotIndex 1, high) FUNCTION partition(arr, low, high) // 选择最右侧元素作为基准值 pivot ← arr[high] // i 始终指向小于基准值区域的最后一个元素 i ← low - 1 FOR j ← low TO high - 1 IF arr[j] pivot i ← i 1 // 把小于等于基准值的元素交换到左侧 SWAP arr[i] WITH arr[j] // 将基准值放到正确位置 i ← i 1 SWAP arr[i] WITH arr[high] RETURN i规范上值得学习的点有几个。辅助函数partition放在quickSort后面。因为伪代码本质上是一种描述文档读者的阅读顺序是线性的。把辅助函数放主体后面读主体时注意力更集中需要看实现细节时再往后翻。递归终止条件用一句话单独写在函数最前面IF low high RETURN递归函数一定要有清晰、独立的终止条件这比任何循环结构都更需要被明示出来。很多伪代码稿在看不懂的时候都是栽在递归边界上的。SWAP arr[i] WITH arr[j]这种写法是把交换作为伪代码层面的一个原子操作。它很直观但要注意这属于伪代码里允许的语法糖。如果这个交换逻辑正是你想研究的对象那就要展开写如果只是辅助操作用一个SWAP保持描述简洁就好。另外注意partition里i和j的语义在变量命名时已经通过注释明确了。i是小于基准值区域的最后一个元素j是当前扫描指针。这种临界变量如果不写注释回看时十有八九要迷糊。4.3 示例三动态规划——状态转移的注释写法动态规划类算法的伪代码最核心的是状态定义和状态转移方程。逻辑本身往往不复杂但很容易因为这个状态到底是什么意思没写清楚导致整份伪代码失去意义。这里用一个经典的二维 DP 示例最长公共子序列。// 算法longestCommonSubsequence // 输入字符串 a字符串 b // 输出最长公共子序列的长度 // 复杂度O(m * n) 时间O(m * n) 空间 m ← len(a) n ← len(b) // dp[i][j] 表示 a[1..i] 与 b[1..j] 的最长公共子序列长度 // dp 数组大小为 (m1) x (n1)下标从 1 开始方便处理空串情况 dp ← 二维数组大小 (m1) x (n1)初值全 0 FOR i ← 1 TO m FOR j ← 1 TO n IF a[i] b[j] dp[i][j] ← dp[i-1][j-1] 1 ELSE dp[i][j] ← max(dp[i-1][j], dp[j-1]) RETURN dp[m][n]这个例子展示了三个规范要点。第一状态定义必须写在伪代码正文里而不是只藏在脑海里。dp[i][j]表示什么必须在用到之前用注释交代清楚。第二状态转移方程分为两个分支匹配时取左上角加一不匹配时取上方和左方的较大值。每个分支的赋值语句就是完整的转移方程不需要另外画公式。第三dp数组下标从 1 开始这样i-1和j-1在i1或j1时不会越界。这一点我在规范里就注明了使用 DP 时默认在状态数组的外围多留一圈 0 边界。这个例子可能让一部分人困惑为什么伪代码里没写max函数的实现因为在伪代码层面max是数学上约定俗成的符号直接用没问题。这也再次说明,伪代码的关键是把算法逻辑表达清楚而不是把所有东西都推到最底层。5. 常见问题与排查技巧实录这部分是我踩坑踩出来的经验集合每一条都是当初要是有人告诉我该多好级别的内容。5.1 写完过一段时间看不明白问题大多出在头部以前我回看自己的伪代码时常常看不懂后来发现百分之八十的问题都在于没有头部说明。说白了我打开一个文件想快速知道它干嘛结果只有一堆代码没有算法名、没有输入输出说明、没有时间复杂度的标注那就只能从头读起逐行重建上下文。解决办法很简单花 30 秒写头部。每次新建伪代码文件时先把算法名、输入、输出、复杂度四行写好再动笔写主体。这 30 秒的投资回看的时候能帮你把理解时间从半小时压到两分钟。你可以把它当成伪代码的货架标签没贴标签的东西过几周在货架上看到就会陌生。5.2 伪代码和真实代码混在一起了怎么办这是风格控制的另一个重灾区。写着写着就把某个语言特有的写法带进来了。比如在伪代码里写import json、list.sort()、std::max这类库函数调用。一旦出现这种东西伪代码的抽象层次就被打破了读者会被迫思考这是 Python 的语法还是别的语言的思路直接被带偏。我的规矩是伪代码里不允许出现任何特定语言的库函数和语法。真需要表示某个通用操作就用自然语言加参数描述比如按用户ID排序 users。等到动手写正式代码时再去想具体语言里对应的是什么函数。伪代码的层次是做什么真实代码的层次是怎么做。5.3 编辑器选择Word 对齐是灾难Markdown 是真爱如果你还在用 Word 写伪代码大概率体验过这种痛苦缩进老是自动乱跳箭头符号无处安放复制到别处格式全丢。伪代码对缩进和等宽字体极其敏感所以工具选型很重要。我自己的方案是Markdown 文件 代码块 等宽字体。编辑器我用 VS Code 或 Typora 都行关键是代码块内的←、≤这类符号能够稳定显示且粘贴到网页或文档时不会变形。如果你写的伪代码要放进论文或正式文档建议最后再从 Markdown 导出而不是直接在 Word 里排版。另外一个小习惯规范文档和伪代码示例放在同一个项目仓库里后缀名用.md每次写完一份伪代码都对照规范自查一遍。哪怕只是花半分钟扫一眼缩进和关键字也能减少大量格式漂移。5.4 规范本身怎么迭代个人版规范不是一次定死。恰恰相反它应该是活文档。我建议每两三个月或者每次发现自己回看伪代码卡壳的时候都做一次小修把这次让我不舒服的点记录下来判断是规范缺了规则还是自己没遵守规则。如果是前者就给规范补一条。如果是后者那就没什么好说的下次严格点。比如我第一次写的时候没有约定小数除法怎么表示后来发现mid ← (low high) / 2里混用了数学除法和整数除法。我就给规范加了一条伪代码里的/默认表示整数除法需要精确小数时用÷或附注说明。这种迭代过程个人版规范才会越来越贴合你的思维习惯。6. 把规范变成工具做一个极简伪代码检查脚本规范光靠自觉总有松懈的时候。尤其检查代码规范这件事完全可以交给脚本干。我花了一点时间写了个极简的伪代码检查器不解析 AST、不搞复杂语法就抓三类最常见的格式问题缩进层级是否突变、关键字是否拼写一致、括号是否配对。需求不多但用来守住规范底线足够用了。6.1 为什么值得做一个极简 lint 工具网上有不少专门标记伪代码的 LaTeX 宏包和编辑器插件功能强大。但对我这种个人版场景它们太重了安装配置的时间都够我手写十个检查脚本。而且自己的规则自己最清楚通用工具反而不一定能覆盖我关心的那几条。写这个脚本还有一个额外好处它逼着我把规则参数化。以前规则在脑子里模模糊糊写代码的时候必须明确关键字列表是什么缩进多少算一层注释符号怎样算合法。这本身就是一次规范重构价值比脚本本身还大。6.2 脚本设计与实现思路整体设计分三步按行读入伪代码文件跳过空行和注释行对每一行做三类检查关键字的合法性检查行首的IF、FOR、WHILE、RETURN等是否都在合法关键字集合里。括号配对检查统计每行(、[、{的左右括号数量差。缩进一致性检查用行首空格数判断缩进是否与上一非空行差距过大比如突然少了 6 个空格中间却没有RETURN之类的跳出语句。以下是核心 Python 代码非常轻量import re import sys KEYWORDS {IF, ELSE, ELSEIF, FOR, TO, WHILE, FUNCTION, RETURN, READ, PRINT, FOREACH, REPEAT} def check_keywords(line, lineno): tokens re.findall(r[A-Za-z_], line) for token in tokens: if token.upper() in KEYWORDS: expected token.upper() if token ! expected: print(f[关键字] 第 {lineno} 行{token} 应写为 {expected}) def check_parentheses(line, lineno): stack [] pairs {): (, ]: [, }: {} for ch in line: if ch in ([{: stack.append(ch) elif ch in )]}: if not stack or stack[-1] ! pairs[ch]: print(f[括号] 第 {lineno} 行括号不匹配) return stack.pop() if stack: print(f[括号] 第 {lineno} 行存在未闭合的括号) def check_indent(lines): prev_indent 0 for lineno, line in enumerate(lines, 1): if not line.strip() or line.strip().startswith(//): continue indent len(line) - len(line.lstrip()) delta indent - prev_indent if delta % 4 ! 0: print(f[缩进] 第 {lineno} 行缩进不是 4 的倍数当前缩进 {indent}) prev_indent indent def main(filepath): with open(filepath, encodingutf-8) as f: lines f.readlines() check_indent(lines) for lineno, raw in enumerate(lines, 1): line raw.rstrip(\n) if line.strip().startswith(//): continue check_keywords(line, lineno) check_parentheses(line, lineno) if __name__ __main__: main(sys.argv[1])这脚本不检查逻辑只抓形式。但它恰好能守住我规范里最容易破的两条关键字拼写和缩进层级。放在博客的配套仓库里每次写完一份伪代码就跑一下python check_pseudo.py xxx.md三十秒内能扫完全部格式隐患。6.3 工具的使用边界与扩展方向要明确这个脚本的定位它是一位沉默的格式纠察员不是逻辑评审官。它不会告诉你算法对不对也不会告诉你命名好不好。但正因为它轻你才愿意每次都用因为每次都用格式才会一直稳。如果以后需要更深入的功能可以按下面两个方向扩展。一是把它做成 VS Code 扩展或 git hook每次保存或提交时自动跑一遍把格式检查变成流水线。二是扩展检查规则比如增加变量名单词之间不允许有下划线这种命名规则检查。这类需求属于锦上添花等实际需要时再加也不迟。我个人在实际使用中的体会是自动化检查最大的贡献不是抓出多少错误而是它让遵守规范变成一件不需要意志力的事。过去要靠自觉现在交给脚本脑子省下来的空间全部用来想算法本身这才是规范该有的样子。
返回列表