今天给大家分享一个Java经典的面试题,题目是这样的:
本题是LeetCode题库中的49题。
将一个字符串数组进行分组输出,每组中的字符串都由相同的字符组成
举个例子:输入[“eat”,“tea”,“tan”,“ate”,“nat”,“bat”]
输出[[“ate”,“eat”,“tea”],[“nat”,“tan”],[“bat”]]
刚见到这个题,我的第一反应是正则。通过正则表达式加上循环,来筛选判断两个字母是否全部由相同字符组成。结果发现并没有想象中的简单。
于是我转换思路,想到用数组的方式来求,即将每个元素都转化为char类型数组,先判断长度是否相等。然后比较,也就是下面StringArrays
类中的compareReverse
方法。
但是这样效率实在不高,而且还要循环一次。因为会有包含关系的出现(比如abc
就包含abb
),这就需要我们再反向调用一次,这样才得到了最终方法compare
。
后来我想到用排序也可以实现啊,将字符串拆分排序后再合并,如果两个字符串相同,那肯定都由相同的字符组成。于是就有了2.0版本compareNow
:
package day_12_02.zuoye;
import java.util.Arrays;
/**
* @author soberw
*/
public class StringArrays {
/**
* 给定两个字符串,判定第二个串中的字符是否在第一个字符串中全部存在
* contains()当且仅当此字符串包含指定的char值序列时才返回true。
*
* @param a 第一个字符串
* @param b 第二个字符串
* @return boolean
*/
private boolean compareReverse(String a, String b) {
//记录次数
int count = 0;
char[] newB = b.toCharArray();
for (char bb : newB) {
if (a.contains(String.valueOf(bb))) {
count++;
}
}
//相等说明全部包含在内
if (count == a.length()) {
return true;
}
return false;
}
/**
* 对字符串排序
*
* @param str 字符串
* @return 排序后的
*/
private String sortString(String str) {
char[] s = str.toCharArray();
Arrays.sort(s);
return String.valueOf(s);
}
/**
* 判断两个字符串是否由相同字符组成,切长度相同
*
* @param m 第一个字符串
* @param n 第二个字符串
* @return boolean
*/
public boolean compare(String m, String n) {
if (m.length() != n.length()) {
return false;
}
//避免出现子包含情况
return compareReverse(m, n) && compareReverse(n, m);
}
/**
* 对compare方法的改进
*
* @param m 第一个字符串
* @param n 第二个字符串
* @return boolean
*/
public boolean compareNow(String m, String n) {
if (m.length() != n.length()) {
return false;
}
return sortString(m).equals(sortString(n));
}
}
想法实现一
解决了比较问题,于是迎来了第一次测试,一开始我只想到走二维数组来实现,加双重循环判断放入。
package day_12_02.zuoye;
import java.util.Arrays;
/**
* 对StringArrays的测试
*
* @author soberw
*/
public class SATest {
public static void main(String[] args) {
StringArrays sa = new StringArrays();
String[] str = {"eat", "tea", "tan", "ate", "nat", "bat"};
String[][] newStr = new String[str.length][str.length];
for (int i = 0; i < str.length; i++) {
for (int j = 0; j < str.length; j++) {
if (sa.compare(str[i], str[j])) {
newStr[i][j] = str[j];
}
}
}
for (String[] s : newStr) {
System.out.println(Arrays.toString(s));
}
}
}
结果是这样的 😦 我直接凌乱。。。(虽然真的分好组了)
想法不成立。
其实原因很简单,数组局限性太多,必须指定长度,而且会有默认初始值null,当然最后可以去重得到最终结果。但是我实在是嫌麻烦(二维数组去重…)于是果断更换思路。
想法实现二
我们都知道数组是指定长度的,那有没有不指定长度的动态的呢?当然,Java给我们提供了很多,如set,map,list,vector等等。 目前我的想法是创建一个动态的二维数组,和一个临时数组。通过循环,先取出一个单词放在临时数组中,并将原数组该位置值变为null,然后拿临时数组的元素和原数组的所有非null元素比较(通过compareNow
方法),匹配一个放入一个,并将原数组对应元素赋值为null,循环的最后将临时数组放入二维数组中,依次循环下去,直到原数组值全部变为null。
下面实施,我决定选用Vector来存放数据:
package day_12_02.zuoye;
import java.util.Arrays;
import java.util.List;
import java.util.Vector;
/**
* 对StringArrays的测试
*
* @author soberw
*/
public class StringArr {
public static void main(String[] args) {
StringArrays sa = new StringArrays();
//初始数组
Vector<String> v = new Vector<>(List.of("eat", "tea", "tan", "ate", "nat", "bat"));
//保存数据
Vector<Vector<String>> saveV = new Vector<>();
//暂存数组
Vector<String> ver = new Vector<>();
while (true) {
//开始时清空
ver.clear();
for (int j = 0; j < v.size(); j++) {
if (v.get(j) != null) {
ver.add(v.get(j));
v.set(j, null);
break;
}
}
if (ver.size() == 0) {
break;
}
for (int k = 0; k < v.size(); k++) {
if (v.get(k) != null) {
if (sa.compareNow(ver.get(0), v.get(k))) {
ver.add(v.get(k));
v.set(k, null);
}
}
}
System.out.println(ver);
saveV.add(ver);
}
System.out.println(Arrays.toString(saveV.toArray()));
}
}
运行结果:
这是怎么回事,为什么上面输出正确,最后却为空呢。想一想,我终于明白为什么了。saveV.add(ver);这段代码只是添加了ver变量的引用,也就是说ver和saveV的数据都在同一地址单元内。而我每次循环开始都ver.clear()
将ver内的数据清空,那么对应的saveV中的数据也被清空了。
那么如何解决呢,很简单,每次循环重新new一个数组就好了:
package day_12_02.zuoye;
import java.util.Arrays;
import java.util.List;
import java.util.Vector;
/**
* 对StringArrays的测试
*
* @author soberw
*/
public class StringArr {
public static void main(String[] args) {
StringArrays sa = new StringArrays();
//初始数组
Vector<String> v = new Vector<>(List.of("eat", "tea", "tan", "ate", "nat", "bat"));
//保存数据
Vector<Vector<String>> saveV = new Vector<>();
while (true) {
//暂存数组
Vector<String> ver = new Vector<>();
for (int j = 0; j < v.size(); j++) {
if (v.get(j) != null) {
ver.add(v.get(j));
v.set(j, null);
break;
}
}
if (ver.size() == 0) {
break;
}
for (int k = 0; k < v.size(); k++) {
if (v.get(k) != null) {
if (sa.compareNow(ver.get(0), v.get(k))) {
ver.add(v.get(k));
v.set(k, null);
}
}
}
System.out.println(ver);
saveV.add(ver);
}
System.out.println(Arrays.toString(saveV.toArray()));
}
}
结果完美找出:
想法成立。
想法实现三
那么这就是最好的解决方案了吗?我认为当然不是。虽然实现了但我觉得还是太麻烦。还要声明函数,还要加循环判断,还有反复声明数组存放,如果面试时这样写,先不说面试官看了直摇头,就这代码量和需要考虑的点,等你还在改bug呢,其他同学早就交卷了。。。😦
于是催生出了想法三:
充分利用集合的不重复性,先将愿数组所有元素字符排序,放入集合,这样集合就能筛选出所有重复的。
然后只需要把这个集合遍历一遍,与原数组所有元素比较,相同则放入一个临时集合中。最后统一保存在二维集合中去。
开始实施:
package day_12_02.zuoye;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* @author soberw
*/
public class StrArr {
/**
* 对字符串排序
*
* @param str 字符串
* @return 排序后的
*/
private static String sortStr(String str) {
char[] s = str.toCharArray();
Arrays.sort(s);
return String.valueOf(s);
}
public static void main(String[] args) {
//原始数组
String[] str = {"ate", "tae", "tan", "ant", "eat", "bat"};
//中间集合
Set<String> set = new HashSet<>();
//存放最终结果
Set<Set<String>> setLast = new HashSet<>();
for (int i = 0; i < str.length; i++) {
set.add(sortStr(str[i]));
}
for (String s : set) {
//暂存数据
Set<String> seter = new HashSet<>();
//排序后相等的元素放入临时集合
for (int i = 0; i < str.length; i++) {
if (s.equals(sortStr(str[i]))) {
seter.add(str[i]);
}
}
setLast.add(seter);
}
System.out.println(setLast);
}
}
运行结果:
想法成立。
想法实现四
可以看到代码量惊人的减少了,但想法三中设置的中间集合让我灵光一现。我们都知道数组是通过索引下标来走的:0,1,2,3…索引是不可控的,我想着能不能控制索引,将索引的值改为将要判断的值呢?这样就不用通过中间集合了,可以直接通过索引值调用元素了。于是我想到了,Java给我们提供了Map
,你可以理解为键值对的一种集合。
实现如下:
package day_12_02.zuoye;
import java.util.*;
/**
* @author soberw
*/
public class SATrue {
public static void main(String[] args) {
String[] strs = {"ate", "tae", "tan", "ant", "eat","bat"};
System.out.println(new SATrue().grouper(strs));
}
public List<List<String>> grouper(String[] strs) {
if (strs == null || strs.length == 0) {
return new ArrayList<>();
}
Arrays.sort(strs);
Map<String, List<String>> map = new HashMap<>();
for (String str : strs) {
char[] c = str.toCharArray();
Arrays.sort(c);
String sortedStr = String.valueOf(c);
if (!map.containsKey(sortedStr)) {
map.put(sortedStr, new ArrayList<>());
}
map.get(sortedStr).add(str);
}
return new ArrayList<>(map.values());
}
}
运行结果:
想法成立。
相比于想法三又简化了一步,这是我目前能够想到的最优解了。
一个看似简单的题目,实现起来却并不简单。刚开始我以为本题难点在于筛选出相同的字符组成的元素,结果等真正实现却发现,难点在于如何把它们表现在二维数组中,即如何分组显示。
如果你们有更好的想法,欢迎评论区留言。