题目描述

✅ 面试题 17.11. 单词距离

image-20260929010521705

题意分析

在单词数组中,求两个不同目标词 word1、word2 出现位置的最小下标距离。两个词都在数组中出现过,相邻位置的距离为 1,不需要再减 1。

单次查询只需扫描数组;题面还要求考虑同一文本上的多次不同查询,这时可以预处理各词位置,避免每次重新扫描全文。

解法:维护两个最近位置

核心思路

[!blue]

用 last1、last2 分别记录扫描前缀中两个目标词最近一次出现的下标,初始为 -1,表示尚未遇到。遇到某个目标词时更新对应下标,两者都有效后,用它们的绝对差更新最小距离。

对当前新出现的位置 i,另一个词的历史下标都在它左边,其中最大的下标离 i 最近。更早位置只会产生更大距离,因此只保留另一个词最近的位置就足够。

任意合法词对都会在较晚的那个位置被读到时接受比较:如果较早位置已经被同类词的新位置替代,替代后的距离只会更小。所以丢弃旧位置不会漏掉最优答案。处理完每个下标后,answer 始终是已扫描前缀中的最短距离。

解题步骤

  • 两个最近下标初始化为 -1,答案初始化为数组长度,这是合法下标距离的上界。
  • 从左到右扫描,命中 word1 就更新 last1,否则若命中 word2 则更新 last2。
  • 两个下标都有效时,用 abs(last1 - last2) 更新答案。
  • 扫描完成后返回答案。题目保证两个词都出现,所以最终一定存在有效距离。

代码实现

class Solution {
    public int findClosest(String[] words, String word1, String word2) {
        int last1 = -1;
        int last2 = -1;
        int answer = words.length;

        for (int i = 0; i < words.length; i++) {
            if (words[i].equals(word1)) {
                last1 = i;
            } else if (words[i].equals(word2)) {
                last2 = i;
            }

            if (last1 != -1 && last2 != -1) {
                answer = Math.min(answer, Math.abs(last1 - last2));
            }
        }

        return answer;
    }
}
func findClosest(words []string, word1 string, word2 string) int {
    last1, last2 := -1, -1
    answer := len(words)

    for i, word := range words {
        if word == word1 {
            last1 = i
        } else if word == word2 {
            last2 = i
        }

        if last1 != -1 && last2 != -1 {
            distance := last1 - last2
            if distance < 0 {
                distance = -distance
            }
            answer = min(answer, distance)
        }
    }

    return answer
}

复杂度分析

  • 时间复杂度:按单词比较为常数计算时是 $O(n)$;计入字符串比较,若数组总字符数为 S,上界为 $O(S+n)$。
  • 空间复杂度:$O(1)$。只保存两个最近下标和答案。

关键点总结

[!green]

  • 对固定的新位置,另一个词的最近历史位置就是最优选择。
  • 每个最优词对都会在较晚位置出现时被考虑,或被距离更小的词对替代。
  • -1 只表示尚未出现,必须等两边都有效后再计算距离。

进阶:预处理位置表 + 双指针

核心思路

[!blue]

同一个文本要查询多次时,构造 WordDistance 对象,将每个单词映射到它的全部出现下标。按原数组顺序追加,这些列表天然递增,不需要另行排序。索引只构建一次,之后在同一对象上反复调用 shortest / Shortest。

一次查询只取两个目标词的位置表 a、b,用 i、j 指向当前下标。先比较 abs(a[i] - b[j]),再前进数值较小的一侧。若 a[i] < b[j],b 的后续下标只会更大,与固定 a[i] 的距离不会缩小,所以可以安全丢弃 a[i];另一侧同理。

每次淘汰的下标与对面后续位置都不会产生更优答案,更早的对面位置也已经按同样规则处理过,因此不会漏掉最近的一对。任一列表耗尽就结束;查询只移动局部游标,不修改位置表,可以继续用于下一次查询。

解题步骤

  • 构造时顺序扫描一次文本,把每个下标追加到所属单词的位置列表。
  • 查询时读取两个有序列表,初始化两个游标和足够大的最小距离。
  • 比较当前位置的绝对差,更新答案,再推进下标较小的一侧。
  • 一侧用尽后返回最小值。沿用原题前提:查询词不同,且都存在于文本中。

代码实现

class WordDistance {
    private final Map<String, List<Integer>> pos = new HashMap<>();

    public WordDistance(String[] words) {
        for (int i = 0; i < words.length; i++) {
            pos.computeIfAbsent(words[i], key -> new ArrayList<>()).add(i);
        }
    }

    public int shortest(String word1, String word2) {
        List<Integer> a = pos.get(word1);
        List<Integer> b = pos.get(word2);
        int i = 0;
        int j = 0;
        int best = Integer.MAX_VALUE;

        while (i < a.size() && j < b.size()) {
            best = Math.min(best, Math.abs(a.get(i) - b.get(j)));

            if (a.get(i) < b.get(j)) {
                i++;
            } else {
                j++;
            }
        }

        return best;
    }
}
type WordDistance struct {
    pos map[string][]int
}

func Constructor(words []string) WordDistance {
    pos := make(map[string][]int)
    for i, word := range words {
        pos[word] = append(pos[word], i)
    }
    return WordDistance{pos: pos}
}

func (w *WordDistance) Shortest(word1 string, word2 string) int {
    a := w.pos[word1]
    b := w.pos[word2]
    i, j := 0, 0
    best := 1 << 30

    for i < len(a) && j < len(b) {
        distance := a[i] - b[j]
        if distance < 0 {
            distance = -distance
        }
        if distance < best {
            best = distance
        }

        if a[i] < b[j] {
            i++
        } else {
            j++
        }
    }

    return best
}

复杂度分析

  • 时间复杂度:构造期望 $O(S+n)$,S 为文本总字符数,包含哈希成本。单次双指针查询为 $O(p+q)$,p、q 是两个词的出现次数,另有查询词本身的哈希成本。
  • 空间复杂度:索引占 $O(n)$,所有位置表一共保存 n 个下标;单次查询只需要 $O(1)$ 辅助空间。

关键点总结

[!green]

  • 重复查询复用同一份位置表,不应在每次查询时重新构造对象。
  • 必须保存全部位置,最短距离不一定发生在两个词最后出现的地方。
  • 较小下标与对面后续下标只会更远,所以每轮淘汰较小一侧。

易错点总结

[!yellow]

  • Java 字符串应使用 equals 比较内容,不能用 == 比较对象是否相同。
  • 单次扫描的未出现标记不能参与距离计算,也不能只记录每个词首次出现的位置。
  • 位置差需要取绝对值,不能让负数错误成为最小距离。
  • 多次查询时只保存每个词最后位置,会丢失文本前面更近的配对。
  • 两条位置表不能无条件同时推进,否则可能跳过更近的组合。
  • 查询游标应是本次调用的局部状态,不能消耗或修改后续查询还要复用的位置表。

相似题目

题目 难度 关联与区别
821. 字符的最短距离 简单 同样利用离当前位置最近的目标位置,本题求两个单词出现位置的最小距离。
244. 最短单词距离 II 中等 当同一文本有多次查询时,预处理每个词的位置列表并双指针求最近距离。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/55833553
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!