一个变位词检查的最佳解决方案是什么?

6

我正在处理一个排列/字谜问题,想要了解检查的最有效方法。 现在,我正在使用Java,并且有一个包括排序在内的库用于所有事情。 检查两个字符串是否为字谜的第一种方法是检查长度,以某种方式对它们进行排序,然后比较每个索引上的字符串。以下是代码:

private boolean validAnagram(String str, String pair) {
if(str.length() != pair.length()){
    return false;
}

char[] strArr = str.toCharArray();
char[] pairArr = pair.toCharArray();


Arrays.sort(strArr);
str = new String(strArr);

Arrays.sort(pairArr);
pair = new String(pairArr);

for(int i = 0; i<str.length(); i++){
    if(str.charAt(i) != pair.charAt(i)){
        return false;
    }
}
return true;
}

或者,我想基于ascii值进行检查并避免对每个可能的字符进行检查会更容易。以下是代码:

private boolean validAnagram(String str, String pair) {
if(str.length() != pair.length()){
    return false;
}

char[] strArr = str.toCharArray();
char[] pairArr = pair.toCharArray();



int strValue = 0;
int pairValue = 0;

for(int i =0; i < strArr.length; i++){
    strValue+= (int) strArr[i];
    pairValue+= (int) pairArr[i];
}

if(strValue != pairValue){
    return false;
}
return true;
}

那么,哪种方案更好呢?我对Arrays提供的排序方法不是很了解,但这似乎是我在互联网上寻找答案时得到的更常见的答案。这让我想知道是否有什么我不知道的东西。


1
你可以直接比较数组中的字符,而不是将 char[] 转换回 String 然后再使用 charAt() - QBrute
这很令人困惑。你只想要变位词还是任何排列?检查它们的方法大相径庭。 - fge
5
第二种解决方案应该是不起作用的。它会对“ac”和“bb”返回真。 - Bastien Aracil
Bastien,你说得非常正确 :) - Drew L. Facchiano
13个回答

4

我能够编译出更简单易懂的解决方案……

    static boolean isAnagram(String a, String b) {
    if (a.length() == b.length()){
        char[] arr1 = a.toLowerCase().toCharArray();
        char[] arr2 = b.toLowerCase().toCharArray();
        Arrays.sort(arr1);
        Arrays.sort(arr2);
        if (Arrays.equals(arr1, arr2)) return true;
        else return false;
    }else return false;
}

您好,Justin


4
有几种方法可以检查两个字符串是否是变位词。 你的问题是,哪一个解决方案更好。 你的第一个解决方案有排序逻辑。 排序的最坏情况复杂度为(nlogn)。 你的第二个逻辑仅使用一个循环,其复杂度为O(n)。
因此,在这两个解决方案中,只有O(n)复杂度的第二个解决方案比第一个更好。
一种可能的解决方案:
private boolean checkAnagram(String stringOne , String stringTwo){
        char[] first = stringOne.toLowerCase().toCharArray(); 
        char[] second = stringTwo.toLowerCase().toCharArray();
        // if length of strings is not same 
        if (first.length != second.length)
            return false;
        int[] counts = new int[26]; 
        for (int i = 0; i < first.length; i++){
            counts[first[i]-97]++;  
            counts[second[i]-97]--;   
        }
        for (int i = 0; i<26; i++)
            if (counts[i] != 0)
                return false;
        return true;
    }


嘿,Pratik!那是我的最初想法。然而,有人指出我的ASCII解决方案存在一个重大问题。Reddit上的这位好心人指出:“如果你给它字符串AD和BC。第一个的ASCII值为65和68,第二个的值为66和67。它们的总和都是133,会被你的算法视为相等。”看起来有一些解决方法。但就问题而言,修复边缘情况似乎不值得。 - Drew L. Facchiano
请翻译以下关于编程的内容,从英文到中文。仅返回翻译后的文本:完整帖子在此处:https://www.reddit.com/r/learnprogramming/comments/4rjg9x/which_is_the_better_anagram_solution/ - Drew L. Facchiano
我能理解。将每个字符映射到布尔值,然后比较这两个映射。但这仍然似乎比排序方法的运行时间更长。 - Drew L. Facchiano
我已经添加了一个解决方案,请看一下。排序方法在最坏情况下的复杂度为(nlogn),但这种方法在最坏情况下的复杂度为o(n)。 - Pratik Upacharya
如果字符串中包含字母表(a到z或A到Z)以外的任何内容,它也会崩溃。 - ForguesR
显示剩余2条评论

3

这是一个非常简单的实现。

public boolean isAnagram(String strA, String strB) {
  // Cleaning the strings (remove white spaces and convert to lowercase)
  strA = strA.replaceAll("\\s+","").toLowerCase();
  strB = strB.replaceAll("\\s+","").toLowerCase();

  // Check every char of strA and removes first occurence of it in strB
  for (int i = 0; i < strA.length(); i++ ) {
    if (strB.equals("")) return false;  // strB is already empty : not an anagram
    strB = strB.replaceFirst(Pattern.quote("" + strA.charAt(i)), "");
  }

  // if strB is empty we have an anagram
  return strB.equals("");
}

最后一点:
System.out.println(isAnagram("William Shakespeare", "I am a weakish speller")); // true

1
根据维基百科的定义:变位词是由不同单词或短语的字母重新排列形成的单词或短语,通常要求使用所有原始字母恰好一次。 - ForguesR
答案:AR不是RAT的字谜。 - user1090751
@user1090751 不行,因为“T”没有被使用。“Art”是“Rat”的易位构词。 - ForguesR
变位词不一定要使用所有字母......来源:https://zh.wikipedia.org/wiki/变位词 - user1090751
...但通常都是这样的。这里提供的所有解决方案都是指普通的字谜游戏。欢迎提供您的答案。 - ForguesR
我没有解决方案,你的解决方案也不是通用的,不能被视为正确答案。 - user1090751

1
我的解决方案: 时间复杂度 = O(n)
public static boolean isAnagram(String str1, String str2) {
    if (str1.length() != str2.length()) {
        return false;
    }

    for (int i = 0; i < str1.length(); i++) {
        char ch = str1.charAt(i);

        if (str2.indexOf(ch) == -1) 
            return false;
        else
            str2 = str2.replaceFirst(String.valueOf(ch), " ");
    }

    return true;
}

测试用例:

@Test
public void testIsPernutationTrue() {
    assertTrue(Anagram.isAnagram("abc", "cba"));
    assertTrue(Anagram.isAnagram("geeksforgeeks", "forgeeksgeeks"));
    assertTrue(Anagram.isAnagram("anagram", "margana"));
}

@Test
public void testIsPernutationFalse() {
    assertFalse(Anagram.isAnagram("abc", "caa"));
    assertFalse(Anagram.isAnagram("anagramm", "marganaa"));
}

3
这是O(n^2)的,因为每次str2.indexOf需要遍历整个字符串。 - Thilo

1

最佳解决方案取决于您的目标、代码大小、内存占用或最小计算量。

一个非常酷的解决方案,尽可能少的代码,不是最快的O(nlog n),在Java 8中内存效率相当低:

public class Anagram {
  public static void main(String[] argc) {
    String str1 = "gody";
    String str2 = "dogy";

    boolean isAnagram =
    str1.chars().mapToObj(c -> (char) c).sorted().collect(Collectors.toList())
    .equals(str2.chars().mapToObj(c -> (char) c).sorted().collect(Collectors.toList()));

    System.out.println(isAnagram);
  }
}

这个解决方案有一些缺陷。根据你的解决方案,你对方法参数中接收到的字符串进行字符排序,但是你没有忽略空格和大写字母,因此例如:"isAnagram("William Shakespeare", "I am a weakish speller")" 上面提到的返回 false 而不是 true。 - K.Rzepecka

1
我尝试了几种使用集合的解决方案,并使用您提供的示例数组运行了每个解决方案1000万次进行测试:

private static String[] input = {"tea", "ate", "eat", "apple", "java", "vaja", "cut", "utc"};

首先,这是我用来调用这些算法的方法:
public static void main(String[] args) {
    long startTime = System.currentTimeMillis();
    for (int x = 0; x < 10000000; x++) {
        Set<String> confirmedAnagrams = new HashSet<>();
        for (int i = 0; i < (input.length / 2) + 1; i++) {
            if (!confirmedAnagrams.contains(input[i])) {
                for (int j = i + 1; j < input.length; j++) {
                        if (isAnagrams1(input[i], input[j])) {
                            confirmedAnagrams.add(input[i]);
                            confirmedAnagrams.add(input[j]);
                        }
                }
            }
        }
        output = confirmedAnagrams.toArray(new String[confirmedAnagrams.size()]);
    }
    long endTime = System.currentTimeMillis();
    System.out.println("Total time: " + (endTime - startTime));
    System.out.println("Average time: " + ((endTime - startTime) / 10000000D));
}

我将使用基于字符哈希集的算法进行翻译。我将每个单词的每个字符添加到哈希集中,如果哈希集的长度不等于初始单词的长度,则它们不是变位词。
我的算法及其运行时间: 算法1:
    private static boolean isAnagrams1(String x, String y) {
    if (x.length() != y.length()) {
        return false;
    } else if (x.equals(y)) {
        return true;
    }

    Set<Character> anagramSet = new HashSet<>();
    for (int i = 0; i < x.length(); i++) {
        anagramSet.add(x.charAt(i));
        anagramSet.add(y.charAt(i));
    }

    return anagramSet.size() != x.length();
}

这是运行时间:
Total time: 6914
Average time: 6.914E-4

Algorithm 2

private static boolean isAnagrams2(String x, String y) {
    if (x.length() != y.length()) {
        return false;
    } else if (x.equals(y)) {
        return true;
    }

    Set<Character> anagramSet = new HashSet<>();
    char[] xAr = x.toCharArray();
    char[] yAr = y.toCharArray();
    for (int i = 0; i < xAr.length; i++) {
        anagramSet.add(xAr[i]);
        anagramSet.add(yAr[i]);
    }

    return anagramSet.size() != x.length();
}

是否已运行:

Total time: 8752
Average time: 8.752E-4

算法3

对于这个算法,我决定将Set传递进去,因此每个循环只创建一次,并在每次测试后清除它。

    private static boolean isAnagrams3(Set<Character> anagramSet, String x, String y) {
    if (x.length() != y.length()) {
        return false;
    } else if (x.equals(y)) {
        return true;
    }

    for (int i = 0; i < x.length(); i++) {
        anagramSet.add(x.charAt(i));
        anagramSet.add(y.charAt(i));
    }

    return anagramSet.size() != x.length();
}

是否运行时间为:

Total time: 8251
Average time: 8.251E-4

算法4

这个算法不是我写的,它属于Pratik Upacharya,他也回答了这个问题,为了让我进行比较:

    private static boolean isAnagrams4(String stringOne, String stringTwo) {
    char[] first = stringOne.toLowerCase().toCharArray();
    char[] second = stringTwo.toLowerCase().toCharArray();
    // if length of strings is not same 
    if (first.length != second.length) {
        return false;
    }
    int[] counts = new int[26];
    for (int i = 0; i < first.length; i++) {
        counts[first[i] - 97]++;
        counts[second[i] - 97]--;
    }
    for (int i = 0; i < 26; i++) {
        if (counts[i] != 0) {
            return false;
        }
    }
    return true;
}

有这个运行时:

Total time: 5707
Average time: 5.707E-4

当然,这些运行时间对于每次测试都是不同的,为了进行适当的测试,需要更大的示例集,以及更多的迭代。*编辑,因为我在初始方法中犯了一个错误,Pratik Upacharya的算法似乎更快。

1
你的 Algorithm 1 对于 isAnagrams1("good", "dogg") 返回了 true,你需要确保每个字符出现的次数都相同。 - κροκς
1
是的,那么“Set”这个东西就不会真正起作用了。对此感到抱歉。 - Propagandian
2
你可以使用 HashMap<Character, Integer> 并且以类似于Pratik处理数组的方式增加/减少计数。 - κροκς

1
使用原始数据类型的解决方案。
boolean isAnagram(char input1[], char input2[]) {
    int bitFlip = 32;

    if(input2.length != input1.length){return false;}

    boolean found = false;
    for (int x = 0; x < input1.length; x++) {
        found = false;
        for (int y = 0; y < input2.length; y++) {
             if (!found && ((input1[x] | bitFlip)) ==
             ( (input2[y] | bitFlip))) {
                found = true;
                input2[y] = 0;
            }
        }
        if (!found) {
            break;
        }
    }
    return found ;
}

这种方法不依赖于任何排序实用程序。它通过迭代查找值,然后将其设置为零,以避免具有2个字母“o”的重复字符输入,例如“pool”和“loop”。
它还通过翻转位来忽略大小写,而不是依赖于toLowerCase(),因为如果第6位(十进制32)为1,则为小写字母,如果为零,则为大写字母。
这是直接的字节操作,因此在像图像操作中使用时性能更好。也许缺点是O(n^2)。
此解决方案已在hackerrank上得到测试。

作为一个程序相关的内容,你能解释一下你的解决方案为什么更好吗? - Crocsx

0

简单的 Kotlin 解决方案

fun IsAnagram(s1: String, s2: String): Boolean {
    return s1.groupBy { it } == s2.groupBy { it }
}

GroupBy 的渐进时间复杂度为 O(n),上述的时间复杂度也为 O(n)


0
最近有一位招聘人员让我解决了这个问题。 在研究这个问题时,我想出了一个解决两种变位词问题的方法。
问题1: 确定文本中是否存在变位词。
问题2: 确定正式变位词是否存在于文本中。 在这种情况下,变位词必须与您要比较的文本大小相同。在前一种情况下,两个文本不需要是相同的大小。
只需包含另一个即可。
我的方法如下:
设置阶段: 首先创建一个变位词类。这将仅将文本转换为Map,其中键是所讨论的字符,值包含输入字符的出现次数。 我假设这最多需要O(n)时间复杂度。 由于这最多需要两个映射,最坏情况下的复杂度将是O(2n)。至少我对渐近符号的朴素理解是这样的。
处理阶段: 你所需要做的就是循环遍历两个Map中较小的那个,并在较大的Map中查找它。如果不存在或者存在但出现次数不同,则无法通过变位词测试。
这里是用于确定是否存在字谜的循环:
    boolean looking = true;
        for (Anagram ele : smaller.values()) {
            Anagram you = larger.get(ele);
                if (you == null || you.getCount() != ele.getCount()) {
                    looking = false;
                    break;
                }
        }
        return looking;

请注意,我创建了一个ADT来包含正在处理的字符串。它们首先被转换为Map。
以下是创建Anagram对象的代码片段:
    private void init(String teststring2) {
        StringBuilder sb = new StringBuilder(teststring2);
        for (int i = 0; i &lt sb.length(); i++) {
            Anagram a = new AnagramImpl(sb.charAt(i));
            Anagram tmp = map.putIfAbsent(a, a);
            if (tmp != null) {
                tmp.updateCount();
            }
        }
    }

0

我想出了一个解决方案,甚至没有使用任何26个字符的数组...看看这个:

StringBuffer a = new StringBuffer();
        a.append(sc.next().toLowerCase());

        StringBuffer b = new StringBuffer();
        b.append(sc.next().toLowerCase());
        if(a.length() !=b.length())
        {
            System.out.println("NO");
            continue;
        }
        int o =0;
        for(int i =0;i<a.length();i++)
        {
            if(a.indexOf(String.valueOf(b.charAt(i)))<0)
            {
               System.out.println("NO");
               o=1;break; 

            }
        }
        if(o==0)
         System.out.println("Yes");

1
这是O(n^2),因为a.indexOf每次都需要运行整个字符串。 - Thilo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接