青云算法面试题干货-相似的字符串组-LeetCode第839题

问题:如果交换字符串X中的两个字符能得到字符串Y,那么两个字符串X和Y相似。例如,字符串"tars"和"rats"相似(交换下标0和2的两个字符)、字符串"rats"和"arts"相似(交换下标0和1的字符),但字符串"star"和"tars"不相似。输入一个字符串数组,根据字符串的相似性分组,请问能把输入数组分成几组?

例如,输入数组["tars","rats","arts","star"],可以分成2组,一组为{"tars", "rats", "arts"},另一组为{"star"}。值得注意的是,虽然 "tars"和"arts"并不相似,但它们被分到一组。如果一个字符串至少和一组字符串中的一个相似,那么它就可以放到该组里去。

假设输入数组中的所有字符串长度相同,并且两两互为变位词anagrams,即字符串中出现的字符以及每个字符出现的次数都相同。

分析:这是LeetCode的第839题。 

解法一:子图的遍历

我们把输入数组中的每个字符串看成图中的一个节点,如果两个字符串相似,那么在图中它们对应的两个节点有一条边相连。按照这个规则,根据数组["tars","rats","arts","star"]可以生成下面的图:

由单词组成的图

从上图我们可以看出,一组相似字符串构成一个连通的子图。由于图中有两个子图,一个子图包含3个节点,一个子图只有1个节点。相应地,我们可以把输入的数组分拆成两个相似的字符串组。

于是,问题就可以转换成找出一个图中连通子图的数目。顺序扫描数组,如果一个字符串还没有把它归类到某个相似的字符串组(即对应的节点还没有加入某个子图),我们就从该节点出发遍历所有和它连通的节点,这样就找到一个连通的子图。每找到一个子图,子图的数目加1。

从一个节点出发,遍历所有和它连通的节点,可以用广度优先搜索算法或深度优先搜索算法。下面的参考代码是基于广度优先搜索算法:

public int numSimilarGroups(String[] A) {
    Set<String> unvisited = new HashSet<>();
    for (String word : A) {
        unvisited.add(word);
    }

    int groups = 0;
    for (String str : A) {
        if (!unvisited.contains(str)) {
            continue;
        }

        groups++;
        Queue<String> queue = new LinkedList<>();
        queue.offer(str);
        unvisited.remove(str);
        while (!queue.isEmpty()) {
            String first = queue.poll();
            List<String> neighbors = new LinkedList<>();
            for (String word : unvisited) {
                if (areSimilar(first, word)) {
                    neighbors.add(word);
                }
            }

            for (String neighbor : neighbors) {
                queue.offer(neighbor);
                unvisited.remove(neighbor);
            }
        }
    }

    return groups;
}

在上述代码中,变量unvisited保存所有还没有加入到某个子图的节点。最开始的时候它包含所有的字符串,之后每遍历一个节点,就从unvisited里删除一个节点。

函数areSimilar用来判断两个字符串是不是相似。如果两个单词相似,它们在图中的节点相互连通。根据题目的要求,两个变位词如果只有两个字符是不同的,那么它们就相似。该函数的代码如下:

private boolean areSimilar(String str1, String str2) {
    int diffCount = 0;
    for (int i = 0; i < str1.length(); ++i) {
        if (str1.charAt(i) != str2.charAt(i)) {
            diffCount++;
            if (diffCount > 2) {
                return false;
            }
        }
    }

    return true;
}

解法二:并查集

还是把输入数组中的每个字符串看成图中的一个节点。如果两个字符串相似,那么它们对应的节点应该属于同一个子图。

我们想假设每个节点属于一个只包含它自己的子图,然后判断所有两个字符串是不是相似,如果它们相似并且它们分别属于两个不同的子图,那么就把它们所在的子图合并。我们需要做两件事,一是判断两个节点是否属于同一个子图,而是合并两个子图。这是经典的并查集(Union-Find)的应用。

之前在讨论经典面试题“朋友圈”的时候曾详细讨论过并查集,感兴趣的读者可以点击超级链接,了解如果应用并查集并且通过路径压缩做优化。下面是参考代码:

public int numSimilarGroups(String[] A) {
    int[] parents = new int[A.length];
    for (int i = 0; i < parents.length; ++i) {
        parents[i] = i;
    }

    int groups = A.length;
    for (int i = 0; i < A.length; ++i) {
        for (int j = i + 1; j < A.length; ++j) {
            if (getParent(i, parents) != getParent(j, parents)
                && areSimilar(A[i], A[j])) {
                union(i, j, parents);
                groups--;
            }
        }
    }

    return groups;
}

private int getParent(int i, int[] parents) {
    if (parents[i] != i) {
        int parent = parents[i];
        parents[i] = getParent(parent, parents);
    }

    return parents[i];
}

private void union(int i, int j, int[] parents) {
    int parent = getParent(i, parents);
    parents[parent] = parents[j];
}

判断两个字符串是否相似的函数areSimilar和前面一样,不再重复。

更多算法面试题的讨论,欢迎访问博客http://qingyun.io/blogs。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值