給定一個母串 M 和一個子串 S。要求找出 S 的任意排列(即所有字元的全排列) 在 M 中首次出現的起始索引。若不存在,返回 -1。
範例:
M = "qwertyuihgfd"
S = "tyui"
輸出:4 // "tyui" 的排列 "yuit" 等在索引 4 開始處出現
由於 S 的排列數量隨長度呈階乘增長(n!),窮舉所有排列去匹配 M 完全不可行。關鍵觀察:
S 的任意排列在 M 中出現 ⟺ M 中存在一個長度為 |S| 的子串,其字元頻率統計與 S 完全一致。
因此,問題轉化為:在 M 中尋找長度固定為 |S| 的滑動窗口,使窗口內字元頻率表等於 S 的頻率表。
S,統計每個字元出現次數 → target[256](或 unordered_map)。M 前 |S| 個字元,建立窗口頻率表 window[256]。window == target → 返回當前起始索引。window[M[i]]--,window[M[i+|S|]]++,繼續比對。引入 matchCount 變數,記錄「當前窗口中與目標頻率一致的字元種類數」。
matchCount。matchCount。matchCount == 目標字元種類數 時,即為匹配成功。比對降為 O(1)。int findPermutation(const string& M, const string& S) {
if (S.empty()) return 0;
if (M.size() < S.size()) return -1;
vector target(256, 0), window(256, 0);
for (char c : S) target[(unsigned char)c]++;
int required = 0;
for (int cnt : target) if (cnt > 0) required++;
int matchCount = 0;
// 初始窗口 [0, S.size())
for (size_t i = 0; i < S.size(); ++i) {
unsigned char c = M[i];
if (++window[c] == target[c]) matchCount++;
else if (window[c] == target[c] + 1) matchCount--; // 超過目標
}
if (matchCount == required) return 0;
// 滑動
for (size_t i = S.size(); i < M.size(); ++i) {
unsigned char out = M[i - S.size()];
unsigned char in = M[i];
if (window[out] == target[out]) matchCount--;
else if (window[out] == target[out] + 1) matchCount++; // 從超標變回標準
window[out]--;
if (++window[in] == target[in]) matchCount++;
else if (window[in] == target[in] + 1) matchCount--;
if (matchCount == required) return i - S.size() + 1;
}
return -1;
}
原文採用 List<Char> 逐字元移除的方式,邏輯正確但效率較低:
ToCharArray().ToList() 復制子串字元 → O(|S|) 額外開銷。Contains + Remove 為 O(|S|) 操作,總體複雜度達 O(N × |S|²)。本文提供的滑動窗口 + 頻率表方案,將複雜度降至 O(N + |S|),且空間固定,適合大規模文本處理。
unordered_map<char32_t, int> 或使用 ICU 庫。「子串任意排列在母串中出現」本質是定長滑動窗口下的字元頻率完全匹配。拋棄排列枚舉思維,轉而使用頻率表 + 滑動窗口 + 增量比對,即可獲得線性時間解法,是面試與實戰中高頻考點的標準解法。