资讯详情

遍历字符串与数组取下标:各语言写法、翻车现场与工程取舍

📅 2026/10/9 8:02:43 | 华诺云谱 👁 阅读
遍历字符串与数组取下标:各语言写法、翻车现场与工程取舍
写代码这些年我发现自己花在“遍历字符串、数组还要顺手取下标”上的时间远比想象中多。无论是解析一段JSON、处理一份日志还是刷LeetCode时判断两个字符串是否同构本质上都在干同一件事搞清楚此刻游走到哪个位置以及这个位置的元素是什么。很多人觉得遍历嘛for循环套进去就完事了但真到了要“取下标”的环节各种问题就浮出水面——要么下标越界要么删元素时索引错乱要么中文字符串的长度和下标关系搞不清楚。这篇博文就把“遍历取字符串/数组下标”这件事彻底讲透包括各语言的主流写法、常见翻车现场、以及工程里的真实取舍。1. 为什么“取下标”会成为一个独立问题先说个最容易忽略的事实遍历的本质分两种一种是只关心值一种是要同时关心位置。比如要判断一个数组里存不存在某个数字你只需要值就够了下标只是个配角。但如果你要“把字符串里第一个数字字符替换成*”、要“找出数组中连续递增子序列的起点”、要“把二维数组按行优先顺序输出编号”下标就成了主角。没有位置信息后面的操作全都无从谈起。1.1 下标到底是什么为什么总有人弄错下标index就是元素在容器中的偏移量。字符串本质是字符的数组所以字符串和数组在下标这件事上共享同一套逻辑从0开始最大到len-1。最容易出错的点就两个从1开始数。人在自然语言里数东西习惯从1但程序的下标永远从0起步。于是“取第3个元素”经常被写成arr[3]实际取到的是第4个。这个错误在面试手写代码时出现率极高。越界看成空值。很多动态语言Python、JavaScript越界访问数组不报错返回undefined导致后续逻辑在一堆奇怪的值上继续跑。C语言数组越界更危险它可能读到相邻内存里的随机数据而且往往不立刻崩溃等释放内存时才爆雷。1.2 遍历时“需要下标”的典型场景拆解凭我实际写代码的经验需要下标才能完成的场景大概能分成这几类场景为什么必须要下标典型需求位置关联元素值和它在容器中的位置强相关找出第一个重复字符、第k大的数、回文判断里的镜像位位移运算要对相邻元素或前后元素做比较冒泡排序、滑动窗口、区间合并删除与替换修改容器时需要精确定位操作目标遍历删除特定元素、原地去重双指针/首尾指针多个游标同时移动必须有独立计数有序数组两数之和、反转字符串分组/分页按位置切分数据块每3个一组、分页取数据前半段这些场景的共同点是光知道元素本身还不够你还得回答“这个元素排在第几位”。2. 各主流语言遍历取下标的正确姿势不同语言在这个问题上的设计差异挺能体现语言哲学的。下面按我日常写过的语言逐一拆解并给一份可以直接抄的对照表。2.1 Python优雅与陷阱并存Python里遍历数组最舒服的写法是for item in arr但它拿不到下标。要下标标准答案是enumerate# 同时拿下标和值最推荐 for idx, ch in enumerate(s): print(idx, ch) # 只拿下标不关心值用 range(len) for idx in range(len(arr)): print(idx, arr[idx])enumerate还有一个容易忽略的参数start# 从1开始编号适合做序号输出 for idx, name in enumerate(student_list, start1): print(f{idx}. {name})坑在哪Python的字符串是不可变对象你没法通过下标直接修改某个字符必须先转成列表。这个“不能改”的特性让很多从C转过来的开发者不适应s hello # s[0] H # TypeError: str object does not support item assignment chars list(s) chars[0] H s_new .join(chars)另外一提reversed和切片[::-1]虽好但如果你在反转时需要同时拿原下标就得老老实实记录位置信息。常用技巧是倒序遍历# 倒序同时搞删除规避正向删除时下标位移问题 for idx in range(len(lst) - 1, -1, -1): if lst[idx] % 2 0: lst.pop(idx)2.2 JavaScript/TypeScript三个循环三种脾气JavaScript提供了三种遍历思路新手上路时经常混着用而它们的行为差异还是挺大的。// 传统 for 循环最可控下标随手取 const arr [a, b, c]; for (let i 0; i arr.length; i) { console.log(i, arr[i]); } // forEach能拿到下标但无法 break / return arr.forEach((item, index) { console.log(index, item); }); // for...of拿值方便拿下标要靠 entries() for (const [index, item] of arr.entries()) { console.log(index, item); }这里想提醒一个老坑for...in是用来遍历对象键名的虽然它也能作用于数组但拿到的“下标”是字符串而且会包含数组上自定义的扩展属性现实中不建议用它遍历数组。字符串也是同理String.prototype没有直接的entries()方法但可以先用split()把字符串变成字符数组或者直接用for...of遍历字符。要注意的是for...of遍历字符串时按Unicode码点迭代遇到emoji这类代理对字符能正确处理这算是它的一大优势。const s hello; for (const [i, ch] of s.split()) { console.log(i, ch); }2.3 Java增强for循环拿不到下标Java的for (String s : list)写起来很爽但你拿不到当前元素的下标。如果想同时拿到下标和值传统做法是String[] arr {a, b, c}; for (int i 0; i arr.length; i) { System.out.println(i : arr[i]); }对ListInteger这类集合用List的indexOf去反查下标是新手常干的事但元素重复时indexOf永远只返回第一个匹配的位置很容易翻车。更推荐Java 8 的IntStream方式来按索引流式处理IntStream.range(0, list.size()) .forEach(i - System.out.println(i : list.get(i)));这样既保留了流式风格又拿到了下标。或者借助AtomicInteger作为计数器不太优雅但常见于旧代码AtomicInteger idx new AtomicInteger(0); list.forEach(item - { int current idx.getAndIncrement(); System.out.println(current : item); });2.4 C/C指针与下标的暧昧关系C语言里数组下标本质就是指针偏移arr[i]等价于*(arr i)。所以C语言中遍历取下标最自然的方式也是for (int i 0; ...)。但实际干活时会发现其实还可以“用指针移动代替下标”二者各有适用场合。#include stdio.h #include string.h int main() { char *s hello; // 方式一下标 for (int i 0; i strlen(s); i) { printf(%d: %c\n, i, s[i]); } // 方式二指针 int pos 0; for (char *p s; *p ! \0; p) { printf(%d: %c\n, pos, *p); } return 0; }这里有几个容易翻车的细节strlen每次循环都算一遍没有别的问题但字符串一长性能会受影响。习惯做法是循环前先用size_t len strlen(s);存起来。sizeof(s)和strlen(s)不是一回事。在函数内部拿到的是指针变量的大小8字节不是字符串长度。这个我见过的bug不下十次。C字符串没有长度字段只能靠结尾的\0判定停止。所以如果你拿到的字符数组没有正确处理终止符遍历就会越界。C里std::string和std::vector的下标用法更安全但同样的坑还在字符串下标操作返回的是字符引用但std::string在C11之后保证内存连续因此s[0]可以当字符指针用——利用这个特性可以把C string直接传给C接口。2.5 Gorange关键字直接给下标或键Go在这个问题上设计得很干脆for i, v : range arr一步到位i就是下标。而且当你只需要下标、不需要值时可以写for i : range arr这在很多C系语言里做不到这么简洁。arr : []string{a, b, c} for idx, val : range arr { fmt.Println(idx, val) } // 只要下标 for idx : range arr { fmt.Println(idx) } // 遍历字符串得到的是字节位置和Unicode码点 s : hello for idx, r : range s { fmt.Printf(byte%d, rune%c\n, idx, r) }Go中范围遍历字符串时第一个值是该字符的起始字节位置不是字符序号这对处理中文影响很大。比如“你好”中“好”的字节下标是3一个中文字符占3字节不是1。如果你不关心字节位置用for i, r : range s时i会“跳着走”首次接触时容易疑惑。2.6 语言写法速查对照表语言同时取值下标只取下标备注Pythonfor i, c in enumerate(s)for i in range(len(arr))enumerate可直接设置起始值JavaScriptarr.entries() for...offor (let i0; iarr.length; i)for...in请用于对象JavaIntStream.range(0, n)for (int i0; in; i)增强for拿不到下标Cfor (int i0; ilen; i)同左指针等价arr[i]*(arri)C同C或用迭代器std::distance同左std::string内存连续Gofor i, v : range arrfor i : range arr字符串range给字节位置3. 下标相关的经典翻车现场这一节写的全是真实发生过的事。每个坑我都踩过或者帮人排查过具备很强的复现性。3.1 Python“边遍历边删除”的索引错乱这个坑在网络热词里独占一条——“pythonlist遍历删除”。它为什么会成为热词因为错误写法太反直觉了lst [1, 2, 3, 4, 5] for i in range(len(lst)): if lst[i] % 2 0: del lst[i]这段代码不会崩但结果莫名其妙。拿lst [1, 2, 3, 4, 5]跑一遍i1时删除2列表变成[1, 3, 4, 5]i2时拿到的是4而不是期望的3删除4后变成[1, 3, 5]i3越界停止。最后结果[1, 3, 5]看着像对了但如果原列表是[1, 2, 3, 4, 5, 6]结果会变成[1, 3, 5]偶数6被跳过了。因为每次删除都让右侧元素整体左移后续下标全部错位。可靠解法是倒序遍历for i in range(len(lst) - 1, -1, -1): if lst[i] % 2 0: lst.pop(i)倒序删除不会影响尚未访问的元素位置。另一个思路是改造成“保留符合条件的元素”用列表推导式lst [x for x in lst if x % 2 ! 0]这个方案最省心但如果你需要在删除时基于位置做额外逻辑列表推导式就没那么灵活了。3.2 字符串长度与下标中文字符的认知偏差字符串的问题比数组更容易踩坑因为“长度”在不同语言里定义不同。Pythonlen(你好)返回2按Unicode字符计数但字符串的字节长度是len(你好.encode(utf-8))返回6。按下标访问你好[0]得到你没问题。然而一旦做切片切的也是字符位置不是字节位置需要和网络传输UTF-8字节打交道时下标就对不上了。JavaString.length()按UTF-16码元计算绝大多数常用汉字一个char搞定但遇到生僻字或emoji会算成2个char导致下标遍历出现半个字符的尴尬。Java 8可用codePoints()遍历完整码点。C语言你好在代码里是字节串strlen(你好)返回6。s[0]拿到的是字符你的第一个字节。你要是按1个下标取一个“字符”来处理中文一定会被拆散乱码。Golen(你好)也是6字节range按rune遍历时拿到的下标是字节偏移不是字符序号。实际项目中我遇到最多的情况是把用户输入的字符串按utf-8字节位置去截断导致最后一位乱码。这种问题的正确做法是先用语言自带的方式把字符串拆成字符数组或rune数组再按数组下标操作。比如在Go中写[]rune(s)在Python中字符本身就可直接取在Java里先toCharArray()再遍历。3.3 C语言指针移动与数组下标的混淆C语言中数组下标和指针是可以互换的但这个“方便”也带来了经典误解。先看一个让许多人迷惑的句子arr[i]和i[arr]在C语言里是等价的。因为下标运算符的定义就是*(arr i)加法交换律使得i[arr]也成立。这个写法没人会真的用但它能帮你理解下标底层就是指针偏移。容易出错的是“指针移动”和“数组下标”同步问题char *p buf; while (*p ! \0) { // 处理 buf[i] 时总是拿不到 p - buf 的偏移 }如果你用指针遍历的同时还要用下标做随机访问最稳的方式是维护一个int idx p - buf;或者干脆“指针循环 pos自增”双轨并行。我看到过有人把p直接当数组用p[2]确实没问题因为p本身是指针但一旦p偏移过p[2]就不再是buf[2]了。这个问题的核心心法下标应绑定“容器的原点”指针的偏移量应绑定“当前游标位置”。不要把两者混用。3.4 二维数组下标错位谁先谁后二维数组矩阵的遍历顺序是下标问题的另一大重灾区。int matrix[3][4]; // 3行4列 for (int i 0; i 3; i) { for (int j 0; j 4; j) { // matrix[i][j] 是第i行第j列 } }看起来简单但一旦代码写得多了很容易把i 4和j 3写反而编译器不会报错。更隐蔽的坑是遍历时行列使用的下标若内外层互换访问的元素就变成了转置结果数据对不上但代码不崩溃。处理二维下标问题我的经验是永远先写行再写列所有循环边界变量用rows和cols命名别用m、n这种容易混淆的变量。例如rows, cols len(matrix), len(matrix[0]) for r in range(rows): for c in range(cols): print(matrix[r][c])3.5 遍历删除后的“数组下标”与“集合”问题C#等语言里“遍历时删除”同样有坑ListT在foreach里删除会直接抛InvalidOperationException因为集合版式在迭代期间发生了修改。这点很多从JavaScriptforEach中splice不报错但会跳项转过来的人尤其容易中招。正确姿势是倒序for循环或使用List.RemoveAll(Predicate)方法list.RemoveAll(x x % 2 0);RemoveAll内部自己处理了遍历逻辑你不需要关心下标位移。这给我们的通用启发是优先使用容器自带的批量删除API而不是断言你在循环里处理下标。因为API作者已经把元素位移问题处理好了。4. 工程里真正用到“下标”的设计思路刷题和写业务代码有个差别刷题时下标是主角而业务里下标往往只是个工具人。知道工具怎么用还要知道什么时候不用它。4.1 用“语义下标”代替“物理下标”很多业务场景需要的不是物理下标而是逻辑编号。比如分页展示中的序号、列表项的行号这些未必等于数组下标。物理下标是0开始的展示序号一般从1开始所以业务侧常见的做法是for i, item in enumerate(items, start1): print(f{i}. {item})这和刷题中“返回第k个元素”要搞清楚k是从0数还是从1数属于同类问题。我给自己定的规则是接口边界处做一次转换内部全部用物理下标对外输出时再做1映射。切忌一半代码用0基、一半用1基混乱必出Bug。4.2 用下标做Key时要三思前端渲染列表时经常用数组下标作为元素的key。这在拿数据后不做增删排序时没问题但一旦列表支持删除、插入、排序用下标做key就容易出现渲染错乱——React会根据key来复用DOM节点下标变化会导致状态串位。正确准则静态列表下标key可用。动态列表优先用数据的唯一id实在没有唯一id再考虑“下标内容哈希”之类的组合。这背后其实又是一个“下标不稳定”的问题下标只是容器在某个时间点的快照它不是元素的身份。这个认知放之四海皆准。4.3 遍历取下标在算法题里的高频模板拿“判断两个字符串是否同构”这种经典题来说核心就用到下标映射def is_isomorphic(s: str, t: str) - bool: if len(s) ! len(t): return False map_s {} map_t {} for i in range(len(s)): ch_s, ch_t s[i], t[i] if ch_s in map_s: if map_s[ch_s] ! ch_t: return False else: map_s[ch_s] ch_t if ch_t in map_t: if map_t[ch_t] ! ch_s: return False else: map_t[ch_t] ch_s return True这里必须用下标同步遍历两个字符串。如果用zip当然可以但当两个字符串不等长时zip会悄悄截断反而隐藏了bug。所以涉及“对齐比较”时用下标同步更稳妥。再比如“删除有序数组中的重复项”要求原地操作也是典型的既要下标又要值def remove_duplicates(nums): if not nums: return 0 write_idx 0 for read_idx in range(1, len(nums)): if nums[read_idx] ! nums[write_idx]: write_idx 1 nums[write_idx] nums[read_idx] return write_idx 1慢指针write_idx和快指针read_idx两个下标的配合是这类问题的题眼。学这种题时刻意练习“下标变量”的命名和步进逻辑比背解法更有价值。4.4 字符串截取与切片时下标边界怎么定切片操作里最容易出错的是“结束位置到底算不算”。Python中s[0:5]取前5个Java中substring(0, 5)同样取前5个。但很多其他语言或自研框架里substring(0, 5)可能是“包含5”也可能“不包含5”。更糟糕的是有些框架的结束位置是“从0开始的索引”且包含这个索引。我处理这种问题有一个习惯拿到API第一件事看它官网的示例代码而不是翻参数说明。参数说明里“inclusive/exclusive”最容易让人犯迷糊示例一看就懂。另外字符串截断场景下还要考虑“如果结束位置大于字符串长度会怎样”。有的语言直接抛异常有的则截到末尾。业务代码防御性写法建议统一做一次Math.minend min(end, len(s)) s s[:end]4.5 数组下标与循环变量关系的进阶思考写遍历时名义上是“数组下标”实际它常常承担“循环计数”“时间段编号”“坐标位置”等多种角色。比如按二维数组输出棋盘时ij的奇偶可以决定格子颜色。这就要求你理解下标不只是访问元素的门牌号它本身可能是计算素材。处理这类问题时我喜欢先把下标画在草稿纸上i从0到n-1j从0到m-1然后把需要的数据关系标出来。下标问题的绝大部分Bug都能靠画图暴露。5. 最后的实操心得聊到这里其实“遍历取下标”已经超越了语法层面变成一套思维习惯。我自己的代码习惯是先判断这个场景是否真的需要位置信息不需要就用值遍历需要时优先用语言自带的下标获取方式enumerate、range、IntStream等不要自己造轮子。手动维护计数器能不用就不用因为计数器变量最容易在循环分支里被漏更新。最后分享一个我写遍历时的小技巧当下标涉及“目标位置”和“当前位置”时给变量起名别用i、j了事用left、right、writePos、readPos这类带语义的名字。排查下标越界时这种命名能帮你省下一个小时。另一个小习惯是每个循环入口先打印一次i和len(arr)肉眼确认边界条件正确再进入循环体。这做法看着笨但排查越界准确率极高尤其用C和Go这些运行时检查偏弱的语言时早检查早安心。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑