美文网首页
算法 1.8.2 至多包含两个不同字符的最长子串【leetcod

算法 1.8.2 至多包含两个不同字符的最长子串【leetcod

作者: 珺王不早朝 | 来源:发表于2021-01-11 23:40 被阅读0次

    题目描述

    给定一个字符串 s ,找出 至多 包含两个不同字符的最长子串 t ,并返回该子串的长度。

    示例 1:
      输入: "eceba"
      输出: 3

    解释:
      t 是 "ece",长度为3


    示例 2:
      输入: "ccaabbb"
      输出: 5

    解释:
      t 是 "aabbb",长度为5。

    数据结构

    • 指针、哈希表

    算法思维

    • 遍历、双指针、哈希(散列)

    解题要点

    • “范围问题”“同步变化” ==> 双指针
    • “快速查找”“重复匹配” ==> 哈希表

    解题思路


    一. Comprehend 理解题意
    • 要求是子串,不是子序列
    • 子串最多由两个不同字符 按任意数量 排列组合而成
    • 不要求记录子串,只需要记录长度

    二. Choose 选择数据结构与算法
    双指针 + 哈希表 解法
    • 数据结构:指针、哈希表
    • 算法思维:遍历、双指针、哈希(散列)
    解题思路
    1. 声明一个哈希表和一个哈希函数
        哈希函数用于生成字符索引
        哈希表存放该字符在子串中出现的个数
    2. 声明一个整形变量存放“不同字符”的个数,按照题目要求,此个数应当 <= 2
    3. 用左右两个指针遍历整个字符串
        判断右指针字符是否是“新字符”
          如果是新字符,不同字符数 +1
          无论是否为新字符,右指针在哈希表中相应位置的数据都要 +1,表示右指针字符在子串中出现的次数 +1
          不断判断不同字符数是否 > 2
            如果大于2,左指针在哈希表中相应位置的数据 -1,表示左指针字符在子串中出现的次数 -1
            判断左指针在哈希表中相应位置的数据是否 == 0
              如果不为 0,说明当前删除的左指针字符不是子串中的“最后一个”,右移左指针
              如果为 0,说明当前删除的左指针字符是子串中的“最后一个”,不同字符数 -1
    4. 不管任何情况,右指针都照常右移
    5. 每次右指针右移时,计算最长子串的长度
    边界问题
    • 右指针(b)领先左指针(a),因此边界条件为:b < s.length()
    细节问题
    • 子串长度的计算公式:
      如果先计算长度,右指针再右移,则 subLen = b - a +1
      如果右指针先右移,再计算长度,则 subLen = b - a

    三. Code 编码实现基本解法
    class Solution {
        public int lengthOfLongestSubstringTwoDistinct(String s) {
    
            int len = s.length();
    
            //定义哈希表
            int[] hashtable = new int[128];
            for (int i = 0; i < 128; i++){
                hashtable[i] = 0;
            }
    
            int a = 0; //左指针
            int b = 0; //右指针
            int count = 0; //不同字符的个数
            int subLen = 0; //最长子串的长度
            
            //遍历字符串
            while (b < len) {
                char cb = s.charAt(b);
                //如果 b 处字符不存在于哈希表中
                if (hashtable[hash(cb)] == 0){
                    count++; //不同字符个数 +1
                }
                //哈希表对应位置 +1
                hashtable[hash(cb)]++;
                
                //直到不同字符个数 <= 2
                while (count > 2){
                    char ca = s.charAt(a);
                    hashtable[hash(ca)]--; //哈希表中 a 处字符对应位置 -1
                    //如果哈希表中 a 处字符对应位置不为 0
                    if (hashtable[hash(ca)] == 0){
                        count--; //不同字符个数 -1
                    }
                    a++; //a指针右移
                }
                
                //计算子字符串长度
                subLen = Math.max(subLen,b-a+1);
                b++;
            }
            return subLen;
        }
    
        //定义哈希函数
        private int hash(char c){
            return c;
        }
    }
    

    执行耗时:1 ms,击败了 100.00% 的Java用户
    内存消耗:36.6 MB,击败了 99.63% 的Java用户
    时间复杂度:O(n) -- 两次链表的遍历 O(n),查询哈希表 O(1)
    空间复杂度:O(1) -- 一个哈希表 O(1),两个指针 O(1),两个整数变量 O(1)

    四. Consider 思考更优解
    改变算法思维
    • 使用“滑动窗口”的算法思维解决问题

    五. Code 编码实现最优解
    优化解法
    /**
         * 解法一:采用滑动窗口的算法思想,选取 left-right 子串进行验证
         * 如果字符数量没有超过2,则记录此时的最大长度,并且 right++
         * 如果字符数量超过2,则 left++
         * 使用 map 存放出现过的字符及次数
         *
         * (优化算法,使用数组替代 map)
         */
        public int lengthOfLongestSubstringTwoDistinct1(String s) {
            char[] chars = s.toCharArray();
            int len = chars.length;
            Map<Character, Integer> map = new HashMap<>();
            int left = 0, right = 0;
            int count = 0;//不同的字符数量
            int maxLength = 0;
            while (right < len) {
                //在map中获取该字符出现的次数(不存在则为0),次数+1
                int rightNumber = map.getOrDefault(chars[right], 0) + 1;
                map.put(chars[right], rightNumber);//更新map
                if (rightNumber == 1) {//第一次出现
                    count++;
                }
                right++;
                if (count <= 2) {
                    maxLength = Math.max(maxLength, right - left);
                }
                while (count > 2) {
                    //左侧移除一个字符,在map中获取该字符出现的次数-1
                    int leftNumber = map.get(chars[left]) - 1;
                    map.put(chars[left], leftNumber);//更新map
                    if (leftNumber == 0) {
                        count--;
                    }
                    left++;
                }
            }
            return maxLength;
        }
    
    最优解
    /**
         * 最优解
         * 改进1:考虑 ASCII 码表中的128个字符,可以使用数组代替 map,存放每个字符出现的次数
         * 改进2:滑动窗口 只会扩大或者平移(我们要取的就是最大窗口长度)
         *
         * @param s
         * @return
         */
        public int lengthOfLongestSubstringTwoDistinct(String s) {
            final int length = s.length();
            final int[] map = new int[128];
            int right = 0, left = 0;
            //count 为不同字符的数量
            for (int count = 0; right < length; ) {
                //右侧新字符进入窗口
                if (map[s.charAt(right++)]++ == 0) {
                    count++;
                }
                //如果新字符进入窗口后使得 不同字符数量大于2,则左侧窗口也向右滑动一个(窗口平移)
                if (count > 2) {
                    if (--map[s.charAt(left++)] == 0) {
                        count--;
                    }
                }
            }
            return right - left;
        }
    

    执行耗时:4 ms,击败了 85.81% 的Java用户
    内存消耗:36.8 MB,击败了 96.08% 的Java用户
    时间复杂度:O(n) -- 字符串的遍历 O(n),哈希表的查询 O(1)
    空间复杂度:O(1) -- 定长的哈希表 O(1),双指针 O(1)

    六. Change 变形与延伸

    === 待续 ===

    相关文章

      网友评论

          本文标题:算法 1.8.2 至多包含两个不同字符的最长子串【leetcod

          本文链接:https://www.haomeiwen.com/subject/kzmjaktx.html