计算机科学速成课10-14集精讲:编程语言、算法与编译原理
1. 从零构建编程认知为什么这五节课值得反复咀嚼很多人学编程的路径是这样的先装个环境然后跟着教程敲几行代码跑通了就觉得自己会了。但一旦遇到稍微复杂的问题比如“为什么这段循环会死掉”“为什么这个变量在函数里改了外面没变”立刻就卡住了。这种“知其然不知其所以然”的状态根源在于跳过了对计算机底层运行逻辑的理解。《计算机科学速成课》第10到14集恰好覆盖了从“程序怎么被组织”到“代码怎么变成机器能跑的东西”这条完整链路。具体来说这五集分别涉及编程语言的演进逻辑、语句与函数的结构化组织、算法与数据结构的基础思维、以及编译器与解释器的核心差异。这些内容不是教你写某个具体语言的语法而是帮你建立一套“计算机如何执行程序”的心智模型。这套内容适合谁如果你已经能写一些代码但总觉得基础不牢、遇到新语言上手慢、看源码像看天书那这五集就是给你补底层认知的。如果你是完全零基础也能看但建议至少先动手写过几十行代码否则容易变成“听懂了但用不上”。我自己带过不少新人发现一个规律凡是能把这五集内容用自己的话讲清楚的人后续学任何框架和语言都不会太吃力。因为他们脑子里有了一张地图知道新东西该往哪儿放。接下来我会把这五集的核心内容拆开结合我自己的理解和实操经验把每个知识点讲透。不是照搬视频里的原话而是告诉你这些概念在实际写代码时到底意味着什么以及新手最容易在哪儿栽跟头。2. 编程语言与程序结构从“能跑就行”到“写得明白”2.1 编程语言的演进为什么会有这么多语言视频里用了一个很直观的类比早期程序员直接用机器码写程序就是一堆0和1。后来有了汇编语言用助记符代替二进制指令比如用MOV表示数据搬运。再后来才有了高级语言比如C、Python、Java。这个演进过程的核心驱动力只有一个降低人类表达意图的成本。机器码是给机器看的汇编是给稍微勤快点的机器操作员看的高级语言才是给普通人看的。但这里有个关键点很多人忽略高级语言并没有“消灭”底层它只是把底层封装起来了。你写一行a b c在Python里可能对应好几条字节码指令在C里可能对应几条汇编指令最终都变成CPU能执行的机器码。我个人的经验是理解这个层级关系之后学新语言会快很多。比如你从Python转到Go发现Go有指针、有显式类型声明看起来麻烦但如果你知道这些概念在底层对应什么就不会觉得是“多此一举”。Go的指针其实就是让你直接操作内存地址C语言里也有只是Python帮你隐藏了。语言没有优劣只有适用场景不同。写脚本用Python写高性能服务用Go或Rust写跨平台应用用Java或Kotlin选择取决于你对性能、开发效率、生态的权衡。注意不要陷入“语言之争”。我见过太多人在网上吵哪个语言最好吵完发现自己哪个都不精。正确的做法是选一门主流语言深入下去把编程思维练出来再横向扩展。2.2 语句与函数程序的基本积木视频里讲到了变量、条件语句、循环语句、函数这几个核心概念。这些看起来简单但新手最容易在这里形成坏习惯。先说变量。变量本质上是给内存中的某个位置起个名字。你写x 10计算机就在内存里找一块地方存10然后让x指向它。这里有个坑不同语言对变量的处理方式不一样。在C里int x 10会在栈上分配4个字节在Python里x 10是让x这个标签贴到一个整数对象上。这个差异导致了一个经典问题在Python里函数内修改可变对象会影响外部但重新赋值不会。很多人在这里翻车。def modify_list(lst): lst.append(4) # 会影响外部 def reassign_list(lst): lst [1, 2, 3] # 不会影响外部 my_list [1, 2, 3] modify_list(my_list) print(my_list) # [1, 2, 3, 4] reassign_list(my_list) print(my_list) # 还是 [1, 2, 3, 4]再说条件语句和循环。这两个东西的组合几乎构成了所有程序的逻辑骨架。但新手常犯的错误是嵌套太深。我见过一个函数里套了七层if-else读起来像在走迷宫。解决办法很简单尽早返回。把不满足条件的情况先处理掉剩下的就是正常流程。# 不好的写法 def process(data): if data is not None: if len(data) 0: if data[0] 0: return data[0] * 2 else: return 0 else: return 0 else: return 0 # 好的写法 def process(data): if data is None or len(data) 0: return 0 if data[0] 0: return 0 return data[0] * 2函数是另一个重点。视频里强调了一个概念函数是对重复逻辑的抽象。你写了一个排序算法下次需要排序时直接调用函数就行不用重新写一遍。但函数的价值不止于此它还能隔离变化。比如你写了一个发送邮件的函数哪天邮件服务商换了你只需要改这个函数内部所有调用它的地方都不用动。我个人的习惯是任何一个逻辑只要我写了第二遍就立刻抽成函数。哪怕只有三行代码。这个习惯让我在后期维护时省了大量时间。另外函数命名要能表达意图calculate_total_price比calc好一万倍。2.3 算法与数据结构程序的骨架和血肉第12到13集讲的是算法和数据结构。这部分内容是计算机科学的核心也是面试必考。但视频里讲得比较浅我补充一些实操中真正重要的点。算法是解决问题的步骤。比如你要在一个列表里找某个元素最笨的方法是逐个检查这叫线性查找时间复杂度是O(n)。如果列表已经排好序你可以用二分查找时间复杂度降到O(log n)。这个差异在小数据量下不明显但数据量大了就是天壤之别。我做过一个测试在100万个有序整数里找一个数线性查找平均要检查50万次二分查找最多20次。这就是算法的力量。数据结构是组织数据的方式。数组、链表、栈、队列、哈希表、树、图每种结构都有它的适用场景。视频里重点讲了数组和链表我补充一个实际开发中最常用的哈希表。在Python里叫字典在Java里叫HashMap。它的核心优势是查找、插入、删除的平均时间复杂度都是O(1)。你用它来统计词频、缓存结果、去重都非常高效。# 用哈希表统计词频 words [apple, banana, apple, cherry, banana, apple] word_count {} for word in words: if word in word_count: word_count[word] 1 else: word_count[word] 1 # 更简洁的写法 from collections import Counter word_count Counter(words) print(word_count) # Counter({apple: 3, banana: 2, cherry: 1})但哈希表不是万能的。它的问题在于哈希冲突和无序性。如果两个不同的键算出了相同的哈希值就需要额外处理。另外哈希表里的元素没有固定顺序如果你需要按插入顺序遍历得用有序字典或者额外维护一个列表。实操心得新手学数据结构不要死记硬背。拿一个实际场景去套。比如你要实现一个“撤销”功能就用栈要实现“任务队列”就用队列要快速判断一个元素是否出现过就用哈希表。场景驱动学习比啃教材快十倍。3. 编译器与解释器代码到底是怎么跑起来的3.1 编译型与解释型的本质区别第14集讲的是编译器与解释器。这是很多人模糊的地方。简单说编译器把源代码一次性翻译成机器码解释器逐行翻译并执行。但这只是表面区别真正的差异在于执行时机和运行效率。编译型语言如C、C、Go、Rust在运行前需要一个编译步骤生成可执行文件。这个文件直接包含机器码运行时不需要额外的翻译层所以启动快、执行效率高。但缺点是跨平台麻烦你在Windows上编译的程序拿到Linux上跑不了得重新编译。解释型语言如Python、JavaScript、Ruby在运行时逐行翻译。好处是跨平台方便只要有对应的解释器就能跑。缺点是执行效率低因为每次运行都要翻译一遍。而且解释器本身也要占用资源。但现实情况比这复杂。Java是“编译成字节码然后在虚拟机上解释执行”同时有JIT即时编译技术把热点代码编译成机器码兼顾了跨平台和性能。Python也有PyPy这样的JIT实现比标准CPython快很多。所以**“编译型”和“解释型”不是非黑即白的分类而是一个光谱**。我个人的经验是理解这个差异能帮你更好地选择工具和排查问题。比如你写了一个Python脚本发现跑得慢你可以用cProfile找出热点然后用C扩展或者Cython重写那部分。如果你用Go写服务编译出来的二进制文件直接扔到服务器上就能跑不用装运行时部署非常干净。3.2 从源代码到可执行文件编译的四个阶段视频里提到了编译器的几个阶段词法分析、语法分析、语义分析、代码生成。我展开讲一下因为理解这个过程能帮你写出更好的代码也能在报错时快速定位问题。词法分析是把源代码拆成一个个“词法单元”。比如int x 10;会被拆成int、x、、10、;。这个阶段主要检查有没有非法字符。语法分析是把词法单元组织成“语法树”。比如int x 10;会被识别为一个变量声明语句。这个阶段检查语法是否正确。如果你少写了一个分号或者括号不匹配就会在这里报错。语义分析是检查语法树是否有意义。比如你声明了一个int变量却给它赋了一个字符串这里就会报错。或者你调用了一个不存在的函数也会在这里被发现。代码生成是把语法树翻译成目标代码。可以是机器码也可以是字节码还可以是另一种高级语言。比如TypeScript编译器就是把TS代码翻译成JS代码。# 以GCC为例可以看到编译的各个阶段 gcc -E test.c -o test.i # 预处理 gcc -S test.i -o test.s # 编译成汇编 gcc -c test.s -o test.o # 汇编成目标文件 gcc test.o -o test # 链接成可执行文件注意新手看到编译错误就慌其实错误信息里包含了大量线索。比如“expected ‘;’ before ‘}’ token”就是告诉你少了个分号。学会读错误信息是编程进阶的第一步。3.3 解释器的执行模型为什么Python慢但灵活解释器的工作方式和编译器不同。它不生成独立的可执行文件而是边翻译边执行。Python的解释器CPython会先把源代码编译成字节码然后由虚拟机逐条执行字节码。这个字节码是平台无关的所以Python代码可以在任何有Python解释器的平台上运行。但这也带来了性能问题。每条字节码指令都要经过虚拟机的调度比直接执行机器码慢很多。而且Python是动态类型语言变量的类型在运行时才能确定这又增加了额外的检查开销。# Python的字节码可以通过dis模块查看 import dis def add(a, b): return a b dis.dis(add) # 输出 # 2 0 LOAD_FAST 0 (a) # 2 LOAD_FAST 1 (b) # 4 BINARY_ADD # 6 RETURN_VALUE这段字节码的意思是加载局部变量a加载局部变量b执行加法返回结果。每条指令都要经过虚拟机解释所以比C语言直接编译成机器码慢。但Python的灵活性也是编译型语言比不了的。你可以在运行时动态创建类、修改对象属性、甚至执行字符串形式的代码。这种灵活性在写脚本、做数据分析、快速原型开发时非常有用。我个人的体会是不要拿Python和C比性能就像不要拿卡车和跑车比速度。它们的设计目标不同。Python追求开发效率C追求执行效率。在实际项目中我经常用Python做原型验证确认逻辑正确后再把性能关键部分用C或Rust重写。这种“混合编程”的策略兼顾了开发速度和运行效率。4. 实操中的常见坑与排查技巧4.1 变量作用域为什么函数里改了外面没变这是新手最容易困惑的问题之一。看下面这段代码count 0 def increment(): count 1 increment() print(count) # 报错UnboundLocalError为什么会报错因为Python在编译函数时发现count被赋值了就把它当作局部变量。但count 1需要先读取count的值而局部变量还没定义所以报错。解决办法是用global声明count 0 def increment(): global count count 1 increment() print(count) # 1但更好的做法是避免使用全局变量。全局变量会让代码难以调试因为你不知道谁在什么时候改了它。我通常用参数和返回值来传递数据def increment(count): return count 1 count 0 count increment(count) print(count) # 1实操心得如果你发现自己在用global先停下来想想有没有更好的设计。大多数情况下把数据作为参数传入、作为返回值传出是更清晰的做法。4.2 循环里的闭包陷阱这个问题在JavaScript和Python里都存在。看代码funcs [] for i in range(3): funcs.append(lambda: i) for f in funcs: print(f()) # 输出2 2 2而不是 0 1 2为什么全是2因为lambda里的i是引用不是值。循环结束后i的值是2所以所有lambda都返回2。解决办法是用默认参数绑定当前值funcs [] for i in range(3): funcs.append(lambda ii: i) for f in funcs: print(f()) # 输出0 1 2这个坑我在实际项目中踩过。当时写了一个事件处理器列表每个处理器需要记住自己的索引结果全变成了最后一个索引。排查了半天才发现是闭包引用的问题。记住闭包捕获的是变量不是值。4.3 递归的栈溢出问题递归是解决某些问题的优雅方式比如遍历树、计算阶乘。但递归有个致命问题每次调用都会在调用栈上压入一帧递归太深就会栈溢出。def factorial(n): if n 0: return 1 return n * factorial(n - 1) print(factorial(1000)) # RecursionError: maximum recursion depth exceededPython默认的递归深度限制是1000。超过就会报错。解决办法有两种一是改用迭代二是调整递归深度限制但不推荐因为可能真的把栈撑爆。# 改用迭代 def factorial(n): result 1 for i in range(1, n 1): result * i return result print(factorial(1000)) # 可以正常计算注意不是所有递归都能轻易改成迭代。比如遍历树结构递归是最自然的写法。这时候可以用显式的栈来模拟递归或者用尾递归优化但Python不支持尾递归优化。在实际工程中如果递归深度可能超过几百层就要考虑换方案。4.4 浮点数精度问题这个问题几乎每个程序员都会遇到。看代码print(0.1 0.2) # 0.30000000000000004 print(0.1 0.2 0.3) # False为什么因为计算机用二进制表示浮点数而0.1和0.2在二进制里是无限循环小数只能近似存储。所以它们的和也是近似值。解决办法不要用比较浮点数。用math.isclose或者指定一个误差范围。import math print(math.isclose(0.1 0.2, 0.3)) # True print(abs((0.1 0.2) - 0.3) 1e-9) # True如果涉及金额计算绝对不要用浮点数。用decimal模块或者把金额转成整数以分为单位来计算。from decimal import Decimal a Decimal(0.1) b Decimal(0.2) print(a b) # 0.3 print(a b Decimal(0.3)) # True这个坑我在做一个电商项目时踩过。当时用浮点数计算订单总价结果出现了0.01元的误差导致对账对不上。后来全部改成用整数分来计算问题才解决。涉及钱的计算精度比什么都重要。4.5 常见问题速查表问题现象可能原因排查方法解决方案函数内修改外部变量无效变量作用域问题检查是否用了global或nonlocal用参数和返回值传递数据循环创建的闭包都返回最后一个值闭包捕获变量而非值打印闭包执行结果用默认参数绑定当前值递归报 RecursionError递归深度超过限制检查递归终止条件改用迭代或显式栈浮点数比较不相等二进制精度问题打印实际值用math.isclose或Decimal程序运行慢算法复杂度高用性能分析工具定位热点优化算法或换数据结构编译报错找不到符号缺少导入或声明检查错误信息中的行号补充导入或修正拼写5. 把知识变成能力我的学习路径建议5.1 不要只看不练视频里讲的东西看一遍觉得懂了但真正写代码时还是会卡。我的建议是每看完一集立刻动手写代码验证。比如看完函数那一集就自己写几个函数一个做计算一个做字符串处理一个做列表操作。看完算法那一集就自己实现一遍线性查找和二分查找对比它们的性能。我见过太多人收藏了一堆教程但真正动手的没几个。编程是练出来的不是看出来的。你不需要把每个概念都背下来但你需要知道遇到问题时该去哪里找答案。5.2 用项目驱动学习单纯学语法很枯燥也容易忘。更好的方式是找一个感兴趣的小项目来做。比如你想学Python可以写一个自动整理文件的脚本想学数据结构可以写一个简单的通讯录管理程序想学算法可以刷几道在线编程题。项目不用大但要有完整的功能。比如一个待办事项管理程序包含添加、删除、标记完成、按状态筛选等功能。写的过程中你会自然用到变量、条件、循环、函数、数据结构甚至文件读写。这种“用中学”的方式比按部就班学语法高效得多。5.3 读别人的代码写代码是输出读代码是输入。两者缺一不可。我建议你找一些开源项目挑一个功能模块试着读懂它。一开始可能很吃力但坚持下来你会学到很多技巧和模式。读代码时重点关注变量命名、函数划分、错误处理、边界条件。这些都是实际工程中最重要的东西但教程里往往讲得少。比如你会发现成熟的代码里到处都是参数校验和异常处理而不是假设输入永远正确。5.4 建立自己的知识体系学到的知识点如果不整理很快就会忘。我习惯用笔记软件建一个“编程知识库”按主题分类语言特性、数据结构、算法、设计模式、工具链、调试技巧。每学到一个新东西就记进去并附上自己的理解和示例代码。这个知识库不需要多正式关键是用自己的话写。比如“哈希表就是键值对集合查找快但无序”比抄一段教科书定义有用得多。当你需要回顾时翻自己的笔记比翻教程快。实操心得我刚开始学编程时总想找“最好的教程”。后来发现最好的教程就是你正在用的那本书或那个视频加上你自己的动手实践。与其纠结选哪个不如先把一个学完。5.5 接受“不懂”是常态编程领域太广了没有人能什么都懂。你不需要成为所有领域的专家只需要在需要的时候能快速学会。我做了十多年开发仍然经常遇到不懂的东西。关键是知道怎么查、怎么试、怎么问。遇到不懂的问题先自己搜再问别人。问的时候要提供足够的信息你想做什么、试了什么、报了什么错、期望是什么。这样别人才好帮你。提问的能力本身就是编程能力的一部分。6. 从这五集延伸出去下一步该学什么如果你已经理解了这五集的内容并且动手写过一些代码接下来可以往几个方向深入。方向一深入一门语言。选Python、Java、Go、JavaScript中的一门系统学习它的标准库、常用框架、最佳实践。比如Python就学collections、itertools、asyncio以及Django或FastAPI。方向二补计算机基础。学操作系统、计算机网络、数据库原理。这些知识能帮你理解程序运行的完整环境。比如你写的代码为什么会有并发问题网络请求为什么会有延迟数据库索引为什么能加速查询。方向三学算法和数据结构进阶。动态规划、图算法、贪心算法、高级数据结构堆、并查集、线段树。这些在面试和解决复杂问题时很有用。方向四做实际项目。找一个真实的需求从零开始设计、编码、测试、部署。这个过程会逼你学会版本控制、调试、测试、部署等工程技能。我个人的路径是先学Python然后补计算机基础再学Go然后做项目。每一步都是因为遇到了实际问题才去学对应的知识。问题驱动学习效率最高也最不容易忘。最后分享一个小技巧如果你觉得某个概念很难理解试着用最简单的语言把它讲给一个完全不懂的人听。如果你能讲清楚说明你真的懂了。如果讲不清楚说明你还有盲区。这个方法我用了很多年非常有效。