无重复字符的最长子串

题目

  • 给定一个字符串,请你找出其中不含有重复字符的最长子串的长度

实例1

输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。

示例 2:

输入: “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。

示例 3:

输入: “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。

解析

方法一:暴力法

暴力法的时间复杂度较高,有可能会出现超时的现象,这里不推荐使用

思路

  • 逐个检查所有的子字符串,看他是否不含有重复的字符

算法

  • 假设我们有一个函数,该函数用来检查子子字符串的所有字符是否是唯一的,若是唯一的他会返回true,否则返回false。我们可以遍历给定字符串s的所有可能的字符串并调用检查字符子串是否唯一的函数,如果返回值为true,则得到了这个满足条件的一个字符子串,且可以得到其当前长度,与之前记录的子串长度做对比,取最大的子串长度,将值记录在指定变量中
  • 为了枚举给定字符串的所有的子字符串,我们需要枚举他们开始和结束的索引,假设开始和结束的索引分别为i和j。那么我们有0 <= i < j <= n。因此,使用i从0到n-1以及j从i+1到n的这两个嵌套的循环,我们可以枚举出s的所有子字符串。
  • 要检查一个字符串是否有重复字符,我们可以使用集合。我们遍历字符串中的所有字符,并且将其逐个放到set中,在放置一个字符之前,我们检查该集合是否已经包含他。如果包含,表示的是有重复的字符,返回false,如果不包含,继续循环,直至循环结束,返回true

代码实现

public class Solution {
    /**
     * 传过来的参数是字符串 用来获取s字符串中不重复的子串的最大的长度
     * @param s
     * @return
     */
    public int lengthOfLongestSubstring(String s) {
        // 字符串的长度
        int n = s.length();
        // 当前最大的子串长度是0
        int ans = 0;
        // 从0 开始,到最后一个结束
        for (int i = 0; i < n; i++) {
            // 从i+1开始,到最后一个下标+1结束(因为j是作为终止下标的,取前不取后,所以后面的判断是<j的,不存在下标越界)
            for (int j = i + 1; j <= n; j++){
                // 判断字符串s的[i,j)是否有重复的字符
                if (allUnique(s, i, j)) {
                    // 选取得到最大的长度
                    ans = Math.max(ans, j - i);
                }
            }
        }
        return ans;
    }

    /**
     * 检查字符子串是否有重复的字符
     * @param s 字符串
     * @param start 当前字符子串的起始下标
     * @param end 当前字符子串的终止下标
     * @return
     */
    public boolean allUnique(String s, int start, int end) {
        // 定义一个Set,该集合用来保存子串的字符,判断是否有重复
        Set<Character> set = new HashSet<Character>();
        // 根据起始下标和终止下标,进行遍历
        for (int i = start; i < end; i++) {
            // 取到当前下标对应的字符
            Character ch = s.charAt(i);
            // 如果集合中有这个字符了,表示已经重复了
            if (set.contains(ch)) {
                // 子串重复,直接返回false
                return false;
            }
            // 若当前子串这个i对应的字符,不在set中,则不重复,添加到set中,继续遍历
            set.add(ch);
        }
        // 返回true,表示字符串s的start到end下标期间没有重复的字符(下标取前不取后)
        return true;
    }
}

复杂度分析

  • 时间复杂度: O(n^3)
  • 空间复杂度: O(min(n,m))
方法二:滑动窗口

算法

  • 暴力算法非常的简单,但是太慢了,我们该如何优化他?
  • 在暴力法中,我们会反复检查一个子字符串中是否含有重复的字符,但这是没有必要的。如果从索引i到j-1之间的子字符S(i, j)已经被检查为没有重复的字符。我们只需要检查S(j)对应的字符是否已经存在于子字符串S(i, j)中
  • 要检查一个字符是否已经存在子字符串中,我们会检查整个子字符串,这将产生一个时间复杂度为O(n^2)的算法,但是我们可以做的更好。
  • 利用HashSet作为滑动窗口,我们可以用O(1)的时间来完成对字符是否在当前的子字符串中的检查
  • 滑动窗口是数组/字符串问题中常用的抽象概念。窗口通常是在数组/字符串中由开始和索引定义的一系列元素的集合,即[i, j)(左闭,右开)。而滑动窗口是可以将两个边界向某一个方向“滑动”的窗口。例如,我们将[i, j)向右滑动了一个元素,则它将变为[i+1, j+1)(左闭右开)
  • 回到我们的问题,我们使用HashSet将字符存储在当前窗口[i, j)(最初j==i)中。然后我们向右侧滑动索引j,如果他不在HashSet中,我们会继续滑动j。直到s[j]已经存在于HashSet中,此时,我们找到没有重复的字符的最长子字符串将会以i开头。
  • 那么如何确定下一次的i的位置呢?我们只需要移动i的下标,就是类似将左窗口向 j 移动,若s[i1]等于s[j]则下一次开始的i的位置为i1 + 1

java代码实现

    /**
     * 用滑动窗口解决寻找字符串s中最大的不重复子串的长度
     * @param s
     * @return
     */
    public int lengthOfLongestSubstring(String s) {
        // 字符串的长度
        int n = s.length();
        // 该集合用来存储子串
        Set<Character> set = new HashSet<Character>();
        // ans是用来存储最大子串的长度的
        // i是窗口的左边界
        // j是窗口的右边界
        int ans = 0, i = 0, j = 0;
        // 当左边界和右边界都小于字符串长度的时候,进入循环
        while (i < n && j < n) {
            // 判断当前窗口右边界的元素是否在set集合中
            if (!set.contains(s.charAt(j))){
                // 若不在集合中,表示没有重复,则将其放到集合中
                set.add(s.charAt(j++));
                // 计算长度
                ans = Math.max(ans, j - i);
            }
            else {
                // 如果当前的右边界的元素在集合中
                // 我们需要跳到,当前集合中存这个s.chartAt(j)的元素的位置,所以有了下面的方法
                // 如果当前元素(s.chartAt(j))和当前set的元素不相等,表示的是不是我们要找的下标,将其从set中删除
                // 删除set的原因就是如果从索引i到j-1之间的子字符S(i, j)已经被检查为没有重复的字符。我们只需要检查S(j)对应的字符是否已经存在于子字符串S(i, j)中
                // Set表示的就是窗口
                set.remove(s.charAt(i++));
            }
        }
        return ans;
    }

复杂度分析

  • 时间复杂度:O(2n) = O(n),在最糟糕的情况下,每个字符将被i和j访问两次(s[i1] = s[j] ; i1==j -1)
  • 空间复杂度:O(min(m,n)),与之前的方法相同,滑动窗口法,需要O(k)的空间,其中k表示Set的大小,而Set的大小取决于字符串n的大小以及字符集/字母m的大小
方法三:优化的滑动窗口
  • 上述的方法最多需要执行2n个步骤,事实上,他可以被进一步的优化为只需要n个步骤。我们可以定义字符到索引的映射,而不是使用集合来判断一个字符是否存在。当我们找到重复的字符的时候,我们可以立即跳过该窗口。
  • 也就是说,如果s[j]在[i, j)范围内有与j1重复的字符,我们不需要不断的增加i,我们可以直接跳过[i, j1]范围内的所有元素,并将i变为j1 + 1

java代码实现

    /**
     * 优化滑动窗口的解法
     * @param s 当前传进来的字符串
     * @return
     */
    public int lengthOfLongestSubstring3(String s) {
        // n表示字符串的长度
        // ans表示的是最大的长度
        int n = s.length(), ans = 0;
        // 窗口这里用Map来表示
        Map<Character, Integer> map = new HashMap<>();
        // i是左窗口边界,j是右窗口边界
        for (int j = 0, i = 0; j < n; j++) {
            // 判断当前窗口是否存在当前遍历到的这个元素
            if (map.containsKey(s.charAt(j))) {
                // 当前元素重复,在窗口中存在
                // 移动左边界,将其移动到相同字符的下一个位置和i当前位置中更靠右的位置,这样就是为了防止i向左移动
                i = Math.max(map.get(s.charAt(j)), i);
            }
            // 如果不存在窗口中,则将其右窗口向右移动,左窗口不动
            ans = Math.max(ans, j - i + 1);
            // 存入map,其中key是元素的值,value是当前值的下一个坐标
            map.put(s.charAt(j), j + 1);
        }
        return ans;
    }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值