ARTS 1101: 最长回文串与子序列匹配、一次 URL 请求的来回之旅与如何验证 AI 的交付

每周完成一个 ARTS: 至少做一个 leetcode 的算法题、阅读并点评至少一篇英文技术文章、学习至少一个技术技巧、分享一篇有观点和思考的技术文章。(也就是 Algorithm、Review、Tips、Share 简称 ARTS)

Algorithm

这周也是两个 Easy 难度的题目,第一道题目:https://leetcode.cn/problems/longest-palindrome/description/?envType=study-plan-v2&envId=selected-coding-interview

image-20261031143628349

这道题目关健就是看数量,看对应的字母出现了几次。比如,上面案例中的 abccccdd 这个内容,如果是类似于 c、d 这种有多个的一般都是可以组成对称的内容。但是还要注意一点,如果是有单独的字母,比如 a 、b 等等,我们也可以用到其中之一,用于构建回文串的。

思路:

  • 先拿到每一个 char 有多少个,这里使用 map 的 merge 方法
  • 之后根据 char 对应的数量进行判断:
    • 如果是偶数的话那么可以自己在最终的 size 加上
    • 如果是奇数的话,需要单独进行 - 1 操作(不可能是负数,以为 map 里面存的至少是 1 个),目的是为了凑偶数,这样后续方便计算 size
    • 但是奇数的情况下,还需要进行特殊处理,这里需要计算一下是否有单独一个字母的情况,如果有的话后续计算 size 的时候需要 + 1 ,但是不能每一个都进行 + 1,因为这个单独的一个字母最终是放到中间位置的,如果多个的话那么就无法对称了,也就不是回文字符串
▼
java
复制代码
class Solution { public int longestPalindrome(String s) { HashMap<Character, Integer> strMap = new HashMap<>(); for (char c : s.toCharArray()) { strMap.merge(c, 1, Integer::sum); } int size = 0; boolean haveSingleChar = false; for (Map.Entry<Character, Integer> entry : strMap.entrySet()) { Integer value = entry.getValue(); if (value % 2 != 0) { haveSingleChar = true; value = value - 1; } if (value <= 0) { continue; } size += value; } return haveSingleChar ? ++size : size; } }

但是这道题目其实也有其他的更节省内存的办法,就是使用 Set 进行判断,而且一次 for 循环就就可以解决:

  • 使用 Set 只需要一次循环即可
  • 当我们 set 插入的时候可以判断是否存在;true 的话就表示之前没有,false 的话就表示之前插入过
  • 最终判断的时候,如果这个 set 的 size 是 0,就表示全部全部都是成对的。如果不是 0 的话就表示有冗余的,之需要 + 1 即可
▼
java
复制代码
class Solution { public int longestPalindrome(String s) { Set<Character> set = new HashSet<>(); int length = 0; for (char c : s.toCharArray()) { boolean ifPresent = set.add(c); if (!ifPresent) { set.remove(c); length = length + 2; } } return set.isEmpty() ? length : length + 1; } }

LeetCode https://leetcode.cn/problems/is-subsequence/description/?envType=study-plan-v2&envId=selected-coding-interview 题目:

image-20261031220538733

这道题目是相对简单的双指针,也算是我第一道双指针的题目了(之前可能刷过但是忘记了),有一些注意事项:

  • 具体例子,还是具体例子,没有具体的例子我真的想不明白

  • 外层循环开始的时候就是按照 index 从前向后,因为外层循环是要被匹配的字母,而内层的就是来源字符串。

  • 外层循环 0 开始,但是内层循环却不可以,如果内存循环每一次都是从 0 开始的话,那么会导致误判,

    比如 下面这个例子,aab. acb。并且开始的 index 也有讲究,必须是最后匹配到的 j 的顺序的下一位,

    因为如果来源字符串中的一个字母已经被匹配到了,那么下一次循环的时候不能再判断当前的字母了

  • 不只需要 index 还需要一个变量判断,这个变量的作用就是判断当前的内层循环,是否找到了对应的需要匹配的字母

    如果一次循环没有找到的话,那么可以直接返回 false 了,因为循环完全的来源字符串都没有对应的字母,直接宣判 false

    并且这个变量,每一次循环开始的时候需要设置成 false 否则,上一次外层循环判断的「垃圾」数据,就会干扰当前这次

    循环的判断

▼
java
复制代码
public boolean isSubsequence(String s, String t) { // 双指针解决 if (s.length() > t.length()) { return false; } int index = 0; boolean isFound = false; for (int i = 0; i < s.length(); i++) { isFound = false; for (int j = index; j < t.length(); j++) { if (t.charAt(j) == s.charAt(i)) { index = j + 1; isFound = true; break; } } if (!isFound) { return false; } } return true; }

PS 为了方便我学习,我还 vibe coding 了一个在线动画演示:https://subsequence-animation.vercel.app/

image-20261031225412751

Review

文章:What happens when you type a URL into your browser?

平时我们访问一个网站,或者自己的服务器请求另一个服务器的接口,看起来只需要一个 URL。但是请求到底怎么找到对方,数据又是怎么回来的?这篇文章从浏览器访问网站开始,把整个过程串了起来。

1、先通过 DNS 找到目标地址。

假设要访问 https://example.com/hello,其中 example.com 是域名,/hello 是要请求的路径。域名方便人记忆,但机器发送数据包需要目标 IP 地址,所以首先需要通过 DNS 解析域名。

解析时,浏览器、操作系统或者 DNS 服务器可能已经缓存了结果。如果没有找到,递归 DNS 服务器会继续查询,直到获得对应的记录,再把结果返回给请求方。拿到 IP 后,请求方才知道要向哪个地址发送数据。

2、数据经过网关和多个路由器,才能到达目标。

如果目标不在当前网络中,数据包通常会先发给默认网关,比如家里的路由器。对于 IPv4,本机可以通过 ARP 查询网关的 MAC 地址,然后把数据发送给网关。这里需要注意,找到的是下一跳的 MAC 地址,不是直接找到远程服务器的 MAC 地址。

路由器收到数据包后,会根据目标 IP 和路由表决定下一跳。数据可能经过运营商的网络、不同网络之间的连接,再经过多个路由器,最终到达目标服务器所在的网络。每个路由器只需要根据自己的路由信息决定接下来往哪里转发,不需要提前知道整条路径。

文章中还使用了 traceroute 查看数据经过的节点。可以用它观察请求大致经过了哪些网络,不过有的节点不会回应探测,所以出现 * 并不一定代表数据无法通过。

3、建立连接,再发送 HTTP 请求。

文章以 TCP 为例:数据到达目标之后,客户端和服务器会先通过 TCP 三次握手建立连接。如果使用 HTTPS,还需要进行 TLS 握手,验证服务器身份并协商加密通信需要的信息。

连接准备好之后,客户端才发送 HTTP 请求。请求中会包含请求方法、路径和请求头等信息,比如 GET /hello 表示希望获取 /hello 对应的资源。服务器收到请求后,可能读取文件,也可能执行应用程序、查询数据库,再生成响应。

这里说的是 HTTP/1.1 或 HTTP/2 使用 TCP 的情况。HTTP/3 使用的是基于 UDP 的 QUIC,连接建立的过程会有所不同。

4、响应如何回到自己这里?

服务器处理完成后,会把状态码、响应头和响应内容发送回来。响应数据同样会经过多个路由器,逐跳转发到客户端所在的网络,再交给对应的程序。回来的路径不一定和请求过去时完全相同。

如果客户端是浏览器,收到 HTML 后还可能继续请求图片、CSS 和 JavaScript 等资源。如果客户端是自己的服务器,那么它拿到响应后,可能解析 JSON、保存数据或者继续处理业务。

还要注意,请求不一定直接到达提供内容的源服务器:如果前面有 CDN,请求可能先到 CDN 节点,由它返回缓存内容;没有缓存时再向源服务器获取。通过这篇文章,我觉得之前分开学习的 DNS、TCP、路由器和 HTTP,终于可以放在一次实际请求里理解了。

如果想继续看路由器、ARP 和数据包封装的细节,还可以参考 What Happens When… 的 ARP process 和 Opening of a socket 部分。这个项目的部分协议描述比较旧,阅读 TLS 和 HTTP 的内容时需要结合现在的协议版本。

Tips

最近使用 Claude Code 比较多,CLI 可以自定义 statusline,用来显示模型、上下文使用情况和用量信息。配置文件位置是 ~/.claude/settings.json,相关配置如下:

▼
json
复制代码
{ "env": { "CLAUDE_CODE_DISABLE_1M_CONTEXT": "1" }, "statusLine": { "type": "command", "command": "bash \"$HOME/.claude/statusline.sh\"" } }
  • 这里的CLAUDE_CODE_DISABLE_1M_CONTEXT 设为 1 可以禁用 1M 上下文(有效上下文也没有 1M 这么多,不如直接用 200k 的 token)
  • statusLine 则指定 Claude Code 刷新状态栏时运行的命令。如果配置文件里已经有其他内容,把这两项合并进去即可。

需要把状态栏脚本放在 ~/.claude/statusline.sh。注意,脚本需要安装 jq,macOS 可以通过 brew install jq 安装。我的脚本内容如下:

▼
bash
复制代码
#!/usr/bin/env bash # Claude Code status line. Requires jq. set -u # Claude Code sends one JSON object to stdin on every refresh. IFS=$'\t' read -r model effort ctx hit used5 reset5 used7 reset7 < <( jq -r ' def percent: if type == "number" then ([0, ([100, .] | min)] | max | round) else "NA" end; [ (.model.display_name // "Unknown"), (.effort.level // "N/A"), (.context_window.used_percentage | percent), (.prompt_cache.hit_ratio | if type == "number" then (. * 100 | round) else "NA" end), (.rate_limits.five_hour.used_percentage | percent), (.rate_limits.five_hour.resets_at // "NA"), (.rate_limits.seven_day.used_percentage | percent), (.rate_limits.seven_day.resets_at // "NA") ] | @tsv ' ) RESET=$'\033[0m' DIM=$'\033[2m' MODEL_COLOR=$'\033[1;35m' CYAN=$'\033[36m' SEPARATOR=$'\033[38;2;130;130;130m' TRACK=$'\033[38;2;120;120;120m' GREEN=$'\033[32m' YELLOW=$'\033[33m' RED=$'\033[31m' percent_text() { if [[ $1 =~ ^[0-9]+$ ]]; then printf '%d%%' "$1"; else printf '%s' '--%'; fi } left_text() { if [[ $1 =~ ^[0-9]+$ ]]; then printf '%d%%' "$((100 - $1))"; else printf '%s' '--%'; fi } reset_after() { local ts=$1 seconds days hours mins if [[ ! $ts =~ ^[0-9]+([.][0-9]+)?$ ]]; then printf '%s' '--'; return; fi seconds=$(( ${ts%%.*} - NOW )) (( seconds < 0 )) && seconds=0 days=$((seconds / 86400)) hours=$(((seconds % 86400) / 3600)) mins=$(((seconds % 3600) / 60)) if (( days > 0 )); then printf '%dd %dh' "$days" "$hours" elif (( hours > 0 )); then printf '%dh %dm' "$hours" "$mins" else printf '%dm' "$mins" fi } # Gradient uses stable, full-width block characters: no fractional glyphs. context_bar() { local pct=$1 width=20 filled=0 i pos r g b offset if [[ $pct =~ ^[0-9]+$ ]]; then filled=$(((pct * width + 50) / 100)); fi for ((i = 0; i < width; i++)); do if (( i >= filled )); then printf '%s░' "$TRACK" continue fi pos=$((i * 100 / (width - 1))) if ((pos <= 50)); then r=$(( 0 + 220 * pos / 50 )) g=200 b=$(( 80 - 80 * pos / 50 )) else offset=$((pos - 50)) r=220 g=$(( 200 - 160 * offset / 50 )) b=$(( 20 * offset / 50 )) fi printf '\033[38;2;%d;%d;%dm█' "$r" "$g" "$b" done printf '%s' "$RESET" } usage_color() { if [[ ! $1 =~ ^[0-9]+$ ]] || (( $1 < 70 )); then printf '%s' "$GREEN" elif (( $1 < 90 )); then printf '%s' "$YELLOW" else printf '%s' "$RED"; fi } NOW=$(date +%s) ctx_label=$(percent_text "$ctx") hit_label=$(percent_text "$hit") five_left=$(left_text "$used5") seven_left=$(left_text "$used7") five_reset=$(reset_after "$reset5") seven_reset=$(reset_after "$reset7") ctx_color=$(usage_color "$ctx") five_color=$(usage_color "$used5") seven_color=$(usage_color "$used7") printf '%s[%s · %s]%s ' "$MODEL_COLOR" "$model" "$effort" "$RESET" context_bar "$ctx" printf ' %s%s%s %s│%s %sHit%s %s%s\n' "$ctx_color" "$ctx_label" "$RESET" "$SEPARATOR" "$RESET" "$CYAN" "$RESET" "$CYAN" "$hit_label" printf '5h %s%s%s left %s·%s reset %s %s│%s 7d %s%s%s left %s·%s reset %s\n' \ "$five_color" "$five_left" "$RESET" "$DIM" "$RESET" "$five_reset" "$SEPARATOR" "$RESET" \ "$seven_color" "$seven_left" "$RESET" "$DIM" "$RESET" "$seven_reset"

Share

文章:More than just code review

这周看到 Simon Willison 分享了一个关于 AI Coding 的观点。Simon 是 Django 的共同创建者之一,也一直在实践和分享 AI 编程。他认为,想要有效使用 Coding Agent,需要能够清楚地告诉它要做什么,并且有把握地验证修改是否正确。

1、能描述需求,也需要知道怎么判断结果。

以前自己写代码时,需要考虑怎么实现;使用 Agent 后,我们可以先描述需求,让它去寻找和修改代码。但是,描述得越清楚,后面越容易判断它有没有完成任务。比如,不只是说「做一个双指针动画」,还可以说明两个指针分别代表什么、什么时候移动,以及匹配成功或失败时应该显示什么。

Agent 把功能做出来之后,还需要回到需求本身,确认结果是否符合预期。如果一开始自己也不知道正确结果是什么,就很难判断它交付的东西是否可靠。

2、验证不只有逐行阅读代码。

Simon 提到,逐行检查代码有时是必要的,但验证修改还有其他方法。比如运行程序、查看实际效果、准备测试数据,以及针对容易出错的情况做测试。这些方法可以和代码审查结合使用,具体选择取决于改动的内容和风险。

以这周的子序列动画为例,除了看页面能否打开,还可以输入 aab 和 acb,观察同一个字符会不会被重复匹配;再试试空字符串、完全匹配和不匹配的情况,看看指针移动和最终结果是否符合预期。这样验证的是实际行为,而不只是代码看起来有没有问题。

3、使用 AI 之后,对问题的理解仍然很重要。

这次做动画演示,也是希望通过具体例子帮助自己理解双指针。如果不清楚题目要求和指针应该怎样移动,即使 AI 很快生成了一个页面,也不容易发现演示里的错误。

我觉得 AI 确实让我们可以更快地尝试想法,但做出来只是第一步。之后还要根据用途决定怎么检查:个人学习工具可以先用几个例子验证,涉及真实用户和重要数据的功能,则需要更充分的测试和审查。理解问题、提出要求和验证结果,依然是使用 AI 时需要自己关注的事情。

0个评论
点击登录,快来和大家讨论吧~
表情
图片
暂无评论
leikooo
作者分享
IP 数据库体积缩小 31%,关键竟然是合并重复子树
4
提到用 Rust 重写项目,很多人的第一反应往往是性能提升。以 Bun 为例,它最初主要采用 Zig 开发,后来在 2026 年发布的 v1.4 版本中将核心代码迁移到了 Rust。用 Rust 重写,性能、安全性往往是大家想要的,但是有没有想过用 Rust 重写还能发现 bug 呢? pgrust 这个项就是,他用 Rust 重新实现 PostgreSQL,保持通信协议和 SQL 方言兼容。为了确认新系统的行为和原版一致,开发者需要对着 PostgreSQL 的实际返回逐个比对。结果查着查着,有些异常不是新代码的锅,PostgreSQL 自己就是这样的。pgrust 开发者 Michael Malis 把其中几个报告给了 PostgreSQL 社区。 大概的问题: 1)SP-GiST 空间索引里有个函数判断点的方位,代码认为上、下、同一水平线三种情况已经穷尽了,末尾放了句「impossible case」兜底。结果在特定浮点边界附近,三种判断同时不成立,程序真走进了这个「不可能」的分支 2)日期格式缓存复用时,一个解析模式的标记没重置,导致同一条 JSONPath 查询在同一个会话里,换个执行顺序结果就不一样。 3)pgcrypto 的 crypt() 在某些输入下截断了多字节字符,返回的文本编码不合法。能存进去,但 pg_dump 导出时就炸了。 一个用 Rust 重写的新项目,测着测着,把运行了几十年的 PostgreSQL 里埋着的东西翻了出来。Rust 重写的价值,不一定只在跑得更快、更安全这一件事上。为了做到真正的兼容,开发者得把前人没怎么碰过的边界全走一遍,这个过程本身就在帮原版排雷。
4
ARTS 1025: 唯一字符与同构字符串、省词可能增加阅读负担与重新估算软件的实现成本
5
🎉 1024 程序员节快乐! 祝编程导航里的每一位程序员:offer 多多,薪资翻倍,面试顺利,早日拿下心仪 offer!💻
17
AST:从 Java 代码理解抽象语法树
5
下载 APP