攻克LeetCode 1055:探寻形成字符串的最短路径

一、题目引入

在 LeetCode 的题库中,1055. 形成字符串的最短路径这道题饶有趣味且充满挑战。简单来说,对于给定的源字符串 source 和目标字符串 target,我们要找出源字符串中能通过串联形成目标字符串的子序列的最小数量。如果无法通过串联源字符串中的子序列来构造目标字符串,那就得返回 -1。
就拿一个简单的例子来说,若 source = “abc”,target = “abcbc”,目标字符串 “abcbc” 可以由源字符串 “abc” 的两个子序列 “abc” 和 “bc” 串联而成,所以答案是 2。这道题的关键就在于巧妙地利用源字符串的子序列,以最少的数量拼凑出目标字符串。它考察的不仅仅是对字符串操作的熟悉程度,更是对如何高效利用已有资源(源字符串)来达成目标(构建目标字符串)的思维能力。接下来,咱们就深入探讨一下解题的思路与方法。
二、解题思路剖析

(一)核心思路
解决这道题的核心思路,是通过循环遍历源字符串 source,尽可能多地匹配目标字符串 target 中的字符。每一轮匹配,都要从源字符串的开头去尝试匹配目标字符串中尚未匹配的部分,在这一轮中,只要源字符串中的字符能与目标字符串当前位置的字符对上,就赶紧匹配上,让目标字符串的匹配指针往后挪一挪,如此循环往复,直到目标字符串全部匹配完,记录下这一过程中使用源字符串子序列的数量,就是答案。但要是某一轮下来,目标字符串的匹配指针纹丝未动,这时候就得警惕了,赶紧瞅瞅当前目标字符串指向的字符,在源字符串里到底有没有,如果没有,那很遗憾,没办法通过源字符串构建目标字符串,直接返回 -1 。
(二)算法流程示例
咱们以 source = “abc”,target = “abcbc” 为例,详细看看算法流程。
初始化阶段:设置一个计数变量 count,初始化为 0,它用来统计使用源字符串子序列的个数;再设一个索引变量 index,初始化为 0,代表目标字符串中当前匹配到的位置。
第一轮匹配:从源字符串 “abc” 的开头开始,依次与目标字符串 “abcbc” 中从位置 0 开始的字符进行比较。当源字符串的 “a” 与目标字符串的 “a” 匹配上了,index 就往后移一位,变成 1;接着源字符串的 “b” 又和目标字符串的 “b” 匹配,index 变为 2;再然后源字符串的 “c” 与目标字符串的 “c” 匹配,index 更新为 3。这一轮结束,成功匹配了 “abc”,count 加 1,变为 1。
第二轮匹配:继续从源字符串 “abc” 的开头,匹配目标字符串 “abcbc” 中位置 3 及之后的字符。源字符串的 “b” 与目标字符串位置 3 的 “b” 匹配,index 变为 4;源字符串的 “c” 与目标字符串位置 4 的 “c” 匹配,index 变为 5,至此目标字符串全部匹配完。这一轮结束,又成功匹配了 “bc”,count 再加 1,变为 2。
最终结果:由于目标字符串已经全部匹配,此时 count 的值 2 就是我们要的答案,即源字符串 “abc” 中能通过串联形成目标字符串 “abcbc” 的子序列的最小数量为 2。
三、代码实现详解

(一)多种语言示例呈现
下面为大家展示几种常见编程语言的解题代码。
首先是 Java 代码:
public class LeetCode1055 {
public int shortestWay(String source, String target) {
int count = 0;
int index = 0;
Set sourceSet = new HashSet<>();
for (char c : source.toCharArray()) {
sourceSet.add(c);
}
while (index < target.length()) {
int oldIndex = index;
for (int i = 0; i < source.length(); i++) {
if (index < target.length() && source.charAt(i) == target.charAt(index)) {
index++;
}
}
if (oldIndex == index) {
if (!sourceSet.contains(target.charAt(index))) {
return -1;
}
}
count++;
}
return count;
}
}

再看看 Python 代码:
def shortestWay(source, target):
count = 0
index = 0
source_set = set(source)
for char in target:
if char not in source_set:
return -1
while index < len(target):
old_index = index
for i in range(len(source)):
if index < len(target) and source[i] == target[index]:
index += 1
if old_index == index:
index += 1
count += 1
else:
count += 1
return count

还有 C++ 代码:

include

include

include <unordered_set>

using namespace std;
int shortestWay(string source, string target) {
int count = 0;
int index = 0;
unordered_set sourceSet;
for (char c : source) {
sourceSet.insert(c);
}
while (index < target.length()) {
int oldIndex = index;
for (int i = 0; i < source.length(); i++) {
if (index < target.length() && source[i] == target[index]) {
index++;
}
}
if (oldIndex == index) {
if (sourceSet.find(target[index]) == sourceSet.end()) {
return -1;
}
}
count++;
}
return count;
}

(二)代码逐行解读
这里咱们以 Java 代码为例,逐行解读一下。
public int shortestWay(String source, String target) {:这是定义了一个名为shortestWay的公共方法,它接收两个字符串参数source和target,并返回一个整数,这个整数就是我们要求的源字符串子序列的最小数量。
int count = 0;:初始化一个变量count,用来记录使用源字符串子序列的个数,一开始当然是 0 啦,因为还没开始匹配呢。
int index = 0;:定义index变量,代表目标字符串中当前匹配到的位置,初始化为 0,也就是从目标字符串的开头开始匹配。
Set sourceSet = new HashSet<>();:创建一个HashSet集合,用来存放源字符串中的字符,后续可以快速判断某个字符是否在源字符串中出现过。
for (char c : source.toCharArray()) { sourceSet.add(c); }:这是一个增强 for 循环,将源字符串source转换成字符数组,然后把每个字符都添加到sourceSet集合中,这样就把源字符串的字符都 “收集” 起来了。
while (index < target.length()) {:只要index小于目标字符串的长度,就说明目标字符串还没匹配完,继续循环匹配。
int oldIndex = index;:在每一轮匹配源字符串之前,先记录当前的index值,后续要用它来判断这一轮有没有匹配到新的字符。
for (int i = 0; i < source.length(); i++) { if (index < target.length() && source.charAt(i) == target.charAt(index)) { index++; } }:这个内层循环是核心匹配过程,从源字符串的开头,依次比较每个字符与目标字符串当前位置(由index指示)的字符,如果对上了,就把index往后移一位,意味着成功匹配了一个字符。
if (oldIndex == index) { if (!sourceSet.contains(target.charAt(index))) { return -1; } }:当一轮匹配完,如果发现oldIndex和index相等,说明这一轮没匹配到新字符,这时候得看看目标字符串当前指向的字符(target.charAt(index))在不在源字符串集合sourceSet里,如果不在,那没办法通过源字符串构建目标字符串,直接返回 -1。
count++;:如果这一轮成功匹配了新字符,或者虽然没匹配新字符但目标字符串当前字符在源字符串里,就把count加 1,表示使用了一个新的源字符串子序列。
return count;:当目标字符串全部匹配完,循环结束,此时count的值就是我们要的答案,即源字符串中能通过串联形成目标字符串的子序列的最小数量,直接返回它。
四、复杂度分析

(一)时间复杂度
从代码中可以清晰地看到,存在两层循环嵌套。外层循环是依据目标字符串 target 的长度来进行的,只要目标字符串还没遍历完,就会持续循环,其执行次数与目标字符串的长度 n 紧密相关。而内层循环呢,则是针对源字符串 source 的每一个字符,去尝试匹配目标字符串中的字符,它的执行次数取决于源字符串的长度 m。每一次外层循环的迭代,都伴随着内层循环完整地遍历一次源字符串。所以综合来看,这种循环嵌套的结构使得时间复杂度达到了 O (m * n),也就是说,随着源字符串和目标字符串长度的增加,算法的运行时间会以它们长度乘积的量级增长。
(二)空间复杂度
在代码里,我们引入了一个 HashSet(或其他语言类似的集合结构,如 Python 的 set、C++ 的 unordered_set),它的作用是存储源字符串中的字符,方便快速判断某个字符是否在源字符串中出现过。在最糟糕的情况下,源字符串的所有字符都各不相同,那么 HashSet 需要存储源字符串中的每一个字符,其空间占用量最大会达到源字符串的长度 m。除此之外,代码中其他变量所占用的空间相对固定,不会随着输入字符串长度的变化而大幅变化。所以综合起来,空间复杂度就是 O (m)。这里可以思考一下,如果想要优化空间复杂度,是否可以在不影响功能的前提下,对 HashSet 的使用进行调整,或者寻找其他更节省空间的数据结构来替代,这也是算法优化的一个有趣方向。
五、实战演练与技巧总结

(一)更多示例题目练习
咱们来试试这道题:若 source = “xy”,target = “xxyxy”,按照咱们之前讲的思路,动手试试吧。
答案揭晓:首先,初始化 count 为 0,index 为 0。第一轮,从 source = “xy” 的开头匹配 target = “xxyxy”,source 的 “x” 与 target 的第一个 “x” 匹配,index 变为 1;接着 source 的 “y” 与 target 的第二个 “x” 不匹配,继续往后,source 的 “y” 与 target 的第三个 “y” 匹配,index 变为 2。这一轮结束,count 加 1 变为 1。第二轮,继续从 source 开头匹配 target 中位置 2 及之后的字符,source 的 “x” 与 target 的第四个 “x” 匹配,index 变为 3;source 的 “y” 与 target 的第五个 “y” 匹配,index 变为 4,此时 target 全部匹配完,count 再加 1 变为 2。所以答案是 2。通过这个例子,大家是不是对解题思路更清晰了呢?要是把题目稍微改改,source = “xyx”,target = “xxyxy”,大家再思考思考,看看能不能举一反三。
(二)解题技巧提炼
特殊情况优先判断:在一开始,咱们就得瞅瞅源字符串 source 或者目标字符串 target 是不是为空。要是 source 为空,那肯定没办法构建目标字符串,直接返回 -1;要是 target 为空,这时候返回 0 就行,因为不需要任何源字符串的子序列。这一步看似简单,却能帮咱们快速排除一些特殊情况,让后续的解题过程更顺畅。
巧用数据结构:像代码里用到的 HashSet(或者其他语言对应的集合结构),它能快速判断一个字符在源字符串中存不存在。在匹配过程中,要是发现某一轮没匹配到新字符,就能迅速用它来检查目标字符串当前指向的字符是不是源字符串里没有的,避免做无用功,大大提高解题效率。
双指针的灵活运用:在代码实现里,用一个指针指向源字符串,一个指针指向目标字符串,通过巧妙移动这两个指针,来实现字符的匹配。这种双指针的技巧,在很多字符串处理问题里都大有用武之地,大家要好好掌握,以后遇到类似题目,就能迅速反应过来啦。希望这些技巧能助力大家在算法学习的道路上披荆斩棘,攻克更多难题。

posted @   东百牧码人  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
历史上的今天:
2022-12-30 Wix自定义操作如何以管理员身份运行
2015-12-30 sql语句优化之not in
点击右上角即可分享
微信分享提示