Anagrams

https://oj.leetcode.com/problems/anagrams/

Given an array of strings, return all groups of strings that are anagrams.

Note: All inputs will be in lower-case.

解题思路:

这题在面试中出现概率极高,到底有多高,从start-up到FLAG,基本都会问。为啥这么高,等看完解题方法也许就知道了。

啥叫anagram?两个互为anagram的字符串,就是说这两个字符串用一模一样的char组成,只是顺序不同,这里也包含每个char的数量都是一样的。比如aaood和aoaod,就是一对anagrams。

回到这题目上,这题目其实是有歧义的。啥叫返回所有anagrams的字符串组?如果有好几对呢?其实题目的意思是,别把他们分开,一股脑全塞进list就行。这么ambiguous的表述,这里先不管。

那么这题可以分为两个问题。如果判断两个字符串为anagrams?以及,如何在这个String[]中找出所有的anagrams对。

我们先来看第一个问题。判断两个字符串为anagrams有两个方法。一种是将每个字符串内部按字符的顺序(也就是ASCII顺序)排序,然后看排序后的结果是否一致,如果相同,就是anagrams。这种算法的时间复杂度为O(nlogn),n为字符串的长度。如果采用其他线性排序的方法,比如基数排序,也可以在O(n)的时间内完成。第二种方法是,统计A字符串中每个字符出现的次数,放入HashMap<Character, Integer>中。然后对于字符串B,做同样的工作。最后看两个HashMap是不是euqal,或者遍历B,遇到一个char就从A的HashMap中value--,结果HashMap为空,并且B中的char一定都在这个HashMap内,就证明是Anagrams了。这种方法的时间复杂度为O(n)。

编程珠玑中还提到一种比较巧妙的方法,在统计char出现次数的基础上,将ababac直接转化为a3b2c1这样形式的字符串,那么anagrams的结果字符串一定是equal的,而且这个操作可以在线性时间内完成。比如设计一个int [26]。index表示'a'-'z',int[i]表示出现的次数。

综上所述,我们可以看到的,比较字符串是否互为anagrams,就是将它们转为一种一致的code。我们认为他们是anagrams的,这种code一定equal。是不是和hashmap中计算hashcode的思想很像?回忆一下HashMap的hashCode()方法是如何做的,才能保证良好的collision和chaining?一般而言,首先将key转化为自然数,对于字符串的key,可以视作128为底的ASCII数值组成的自然数。然后可以通过取一个合适的质数的模的方法来取得一个hashCode。

通过这样的思路,我们发现对于两个字符串互为anagrams的视野可以打的更开。下面的作者给出了一个方法。

http://fisherlei.blogspot.jp/2013/01/leetcode-anagrams.html

他在转化为a3b2c1这个方法的基础上,取任意一个较小的质数为底,比如7。然后求这个7进值数的值,结果当然要是long的。他将这个方法称为求水印long foorprint(String str)。这个思路在很多地方都可以考虑。当时这个方法不能严格通过collision,是不是对于不是anagrams的string,这个code一定不同?似乎不太严格。评论里有人在stackoverflow上给了另一个严格的方法。

http://stackoverflow.com/questions/18781106/generate-same-unique-hash-code-for-all-anagrams

将'a'-'z'每个char对应一个质数(prime number),比如3,5,7,11等,然后string的这个code就是所有char的乘积。这个code对于不是anagrams的string一定是不同的。但是可能很快越界了。这里不讨论了。

我们看到这个问题可以考HashMap的很多细节,还能考排序,时间复杂度,空间耗费,所以面试都爱问它。

接下来看第二个问题,我们已经知道如何比较两个字符串是否是anagrams了,怎么在这个String[]中找出所有的anagrams对?两两对比每个string的上述code?时间复杂度为O(n^2),可行但是会超时。神马?有了code还这么烦?直接用map就行了。anagrams的转化后的code的hashCode一定是相同的,因为这个code就相同。下面的方法就多了,总之可以在O(1)的时间内获得比对,加上遍历,整个方法就只需要O(n)了。

下面是一个对str内部char排序的方法。需要注意的是,第一次放入map的str也要加入到最终的list中,所以要特殊的标记他是否被取出来加入过。思路如下:

1. 遍历strs,对每个str内部按char排序,获得一个新的chars[]。

2. 看看map里有没有这个chars.

3. 没有的话,把<chars, index>塞进map。

4. 如果有的话,看看map.get(chars)的值是不是-1。

5. 不是-1,先把strs[map.get(chars)]放进list,再把当前str放进list。然后把map.get(chars)置为-1.

6. 是-1,仅仅把当前str放进list。

7. 循环1-6直到strs结束。

public class Solution {

    public List<String> anagrams(String[] strs) {

        List<String> result = new ArrayList<String>();

        //integer的作用用来记录已经存在map中的str是否被加入过result中

        //String为strs[i]内字符排序完成后的字符串,Integer为下标i

        Map<String, Integer> map = new HashMap<String, Integer>();

        

        for(int i = 0; i < strs.length; i++){

            char[] chars = strs[i].toCharArray();

            Arrays.sort(chars);

            //如果map里已经有

            if(map.containsKey(String.valueOf(chars))){

                //而且map内的前面还没有加入到result中

                if(map.get(String.valueOf(chars)) != -1){

                    //首先获得map里这个值对应的下标,再将它放入result

                    //因为result中放入的都是排序过后的值,不能直接放入,所以要先取得它在原strs中的下标i

                    result.add(strs[map.get(String.valueOf(chars))]);

                    //再将这个已有的值置为-1,下次再遇到的时候不再放入result了

                    map.put(String.valueOf(chars), -1);

                }

                //同时将当前字符串也放入result

                result.add(strs[i]);

            }else{

                //map内没有的话,就将当前排序过后的值和他的下标放入map,待后面的去check

                map.put(String.valueOf(chars), i);

            }

        }

        return result;

    }

}

 这个方法的时间复杂度为O(n*klogk),n为strs[]的size,k为每个字符串的长度,空间复杂度为O(n),用了一个map。

下面我们不用排序,再来实现一个将ababac直接转化为a3b2c1r的方法。将上面O(n*klogk)简化为O(n*k)。

除了将str进行排序的代码换成了取得str的code,也就是map中key的变化,其他都没有变化。

public class Solution {

    public List<String> anagrams(String[] strs) {

        List<String> result = new ArrayList<String>();

        //integer的作用用来记录已经存在map中的str是否被加入过result中

        Map<String, Integer> map = new HashMap<String, Integer>();

        

        for(int i = 0; i < strs.length; i++){

            String code = getCode(strs[i]);

            if(map.containsKey(code)){

                if(map.get(code) != -1){

                    result.add(strs[map.get(code)]);

                    map.put(code, -1);

                }

                result.add(strs[i]);

            }else{

                map.put(code, i);

            }

        }

        return result;

    }

    

    public String getCode(String str){

        String code = "";

        int[] num = new int[26];

        for(int i = 0; i < str.length(); i++){

            num[str.charAt(i) - 'a']++;

        }

        for(int i = 0; i < num.length; i++){

            if(num[i] > 0){

                code = code + String.valueOf((char)('a' + i)) + num[i];

            }

        }

        return code;

    }

}

 

你可能感兴趣的:(r)