无重复字符的最长公共子串的长度

Length of the Longest Common Substring without repeating characters

给定"abcabcbb",答案是"abc",长度是3

给定"bbbbb",答案是"b",长度为1。

给定"pwwkew",答案是"wke",长度为3。注意答案必须是子串,"pwke"是子序列,不是子字符串。

我想出了一个可行的解决方案,但在几个测试用例中都失败了。然后我找到了一个更好的解决方案并重写了它以尝试理解它。下面的解决方案完美无缺,但是在与这个东西斗争了大约 2 个小时之后,我仍然不明白为什么这行代码有效。

import java.util.*;
import java.math.*;

public class Solution {

  public int lengthOfLongestSubstring(String str) {

  if(str.length() == 0)
    return 0;

  HashMap<Character,Integer> map = new HashMap<>();
  int startingIndexOfLongestSubstring = 0;
  int max = 0;

  for(int i = 0; i < str.length(); i++){
      char currentChar = str.charAt(i); 
      if(map.containsKey(currentChar))
         startingIndexOfLongestSubstring = Math.max(startingIndexOfLongestSubstring, map.get(currentChar) + 1);

      map.put(currentChar, i);
      max = Math.max(max, i - startingIndexOfLongestSubstring + 1);

      }//End of loop

    return max;

   }
}

有问题的行是

max = Math.max(max, i - startingIndexOfLongestSubstring + 1);

我不明白为什么会这样。我们取之前最大值之间的最大值,以及当前索引与当前最长子字符串的起始索引之间的差异,然后加 1。我知道代码正在获取当前索引与startingIndexOfSubstring,但我无法概念化为什么它可以为我们提供预期的结果;有人可以向我解释这一步吗,特别是为什么它有效?

因为

i - startingIndexOfLongestSubstring + 1

istartingIndexOfLongestSubstring 索引之间的字符数。例如位置 2 和 3 之间有多少个字符? 3-2=1 但我们有 2 个字符:位置 2 和位置 3。

我已经在代码中描述了每个动作:

public class Solution {

    public int lengthOfLongestSubstring(String str) {

        if(str.length() == 0)
            return 0;

        HashMap<Character,Integer> map = new HashMap<>();
        int startingIndexOfLongestSubstring = 0;
        int max = 0;

        // loop over all characters in the string
        for(int i = 0; i < str.length(); i++){
            // get character at position i
            char currentChar = str.charAt(i);
            // if we already met this character
            if(map.containsKey(currentChar))
                // then get maximum of previous 'startingIndexOfLongestSubstring' and 
                // map.get(currentChar) + 1 (it is last occurrence of the current character in our word before plus 1)
                // "plus 1" - it is because we should start count from the next character because our current character 
                // is the same
                startingIndexOfLongestSubstring = Math.max(startingIndexOfLongestSubstring, map.get(currentChar) + 1);

            // save position of the current character in the map. If map already has some value for current character 
            // then it will override (we don't want to know previous positions of the character)
            map.put(currentChar, i);
            // get maximum between 'max' (candidate for return value) and such value for current character
            max = Math.max(max, i - startingIndexOfLongestSubstring + 1);

        }//End of loop

        return max;

    }
}

我平时不太会解释,举个例子试试吧。

字符串是 "wcabcdeghi".

暂时忘掉代码,假设我们正在努力想出一个逻辑。

  1. 我们从 w 开始,一直走到 c -> a -> b -> c。
    我们需要在这一点上停下来,因为 "c" 正在重复。所以我们需要一个映射来存储一个字符是否重复。 (在代码中: map.put(currentChar, i);
  2. 既然我们知道了一个字符是否重复,我们需要知道最大值是多少。到目前为止的长度。 (在代码中-max
  3. 现在我们知道跟踪前 2 个变量 w->c 的计数没有意义。这是因为包括这个在内,我们已经拿到了Max。价值。所以从下一次迭代开始,我们只需要检查 a -> b -> soon 的长度。
    让我们有一个变量(在代码中 -startingIndexOfLongestSubstring 来跟踪这个。 (这应该被命名为 startingIndexOfNonRepetativeCharacter,然后我又不喜欢命名)。
  4. 现在我们再次继续,但是我们还没有最终确定如何跟踪我们当前正在解析的子字符串。 (即来自 abcd...)
    想到这一点,我只需要 "a" 所在的位置(即 startingIndexOfNonRepetativeCharacter),所以要知道当前子字符串的长度,我需要做的就是 (在代码中-)i - startingIndexOfLongestSubstring + 1(当前字符位置-非重复字符长度+(减法不包含两边所以加1)。我们称之为currentLength
  5. 等等,我们要用这个计数做什么。每次我们找到一个新变量时,我们都需要检查这个 currentLength 是否可以打破我们的最大值。 所以(代码中-max = Math.max(max, i - startingIndexOfLongestSubstring + 1);
  6. 现在我们已经涵盖了我们需要的大部分语句,并且根据我们的逻辑,每当我们遇到一个已经存在的变量时,我们所需要的只是 startingIndexOfLongestSubstring = map.get(currentChar)。那我们为什么要做 Max?
    考虑 String 为 "wcabcdewghi" 的场景。当我们开始将新计数器处理为 a -> b -> c -> d -> e -> w 此时,我们的逻辑会检查该字符之前是否存在。从现在开始,它从索引“1”开始计数。这完全搞乱了整个计数。所以我们需要确保,我们从 map 中获取的下一个索引总是大于我们计数的起点(即,仅当字符出现在 startingIndexOfLongestSubstring 之前时,select 来自 map 的字符。

希望我已经回答了代码中的所有行,主要是如果解释是可以理解的。