数组

大家好,我是老马。

今天我们一起来学习一下数组这种数据结构。

主要知识

数组需要拆分下面几个部分:

  1. 理论介绍

  2. 源码分析

  3. 数据结构实现?

  4. 题目练习(按照算法思想分类)

  5. 梳理对应的 sdk 包

  6. 应用实战

因为这个是 leetcode 系列,所以重点是 4、5(对4再一次总结)。

为了照顾没有基础的小伙伴,会简单介绍一下1的基础理论。

简单介绍1,重点为4。其他不是本系列的重点。

76. 最小覆盖子串

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。

如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 “” 。

注意:

对于 t 中重复字符,我们寻找的子字符串中该字符数量必须不少于 t 中该字符数量。 如果 s 中存在这样的子串,我们保证它是唯一的答案。

示例 1:

输入:s = “ADOBECODEBANC”, t = “ABC” 输出:”BANC” 解释:最小覆盖子串 “BANC” 包含来自字符串 t 的 ‘A’、’B’ 和 ‘C’。

示例 2:

输入:s = “a”, t = “a” 输出:”a” 解释:整个字符串 s 是最小覆盖子串。 示例 3:

输入: s = “a”, t = “aa” 输出: “” 解释: t 中两个字符 ‘a’ 均应包含在 s 的子串中, 因此没有符合条件的子字符串,返回空字符串。

提示:

m == s.length n == t.length 1 <= m, n <= 10^5 s 和 t 由英文字母组成

进阶:你能设计一个在 o(m+n) 时间内解决此问题的算法吗?

v1-暴力解法

思路

首先我们就是最朴素的暴力解法。

1)用 map 记录 t 所以字符出现的次数

2)遍历 s 中所有的可能性,找到满足条件的最短长度。

解法

    public String minWindow(String s, String t) {
        //
        char[] tcs = t.toCharArray();
        char[] scs = s.toCharArray();

        int left = 0;
        int right = 0;

        // 遍历所有的可能性
        String result = null;

        // 字符统计,可以优化为数组
        Map<Character, Integer> targetCountMap = new HashMap<>();
        for(char c : tcs) {
            Integer count = targetCountMap.getOrDefault(c, 0);
            count++;
            targetCountMap.put(c, count);
        }

        for(left = 0; left < s.length(); left++) {
            for(right = left; right < s.length(); right++) {
                // 判断字符的可能性
                int len = right-left+1;

                // 长度不够,直接跳过
                if(len < t.length()) {
                    continue;
                }

                // 然后判断
                Map<Character, Integer> sourceCountMap = new HashMap<>();
                StringBuilder stringBuilder = new StringBuilder();
                for(int i = left; i <= right; i++) {
                    char c = scs[i];
                    Integer count = sourceCountMap.getOrDefault(c , 0);
                    count++;
                    sourceCountMap.put(c, count);
                    stringBuilder.append(c);
                }

                // 判断是否满足条件
                if(isMatchCondition(sourceCountMap, targetCountMap)) {
                    if(result == null) {
                        result = stringBuilder.toString();
                    } else {
                        if(result.length() > stringBuilder.length()) {
                            result = stringBuilder.toString();
                        }
                    }
                }
            }
        }


        // 默认为空
        if(result == null) {
            result = "";
        }
        return result;
    }

    private boolean isMatchCondition(Map<Character, Integer> sourceCountMap,
                                     Map<Character, Integer> targetCountMap) {
        if(sourceCountMap.size() < targetCountMap.size()) {
            return false;
        }


        // t 的每个 char,s 都有大于等于的
        for(Map.Entry<Character, Integer> entry : targetCountMap.entrySet()) {
            Character tc = entry.getKey();
            Integer tCount = entry.getValue();

            Integer sCount = sourceCountMap.get(tc);
            if(sCount == null) {
                return false;
            }
            if(sCount < tCount) {
                return false;
            }
        }
        return true;

    }

效果

超出时间限制

224 / 268 个通过的测试用例

反思

这个属于最容易想到的解法,无任何优化。

v2-暴力版本优化

思路

其实可以换一种思路,我们首先构建 targetCharCountMap

然后用滑动窗口,每入栈一个 c,就在 targetCharCountMap 中减去对应的次数;每出栈一个,则加上。

次数为零,删除这个 char 对应的 key。

那么判断条件就变成了,如果 targetCharCountMap 为空,则满足条件。

然后可以提前跳出,我们主要改进下暴力的性能。

实现

    public String minWindow(String s, String t) {
        //
        char[] tcs = t.toCharArray();
        char[] scs = s.toCharArray();
        Map<Character, Integer> targetCountMap = new HashMap<>();
        for(char c : tcs) {
            Integer count = targetCountMap.getOrDefault(c, 0);
            count++;
            targetCountMap.put(c, count);
        }

        String result = null;
        int left = 0;
        int right = 0;
        for(left = 0; left < s.length(); left++) {
            // 然后判断
            Map<Character, Integer> tempTargetCountMap = new HashMap<>(targetCountMap);
            StringBuilder stringBuilder = new StringBuilder();

            for(right = left; right < s.length(); right++) {
                char c = scs[right];
                stringBuilder.append(c);

                Integer count = tempTargetCountMap.get(c);
                if(count != null) {
                    count--;
                    tempTargetCountMap.put(c, count);

                    if(count == 0) {
                        tempTargetCountMap.remove(c);
                    }
                }

                if(tempTargetCountMap.isEmpty()) {
                    // 满足
                    if(result == null) {
                        result = stringBuilder.toString();
                    } else {
                        if(result.length() > stringBuilder.length()) {
                            result = stringBuilder.toString();
                        }
                    }
                    // 当前循环终止
                    break;
                }
            }
        }

        // 默认为空
        if(result == null) {
            result = "";
        }
        return result;
    }

效果

超出时间限制 265 / 268 个通过的测试用例

反思

有一点进步,但是还不够。

那么要如何才可以优化呢?

v3-滑动窗口

思路

这里慢,其实主要慢在我们必须循环两次。

但是实际上是可以优化的。

假设我们定义 left right 两个位置。right 代表当前位置,left 代表开始位置。

最核心的一点在于,如果我们只能遍历一次,在满足条件的时候,left 什么时候可以向右?

答案是:在满足条件之后,我们可以尝试 left 向右,一直到不满足,说明 left-1 的位置是最后一个满足条件的。

解法

public String minWindow(String s, String t) {
        char[] tcs = t.toCharArray();
        final Map<Character, Integer> targetCountMap = new HashMap<>();
        for(char c : tcs) {
            Integer count = targetCountMap.getOrDefault(c, 0);
            count++;
            targetCountMap.put(c, count);
        }

        String result = null;
        int left = 0;
        Map<Character, Integer> tempMap = new HashMap<>();
        for(int right = 0; right < s.length(); right++) {
            addChar(s, right, tempMap);

            // 判断多少个字符满足条件,这里只是为了加速判断,先不做也行

            // 满足条件
            if(isMatchCondition(tempMap, targetCountMap)) {
                // 当条件满足的时候,可以尝试让 left 指针像右移动。一直到条件不符合的时候终止,此时 left 在最右边,距离最短。
                // 此时类似于模拟出栈
                while (isMatchCondition(tempMap, targetCountMap)) {
                    removeChar(s, left, tempMap);
                    left++;
                }
                // 恢复正常位置
                left--;
                addChar(s, left, tempMap);

                // 满足
                if(result == null) {
                    result = buildString(s, left, right);
                } else {
                    int len = right-left+1;
                    if(result.length() > len) {
                        result = buildString(s, left, right);
                    }
                }
            }
        }

        // 默认为空
        if(result == null) {
            result = "";
        }
        return result;
    }

    private void addChar(String s,
                            int index,
                            Map<Character, Integer> tempMap) {
        char c = s.charAt(index);
        Integer curCount = tempMap.getOrDefault(c, 0);
        curCount++;
        tempMap.put(c, curCount);
    }

    private void removeChar(String s,
                            int index,
                            Map<Character, Integer> tempMap) {
        char c = s.charAt(index);
        Integer curCount = tempMap.getOrDefault(c, 0);
        curCount--;
        tempMap.put(c, curCount);
    }

    // 这个应该也可以优化 这里为了逻辑清晰。先不优化
    private String buildString(String s, int left, int right) {
        StringBuilder stringBuilder = new StringBuilder();
        for(int i = left; i <= right; i++) {
            char c = s.charAt(i);
            stringBuilder.append(c);
        }
        return stringBuilder.toString();
    }

    // 后续可以用有多少个字符满足来加速判断,这里为了逻辑清晰。先不加。
    private boolean isMatchCondition(Map<Character, Integer> sourceCountMap,
                                     Map<Character, Integer> targetCountMap) {
        if(sourceCountMap.size() < targetCountMap.size()) {
            return false;
        }

        // t 的每个 char,s 都有大于等于的
        for(Map.Entry<Character, Integer> entry : targetCountMap.entrySet()) {
            Character tc = entry.getKey();
            Integer tCount = entry.getValue();

            Integer sCount = sourceCountMap.get(tc);
            if(sCount == null) {
                return false;
            }
            if(sCount < tCount) {
                return false;
            }
        }
        return true;

    }

效果

349ms 击败 5.66%

AC 了,但是性能很差。

反思

这里我们为了方便理解,加了很多基础的方法。

按理说,我们可以对这些方法都优化一下。

v4-滑动窗口性能优化

优化1-满足条件判断

思路

我们用一个 matchCharCount 来统计多少个字符满足条件,避免判断满足条件特别慢的问题

维护更新下这个字段即可。

代码

    public String minWindow(String s, String t) {
        char[] tcs = t.toCharArray();
        final Map<Character, Integer> targetCountMap = new HashMap<>();
        for(char c : tcs) {
            Integer count = targetCountMap.getOrDefault(c, 0);
            count++;
            targetCountMap.put(c, count);
        }

        String result = null;
        int left = 0;
        Map<Character, Integer> tempMap = new HashMap<>();
        int matchCharCount = 0;

        for(int right = 0; right < s.length(); right++) {
            // 判断多少个字符满足条件,这里只是为了加速判断,先不做也行
            char c = s.charAt(right);
            Integer curCount = tempMap.getOrDefault(c, 0);
            curCount++;
            tempMap.put(c, curCount);
            // 刚好等于时+1
            if(targetCountMap.containsKey(c)
                && targetCountMap.get(c).equals(curCount)) {
                matchCharCount++;
            }

            // 满足条件
            if(matchCharCount == targetCountMap.size()) {
                // 当条件满足的时候,可以尝试让 left 指针像右移动。一直到条件不符合的时候终止,此时 left 在最右边,距离最短。
                // 此时类似于模拟出栈
                while (matchCharCount == targetCountMap.size()) {
                    char leftChar = s.charAt(left);

                    int leftCharCount = tempMap.getOrDefault(leftChar, 0);
                    leftCharCount--;
                    tempMap.put(leftChar, leftCharCount);
                    // +1 刚好等于目标
                    if(targetCountMap.containsKey(leftChar)
                            && targetCountMap.get(leftChar).equals(leftCharCount+1)) {
                        matchCharCount--;
                    }

                    // 向右移动
                    left++;
                }
                // 恢复正常位置
                left--;
                char leftRecoveryChar = s.charAt(left);
                tempMap.put(leftRecoveryChar, targetCountMap.get(leftRecoveryChar));
                matchCharCount++;

                // 满足
                if(result == null) {
                    result = buildString(s, left, right);
                } else {
                    int len = right-left+1;
                    if(result.length() > len) {
                        result = buildString(s, left, right);
                    }
                }
            }
        }

        // 默认为空
        if(result == null) {
            result = "";
        }
        return result;
    }

效果

35ms 击败 31.70%

优化2-移除 stringBuilder

思路

其实,直接通过 s.substring(left, right + 1) 来计算结果更加直接。

实现

public String minWindow(String s, String t) {
        char[] tcs = t.toCharArray();
        final Map<Character, Integer> targetCountMap = new HashMap<>();
        for(char c : tcs) {
            Integer count = targetCountMap.getOrDefault(c, 0);
            count++;
            targetCountMap.put(c, count);
        }

        String result = null;
        int left = 0;
        Map<Character, Integer> tempMap = new HashMap<>();
        int matchCharCount = 0;

        for(int right = 0; right < s.length(); right++) {
            // 判断多少个字符满足条件,这里只是为了加速判断,先不做也行
            char c = s.charAt(right);
            Integer curCount = tempMap.getOrDefault(c, 0);
            curCount++;
            tempMap.put(c, curCount);
            // 刚好等于时+1
            if(targetCountMap.containsKey(c)
                && targetCountMap.get(c).equals(curCount)) {
                matchCharCount++;
            }

            // 满足条件
            if(matchCharCount == targetCountMap.size()) {
                // 当条件满足的时候,可以尝试让 left 指针像右移动。一直到条件不符合的时候终止,此时 left 在最右边,距离最短。
                // 此时类似于模拟出栈
                while (matchCharCount == targetCountMap.size()) {
                    char leftChar = s.charAt(left);

                    int leftCharCount = tempMap.getOrDefault(leftChar, 0);
                    leftCharCount--;
                    tempMap.put(leftChar, leftCharCount);
                    // +1 刚好等于目标
                    if(targetCountMap.containsKey(leftChar)
                            && targetCountMap.get(leftChar).equals(leftCharCount+1)) {
                        matchCharCount--;
                    }

                    // 向右移动
                    left++;
                }
                // 恢复正常位置
                left--;
                char leftRecoveryChar = s.charAt(left);
                tempMap.put(leftRecoveryChar, targetCountMap.get(leftRecoveryChar));
                matchCharCount++;

                // 满足
                if(result == null) {
                    result = s.substring(left, right + 1);
                } else {
                    int len = right-left+1;
                    if(result.length() > len) {
                        result = s.substring(left, right + 1);
                    }
                }
            }
        }

        // 默认为空
        if(result == null) {
            result = "";
        }
        return result;
    }

效果

25ms 击败 36.36%

有提升,但是因为大家的算法比较优,看起来百分比不大

优化三-数组哈希替代 HashMap

思路

对于字符集合有限的,我们可以用 index 数组,来替代 hashMap。

代码也可以变得更加优雅。

实现

    public String minWindow(String s, String t) {
        char[] tcs = t.toCharArray();
        int[] targetCountMap = new int[128];
        for(char c : tcs) {
            targetCountMap[c]++;
        }
        // 这里用 set 会更快吗?
        int targetCharSize = 0;
        for(int i = 0; i < 128; i++) {
            if(targetCountMap[i] > 0) {
                targetCharSize++;
            }
        }

        String result = null;
        int left = 0;
        int[] tempMap = new int[128];
        int matchCharCount = 0;

        for(int right = 0; right < s.length(); right++) {
            // 入
            char c = s.charAt(right);
            tempMap[c]++;
            // 刚好等于时+1
            if(targetCountMap[c] > 0
                    && targetCountMap[c] == tempMap[c]) {
                matchCharCount++;
            }

            // 满足条件
            if(matchCharCount == targetCharSize) {
                // 当条件满足的时候,可以尝试让 left 指针像右移动。一直到条件不符合的时候终止,此时 left 在最右边,距离最短。
                // 此时类似于模拟出栈
                while (matchCharCount == targetCharSize) {
                    // 移除字符
                    char leftChar = s.charAt(left);
                    tempMap[leftChar]--;
                    // +1 刚好等于目标
                    if(targetCountMap[leftChar] > 0
                            && targetCountMap[leftChar] == (tempMap[leftChar]+1)) {
                        matchCharCount--;
                    }

                    // 向右移动
                    left++;
                }
                // 恢复正常位置
                left--;
                char leftRecoveryChar = s.charAt(left);
                tempMap[leftRecoveryChar] = targetCountMap[leftRecoveryChar] ;
                matchCharCount++;

                // 满足
                if(result == null) {
                    result = s.substring(left, right + 1);
                } else {
                    int len = right-left+1;
                    if(result.length() > len) {
                        result = s.substring(left, right + 1);
                    }
                }
            }
        }

        // 默认为空
        if(result == null) {
            result = "";
        }
        return result;
    }

效果

4ms 击败 78.39%

此时已经是第一梯队的算法。

可以发现,同样的算法,使用基本类型,其实性能提升非常大。

小结

希望本文对你有帮助,如果有其他想法的话,也可以评论区和大家分享哦。

各位极客的点赞收藏转发,是老马持续写作的最大动力!

感兴趣的小伙伴可以关注一波,精彩内容,不容错过。