一、题目复述
leetCode 算法题目:
https://leetcode-cn.com/problems/H6lPxb/
简单描述为:
1、提供一字符串数组,数组字符串元素之间是字母异位词 。例如"aaabb" 与"baaab"; 即,组成字符串的字母相同,并且字母出现的次数相同。字母位置可以不同。
2、定义两个字符串相似:一个字符串,至多交换一次字母的位置后,可以变成另一个字符串,则两个字符串相似。例如 “ab” 与 “ba”; “wbw” 与"bww";“stars” 与 “tsars”;
3、给定一个字符串列表 strs。满足条件1;请问 strs 中有多少个相似字符串组?
二、自己的题解
首先最先想到的是写一个方法,判断两个字符串是否相似,比较简单,直接给代码:
@Test
public void test() {
System.out.println(isSimilarString("tars", "rats"));
System.out.println(isSimilarString("tars", "star"));
}
private boolean isSimilarString(String str1, String str2) {
if (str1.length() != str2.length()) {
return false;
}
List<Integer> diffIndexList = new ArrayList<>();
for (int i = 0; i < str1.length(); i++) {
if (str1.charAt(i) != str2.charAt(i)) {
diffIndexList.add(i);
if (diffIndexList.size() > 2) {
return false;
}
}
}
if (diffIndexList.size() == 0) {
return true;
}
if (diffIndexList.size() == 2) {
return str1.charAt(diffIndexList.get(0)) == str2.charAt(diffIndexList.get(1)) && str1.charAt(diffIndexList.get(1)) == str2.charAt(diffIndexList.get(0));
}
return false;
}
后面的代码先给思路,大家可以试试:
遍历字符串数组,先 Str0 与 Str1 比较相似,如果相似,则两个字符串放到一个集合里面。如果不相似,则两个字符串各自放到一个集合里面。Str2 依次与前面的集合比较,如果遇到集合的元素与 Str2 相似,则把 Str2 加入这个已经存在的集合。否则,Str2 自创一个集合。
public int numSimilarGroups(String[] strs) {
List<List<String>> group = new ArrayList<>();
for (int i = 0; i < strs.length; i++) {
String str = strs[i];
boolean addFlag = false;
groupLoop:
for (List<String> list : group) {
for (String s : list) {
if (isSimilarString(s, str)) {
list.add(str);
addFlag = true;
break groupLoop;
}
}
}
if (!addFlag) {
List<String> listObj = new ArrayList<>();
listObj.add(str);
group.add(listObj);
}
}
return group.size();
}
然后简单测试,就提交了,结果报错,报错提示案例为:
想半天,没有思路的时候,反复回去读题,理解题目:“tars” 和 “rats” 是相似的 (交换 0 与 2 的位置); “rats” 和 “arts” 也是相似的,但是 “star” 不与 "tars"相似 简化为 A 和B相似,B和C相似,但是A和C不相似。这与数学中的相似是不同的。所以,可能遍历字符的时候,出现的是 A,C,B,A和C不相似,创建了两个集合,但是B与A,B与C相似,这样,遍历到B的时候,需要将 A,C所在的两个集合合并为一个相似的集合。其实我们可以将相似理解为连通,这样就符合我们的思路:
所以修改自己的代码:
public int numSimilarGroups(String[] strs) {
List<List<String>> group = new ArrayList<>();
for (int i = 0; i < strs.length; i++) {
String str = strs[i];
List<Integer> similarListIndex = new ArrayList<>();
groupLoop:
for (int j = 0; j < group.size(); j++) {
List<String> list = group.get(j);
for (String s : list) {
if (isSimilarString(s, str)) {
similarListIndex.add(j);
break;
}
}
}
if (similarListIndex.isEmpty()) {
List<String> listObj = new ArrayList<>();
listObj.add(str);
group.add(listObj);
} else if(similarListIndex.size() == 1) {
group.get(similarListIndex.get(0)).add(str);
} else {
List<String> mergeList = new ArrayList<>();
mergeList.add(str);
for (Integer listIndex : similarListIndex) {
mergeList.addAll(group.get(listIndex));
}
int modify = 0;
for (int listIndex : similarListIndex) {
group.remove(listIndex - modify);
modify++;
}
group.add(mergeList);
}
}
return group.size();
}
综合提交代码:https://leetcode-cn.com/problems/H6lPxb/
三、代码学习-并查集
做完之后,看看官方题解,没有很多解释,看了一个小时,稍稍懂了,补充代码的解释如下。顺便也学习了新知识——并查集。
别人的代码:
class Solution {
public int numSimilarGroups(String[] strs) {
int n = strs.length;
int cnt = n;
// 并查集的初始化,一开始,每个元素的父亲,或者说祖先是自己。
int[] fathers = new int[n];
for(int i = 0; i < n; ++i){
fathers[i] = i;
}
// 一开始,默认的分组等于数组长度,每个元素各自为一组
// 遍历采用的是组合数的方法,每两个元素只要组合在一起一次,判断是否相似。
for(int i = 0; i < n; ++i){
for(int j = i+1; j < n; ++j){
// 遇到后面的元素与前面的元素相似的, 朴素的想法是,两个元素可以放一组了。组数可以减一了。但是:
// ***难点***
// 再,判断相似的元素是否有共同的祖先,
// 如果祖先相同,例如 A与B相似,A是B的祖先;A与C相似,A是C的祖先;A已经把B,C拉到一个组了。遍历到B,C的时候,祖先都是A,就不用再减一了。他们的组数的影响已经记录过了。
// 如果祖先不相同,就可以减一。
//情况一:原始转态,祖先都是自己。
//情况二:不同组的合并。例如 A与B 相似,A与C不相似,B与C相似;B的祖先是A,C的祖先是C,不相同,把C加入A的子孙。组数减一。
if(isSimilar(strs[i], strs[j]) && union(fathers, i, j)){
cnt--;
}
}
}
return cnt;
}
public boolean isSimilar(String s1, String s2){
int cnt = 0;
for(int i = 0; i < s1.length(); ++i){
if(s1.charAt(i) != s2.charAt(i)) cnt++;
}
return cnt<=2;
}
public boolean union(int[] fathers, int i, int j){
int a = findFather(fathers, i);
int b = findFather(fathers, j);
if(a != b){
fathers[a] = b; // 这里 fathers[b] = a; 也行,字符相似,谁做祖先都可以,只要保持统一,让后面比较的元素都做子孙就行。
return true;
}
return false;
}
public int findFather(int[] fathers, int i){
if(fathers[i] != i){
// 引用递归来求解。
fathers[i] = findFather(fathers, fathers[i]);
}
return fathers[i];
}
}
作者:SloanCheng
链接:https://leetcode-cn.com/problems/H6lPxb/solution/tu-bfshe-bing-cha-ji-liang-chong-fang-fa-hmof/
来源:力扣(LeetCode)
代码中所有注释为本文作者添加。
总结算法的核心思路是:
fathers[x] = y;
fathers[y] = y;
添加了一个fathers[]数组,来记录下标为X的元素与下标为Y的元素之间的关系。
设置数组关系的时候,引用递归来求解。
其他思考:这样的关系可以是相似,节点的连通性,菜单的父子关系,等等业务场景下使用。
优缺点分析:
引用的代码,效率高,直接求结果,给出相似的有几组,但是具体哪些字符在一个组是不知道的。业务场景下,有时候我们可能希望知道,有相同关系的一组元素具体有哪几个。这个时候,用我的第一种方法,只要查看group集合就可以知道。
并查集还有更丰富的知识,自行百度。