刘旭的个人网站

问题3 无重复字符的最长子串 ★☆☆☆☆

https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/

滑动窗口的解法可以从暴力解法的思路中得到启发。比如说使用暴力解法,我们遍历n次,每次以s[i]为起点开始遍历直到碰到了重复字符s[j],本次遍历结束,然后开始以s[i+1]为起点开始下一轮遍历。这样操作的问题在哪呢?就在于下一轮遍历以s[i+1]为起点,这样就太笨了。因为s[i]到s[j-1]是确定没有重复字符的,假设s[j]和s[i]到s[j-1]中的某个s[k]是重复的,那么至少应该从s[k+1]开始下一轮遍历,这样就减少了很多重复计算。

    public int lengthOfLongestSubstringWithoutRepeatingCharacters(String s) {
        int len = s.length();

        Map<Character, Integer> index = new HashMap<>();

        int max = 0;
        int left = 0;
        for (int right = 0; right < len; right++) {
            char c = s.charAt(right);
            if (index.containsKey(c)) {
                int lastIndex = index.get(c);
                if (lastIndex + 1 > left) {
                    left = lastIndex + 1;
                }
            }
            index.put(c, right);
            max = Math.max(max, right - left + 1);
        }

        return max;
    }

问题438 找到字符串中所有字母异位词 ★★☆☆☆

https://leetcode-cn.com/problems/find-all-anagrams-in-a-string/

这个题目可以从暴力解法的思路入手,即顺序遍历所有子串,判断每个子串是不是p的anagram. 第一个关键点,如何判断一个子串是不是p的anagram呢?比较直接的思路是使用两个计数数组,分别计数两个字符串中每个字母的个数,然后比较这两个数组的计数是否相等。第二个关键点,如何优化计算效率呢?题目标签滑动窗口具有一定的启发性,顺序遍历子串时后一个子串的计数可以基于前一个子串的计数而来,就像一个滑动的窗口,每次右移一位,吃进右侧字母的同时吐出左侧字母。

	public List<Integer> findAnagrams(String s, String p) {
		List<Integer> ret = new ArrayList<>();

		int plen = p.length();
		int slen = s.length();
		if (slen < plen) {
			return ret;
		}

		int[] target = new int[26];
		int[] rolling = new int[26];

		for (int i = 0; i < plen; i++) {
			target[p.charAt(i) - 'a']++;
			rolling[s.charAt(i) - 'a']++;
		}
		if (compare(target, rolling)) {
			ret.add(0);
		}

		for (int i = 1; i <= s.length() - plen; i++) {
			if (s.charAt(i-1) != s.charAt(i+plen-1)) {
				rolling[s.charAt(i-1) - 'a']--;
				rolling[s.charAt(i+plen-1) - 'a']++;
			}

			if (compare(target, rolling)) {
				ret.add(i);
			}
		}

		return ret;
	}

	private boolean compare(int[] target, int[] rolling) {
		for (int i = 0; i < 26; i++) {
			if (target[i] != rolling[i]) {
				return false;
			}
		}

		return true;
	}

问题567 字符串的排列 ★★☆☆☆

https://leetcode-cn.com/problems/permutation-in-string/

这个题目和问题438几乎是一样的,相比问题438还更简单些,只需要判断是否存在子串即可,不再赘述。

	public boolean checkInclusion(String s1, String s2) {
		int len1 = s1.length();
		int len2 = s2.length();

		if (len1 > len2) {
			return false;
		}

		int[] target = new int[26];
		int[] slide = new int[26];
		for (int i = 0; i < len1; i++) {
			target[s1.charAt(i) - 'a']++;
			slide[s2.charAt(i) - 'a']++;
		}

		if (Arrays.equals(target, slide)) {
			return true;
		} else {
			for (int i = 1; i <= len2 - len1; i++) {
				char out = s2.charAt(i - 1);
				char in = s2.charAt(i + len1 - 1);
				if (out == in) {
					continue;
				}
				slide[out - 'a']--;
				slide[in - 'a']++;
				if (Arrays.equals(target, slide)) {
					return true;
				}
			}
		}

		return false;
	}

问题209 长度最小的子数组 ★★☆☆☆

https://leetcode-cn.com/problems/minimum-size-subarray-sum/

这个题目可以算是比较典型的滑动窗口题目了,滑动窗口的解法理解起来并不难,循环不变量是,每次循环先右指针向右扩大窗口,持续扩大直到当前窗口内元素之和大于等于target,然后开始左指针向右缩小窗口,持续缩小直到当前窗口内元素之和小于target。在这个过程中,及时更新满足条件的最小子数组长度。

	public int minSubArrayLen(int target, int[] nums) {
		int min = Integer.MAX_VALUE;
		int left = 0, right = 0;
		int sum = 0;

		while (right < nums.length) {
			sum += nums[right];

			while (sum >= target) {
				min = Math.min(min, right - left + 1);
				sum -= nums[left];
				left++;
			}

			right++;
		}

		return min == Integer.MAX_VALUE ? 0 : min;
	}

问题713 乘积小于K的子数组 ★★☆☆☆

https://leetcode-cn.com/problems/subarray-product-less-than-k/

这个题目其实和问题209很像,利用滑动窗口来解答的思路都是,先向右扩展右指针,直到左右指针窗口内的元素不再满足某个要求,开始向右扩展左指针,在这个过程中,维护一个目标计算结果。

	public int numSubarrayProductLessThanK(int[] nums, int k) {
		if (k <= 1) {
			return 0;
		}

		int cnt = 0;

		int left = 0, right = 0, product = 1;
		while (right < nums.length) {
			product *= nums[right];

			while (product >= k) {
				product /= nums[left];
				left++;
			}

			cnt += right - left + 1;

			right++;
		}

		return cnt;
	}

问题187 重复的DNA序列 ★★★☆☆

https://leetcode-cn.com/problems/repeated-dna-sequences/

在知晓题目主标签是sliding window的条件下,还是比较容易想到通过滑动窗口来降低计算成本。但是具体怎样将一个固定长度的子串映射为某个特征值呢?一开始我想的是把这个子串当作一个26进制的数来计算,但是很快发现溢出问题。后面参考了官方题解才想到,为了保证计算速度,往2进制靠拢才是正确思路。

直接计算子串的哈希值,然后判断是否相同子串,这样可以吗?理论上,这样是不可以的,因为可能出现哈希碰撞,即相同子串的哈希值相等,但哈希值相等的子串不一定是相同子串。

另外,为什么题目背景是DNA序列呢?把字符串限定在由ACGT组成,其实就是为了简化二进制编码,只需要2位二进制位就可以完成对ACGT的编码。其实,即便题目放开为全部英文字母,这个题目的解决方法也是一样的,无非是需要5位二进制位才能编码26个英文字母。

	private Map<Character, Integer> map = new HashMap<Character, Integer>() ;

	public List<String> findRepeatedDnaSequences(String s) {
		if (s == null) {
			return Collections.emptyList();
		}

		int len = s.length();
		if (len <= 10) {
			return Collections.emptyList();
		}

		List<String> ret = new ArrayList<>();

		Map<Integer, Integer> cnt = new HashMap<>();

		int hash = 0;
		for (int i = 0; i < 10; i++) {
			hash = hash << 2 | map.get(s.charAt(i));
		}
		cnt.put(hash, 1);

		for (int i = 1; i <= len - 10; i++) {
			char in = s.charAt(i + 9);
			hash = hash << 2 | map.get(in);
			hash = hash & 0b00000000000011111111111111111111;

			cnt.put(hash, cnt.getOrDefault(hash, 0) + 1);

			if (cnt.get(hash) == 2) {
				ret.add(s.substring(i, i + 10));
			}
		}

		return ret;
	}