偶然看到一个字节跳动的面试题,研究记录一下
给定一个字符串,得出最长不重复子串
比如 adca 结果为 adc
adcdedfckop结果为edfckop
解法一 暴力迭代:
遍历出给定字符串的所有子串,判断其中是否有重复字符,没有则记录长度,与下一次也无重复字符的子串比较长度,最长的即为所求
public static String noDuplicate(String str) {
if(str==null||str.length()==0){
return null;
}
Set<String> set = new HashSet<>();
String result = "";
System.out.println("length:" + str.length());
//获得所有子串
for (int i = 0; i < str.length(); i++) {
for (int j = i + 1; j <= str.length(); j++) {
String s = str.substring(i, j);
set.add(s);
}
}
int max = 0;
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
LinkedHashSet<String> setchar = new LinkedHashSet<>();
String st = iterator.next().toString();
//子串去重放入set
for (int a = 0; a < st.length(); a++) {
setchar.add(String.valueOf(st.charAt(a)));
}
//长度不变认为不重复,取最长子串
if(setchar.size()==st.length()){
int len = st.length();
if(max<len){
max = Math.max(max, len);
result = st;
}
}
}
System.out.println(max);
return result;
}
暴力法的时间复杂度为:n2+n2===>2n2 即O(n2) ,显然不是面试管期待的解法
解法二 滑动窗口,有回溯
/**
* 滑动窗口:保证窗口中都是不重复的子串实现 _有回溯
* 从每个位置开始找最长不重复串0-n,1-n,...n-1-n
* 保留上述步骤中最长串开始位置和长度
* @param s
* @return
*/
public static String getMaxsubHuisu(String s) {
if (s == null || s.length() == 0) {
return null;
}
int start = 0;//滑动窗口的开始值
int maxlen = 0;
int len = 0;//窗口长度
int startMaxIndex = 0;//最长子串的开始值
Map<Character, Integer> map = new HashMap<>();//存储窗口内字符跟位置,用于判断是否重复
int i;
for (i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
Integer value = map.get(ch);
if (map.containsKey(ch)) {//出现重复字符
start = value + 1;//保存下一个窗口值
len = 0;窗口长度归0
map = new HashMap<>();//窗口内字符置空
i=value;//下次从重复的值位置开始找最长不重复串
} else {
map.put(ch, i);//不存在重复的,就存入map
len++;//每次进来长度自增1
if (len > maxlen) {//如果当前的窗口长度>最长字符串则,更新最长串,跟最长子串开始位置
maxlen = len;
startMaxIndex = start;
}
}
}
return s.substring(startMaxIndex, (startMaxIndex + maxlen));//截取字符串
}
从第一个下标开始找最长不重复串0:n,1:n,…n-1:n最后筛选出最长串
最好情况下不发生回溯时间复杂度O(n)
最坏情况,每次都回溯O(2n)
平均值为O(3/2 *n)
解法三 滑动窗口无回溯
/**
* 滑动窗口:保证窗口中都是不重复的子串实现 _无回溯
* @param s
* @return
*/
public static String getMaxsub(String s) {
if (s == null || s.length() == 0) {
return null;
}
int start = 0;//滑动窗口的开始值
int maxlen = 0;
int len = 0;
int startMaxIndex = 0;//最长子串的开始值
Map<Character, Integer> map = new HashMap<>();
int i;
for (i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
Integer value = map.get(ch);
if (value != null && value >= start) {//字符重复,且字符的位置在窗口范围内,此时窗口需要向右伸缩
start = value + 1;//新窗口开始位置,重复字符的下一个位置
len = i - value;//长度为当前的遍历字符的位置-重复字符的位置
} else {
len++;//若不存在,则字符+1
if (len > maxlen) {//若当前窗口的长度>最长子串的位置,则更新最长子串的长度跟最长子串的起始位置
maxlen = len;
startMaxIndex = start;
}
}
map.put(ch, i);//遍历的 字符存入map中,重复的则覆盖用于判断是否重复
}
return s.substring(startMaxIndex, (startMaxIndex + maxlen));
}
思路分析,有回溯的滑动串口是不断扩大直到出现重复,窗口后移重复查找的过程,无回溯的方法,就要让窗口一直向前,实现一个可伸缩的窗口
如图,可理解为一个动态可伸缩窗口,窗口不断向前,右移的条件是窗口内出现重复字符,则窗口右移至窗口内第一个重复字符的下一个位置,舍弃的字符虽然是不重复的,但是肯定没有已经记录的最大子串长度更长且不会再变长,所以可以舍弃继续遍历字符拉伸窗口长度
很明显无回溯时间复杂度为:O(n)
如果理解了第三种方法,下面的方法更加简洁巧妙
public int lengthOfLongestSubstring(String s) {
int r = 0, l = 0, res = 0;
HashSet<Character> set = new HashSet<>();
while(r<s.length()){
char c = s.charAt(r++);
while(set.contains(c)){
set.remove(s.charAt(l++));
}
set.add(c);
res = Math.max(res,set.size());
}
return res;
}