FindAllAnagramsInAStringOPT [source code]


public class FindAllAnagramsInAStringOPT {
static
/******************************************************************************/
public class Solution {
    public List<Integer> findAnagrams(String s, String p) { //使用hash table+滑动窗口       
        List<Integer> list = new ArrayList<>();
        char[] chp = p.toCharArray(); 
        char[] chs = s.toCharArray();
        int count = chp.length;
        int left = 0;
        int right = 0;
        int hash[] = new int[256];
        for (char c : chp) {
            hash[c]++;
        }
        while (right < chs.length) {
            char c = chs[right++];
            if (hash[c]-- >= 1) count--;
            while (right - left > chp.length) {
                char prev = chs[left++];
                if (hash[prev]++ >= 0) count++;
            }
            if (count == 0) {
                list.add(left);
            }
        }
        return list;
    }
}
/******************************************************************************/

    public static void main(String[] args) {
        FindAllAnagramsInAStringOPT.Solution tester = new FindAllAnagramsInAStringOPT.Solution();
        String[] inputs = {
            "cbaebabacd", "abc",
            "abab", "ab",
            "abdkaabacdkajfl", "babc",
            "abdkaabbacdkajfl", "babc",
        };
        for (int i = 0; i < inputs.length / 2; i++) {
            String s = inputs[2 * i];
            String p = inputs[1 + 2 * i];
            System.out.println(s + " VS " + p + " -> " + tester.findAnagrams(s, p));
        }
    }
}

这个是 submission 最优解, 10(99.96);

这个算法的核心思路是sliding window, G4G上面学了一下, 这个算法其实就是我一开始想要做到的avoid backup的算法, 虽然没有 KMP 那么复杂, 但是一定程度上做到了 reuse 历史信息;

这个算法刚开始没有看懂, 是因为这个人的逻辑非常聪明, 把sliding window跟这个问题的逻辑融合的非常紧密, 导致整个代码的逻辑做到了最简化, 看起来有点累人.

看这个核心循环的逻辑:

while (right < chs.length) {  
    char c = chs[right++];  
    if (hash[c]-- >= 1) count--;  
    while (right - left > chp.length) {  
        char prev = chs[left++];  
        if (hash[prev]++ >= 0) count++;  
    }  
    if (count == 0) {  
        list.add(left);  
    }  
}

首先这里他这个逻辑的主要骨干就是, 用一个 counts/hash 的 array 来记录p 里面每个字母的frequency, 然后走 chs, 每当(right) 走到一个字母, 无论这个字母是否在 chp 当中(是 hit 还是 miss), 最后在 hash 里面, 我们都当做是一个hit, 或者当做是一个 downvote;
sliding window算法逻辑上需要处理的一个核心问题就是当 window 移动的时候, 如何更新类似sum 的各种内容. 在算 sum 的题目里面, 这个比较简单, 直接-left + right就行了. 但是这个题目本身没有这么简单.
这里的做法是, 只保留 window 内部的 downvote, 也就是说如果我们 slide window 了, 那么你给 left 的 downvote 要撤销(对应的, 就是还给他一个 upvote), 同时因为 right 即将进入 window, 所以要给他一个 downvote;
如何判断找到了一个 anagram? 他这里的这个 count, 保存的其实是, chp 里面的所有字母里面, 可以继续被 downvote 的余额. 比如 p 是"abc", 那么一开始就是 abc 一人给一个1, 那么 chp 里面所有的字母总共可以被 downvote 3次. babc 的话, 一开始就是4次. 这个数量的初始值刚好等于chp 的长度;

if (hash[c]-- >= 1) count--; 的作用就是 downvote. c 保存的是 right 的 character, 即将进入 window, 所以给一个 downvote;

if (count == 0) {  
    list.add(left);  
}

如果没有余额了, 意思就是当前 window 里面的字母constituency and frequency都跟 chp 一模一样(不然不可能被 downvote 完), 所以可以直接 res.add 了;

while (right - left > chp.length) {  
    char prev = chs[left++];  
    if (hash[prev]++ >= 0) count++;  
}

这个循环完成的就是一个 slide 的过程. 这个 header 判断的虽然是大于, 其实最多只可能超1;
body 里面第一行很简单, 就是移动 left.
hash++这个就是类似一个退款的操作, prev 反正要出去了, 所以要还给他一个 upvote;
count++这个刚开始可能有点无法理解: 要记住我们这里的 count 记录的其实是 chp 的字母的余额. 所以 count 收到退款的前提必须是 prev 是 chp 内部的一个;
可以有两个角度来理解这个 count 的退款:

  • 首先记住, 只有处于 window 内部的人才会被 downvote. 如果 prev 不是 chp内部的, 那么 prev 这个时候的 hash 一定是负值(不是 chp 的内部的, hash 初始值是0, 然后加他的时候被 downvote 了, 现在肯定至少是-1), 所以我们踢 prev 的时候, 不需要改变 count. 如果 prev 是 chp 内部的, 那么prev 这个时候的 hash 一定至少是0(我们加进来的时候的 downvote, 每一个 downvote 对应的其实是one occurrence in chp, 所以不论 prev 当时的hash 初始值是多少, 不可能被 downvote 到0以下), 这样的 prev 被踢的时候我们就应该给 count 退款;
  • 另外一个角度理解, 其实这里的这一句跟前面的那一句是对应的:
if (hash[c]-- >= 1) count--;  
...  
if (hash[prev]++ >= 0) count++;

加进来的时候, 至少是1, 踢出去的时候, 至少是0, 这个就是 chp 内部的 char 的特征.

这个退款思路也是反映了这个代码的精炼度.
如果看到这里, 基本上这个算法的大体思路已经理解, 剩下的就是一些 var 更新的 Corner Case 的设置和调整了; 注意的是这里虽然说是left and right, 但是(before each iteration)这一个点, window 其实是left .. right - 1的范围. this iteration处理的就是add right, 然后kick left; 这样实际上想要保持的其实是right - 1 - left + 1 == lenP的状态, 所以这里他 slide 的 header 的判断才会使用


这个是 discussion 上面看到的一个类似的版本:

public List<Integer> findAnagrams(String s, String p) {  
    List<Integer> list = new ArrayList<>();  
    if (s == null || s.length() == 0 || p == null || p.length() == 0) return list;  
    int[] hash = new int[256]; //character hash  
    //record each character in p to hash  
    for (char c : p.toCharArray()) {  
        hash[c]++;  
    }  
    //two points, initialize count to p's length  
    int left = 0, right = 0, count = p.length();  
    while (right < s.length()) {  
        //move right everytime, if the character exists in p's hash, decrease the count  
        //current hash value >= 1 means the character is existing in p  
        if (hash[s.charAt(right++)]-- >= 1) count--;   

        //when the count is down to 0, means we found the right anagram  
        //then add window's left to result list  
        if (count == 0) list.add(left);  

        //if we find the window's size equals to p, then we have to move left (narrow the window) to find the new match window  
        //++ to reset the hash because we kicked out the left  
        //only increase the count if the character is in p  
        //the count >= 0 indicate it was original in the hash, cuz it won't go below 0  
        if (right - left == p.length() && hash[s.charAt(left++)]++ >= 0) count++;  
    }  
    return list;  
}

这个代码上的改动很小, 就是把里面的循环换成一个 if 了(反正也只可能有一种情况). 而且他的 comment 跟我的解释基本是差不多的;


这个是当时加了 log 用来 debug 的代码版本:

public class Solution {  
    public List<Integer> findAnagrams(String s, String p) { //使用hash table+滑动窗口         
        List<Integer> list = new ArrayList<>();  
        char[] chp = p.toCharArray();   
        char[] chs = s.toCharArray();  
        int count = chp.length;  
        int left = 0;  
        int right = 0;  
        int hash[] = new int[256];  
        for (char c : chp) {  
            hash[c]++;  
        }  
        while (right < chs.length) {  
            Printer.printSeperator();//debug  
            System.out.println("start: ");//debug  
            System.out.println((char) 27 + "[31m" + "left: " + left + ", right: " + right + ", count: " + count + (char) 27 + "[0m");//debug  
            Printer.printSlidingWindow(chs, left, right);//debug  
            for (int i = 0; i < 256; i++) if (hash[i] != 0) System.out.println(((char) i) + " = " + hash[i]);//debug  
            char c = chs[right++];  
            if (hash[c]-- >= 1) count--;  
            while (right - left > chp.length) {  
                char prev = chs[left++];  
                if (hash[prev]++ >= 0) count++;  
            }  
            if (count == 0) {  
                list.add(left);  
            }  
            System.out.println("end: ");//debug  
            System.out.println((char) 27 + "[32m" + "left: " + left + ", right: " + right + ", count: " + count + (char) 27 + "[0m");//debug  
            Printer.printSlidingWindow(chs, left, right);//debug  
            for (int i = 0; i < 256; i++) if (hash[i] != 0) System.out.println(((char) i) + " = " + hash[i]);//debug  
        }  
        return list;  
    }  
}

这个跑出来的 log 看起来理解算法就简单很多了;


Problem Description

Given a string s and a non-empty string p, find all the start indices of p's anagrams in s.

Strings consists of lowercase English letters only and the length of both strings s and p will not be larger than 20,100.

The order of output does not matter.

Example 1:

Input:
s: "cbaebabacd" p: "abc"

Output:
[0, 6]

Explanation:
The substring with start index = 0 is "cba", which is an anagram of "abc".
The substring with start index = 6 is "bac", which is an anagram of "abc".
Example 2:

Input:
s: "abab" p: "ab"

Output:
[0, 1, 2]

Explanation:
The substring with start index = 0 is "ab", which is an anagram of "ab".
The substring with start index = 1 is "ba", which is an anagram of "ab".
The substring with start index = 2 is "ab", which is an anagram of "ab".
Difficulty:Easy
Total Accepted:33.2K
Total Submissions:99K
Contributor: Stomach_ache
Companies
amazon
Related Topics
hash table
Similar Questions
Valid Anagram Permutation in String

results matching ""

    No results matching ""