438. 找到字符串中所有字母异位词

438. 找到字符串中所有字母异位词 - 力扣(LeetCode)

class Solution {
public:
  vector<int> findAnagrams(string s, string p) {
    vector<int> Result;
    unordered_map<char, int> PMap;
    unordered_map<char, int> SMap;

    int L = 0, R = p.length();
    // Initial fill-ins.
    for (char C : p)
      ++PMap[C];
    for (int i = 0; i < R; ++i)
      ++SMap[s[i]];

    while (R <= s.size()) {
      if (PMap == SMap)
        Result.push_back(L);
      if (--SMap[s[L]] == 0)
        SMap.erase(s[L]);
      ++SMap[s[R]];

      ++L;
      ++R;
    }
    return Result;
  }
};

其实,对于牵涉到「存在」或「计数」的字符串题目,完全不需要用 map 或者 set,因为……char 类型的范围一共才只有 \([-128, 127]\)。直接利用 ASCII 字符的范围,往桶里填东西便是。

#define I_(x) ((x) - 'a')

class Solution {
public:
  vector<int> findAnagrams(string s, string p) {
    if (s.length() < p.length())
      return {};

    vector<int> Result;
    vector<int> PMap(26);
    vector<int> SMap(26);

    int L = 0, R = p.length();
    // Initial fill-ins.
    for (int i = 0; i < R; ++i) {
      ++PMap[I_(p[i])];
      ++SMap[I_(s[i])];
    }
    if (PMap == SMap)
      Result.push_back(0);

    while (R < s.size()) {
      --SMap[I_(s[L++])];
      ++SMap[I_(s[R++])];

      if (PMap == SMap)
        Result.push_back(L);
    }
    return Result;
  }
};

但是要注意,你可以发现这个版本相比用 map 的版本多了很多 guards,这是因为用桶的健壮度是不如用 map 的,在做题的时候也要注意。其实如果常数卡得不是很严,优先用 map 完全是可以的。

二刷

class Solution {
public:
  vector<int> findAnagrams(string s, string p) {
    if (s.size() < p.size())
      return {};

    vector<int> Result;
    vector<int> SChars(26);
    vector<int> PChars(26);
    for (int i = 0; i < p.size(); ++i) {
      ++PChars[p[i] - 'a'];
      ++SChars[s[i] - 'a'];
    }

    if (SChars == PChars)
      Result.push_back(0);
    for (int i = 0; i < s.size() - p.size(); ++i) {
      --SChars[s[i] - 'a'];
      ++SChars[s[i + p.size()] - 'a'];
      // Putting this statement at beginning end would cause we miss the last
      // window. We can only put it here and add a special case before for-loop.
      if (SChars == PChars)
        Result.push_back(i + 1);
    }
    return Result;
  }
};

基本上也还是官解计数的那套思路,但是这里把各个地方的处理解释得更清楚了——比如为什么我们要手动处理 0 处的窗口,以及为什么比较和 push_back() 要放在循环的最后。其实看这个结构的话应该能感觉到,本质上其实是因为没有 do-for 这种控制流。

至于把数组比较变为差值的方法,关键在于注意我们给 \(s\) 和 \(p\) 二者设置的权重是正还是负。这里,\(s\) 的权重是 \(+1\),而 \(p\) 的权重是 \(-1\),这也影响了后面执行滑动窗口时我们对 Diff 变量处理前进行的判断。

class Solution {
public:
  vector<int> findAnagrams(string s, string p) {
    if (s.size() < p.size())
      return {};

    vector<int> Result;
    vector<int> DiffSet(26);
    // Number of different characters between s and p.
    int Diff = 0;
    for (int i = 0; i < p.size(); ++i) {
      // Positive means that patterns have the character while string not;
      // negative vice versa. Keep in mind that the increment/decrement
      // operators are bound to s/p respectively. We'll be using this convention
      // until the program's end.
      ++DiffSet[s[i] - 'a'];
      --DiffSet[p[i] - 'a'];
    }
    // Get the difference.
    for (int D : DiffSet) {
      if (D != 0)
        ++Diff;
    }

    if (!Diff)
      Result.push_back(0);
    for (int i = 0; i < s.size() - p.size(); ++i) {
      // The letter being REMOVED differed from the pattern.
      if (DiffSet[s[i] - 'a'] == 1)
        --Diff;
      // The letter being REMOVED was in the pattern.
      if (DiffSet[s[i] - 'a'] == 0)
        ++Diff;
      // Remove it.
      --DiffSet[s[i] - 'a'];

      // The letter being ADDED will be in the pattern.
      if (DiffSet[s[i + p.size()] - 'a'] == -1)
        --Diff;
      // The letter being ADDED will differ from the pattern.
      if (DiffSet[s[i + p.size()] - 'a'] == 0)
        ++Diff;
      // Add it.
      ++DiffSet[s[i + p.size()] - 'a'];

      if (!Diff)
        Result.push_back(i + 1);
    }
    return Result;
  }
};

这个滑动窗口加加减减、时态过去将来的确实比较容易搞混,一般不轻易写这种,面试官问你了你再写吧。

三刷

class Solution {
public:
  vector<int> findAnagrams(string s, string p) {
    if (p.length() > s.length())
      return {};

    vector<int> Result;

    array<int, 26> pSet;
    pSet.fill(0);
    for (char X : p)
      ++pSet[X - 97];

    array<int, 26> sSet;
    sSet.fill(0);
    for (int i = 0; i < p.length(); ++i)
      ++sSet[s[i] - 97];

    // This one differs from the last one as its window has a fixed length.
    // It actually makes things easier.
    for (int i = 0; i < s.length() - p.length(); ++i) {
      if (sSet == pSet)
        Result.emplace_back(i);

      ++sSet[s[i + p.length()] - 97];
      --sSet[s[i] - 97];
    }

    if (sSet == pSet)
      Result.emplace_back(s.length() - p.length());
    return Result;
  }
};

这个题跟3. 无重复字符的最长子串相比简单的地方就在于这个窗口是固定尺寸的,所以不需要左端点指针的参与——但是二刷里那个Diff法确实有点意思,虽然我感觉我也不一定会写。

还有就是std::array是默认构造的,不需要手动fill()。