题目描述
思路解析
一句话答案:LeetCode 821 字符的最短距离用两遍扫描:从左扫一遍记每位到最近 c 的左距、从右扫一遍记右距,每位取两者的 min 就是答案,时间 O(n)、空间 O(1)。
answer[i] 要算的是什么距离
给字符串 s 和字符 c(题目保证 c 在 s 里至少出现一次),要为每个下标 i 求它到最近那个 c 的距离,即下标差的绝对值 abs(i-j)。题面 s="loveleetcode"、c="e",e 在下标 3、5、6、11,输出 [3,2,1,0,1,0,0,1,2,2,1,0]:是 e 的位置为 0,越远数越大。
每个位置都向两边找一遍,慢在哪
O(n²) 是这么来的:站在每位往左右各走一趟找最近的 c、取较近那段,这是最容易想到的暴力。s 有 n 个字符,每个位置最坏要扫遍整串才碰到 c,n 个位置各扫 n 步就是 O(n²)。串长上万就明显拖不动。
一个位置的最近 c,非左即右
对某个位置来说,离它最近的 c 要么在左边、要么在右边,取近的就是答案。可从左走时右边的 c 还没扫到,一遍只知一侧。于是分两遍:第一遍从左到右,求每位到左边最近 c 的距离;第二遍从右到左,求到右边最近 c 的距离;同一位置两个方向取 min,就是真正的最近距离。
pre、suf 两个变量怎么一路更新
先把答案数组每格填成 n(串长,是距离的上界);pre 初始设成负无穷,表示左边还没出现过 c。第一遍从左往右:遇到 c 就把 pre 更新成当前下标,再让 ans[i] 取「原值」和「i 减 pre」里更小的。左边一直没 c 时 i 减 pre 是无穷大,那格保持上界,留给第二遍补。第二遍从右往左对称:遇到 c 更新 suf,ans[i] 再和「suf 减 i」取一次 min。两遍都用减法、方向保证非负,天然就是绝对值距离,不用再套 abs。
两遍扫过 loveleetcode,各得什么
第一遍从左,pre 记左边最近的 e:下标 0、1、2(lov)左边没 e,停在上界;下标 3 遇 e,pre=3、ans=0;下标 4 得 1;下标 5、6 遇 e,pre 更新为 5、6、两格 0;下标 7 到 10 依次 1、2、3、4;下标 11 遇 e,ans=0。
第二遍从右,suf 记右边最近的 e,取一次 min:下标 10、9 右距 1、2 比原来的 4、3 近,更新成 1、2;下标 8、7 右距 3、4 比原来的 2、1 远,保持 2、1;下标 6、5 遇 e 更新 suf;下标 4 得 1(与原相等);下标 3 遇 e;下标 2、1、0 得 1、2、3,补上前三格。最终得到 [3,2,1,0,1,0,0,1,2,2,1,0]。
c 挤在串的一头,就全靠另一遍兜底
两遍都是从头到尾的线性遍历,时间 O(n);除答案数组外只多用 pre、suf 两个变量,空间 O(1)。两个边界说明为什么少一遍就错:c 全挤在最右端(如 s="aaab"、c="b"),第一遍从左走时前面每个位置左边都没 b,左距全停在上界,答案全靠第二遍从右补出;c 在最左端就换第一遍兜底。另一个易错点:pre 别填 0,那等于假装下标 0 处有个 c,会把开头本该由另一遍补的位置压成错误小距离——填负无穷,才能把「还没遇到 c」和「距离真是 0」分开。
▶ 动画逐步走查(文字版)——想跟着上方动画一帧帧对照就展开
核心就八个字:左右各扫一遍,取 min。下面每一帧都在套它。
先把目标字符 e 全找出来:绿色这四个位置(下标 3、5、6、11)本身到 e 的距离都是 0。其余位置的距离,就看它离最近的绿色有多远。
走到下标 0(字符 l),它左边一个 e 都还没出现,到左边 e 的距离先记成无穷大 ∞,等第二遍从右边补。
走到下标 1(字符 o),它左边一个 e 都还没出现,到左边 e 的距离先记成无穷大 ∞,等第二遍从右边补。
走到下标 2(字符 v),它左边一个 e 都还没出现,到左边 e 的距离先记成无穷大 ∞,等第二遍从右边补。
走到下标 3,正好是 e。它本身距离为 0,同时把 pre 记成 3,往后的位置就拿它当「左边最近的 e」。
走到下标 4(字符 l),左边最近的 e 在 3,距离就是 4 减 3 等于 1。
走到下标 5,正好是 e。它本身距离为 0,同时把 pre 记成 5,往后的位置就拿它当「左边最近的 e」。
走到下标 6,正好是 e。它本身距离为 0,同时把 pre 记成 6,往后的位置就拿它当「左边最近的 e」。
走到下标 7(字符 t),左边最近的 e 在 6,距离就是 7 减 6 等于 1。
走到下标 8(字符 c),左边最近的 e 在 6,距离就是 8 减 6 等于 2。
走到下标 9(字符 o),左边最近的 e 在 6,距离就是 9 减 6 等于 3。
走到下标 10(字符 d),左边最近的 e 在 6,距离就是 10 减 6 等于 4。
走到下标 11,正好是 e。它本身距离为 0,同时把 pre 记成 11,往后的位置就拿它当「左边最近的 e」。
第一遍扫完了。每个位置都拿到了「到左边最近 e」的距离。注意开头 lov 三个位置还是 ∞,因为它们左边根本没有 e。这正是要扫第二遍的原因。
从右往左走到下标 11,又是 e,把 suf 记成 11,它本身答案就是 0。
走到下标 10,右边最近的 e 在 11,右距 1。和第一遍的左距 4 比,取小的:答案 = 1。右边更近,更新成它。
走到下标 9,右边最近的 e 在 11,右距 2。和第一遍的左距 3 比,取小的:答案 = 2。右边更近,更新成它。
走到下标 8,右边最近的 e 在 11,右距 3。和第一遍的左距 2 比,取小的:答案 = 2。左边本来就更近,保持不变。
走到下标 7,右边最近的 e 在 11,右距 4。和第一遍的左距 1 比,取小的:答案 = 1。左边本来就更近,保持不变。
从右往左走到下标 6,又是 e,把 suf 记成 6,它本身答案就是 0。
从右往左走到下标 5,又是 e,把 suf 记成 5,它本身答案就是 0。
走到下标 4,右边最近的 e 在 5,右距 1。和第一遍的左距 1 比,取小的:答案 = 1。左右一样近,取最小还是 1,答案保持不变。
从右往左走到下标 3,又是 e,把 suf 记成 3,它本身答案就是 0。
走到下标 2,右边最近的 e 在 3,右距 1。和第一遍的左距 ∞ 比,取小的:答案 = 1。右边更近,更新成它。
走到下标 1,右边最近的 e 在 3,右距 2。和第一遍的左距 ∞ 比,取小的:答案 = 2。右边更近,更新成它。
走到下标 0,右边最近的 e 在 3,右距 3。和第一遍的左距 ∞ 比,取小的:答案 = 3。右边更近,更新成它。
两遍合起来,每个位置都拿到了「左边最近」和「右边最近」里更小的那个,这就是到最近 e 的真实距离。最终答案 [3, 2, 1, 0, 1, 0, 0, 1, 2, 2, 1, 0],和题目给的输出完全一致。
c 在最左或最右是典型边界:一遍全 ∞,必须靠另一遍补齐,这就是为什么要两遍。
两个高频追问:为什么两遍最优、c 不出现怎么办。
参考代码
from __future__ import annotationsfrom typing import *from collections import *from functools import *from itertools import *from math import *from heapq import *from bisect import *class TreeNode: def __init__(self, val=0, left=None, right=None): self.val = val self.left = left self.right = rightclass ListNode: def __init__(self, val=0, next=None): self.val = val self.next = nextclass Solution: def shortestToChar(self, s: str, c: str) -> List[int]: n = len(s) ans = [n] * n pre = -inf for i, ch in enumerate(s): if ch == c: pre = i ans[i] = min(ans[i], i - pre) suf = inf for i in range(n - 1, -1, -1): if s[i] == c: suf = i ans[i] = min(ans[i], suf - i) return ans复杂度
- 时间:O(n),从左扫一遍 + 从右扫一遍,共两遍线性
- 空间:O(1),除答案数组外,只用 pre、suf 两个变量(答案数组是必须的输出)
易错点
面试追问把动画讲成自己的话
追问能不能只用一遍扫描解决?
追问如果 c 在串里一次都不出现会怎样?
这道题到这就讲完了。动画和文字是同一套思路——别光看,关掉页面自己默写一遍。然后顺着主线继续:
翻转图像
LeetCode 832 · 简单 · 沿着 双指针套路 继续往下推进
把这道题真正学会,再走
图解算法年卡 ¥99 /年
- ✓本题每步动画的吴师兄语音讲解(全站陆续覆盖)
- ✓小欧带 8 步通关训练——追问到不看答案也能写对、能 30 秒讲给面试官听
- ✓学习报告,记录每道题的掌握程度
76k+ GitHub Star · 吴师兄开源图解算法,几十万开发者在看的算法讲解
想成体系刷透这类套路?去图解算法专题