算法日记 - Day3

无重复字符的最长子串

我们先说思路

  • 因为考虑是要最长子串,没有重复字符,所以利用 Set 存储已经遍历过的字符。
  • 当遇到一个我们已经存储过的字符时,比如 a,那我们就需要从最前面把遍历过的字符直到我们之前读过 a 去掉,来保证这次新的子串没有重复字符
  • 当遇到一个我们已经存储过的字符时,也就是我们统计子串长度的时候,不过还需要注意,如果只有一个字符或者是遍历到数组末尾而没有出现字符重复时,是不会统计长度的,所以还需要最后再加上一次统计。
  • 那就还是利用双指针,i, j,负责表示当前子串的边界
java 复制代码
public int lengthOfLongestSubstring(String s) {
    Set<Character> charSet = new HashSet<>();
    char[] charArr = s.toCharArray();
    int length = s.length();

    int i = 0, j = 0, ans = 0;
    for (; i < length; i++) {
        if (!charSet.contains(charArr[i])) {
            charSet.add(charArr[i]);
            continue;
        }
        ans = Math.max(ans, i - j); // 子串是 [j, i)
        for (; j < i; j++) {
            if (charArr[j] == charArr[i]){
                j++;
                break;
            }
            charSet.remove(charArr[j]);
        }
    }
    ans = Math.max(ans, i - j);
    return ans;
}

当然,我们还可以做一个小小的优化,比如,当我们遍历时出现重复字符 a 时我们是从前面一个一个剔除找到我们之前遍历过的 a,那我们能不能直接就找到呢?

Set 换为 Map,由 Map 记录字符下标。但是还是有一个问题,比如 [a, b, b, a],当遍历到第一个 a 时加入了 Map,第一个 b 加入了 Map,第二个 b 时,是从 Map 中找到了第一个 b,并让 j = map.get('b') + 1,当前我们的子串是不包含 a 的,但是 Map 中是有的,之后我们遍历到第二个 a 时,啪唧,发现 Map 中有!我们能继续 j = map.get('a') + 1 吗,不,不可以,我们应该保持 j 不动,然后更新 Map 中到 a

java 复制代码
public int lengthOfLongestSubstring(String s) {
    Map<Character, Integer> map = new HashMap<>();

    int j = 0, ans = 0;

    for (int i = 0; i < s.length(); i++) {
        char c = s.charAt(i);

        if (!map.containsKey(c)) {
            map.put(c, i); // 不包含则加入
            ans = Math.max(ans, i - j + 1); // 更新答案
            continue;
        }

        j = Math.max(j, map.get(c) + 1); // 包含了,变动 j
        map.put(c, i); // 更新下标
        ans = Math.max(ans, i - j + 1);
    }

    return ans;
}

但是这样有点重复,putans 更新重复,所以改成下面这种。

java 复制代码
public int lengthOfLongestSubstring(String s) {
    Map<Character, Integer> map = new HashMap<>();

    int j = 0, ans = 0;
    for (int i = 0; i < s.length(); i++) {
        if (map.containsKey(s.charAt(i))) {
            j = Math.max(j, map.get(s.charAt(i)) + 1);
        }
        map.put(s.charAt(i), i); // 更新或者插入 charAt(i) 的 map
        ans = Math.max(ans, i - j + 1); // 每一轮计算长度
    }
    return ans;
}

找到字符串中所有字母异位词

  • sp 仅包含小写字母,那我们可以使用一个大小为 26 的数组统计字符数
  • 子串长度固定,为 p.length()

这个题目最简单的思路是,两个数组统计 p 的字符数,以及子串的字符数,然后比较,如果相等就是异位词,起始下标是答案的一种

java 复制代码
public List<Integer> findAnagrams(String s, String p) {
    if (s.length() < p.length()) return new ArrayList<>();

    int sLen = s.length(), pLen = p.length();
    int[] sCount = new int[26], pCount = new int[26];
    List<Integer> ans = new ArrayList<>();
    for (int i = 0; i < pLen; i++) {
        ++sCount[s.charAt(i) - 'a'];
        ++pCount[p.charAt(i) - 'a'];
    }

    if (Arrays.equals(sCount, pCount)) ans.add(0);
    
    for (int i = 0; i < sLen - pLen; ++i) {
    	//	 整体后移一位
        --sCount[s.charAt(i) - 'a'];
        ++sCount[s.charAt(pLen + i) - 'a'];

        if (Arrays.equals(sCount, pCount)) ans.add(i + 1);
    }
    
    
    return ans;
}

很明显,有可以优化的点,

  1. 我们不需要知道子串或者 p 有某个字符有多少个,我们只关注他们的差距
  2. 不需要每次都调用 Arrays.equals 进行比较,我们可以通过一个变量,计数 differ 记录有多少字符不满足个数一致,当它为 0 时就是一个答案。
java 复制代码
public List<Integer> findAnagrams(String s, String p) {
    if (s.length() < p.length()) return new ArrayList<>();

    int sLen = s.length(), pLen = p.length();
    int[] count = new int[26];
    int differ = 0;
    List<Integer> ans = new ArrayList<>();
    for (int i = 0; i < pLen; i++) {
        ++count[s.charAt(i) - 'a'];
        --count[p.charAt(i) - 'a']; // 位置信息表示子串多了几个,需要减去的个数
    }

    for (int i = 0; i < 26; i++) {
        if (count[i] != 0) differ += Math.abs(count[i]);
    }

    if (differ == 0) ans.add(0);
    
    for (int i = 0; i < sLen - pLen; ++i) {
        if (count[s.charAt(i) - 'a'] >= 1) differ--;
        else if (count[s.charAt(i) - 'a'] < 1) differ++;
        --count[s.charAt(i) - 'a'];

        if (count[s.charAt(pLen + i) - 'a'] <= -1) differ--;
        else if (count[s.charAt(pLen + i) - 'a'] > -1) differ++;
        ++count[s.charAt(pLen + i) - 'a'];

        if (differ == 0) ans.add(i + 1);
    }
    
    
    return ans;
}

区域和检索-数组不可变

典型前缀和

java 复制代码
class NumArray {

    private final int[] sums;

    public NumArray(int[] nums) {
	    sums = new int[nums.length];
    	if(nums.length == 0) return;
        sums[0] = nums[0];
        for (int i = 1; i < nums.length; i++) {
            sums[i] = sums[i - 1] + nums[i];
        }
    }
    
    public int sumRange(int left, int right) {
        if (left <= 0) return sums[right];
        return sums[right] - sums[left - 1];
    }
}

这里我们是用 sums[i] 表示数组 [0,i] 的和,但是初始化的时候我们需要额外考虑 sums[0],因为计算前缀和的时候是从下标 1 开始,需要额外初始化一下,另外计算 sumRange 的时候,我们计算 [left, right] 是使用 sums[right] - sums[left - 1],即 [0, right] - [0, left - 1] = [left, right],但是我们需要额外考虑 left = 0 的时候

所以这里还可以小优化一下,多定义一个 sums[0] = 0,然后用 sums[i + 1] 表示 [0, i] 下标值的和,

java 复制代码
class NumArray {

    private final int[] sums;

    public NumArray(int[] nums) {
        sums = new int[nums.length + 1];
        for (int i = 0; i < nums.length; i++) {
            sums[i + 1] = sums[i] + nums[i];
        }
    }
    
    public int sumRange(int left, int right) {
        return sums[right + 1] - sums[left];
    }
}
相关推荐
白白白小纯1 小时前
算法篇—反转链表
c语言·数据结构·算法·leetcode
萧瑟余晖1 小时前
Java深入解析篇九之NIO详解
java·网络·nio
Achou.Wang1 小时前
深入理解go语言-第5章 并发编程——Go的灵魂
大数据·算法·golang
韭菜炒鸡肝天2 小时前
VTK开发笔记(一):VTK介绍,Qt..+VSx+VTK.编译
开发语言·笔记·qt
The Chosen One9852 小时前
高进度算法模板速记(待完善)
java·前端·算法
Aaron - Wistron2 小时前
Web API C# (Furion版)带 单元测试
开发语言·后端·c#
Dxy12393102163 小时前
Python项目打包成EXE完整教程(PyInstaller实战避坑)
开发语言·python
0566464 小时前
Python康复训练——常用标准库
开发语言·python·学习
极光代码工作室4 小时前
基于SpringBoot的课程预约系统
java·springboot·web开发·后端开发