力扣刷题笔记

剑指 Offer II 004. 只出现一次的数字

给你一个整数数组nums,除某个元素仅出现一次外,其余每个元素都恰出现三次。请你找出并返回那个只出现了一次的元素。
题解一 哈希表
用哈希表存储每一个元素及其出现的次数。

public int singleNumber(int[] nums) {
    Map<Integer, Integer> map = new HashMap<>();
    for (int x : nums) map.put(x, map.getOrDefault(x, 0) + 1);
    for (int x : map.keySet()) if (map.get(x) == 1) return x;
	return -1;
}

题解二 位数统计
直接确定答案数的每一个二进制位。出现三次的元素,每个二进制位的和都是3个1或3个0,因此对3取模为0。因此可以将所有元素的每个二进制位求和,对3取模的余数即是答案数在该位上的取值(余数只可能为0或1)。

public int singleNumber(int[] nums) {
	int ans = 0;
    for (int i = 0; i < 32; ++i) {
    	int total = 0;
        for (int num: nums) total += ((num >> i) & 1);
       	if (total % 3 != 0) ans |= (1 << i);
	}
    return ans;
}

题解三 DFA
针对题解二,该方法将所有二进位当作整体同时处理。用aibi表示当前第i个二进制位求和后对3取模的余数,用xi表示当前数的第i位。用00、01、10分别表示余数为0、1、2。如果用aibi表示当前状态,xi表示输出,那么问题即可用DFA表示:

采用真值表给出ai和bi的表达式(这里的i表示某一时刻而非位数):
{ a i = ( ¬ a i − 1 ∧ b i − 1 ∧ x ) ∨ ( a i ∧ ¬ b i − 1 ∧ ¬ x ) b i = ¬ a i − 1 ∧ ( b i − 1 ⊕ x ) \begin{cases} a_{i}=(\neg a_{i-1}\wedge b_{i-1}\wedge x)\vee(a_{i}\wedge \neg b_{i-1}\wedge \neg x)\\ b_{i}=\neg a_{i-1}\wedge(b_{i-1}\oplus x)\\ \end{cases} {ai=(¬ai1bi1x)(ai¬bi1¬x)bi=¬ai1(bi1x)
上述表达式还可进一步简化为:
{ b i = ¬ a i − 1 ∧ ( b i − 1 ⊕ x ) a i = ¬ b i ∧ ( a i − 1 ⊕ x ) \begin{cases} b_{i}=\neg a_{i-1}\wedge(b_{i-1}\oplus x)\\ a_{i}=\neg b_{i}\wedge(a_{i-1}\oplus x)\\ \end{cases} {bi=¬ai1(bi1x)ai=¬bi(ai1x)

public int singleNumber(int[] nums) {
    int a = 0, b = 0;
    for (int num : nums) {
    	b = ~a & (b ^ num);
        a = ~b & (a ^ num);
    }
	return b;
}

剑指 Offer II 005. 单词长度的最大乘积

给定一个字符串数组words,请计算当两个字符串words[i]和words[j]不包含相同字符时,它们长度的乘积的最大值。假设字符串中只包含英语的小写字母。如果没有不包含相同字符的一对字符串,返回0。
题解 位掩码
由于小写字母组成的单词最多有26个不同字符,因此可以用一个26位二进制数组成的掩码来某个单词的状态,第i位为1表示第i个字符出现过。
优化:当两个不同单词组成的掩码相同时,只需要留下长度更大的那个,因此可以用哈希表来存储每个掩码及其对应的最大单词长度。

public int maxProduct(String[] words) {
    Map<Integer, Integer> map = new HashMap<>();
	for (String word : words) {
    	int mask = 0;
        for (char c : word.toCharArray()) mask |= 1 << (c-'a');
        int len = word.length();
        if (len > map.getOrDefault(mask, 0)) map.put(mask, len);
    }
    int maxPro = 0;
    Set<Integer> maskSet = map.keySet();
	for (int mask1 : maskSet) {
    	int len = 0;
        for (int mask2 : maskSet) if ((mask1 & mask2) == 0) len = Math.max(len, map.get(mask2));
    	maxPro = Math.max(maxPro, map.get(mask1) * len);
    }
	return maxPro;
}

剑指 Offer II 007. 数组中和为 0 的三个数

给你一个整数数组nums,判断是否存在三元组[nums[i],nums[j],nums[k]]满足i!=j、i!=k且j!=k,同时还满足nums[i]+nums[j]+nums[k]==0。请你返回所有和为0且不重复的三元组。
题解 排序+双指针
当枚举的三元组 ( a , b , c ) (a,b,c) (a,b,c)满足 a ≤ b ≤ c a\leq b\leq c abc 时,并且在遍历这三个元素中的任意一个时,如果都能够保证相邻两次枚举到的值不同,那么就能够保证前后枚举到的两个三元组不重复。此外,当固定 a a a b b b时,只有唯一的 c c c满足 a + b + c = 0 a+b+c=0 a+b+c=,并且对于 b ′ > 0 , a + b ′ + c ′ = 0 b'>0,a+b'+c'=0 b>0,a+b+c=0,一定有 c ′ < c c'<c c<c。基于上述思想,可以在对 a a a遍历的基础上,设置双指针,第一个指针从小到大枚举 b b b,第二个指针从大到小枚举 c c c,同时满足第一个指针必须小于第二个指针。

public List<List<Integer>> threeSum(int[] nums) {
		Arrays.sort(nums);
        List<List<Integer>> ans = new ArrayList<>();
        for (int first = 0; first < nums.length; first++) {
        	if (first > 0 && nums[first] == nums[first - 1]) continue;
        	for (int second = first + 1, third = nums.length - 1; second < third ; second++) {
        		if (second > first + 1 && nums[second] == nums[second-1]) continue;
                while (second < third && nums[first] + nums[second] + nums[third] > 0) third--;
        		if (second >= third) break;
        		if (nums[first] + nums[second] + nums[third] == 0) ans.add(Arrays.asList(nums[first], nums[second], nums[third]));
        	}
        }
        return ans;
    }

剑指 Offer II 008. 和大于等于 target 的最短子数组

给定一个含有n个正整数的数组和一个正整数target。找出该数组中满足其和≥target的长度最小的连续子数[ n u m s l num_{s_{l}} numsl, n u m s l + 1 num_{s_{l+1}} numsl+1,…, n u m s r − 1 num_{s_{r-1}} numsr1, n u m s r num_{s_{r}} numsr],并返回其长度。如果不存在符合条件的子数组,返回0。
题解一 前缀和+二分查找
首先构造一个数组sum,sum[i]表示前i个元素的前缀和,i=1,2,…,n。于是有递推式:sum[i]=sum[i-1]+num[i-1],sum[0]=0。由于题目给的数组元素都是正整数,前缀和严格递增,故问题描述如下:在固定i的情况下考虑用二分法找出最小的j满足:sum[j]-sum[i] ≥ \geq target。

public int minSubArrayLen(int t, int[] nums) {
	int n = nums.length, ans = n + 1, sum[] = new int[n + 1];
    for (int i = 1; i <= n; i++) sum[i] = sum[i - 1] + nums[i - 1];
    for (int i = 0; i < n; i++) {
    	int s = sum[i], d = s + t;
        int l = i + 1, r = n;
        while (l < r) {
        	int mid = l + r >> 1;
            if (sum[mid] >= d) r = mid;
            else l = mid + 1;
        }
        if (sum[l] >= d) ans = Math.min(ans, l-i);
    }
	return ans == n + 1 ? 0 : ans;
}

题解二 滑动窗口
定义两个指针 s t a r t start start e n d end end,分别表示滑动窗口的开始和结束位置,用于维护变量 s u m sum sum( s u m sum sum= ∑ i = s t a r t e n d n u m [ i ] \sum_{i=start}^{end}num[i] i=startendnum[i])。初始状态下, s t a r t start start e n d end end都指向0。每一轮迭代,如果 s u m ≥ t a r g e t sum\geq target sumtarget,更新最小窗口长度并将 s t a r t start start后移;否则将 e n d end end后移。直到 e n d = n end=n end=n,循环结束。
接下来证明正确性。记最小窗口长度为 L m i n L_{min} Lmin A = { x ∣ x ∈ N , 0 ≤ x < n } \mathbf{A}=\{x\vert x\in\mathbf{N},0\leq x< n\} A={xxN,0x<n},需要证明: ∀ ( i , j ) ∈ A × A \forall(i,j)\in\mathbf{A}\times\mathbf{A} (i,j)A×A,一定有 j − i ≥ L m i n j-i\geq L_{min} jiLmin
证明:反证法,假设 ∃ ( i , j ) ∈ A × A \exists(i,j)\in\mathbf{A}\times\mathbf{A} (i,j)A×A,有 j − i < L m i n j-i<L_{min} ji<Lmin。记窗口序列 ω ⊂ A × A \omega\subset\mathbf{A}\times\mathbf{A} ωA×A,该集合由遍历过程中记录的指针对( s t a r t start start, e n d end end)构成。由假设可以得到, ( i , j ) ∉ ω (i,j)\notin\omega (i,j)/ω ∑ k = i j n u m [ k ] ≥ t a r g e t \sum_{k=i}^{j}num[k]\geq target k=ijnum[k]target
e n d end end从初始条件到结束条件的遍历过程来看,其取值覆盖了0到 n − 1 n-1 n1,故 ∃ ( i ′ , j ) ∈ ω \exists(i',j)\in\omega (i,j)ω i ′ < i i'<i i<i(若 i ′ ≥ i i'\geq i ii,一定有 j − i ′ ≤ j − i < L m i n j-i'\leq j-i<L_{min} jiji<Lmin,与题设矛盾),于是有 ∑ k = i ′ j n u m [ k ] ≥ t a r g e t \sum_{k=i'}^{j}num[k]\geq target k=ijnum[k]target。根据窗口的遍历规则, s t a r t start start会从 i ′ i' i一直增加到 i ′ ′ i'' i′′,满足 ∑ k = i ′ ′ j n u m [ k ] ≤ t a r g e t \sum_{k=i''}^{j}num[k]\leq target k=i′′jnum[k]target(num是正整数数组, ∀ q ∈ A \forall q\in\mathbf{A} qA,一定有 ∑ k = q + 1 j n u m [ k ] < ∑ k = q j n u m [ k ] \sum_{k=q+1}^{j}num[k]<\sum_{k=q}^{j}num[k] k=q+1jnum[k]<k=qjnum[k]并且随着 q q q的增大,一定存在 q ′ q' q满足 ∑ k = q ′ j n u m [ k ] ≤ 0 < t a r g e t \sum_{k=q'}^{j}num[k]\leq0< target k=qjnum[k]0<target)。
(1)考虑上式取等的情况:如果 i ′ ′ > i i''>i i′′>i,会有 L m i n ≤ j − i ′ ′ < j − i < L m i n L_{min}\leq j-i''<j-i<L_{min} Lminji′′<ji<Lmin,产生矛盾;如果 i ′ ′ ≤ i i''\leq i i′′i,必然会有 j − i ≤ j − i ′ ′ = ∑ k = i ′ ′ j n u m [ k ] = t a r g e t j-i\leq j-i''=\sum_{k=i''}^{j}num[k]= target jiji′′=k=i′′jnum[k]=target,与假设矛盾。
(2)考虑 ∑ k = i ′ ′ j n u m [ k ] < t a r g e t \sum_{k=i''}^{j}num[k]<target k=i′′jnum[k]<target的情况:如果 i ′ ′ ≤ i i''\leq i i′′i ∑ k = i j n u m [ k ] ≤ ∑ k = i ′ ′ j n u m [ k ] < t a r g e t \sum_{k=i}^{j}num[k]\leq\sum_{k=i''}^{j}num[k]<target k=ijnum[k]k=i′′jnum[k]<target(num是正整数数组),与假设 ∑ k = i j n u m [ k ] ≥ t a r g e t \sum_{k=i}^{j}num[k]\geq target k=ijnum[k]target矛盾;如果 i ′ ′ > i i''>i i′′>i,根据窗口的滑动规则,一定有 ∑ k = i j n u m [ k ] = t a r g e t \sum_{k=i}^{j}num[k]=target k=ijnum[k]=target,故 ( i , j ) ∈ ω (i,j)\in\omega (i,j)ω,与假设矛盾。
综上所述,假设不存在,即原结论得证。

public int minSubArrayLen(int t, int[] nums) {
	int minLen = nums.length + 1;
    for (int start = 0, end = 0, sum = 0; end < nums.length; end++){
    	sum += nums[end];
        while (sum >= t){
        	minLen = Math.min(minLen, end - start +1);
            sum -= nums[start++];
        }
    }
    return minLen == nums.length + 1 ? 0 : minLen;
}

剑指 Offer II 009. 乘积小于 K 的子数组

给定一个正整数数组nums和整数k,请找出该数组内乘积小于k的连续的子数组的个数。
题解一 前缀和+二分查找
为了避免整形溢出,将子数组的乘积对数化即可表示成数组元素在对数化后的前缀和之差;枚举所有的i,在固定i的情况下找出最大的j满足:sum[j]-sum[i]<target。由于sum严格单调递增,可以采用二分法。

public static int numSubarrayProductLessThanK(int[] nums, int k) {
	int ans = 0, n = nums.length;
	double logk = Math.log(k), pref[] = new double[n + 1];
	for (int i = 0; i < n; i++) pref[i + 1] = pref[i] + Math.log(nums[i]);
	for (int i = 0; i < n; i++) {
		int left = i + 1, right = n;
		double tar = pref[i] + logk - 1e-10;
		while (left < right) {
			int mid = left + right + 1>> 1;
			if (pref[mid] >= tar) right = mid - 1;
			else left = mid;
		}
		if (pref[right] < tar) ans += right - i;
	}
	return ans;
}

题解二 滑动窗口
针对正整数数组的连续子数组问题,考虑用滑动窗口。 这里要注意由于整数除法的特殊性,只能在左指针小于等于右指针的情况下维护乘积变量。

public int numSubarrayProductLessThanK(int[] nums, int k) {
        int num = 0;
        for (int start = 0, end = 0, pro = 1; end < nums.length; end++){
            pro *= nums[end];
            while (start <= end && pro >= k) pro /= nums[start++];
            num += end - start + 1;
        }
        return num;
}

剑指 Offer II 010. 和为 k 的子数组

给定一个整数数组和一个整数k,请找到该数组中和为k的连续子数组的个数。
题解 前缀和+哈希表
定义前缀和pre[k]= ∑ i = 0 k − 1 n u m [ i ] \sum_{i=0}^{k-1}num[i] i=0k1num[i],假设 n u m num num在区间子数组[i,j]上的和为tartget,可以得到pre[j+1]-pre[i]=target。因此求解以num[j]为结尾且和为target的某个连续子数组,本质上就是求出[0,i-1]中有多少个k使得sum[k]=sum[j+1]-k的数,哈希表可以很方便实现。

public int subarraySum(int[] nums, int k) {
    int count = 0, pref = 0;
	Map<Integer, Integer> map = new HashMap<>();
	map.put(0, 1);
	for (int i = 0; i < nums.length; i++) {
		pref += nums[i];
		count += map.getOrDefault(pref - k, 0);
		map.put(pref, map.getOrDefault(pref, 0) + 1);
	}
	return count;
}

剑指 Offer II 011. 0 和 1 个数相同的子数组

给定一个二进制数组nums,找到含有相同数量的0和1的最长连续子数组,并返回该子数组的长度。
题解 前缀和+哈希表
如果把0当作-1,那么子数组中0和1的个数相同的等价条件就是和为0。可化为求出和为0的连续子数组的最大长度,用前缀和加哈希表即可实现。

public int findMaxLength(int[] nums) {
    int pref = 0, maxLen = 0;
    HashMap<Integer, Integer> map = new HashMap<>();
    map.put(0, 0);
    for (int i = 0; i < nums.length; i++) {
        pref = nums[i] == 0 ? pref -1 : pref + 1;
        if (map.containsKey(pref)) maxLen = Math.max(maxLen, i + 1 - map.get(pref));
        else map.put(pref, i + 1);
    }
    return maxLen;
}

剑指 Offer II 013. 二维子矩阵的和

给定一个二维矩阵matrix,以下类型的多个请求:
计算其子矩形范围内元素的总和,该子矩阵的左上角为(row1,col1),右下角为(row2,col2)。
实现NumMatrix类:

  • NumMatrix(int[][] matrix)给定整数矩阵matrix进行初始化
  • int sumRegion(int row1,int col1,int row2,int col2)返回左上角(row1,col1)、右下角(row2,col2)的子矩阵的元素总和。

题解一 一维前缀和
问题的关键在于优化每次调用sumRegion的时间开销,对一维数组求前缀和可以将连续子数组的求和时间从线性降为常量级,因此可以对二维数组的每行求一遍前缀和。

class NumMatrix {
    int m, n, pref[][];
    public NumMatrix(int[][] matrix) {
    	m = matrix.length;
        n = matrix[0].length;
        pref = new int[m][n + 1];
        for (int i = 0; i < m; i++) {
			for (int j = 0; j < n; j++) pref[i][j + 1] = pref[i][j] + matrix[i][j];
        }
    }
    public int sumRegion(int row1, int col1, int row2, int col2) {
    	int sum = 0;
    	for (int i = row1; i <= row2; i++) sum += pref[i][col2 + 1] - pref[i][col1];
    	return sum;
    }
}

题解二 二维前缀和
针对题解一仍然需要检索子矩阵的每一行,引入二维前缀和 p ( i , j ) = ∑ p = 0 i − 1 ∑ q = 0 j − 1 m a t r i x [ p ] [ q ] p(i,j)=\displaystyle\sum_{p=0}^{i-1}\sum_{q=0}^{j-1}matrix[p][q] p(i,j)=p=0i1q=0j1matrix[p][q]。为了减少冗余计算,建立递推关系: p ( i + 1 , j + 1 ) = p ( i + 1 , j ) + p ( i , j + 1 ) − p ( i , j ) + m a t r i x [ i + 1 ] [ j + 1 ] p(i+1,j+1)=p(i+1,j)+p(i,j+1)-p(i,j)+matrix[i+1][j+1] p(i+1,j+1)=p(i+1,j)+p(i,j+1)p(i,j)+matrix[i+1][j+1]。左上角、右下角坐标分别为 ( i 1 , j 1 ) 、 ( i 2 , j 2 ) (i_{1},j_{1})、(i_{2},j_{2}) (i1,j1)(i2,j2)的子矩阵元素和可以表达为: ∑ p = i 1 i 2 ∑ q = j 1 j 2 m a t r i x [ p ] [ q ] = f ( i 2 + 1 , j 2 + 1 ) − f ( i 2 + 1 , j 1 ) − f ( i 1 , j 2 + 1 ) + f ( i 1 , j 1 ) \displaystyle\sum_{p=i_{1}}^{i_{2}}\sum_{q=j_{1}}^{j_{2}}matrix[p][q]=f(i_{2}+1,j_{2}+1)-f(i_{2}+1,j_{1})-f(i_{1},j_{2}+1)+f(i_{1},j_{1}) p=i1i2q=j1j2matrix[p][q]=f(i2+1,j2+1)f(i2+1,j1)f(i1,j2+1)+f(i1,j1)

class NumMatrix {
    int m, n, pref[][];
    public NumMatrix(int[][] matrix) {
        m = matrix.length;
        n = matrix[0].length;
        pref = new int[m + 1][n + 1];
        for (int i = 0; i < m; i++) {
			for (int j = 0; j < n; j++) pref[i + 1][j + 1] = pref[i + 1][j] + pref[i][j + 1] - pref[i][j] + matrix[i][j];
        }
    }
    public int sumRegion(int row1, int col1, int row2, int col2) {
    	return pref[row2 + 1][col2 + 1] - pref[row2 + 1][col1] - pref[row1][col2 + 1] + pref[row1][col1];
    }
}

剑指 Offer II 014. 字符串中的变位词

给定两个字符串s1和s2,写一个函数来判断s2是否包含s1的某个变位词。换句话说,第一个字符串的排列之一是第二个字符串的子串。
题解 滑动窗口
先设定s2中的滑动窗口为[left,right],用数组cnt表示每个字符在当前窗口与s1中的出现次数之差,记s1的长度为n。初始时,cnt统计一遍s1,则cnt的值均不为正且元素和为-n;接着用right遍历s2的每一个字符,right每向右一次,cnt统计一次刚进入窗口的字符x。为保证cnt的值非正,若cnt[x]>0,left开始右移,其对应的cnt值也会减少直到cnt的值非正。如果区间[left,right]的长度为n,那么就恰好找到了这样一个区间,为s1的一个排列。
正确性证明:当窗口的长度为n时,其对cnt的元素和贡献值为+n,而s1中的字符对cnt的元素和贡献值为-n,因此此时cnt的元素和一定为0。又由cnt的值非正可知,此时cnt的每个元素值都为0,从而证得该窗口一定是s1的一个排列。

public boolean checkInclusion(String s1, String s2) {
    int[] cnt = new int[26];
    for (char c : s1.toCharArray()) cnt[c-'a']--;
    for (int left = 0, right = 0; right < s2.length(); right++) {
    	int ind = s2.charAt(right)-'a';
    	cnt[ind]++;
    	while (cnt[ind] > 0) cnt[s2.charAt(left++) - 'a']--;
    	if (right - left + 1 == s1.length()) return true;
    }
    return false;
}

剑指 Offer II 015. 字符串中的所有变位词

给定两个字符串s和p,找到s中所有p的变位词的子串,返回这些子串的起始索引。不考虑答案输出的顺序。变位词指字母相同,但排列不同的字符串。
题解 滑动窗口
此题解法同剑指 Offer II 014. 字符串中的变位词

public List<Integer> findAnagrams(String s, String p) {
    List<Integer> ls = new ArrayList<>();
	int cnt[] = new int[128];
	for (char c : p.toCharArray()) cnt[c]--;
	for (int left = 0, right = 0; right < s.length(); right++) {
		cnt[s.charAt(right)]++;
		while (cnt[s.charAt(right)] > 0) cnt[s.charAt(left++)]--;
		if (right - left + 1 == p.length()) ls.add(left);
	}
	return ls;
}

剑指 Offer II 016. 不含重复字符的最长子字符串

给定一个字符串s,请你找出其中不含有重复字符的最长连续子字符串的长度。
题解 滑动窗口
设定滑动窗口[left,right]用来表示当前遍历的字串,用cnt记录当前窗口中出现的各字符次数,为了不出现重复字符串,只要确保cnt的值不大于1即可。遍历right,每向右移动一次,更新cnt,一旦当前字符的cnt值大于1就右移left并更新cnt直到满足条件为止,最后更新最大窗口长度。

public int lengthOfLongestSubstring(String s) {
    int maxLen = 0, cnt[] = new int[128];
    for (int left = 0, right = 0; right < s.length(); right++) {
    	int ind = s.charAt(right);
    	cnt[ind]++;
    	while (cnt[ind] > 1) cnt[s.charAt(left++)]--;
    	maxLen = Math.max(maxLen, right - left + 1);
    }
    return maxLen;
}

剑指 Offer II 017. 含有所有字符的最短字符串

给定两个字符串s和t。返回s中包含t的所有字符的最短子字符串。如果s中不存在符合条件的子字符串,则返回空字符串""。
如果s中存在多个符合条件的子字符串,返回任意一个。
注意:对于t中重复字符,我们寻找的子字符串中该字符数量必须不少于t中该字符数量。
题解 滑动窗口
设定s中的滑动窗口为[left,right],用数组cnt表示各个字符在当前窗口与在t中的出现次数之差。为了确保窗口包含所有的字符串,我们需要确保cnt的所有元素值皆不小于0。当然,为了清楚cnt的所有元素值是否都大于0,设置一个计数器count记录窗口中有多少个t中的字符,每当右指针对应的cnt值小于0,就让count加1。可见当count的值等于t的长度时就不在更新了,因为此时cnt的元素值都非负。为了让窗口尽可能的减小,每当右指针右移一次,就让左指针向右移动到等于0为止,接着再更新最小窗口。

public String minWindow(String s, String t) {
    String str = s + 's';
	int count = -1 * t.length(), cnt[] = new int[128];
	for (char c : t.toCharArray()) cnt[c]--;
	for (int left = 0, right = 0; right < s.length(); right++) {
		if (++cnt[s.charAt(right)] <= 0) count++;
		while (left < s.length() && cnt[s.charAt(left)] > 0) cnt[s.charAt(left++)]--;
		if (count >= 0 && right - left + 1 < str.length()) str = s.substring(left, right + 1); 
	}
	return str.length() > s.length() ? "" : str;
}

剑指 Offer II 020. 回文子字符串的个数

给定一个字符串s,请计算这个字符串中有多少个回文子字符串。
具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。
题解一 动态规划
要列举出所有的回文子串,首先可能会想到先列举所有的子串,逐一判断是否是回文串。可采用动态规划来减少时间开销,设置状态dp[i][j]表示以i为首,以j为尾的子串是否是回文串。更新条件:如果i<j-1,当s[i]==s[j]&&dp[i+1][j-1]==true时更新dp[i][j]为true;如果j>=i>=j-1,当s[i]==s[j]更新dp[i][j]为true。

public int countSubstrings(String s) {
    int cnt = 0, n = s.length(), dp[][] = new int[n][n];
	for (int i = n - 1; i >= 0; i--) {
		dp[i][i] = 1;
		for (int j = i + 1; j < n; j++) {
			if (s.charAt(i) == s.charAt(j)) {
				if (i < j - 1 && dp[i + 1][j - 1] == 1) dp[i][j] = 1;
				else if (i == j - 1) dp[i][j] = 1;
			}
		}
	}
	for (int i = 0; i < n; i++) {
		for (int j = i; j < n; j++) if (dp[i][j] == 1) cnt++;
	}
	return cnt;
}

题解二 中心拓展法
第二种思路是枚举每一个可能的回文中心,然后用两个指针分别向左右两边拓展,当两个指针指向的元素相同的时候就拓展,否则停止拓展。
具体实现的时候,需要考虑如何有序地枚举每一个可能的回文中心,尤其是回文长度的奇偶性不确定。如何用一次循环来搞定,可以通过观察法,假设n=3:

编号i左起始 l i l_{i} li右起始 r i r_{i} ri
000
101
211
312
422

由此可以看出,长度为n的字符串会生成2n-1组回文中心( l i , r i l_{i},r_{i} li,ri), l i l_{i} li= ⌊ i 2 ⌋ \lfloor \frac{i}{2} \rfloor 2i r i r_{i} ri= ⌊ i 2 ⌋ \lfloor \frac{i}{2} \rfloor 2i+1。

public int countSubstrings(String s) {
    int n = s.length(), cnt = 0;
	for (int i = 0; i < (n << 1) - 1; i++) {
		for (int l = i >> 1, r = l + (i & 1); l >= 0 && r < n && s.charAt(l--) == s.charAt(r++);) cnt++;
	}
	return cnt;
}

题解三 Manacher算法
马拉车算法处理奇偶的方式是在所有的相邻字符中间插入#,比如abaa会被处理成#a#b#a#a#,这样可以保证所有找到的回文串都是奇数。记处理后的串为s,以 f ( i ) f(i) f(i)表示s的以第i位为回文中心的最大回文半径,通过观察得知其对应的原串S的回文串长度就是 f ( i ) f(i) f(i)-1。马拉车算法与中心拓展法相比,会在中心拓展之前利用先前计算的信息获取一个尽可能大的初始回文半径,之后再进行中心拓展。假设在求解 f ( i ) f(i) f(i)之前我们应当已经得到了从 [ 1 , i − 1 ] [1,i−1] [1,i1]上所有的 f f f,并且当前已经有了一个最大回文右端点 r m r_{m} rm以及它对应的回文中心 i m i_{m} im。只有 i i i小于 r m r_{m} rm才会初始化一个尽可能大的回文半径,否则为1。当 i i i< r m r_{m} rm时,假设 j j j i i i关于 i m i_{m} im的对称点,更新 f ( i ) f(i) f(i)= m i n { f ( j ) , r m − i + 1 } min\{f(j),r_{m}-i+1\} min{f(j),rmi+1}。在设置完初始回文半径后,做中心拓展即可,注意拓展完要更新 k k k的值。
时间复杂度: O \mathcal{O} O(n)。随着 i i i的更新, r m r_{m} rm只增不减。如果 i i i的最大回文右端点小于 r m r_{m} rm,那么利用对称性可以直接得到 i i i的回文半径;否则,随着 i i i更新完最大回文半径, r m r_{m} rm也一定会更新到其最右端点。由于只遍历字符串一次,所以中心拓展的次数最多为 O \mathcal{O} O(n)。
从时间开销来看,虽然时间复杂度降低到线性,但由于字符串的拼接仍然很耗时,所以从运行的效果看要差点,可以通过将“#”虚拟化实现进一步的优化。(后续改进)

public int countSubstrings(String s) {
    String res = "\u0001#";
	for (char c : s.toCharArray()) res += c + "#";
	res += '\u0002';
	int cnt = 0, k = 0, n = res.length(), rad[] = new int[n];
    for (int i = 1; i < n - 1; i++) {
    	rad[i] = k + rad[k] - i > 1 ? Math.min(rad[k * 2 - i], k + rad[k] - i) : 1;
    	while (res.charAt(i - rad[i]) == res.charAt(i + rad[i])) rad[i]++;
    	if (rad[i] + i > rad[k] + k) k = i;
    	cnt += (rad[i] / 2);
    }
	return cnt;
}

剑指 Offer II 021. 删除链表的倒数第 n 个结点

给定一个链表,删除链表的倒数第n个结点,并且返回链表的头结点。要求仅使用一趟扫描实现。
题解 快慢指针
设置两个指针,快指针超前慢指针n个结点,两个指针同时遍历,直到快指针为空,此时慢指针恰好为倒数第n个结点。为了删除方便,在头结点前设置一个哑结点并让慢指针指向它,快指针指向头结点向后第n个结点,这样快指针就超前慢指针n+1个结点。

public ListNode removeNthFromEnd(ListNode head, int n) {
	ListNode dummy = new ListNode(0, head), fast = head, slow = dummy;
    for (int i = 0; i < n; i++) fast = fast.next;
    while (fast != null) {
    	fast = fast.next;
    	slow = slow.next;
    }
    slow.next = slow.next == null ? null : slow.next.next;
    return dummy.next;
}

剑指 Offer II 022. 链表中环的入口节点

给定一个链表,返回链表开始入环的第一个节点。从链表的头节点开始沿着next指针进入环的第一个节点为环的入口节点。如果链表无环,则返回null。
题解一 哈希表
最简单的做法是,访问每个结点后将其存储到哈希表中,一旦遇到了此前遍历过的节点,就可以判定链表中存在环。

public ListNode detectCycle(ListNode head) {
        Set<ListNode> visited = new HashSet<ListNode>();
        for (ListNode pos = head; pos != null; pos = pos.next) {
        	if (visited.contains(pos)) return pos;
        	else visited.add(pos);
        }
        return null;
    }

题解二 快慢指针
慢指针每走一步,快指针走两步,如果存在环,两个指针一定在环内相交(假设在环内以顺时针方向遍历,环内一共有T个结点,快指针与慢指针在顺时针方向上的步差最多为T-1,由于快时针始终比慢指针多走一步,在经历T-1步后一定能够相遇),因此在相遇时快时针与慢时针的步差一定是环的整数倍,又由于快时针的步数是慢时针的两倍,所以慢时针走了环的整数倍到达相遇点。此时让其中一个时针回到头结点,两者都以先前慢指针的速度遍历,一定会在入环点相遇。(设头结点距离环口a个结点,环口距离初始相遇点b个结点,换种还剩下c个结点。有等式: a a a+ n 1 n_{1} n1( b b b+ c c c)+ b b b= n 2 n_{2} n2( b b b+ c c c) → \rightarrow a a a= c c c+( n 2 n_{2} n2- n 1 n_{1} n1-1)( b b b+ c c c))

public ListNode detectCycle(ListNode head) {
	ListNode fast = head, slow = head;
	while (fast != null) {
		fast = fast.next == null ? null : fast.next.next;
		slow = slow.next;
		if (fast == slow) break;
    }
	if (fast == null) return null;
	for (slow = head; fast != slow; fast = fast.next,slow = slow.fast) {}
	return fast;
}

剑指 Offer II 023. 两个链表的第一个重合节点

给定两个单链表的头节点headA和headB,请找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回null。注意,函数返回结果后,链表必须保持其原始结构。
题解一 哈希表
先遍历第一个链表并将每个结点放入哈希表,接着遍历第二个链表,如果结点已存在哈希表中则此结点是重合结点,第一个重合结点就是我们要找的。

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
	Set<ListNode> visited = new HashSet<ListNode>();
	ListNode pt = headA;
	for (; pt != null; pt = pt.next) visited.add(pt);
	for (pt = headB; pt != null && !visited.contains(pt); pt = pt.next) {}
	return pt;
}

题解二 双指针
让两个指针分别指向两个链表的头结点,同时向后遍历,一旦有一个遍历到表尾,下一步就指向表头,直到两个指针的地址相同,此时指向的就是第一个重合结点。
正确性证明:这里我们只考虑表长不相等的情况。如果不存在相交结点,那么两个指针一定会在访问完另一个表后同时为空;如果存在相交结点,假设表A的表头距离重合结点有a个结点,表B有b个,两个表的重合部分有c个结点,初始指向表A头结点的指针在遍历表A到第一个重合结点处,一共遍历完了a+c+b个结点,初始指向表B头结点的指针在遍历表B到第一个重合结点出,一共遍历完了b+c+a个结点,所以两个指针是同时到达的。

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
	ListNode ptA = headA, ptB = headB;
	while (ptA != ptB) {
		ptA = ptA == null ? headB : ptA.next;
		ptB = ptB == null ? headA : ptB.next;
	}
	return ptA;
}

剑指 Offer II 025. 链表中的两数相加

给定两个非空链表l1和l2来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。
可以假设除了数字0之外,这两个数字都不会以零开头。要求,不能对输入链表进行修改。
题解 栈
由于不能对链表进行修改,因此使用栈来逆序访问链表并模拟整数加法。

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    Deque<Integer> stk1 = new ArrayDeque<>(), stk2 = new ArrayDeque<>();
    for (; l1 != null; l1 = l1.next) {stk1.push(l1.val);}
    for (; l2 != null; l2 = l2.next) {stk2.push(l2.val);}
    ListNode head = null;
    for (int carry = 0; !stk1.isEmpty() || !stk2.isEmpty() || carry != 0; ) {
    	int a = stk1.isEmpty() ? 0 : stk1.pop(), b = stk2.isEmpty() ? 0 : stk2.pop();
    	int val = a + b + carry;
    	carry = val / 10;
    	ListNode cur = new ListNode(val % 10, head);
    	head = cur;
    }
    return head;
}

剑指 Offer II 026. 重排链表

给定一个单链表L的头节点head,单链表L表示为: L 0 → L 1 → … → L n − 1 → L n L_{0}→L_{1}→…→L_{n-1}→L_{n} L0L1Ln1Ln,请将其重新排列后变为: L 0 → L n → L 1 → L n − 1 → L 2 → L n − 2 → … L_{0}→L{n}→L_{1}→L_{n-1}→L_{2}→L_{n-2}→… L0LnL1Ln1L2Ln2。不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
题解一 双端队列
将链表元素全部从双端队列的一端插入,然后每次从左、右各出一个。

public void reorderList(ListNode head) {
    Deque<ListNode> dq = new ArrayDeque<ListNode>();
	ListNode pt, dummy = new ListNode(0, head);
	for (pt = dummy.next; pt != null; pt = pt.next) dq.add(pt);
	for (pt = dummy; !dq.isEmpty(); ) {
		pt.next = dq.remove();
		pt = pt.next;
		if (!dq.isEmpty()) {
			pt.next = dq.removeLast();
			pt = pt.next;
		}
	}
	pt.next = null;
}

题解二 线性表+双指针
将链表转换为线性表,然后利用左右指针来访问。

public void reorderList(ListNode head) {
    List<ListNode> ls = new ArrayList<ListNode>();
	for (ListNode pt = head; pt != null; pt = pt.next) ls.add(pt);
	int left = 0, right = ls.size() - 1;
    ListNode pt = new ListNode();
	for (; left < right; pt = pt.next) {
		pt.next = ls.get(left++);
		pt = pt.next;
		pt.next = ls.get(right--);
	}
	if (left == right) {
        pt.next = ls.get(right);
        pt = pt.next;
    }
	pt.next = null;
}

题解三 快慢指针+反转链表
利用快慢指针找到链表的中间结点,利用此结点将链表后半段反转成另一个链表,再将两个链表交替合并

public void reorderList(ListNode head) {
        //找中点
		ListNode dummy = new ListNode(0, head), slow = dummy, fast = dummy;
		while (fast != null && fast.next != null) {
			fast = fast.next.next;
			slow = slow.next;
		}
		//后半段反转
		ListNode pt = new ListNode();
		while (slow.next != null) {
			ListNode temp = slow.next.next;
			slow.next.next = pt.next;
			pt.next = slow.next;
			slow.next = temp;
		}
		//交替合并
		for (ListNode l1 = head, l2 = pt.next; l2 != null; ) {
			ListNode tp1 = l1.next, tp2 = l2.next;
			l1.next = l2;
			l2.next = tp1;
			l1 = tp1;
			l2 = tp2;
		}
    }

剑指 Offer II 027. 回文链表

给定一个链表的头节点head,请判断其是否为回文链表。
题解一 线性表+双指针
将链表转换为线性表,通过双指针来比较是否为回文串。

public boolean isPalindrome(ListNode head) {
	List<Integer> ls = new ArrayList<>();
	for (ListNode pt = head; pt != null; pt = pt.next) ls.add(pt.val);
	for (int left = 0, right = ls.size() - 1; left < right; ) {
		if (ls.get(left++) != ls.get(right--)) return false;
	}
	return true;
}

题解二 递归
双指针由于从正反同时遍历的特性很容易判断回文串。利用递归反向迭代结点,再利用全局变量正向迭代结点,也可以实现类似的功能。在许多语言中,递归函数的栈开销很大,而且可能造成栈上溢,并不建议使用,这里实现仅为了拓展思维。

private ListNode front;
public boolean recurCheck(ListNode cur) {
	if (cur == null) return true;
	if (!recurCheck(cur.next)) return false;
	if (cur.val != front.val) return false;
	front = front.next;
	return true;
}
public boolean isPalindrome(ListNode head) {
	front = head;
	return recurCheck(head);
}

题解三 快慢指针+反转链表
利用快慢指针找到链表中间结点,将链表后半段倒置,然后将两个链表元素逐一比较即可。

public boolean isPalindrome(ListNode head) {
	ListNode dummy = new ListNode(0, head), slow = dummy, fast = dummy;
	while (fast != null && fast.next != null) {
		fast = fast.next.next;
		slow = slow.next;
	}
	ListNode pt = new ListNode();
	while (slow.next != null) {
		ListNode temp = slow.next.next;
		slow.next.next = pt.next;
		pt.next = slow.next;
		slow.next = temp;
	}
	for (ListNode pt1 = dummy.next, pt2 = pt.next; pt2 != null; ) {
		if (pt1.val != pt2.val) return false;
		pt1 = pt1.next;
		pt2 = pt2.next;
	}
	return true;
}

剑指 Offer II 028. 展平多级双向链表

多级双向链表中,除了指向下一个节点和前一个节点指针之外,它还有一个子链表指针,可能指向单独的双向链表。这些子列表也可能会有一个或多个自己的子项,依此类推,生成多级数据结构。给定位于列表第一级的头节点,请扁平化列表,即将这样的多级双向链表展平成普通的双向链表,使所有结点出现在单级双链表中。
题解一 递归DFS
处理每级链表以层为单位,通过next指针迭代到空为止,当遇到某个node的child指针不为空时,先处理child指向的子链表,再后序处理这一层剩下的结点,这就满足了dfs的条件。当然,要将子链表插入到node与next之间时,需要直到子链表的最后一个结点,这就需要处理每级链表时返回最后一个结点。

public Node dfs(Node cur) {
	Node last = null;
	while (cur != null) {
		if (cur.child != null) {
			Node childLast = dfs(cur.child), next = cur.next;
			cur.next = cur.child;
			cur.child.prev = cur;
			if (next != null) {
				childLast.next = next;
				next.prev = childLast;
			}
            cur.child = null;
			cur = childLast;
		}
		last = cur;
		cur = cur.next;
	}
	return last;
}
public Node flatten(Node head) {
	dfs(head);
	return head;
}

题解二 迭代DFS
与递归版的dfs相比,迭代处理子链表后需要知道如何回到现状态。当碰到child非空时,利用显示栈来保存非空的next指针,于是栈非空就代表上一级链表后面还有结点等着链接。

public Node flatten(Node head) {
	Deque<Node> stk = new ArrayDeque<Node>();
	Node cur = head, last = null;
	for (; cur != null || !stk.isEmpty(); last = cur, cur = cur.next) {
		if (cur == null) {
			cur = last;
			cur.next = stk.pop();
			cur.next.prev = cur;
		}else if (cur.child != null) {
			if (cur.next != null) stk.push(cur.next);
			cur.child.prev = cur;
			cur.next = cur.child;
			cur.child = null;
		}
	}
	return head;
}

题解三 子链表转换为兄弟链表
离开dfs的惯性思维,每次碰到子链表,先不处理全部而只找到它的最后一个结点,再将整个未处理过的子链表插入到node与next之间,就像是将原本的父子关系转换成兄弟一样。这样做的好处是不需要存储一层一层处理子链表时要额外保存的next指针,将空间复杂度降为常数。

public Node flatten(Node head) {
	for (Node cur = head; cur != null; cur = cur.next) {
		if (cur.child == null) continue;
		Node next = cur.next, last = cur.child;
		while (last.next != null) last = last.next;
		if (next != null) {
			last.next = next;
			next.prev = last;
		}
		cur.next = cur.child;
		cur.child.prev = cur;
		cur.child = null;
	}
	return head;
}

剑指 Offer II 029. 排序的循环链表

给定循环单调非递减列表中的一个点,写一个函数向这个列表中插入一个新元素insertVal,使这个列表仍然是循环升序的。给定的可以是这个列表中任意一个顶点的指针,并不一定是这个列表中最小元素的指针。如果有多个满足条件的插入位置,可以选择任意一个位置插入新的值,插入后整个列表仍然保持有序。如果列表为空(给定的节点是null),需要创建一个循环有序列表并返回这个节点。否则,请返回原先给定的节点。
题解 模拟
假设链表非空,记最大值、最小值分别为Max、Min。若链表的值不全相等,Max一定大于Min,对于给定的插入值val有三种情况:
1、Min<=val<=Max。一定可以找到一个点Node,满足Node.val<=val<=Node.next.val;
2、val<Min。val插入到链表开头,或者说是链表末尾,即左邻Max,右邻Min。(循环链表的开头即末尾)
3、Max<val。val的插入位置同2。
若链表元素都相等,插入位置任意。可以用一个指针cur去遍历链表,如果遍历完一遍链表还没有找到上述三种情况,就将val插入到表尾即可。

public Node insert(Node head, int insertVal) {
    if (head == null) {
		head = new Node(insertVal);
		head.next = head;
		return head;
	}
	Node cur = head;
	while (!(cur.val <= insertVal && insertVal <= cur.next.val) && !(cur.val > cur.next.val && (cur.val < insertVal || insertVal < cur.next.val)) && !(cur.next == head)) cur = cur.next;
	cur.next = new Node(insertVal, cur.next);
	return head;
}

剑指 Offer II 030. 插入、删除和随机访问都是 O(1) 的容器

设计一个支持在平均时间复杂度O(1)下,执行以下操作的数据结构:
insert(val):当元素val不存在时返回true,并向集合中插入该项,否则返回false。
remove(val):当元素val存在时返回true,并从集合中移除该项,否则返回false。
getRandom:随机返回现有集合中的一项。每个元素应该有相同的概率被返回。
题解 变长数组+哈希表
变长数组可以满足常数时间的随机访问,哈希表可以满足常数时间的查找、插入和删除,将两者结合,用变成作为容器保存原数据,元素值和索引作为键值对存入哈希表,删除操作时,先将要删除元素与最后一个元素对调,再删除末尾元素即可。由于维护变长数组的添加和删除都要额外时间开销,因此用变量size来维护长度,每次删除最后一个元素,让size–即可,这样不会真的删除元素而又维护了容器的大小,此也叫懒惰删除。

class RandomizedSet {
    List<Integer> ves = new ArrayList<>();
    Map<Integer, Integer> map = new HashMap<>();
    Random rand = new Random();
    int len = 0;
    public boolean insert(int val) {
    	if (map.containsKey(val)) return false;
    	map.put(val, len);
    	if (ves.size() > len) ves.set(len, val);
    	else ves.add(val);
    	len++;
    	return true;
    }
    public boolean remove(int val) {
    	if (!map.containsKey(val)) return false;
    	int ind = map.get(val), last = ves.get(len - 1);
    	ves.set(ind, last);
    	map.put(last, ind);    	
    	map.remove(val);
    	len--;
    	return true;
    }
    public int getRandom() {
    	return ves.get(rand.nextInt(len));
    }
}

剑指 Offer II 031. 最近最少使用缓存

运用所掌握的数据结构,设计和实现一个LRU(Least Recently Used,最近最少使用)缓存机制。
实现 LRUCache 类:
LRUCache(int capacity)以正整数作为容量capacity初始化LRU缓存。
int get(int key)如果关键字key存在于缓存中,则返回关键字的值,否则返回-1。
void put(int key,int value)如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。
进阶:是否可以在O(1)时间复杂度内完成这两种操作?
题解 双向链表+哈希表
哈希表能够在常数时间内找到关键字对应的结点,但要能实现最近最少使用的队列,还得具备链表的功能,并且只有双向链表能够在常数时间内完成删除操作。将其特点综合起来,用链表存储实际结点,将缓存链表中结点的关键字及其结点指针存入链表,每次结点被添加、查找或者更新,都会更新到链表的表头,一旦链表容量满了,就会删除队尾结点连同其在哈希表中的映射。为了方便添加和删除结点,在双向链表的实现中增加一个伪头部和伪尾部。

class LRUCache {
	class DLinkNode {
		int key, value;
		DLinkNode prev, next;
		public DLinkNode() {}
		public DLinkNode(int _key, int _value) {key = _key; value = _value;}
	}
	private int size = 0, capacity;
	private Map<Integer, DLinkNode> map = new HashMap<>();
	private DLinkNode head = new DLinkNode(),tail = new DLinkNode();
	public LRUCache(int _capacity) {
		capacity = _capacity;
		head.next = tail;
		tail.prev = head;
	}
    public int get(int key) {
    	DLinkNode node = map.get(key);
    	if (node == null) return -1;
    	reflashNode(node);
    	return node.value;
    }
	public void put(int key, int value) {
    	DLinkNode node = map.get(key);
    	if (node != null) {
    		node.value = value;
    		reflashNode(node);
    		return;
    	}
    	node = new DLinkNode(key, value);
    	map.put(key, node);
    	addNode(node);
    	if (size > capacity) {
    		map.remove(tail.prev.key);
    		delNode(tail.prev);
    	}
    }
	private void reflashNode(DLinkNode node) {
    	if (node == head.next) return;
    	delNode(node);
    	addNode(node);
	}
	private void addNode(DLinkNode node) {
    	node.next = head.next;
    	head.next.prev = node;
    	node.prev = head;
    	head.next = node;
    	size++;
	}
	private void delNode(DLinkNode node) {
		node.prev.next = node.next;
		node.next.prev = node.prev;
		size--;
	}
}

剑指 Offer II 033. 变位词组

给定一个字符串数组strs,将变位词组合在一起。可以按任意顺序返回结果列表。注意:若两个字符串中每个字符出现的次数都相同,则称它们互为变位词。
题解一 排序+哈希表
判断两个表是否是变为词最简单的方法是将它们的字符串数组按排序规则变形,如果一样则互为变位词。将变形后的字符串作为关键字,将变形后为此字符串的所有字符串以列表形式与关键字一同存入哈希表。

public List<List<String>> groupAnagrams(String[] strs) {
	Map<String, List<String>> map = new HashMap<String, List<String>>();
	for (String str : strs) {
		char[] array = str.toCharArray();
		Arrays.sort(array);
		String key = new String(array);
		List<String> ls = map.getOrDefault(key, new ArrayList<String>());
		ls.add(str);
		map.put(key, ls);
	}
	return new ArrayList<List<String>>(map.values());
}

题解二 计数+哈希表
与题解一相比,该题不再变形每个字符串数组,而是对每个字符串出现的字符计数,再将计数数组映射为字符串作为关键字,其他与上面一致。

public List<List<String>> groupAnagrams(String[] strs) {
	Map<String, List<String>> map = new HashMap<String, List<String>>();
	for (String str : strs) {
		char[] cnt = new char[26];
		for (char c : str.toCharArray()) cnt[c-'a']++;
		String key = new String(cnt);
		List<String> ls = map.getOrDefault(key, new ArrayList<String>());
		ls.add(str);
		map.put(key, ls);
	}
	return new ArrayList<List<String>>(map.values());	
}

剑指 Offer II 034. 外星语言是否排序

某种外星语也使用英文小写字母,但可能顺序order不同。字母表的顺序(order)是一些小写字母的排列。给定一组用外星语书写的单词words,以及其字母表的顺序order,只有当给定的单词在这种外星语中按字典序排列时,返回true;否则,返回false。
题解 字典序索引
根据所给的字母表order,用一个字典序索引数组dic定义每个字符的规定大小,接着类似于字符串比较,逐字符比较即可。对于整个words数组,由于大小关系的传递性,只要比较相邻的每一对即可。

public boolean isAlienSorted(String[] words, String order) {
    int[] dic = new int[26];
	for (int i = 0; i < order.length(); i++) dic[order.charAt(i)-'a'] = i;
	for (int i = 1; i < words.length; i++) {
		boolean flag = false;
		for (int j = 0; j < words[i - 1].length() && j < words[i].length(); j++) {
			if (dic[words[i - 1].charAt(j) - 'a'] > dic[words[i].charAt(j) - 'a']) return false;
			if (dic[words[i - 1].charAt(j) - 'a'] < dic[words[i].charAt(j) - 'a']) {
				flag = true;
				break;
			}
		}
		if (!flag && words[i - 1].length() > words[i].length()) return false;
	}
	return true;
}

剑指 Offer II 035. 最小时间差

给定一个24小时制(小时:分钟"HH:MM")的时间列表,找出列表中任意两个时间的最小时间差并以分钟数表示。
题解 鸽巢原理+排序
将给定的列表排序后,最小时间差一定出现在相邻的两个时间或者是首尾时间。一共有24×60=1440种时间,根据鸽巢原理,如果列表的长度超过1440,就必然会有两个相同的时间,此时答案为0。

public int findMinDifference(List<String> timePoints) {
	if (timePoints.size() > 24 * 60) return 0;
	Collections.sort(timePoints);
	int min = Integer.MAX_VALUE, init = getMinutes(timePoints.get(0)), pre = init;
	for (int i = 1; i < timePoints.size(); i++) {
		int cur = getMinutes(timePoints.get(i));
		min = Math.min(min, cur - pre);
		pre = cur;
	}
	return Math.min(min, init - pre + 1440);
}
private int getMinutes(String str) {
	return ((str.charAt(0) - '0') * 10 + (str.charAt(1) - '0')) * 60 + (str.charAt(3) - '0') * 10 + (str.charAt(4) - '0');
}

剑指 Offer II 036. 后缀表达式

根据逆波兰表示法,求该后缀表达式的计算结果。有效的算符包括+、-、*、/。每个运算对象可以是整数,也可以是另一个逆波兰表达式。
题解一 栈
后缀表达式严格遵循从左到右的运算,因此运算结果与运算符的优先级无关。计算后缀表达式的值时,使用一个栈存储操作数,从左到右遍历后缀表达式,如果遇到操作数,则将操作数入栈;否则将两个操作数取出,使用运算符对两个操作数进行运算,将结果压栈。

public int evalRPN(String[] tokens) {
    Deque<Integer> stk = new LinkedList<>();
	for (String token : tokens) {
		if (token.length() > 1 || (token.charAt(0) >= '0' && token.charAt(0) <= '9')) {
			stk.push(Integer.parseInt(token));
			continue;
		}
		int r = stk.pop(), l = stk.pop();
		switch (token) {
		case "+":
			stk.push(l + r);
			break;
		case "-":
			stk.push(l - r);
			break;
		case "*":
			stk.push(l * r);
			break;
		case "/":
			stk.push(l / r);
		}
	}
	return stk.pop();
}

题解二 数组模拟栈
使用定长数组模拟栈需要事先分配大小,将后缀表达式用转换成二叉树,此二叉树一定满足:只有叶子结点和拥有两个孩子的结点。设两种结点分别有 n 0 n_{0} n0 n 2 n_{2} n2,根据结点与度的关系有: n 0 n_{0} n0+ n 2 n_{2} n2= 2 n 2 2n_{2} 2n2+1 → \to n 0 n_{0} n0= n 2 n_{2} n2+1。所以后缀表达式的长度一定是奇数,又由于叶子结点只能有操作数组成,中间结点结点只能是操作符,因此操作数有 n + 1 2 \frac{n+1}{2} 2n+1个。在最坏情况下,操作数全部入栈后才出现操作符,因此事先分配的数组大小为 n + 1 2 \frac{n+1}{2} 2n+1

public int evalRPN(String[] tokens) {
    int size = 0, stk[] = new int[(tokens.length + 1) >> 1];
	for (String token : tokens) {
		switch (token) {
		case "+":
			stk[--size - 1] += stk[size];
			break;
		case "-":
			stk[--size - 1] -= stk[size];
			break;
		case "*":
			stk[--size - 1] *= stk[size];
			break;
		case "/":
			stk[--size - 1] /= stk[size];
			break;
		default:
			stk[size++] = Integer.parseInt(token);
		}
	}
	return stk[--size];
}

剑指 Offer II 037. 小行星碰撞

给定一个整数数组asteroids,表示在同一行的小行星。对于数组中的每一个元素,其绝对值表示小行星的大小,正负表示小行星的移动方向(正表示向右移动,负表示向左移动)。每一颗小行星以相同的速度移动。找出碰撞后剩下的所有小行星。碰撞规则:两个行星相互碰撞,较小的行星会爆炸。如果两颗行星大小相同,则两颗行星都会爆炸。两颗移动方向相同的行星,永远不会发生碰撞。
题解 栈+模拟
可以用一个栈来保存暂未爆炸的行星,当遍历到一个新的行星aster时,由于栈中的行星都在它的左边,如果aster>0,无论栈顶的行星是向左还是右都不会与此行星发生碰撞,直接入栈即可。下面只考虑aster<0的情况,根据题意,当栈顶行星大于0体积大于或等于-aster的时候,aster一定会爆炸。并且如果栈顶行星体积小于等于-aster的时候,栈顶行星也会爆炸,随即出栈,一直重复以上操作直到不满足条件。

public int[] asteroidCollision(int[] asteroids) {
    Deque<Integer> stk = new ArrayDeque<>();
	for (int aster : asteroids) {
		boolean alive = true;
		while (alive && aster < 0 && !stk.isEmpty() && stk.peek() > 0) {
			alive = stk.peek() < -aster;
			if (stk.peek() <= -aster) stk.pop();
		}
		if (alive) stk.push(aster);
	}
	int ans[] = new int[stk.size()];
	for (int i = 0; i < ans.length; i++) ans[i] = stk.removeLast();
	return ans;
}

剑指 Offer II 038. 每日温度

请根据每日气温列表temperatures,重新生成一个列表,要求其对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用0来代替。
题解一 反向遍历+next数组
维护一个数组next记录每个温度第一次出现的下标,对于特定的某一天而言,要知道的是这一天之后的气温,之前的天气情况无关紧要,因此可以从后往前遍历数组,遍历每一天时,找到温度比它大的next值,并且将这一天温度的next值更新为该天。

public int[] dailyTemperatures(int[] temperatures) {
    int n = temperatures.length, wait[] = new int[n], next[] = new int[101];
	Arrays.fill(next, Integer.MAX_VALUE);
	for (int i = n - 1; i >= 0; i--) {
		int warmerDay = Integer.MAX_VALUE;
		for (int j = temperatures[i] + 1; j <= 100; j++) warmerDay = Math.min(warmerDay, next[j]);
		if (warmerDay < Integer.MAX_VALUE) wait[i] = warmerDay - i;
		next[temperatures[i]] = i;
	}
	return wait;
}

题解二 单调栈
维护一个存储下标的单调栈,从栈底到栈顶的下标对应的温度列表中的温度依次递减。正向遍历温度列表,对于遍历到的每个温度,都与栈顶元素比较,如果栈顶元素对应的温度小于当前温度,则将栈顶元素出栈并更新这一天的等待天数,一直重复直到栈顶元素大于等于该天温度或栈空。

public int[] dailyTemperatures(int[] temperatures) {
    int n = temperatures.length, wait[] = new int[n];
    Deque<Integer> stk = new ArrayDeque<>();
    for (int i = 0; i < n; i++) {
    	while (!stk.isEmpty() && temperatures[i] > temperatures[stk.peek()]) {
    		int prev = stk.pop();
    		wait[prev] = i - prev;
    	}
    	stk.push(i);
    }
    return wait;
}

剑指 Offer II 039. 直方图最大矩形面积

给定非负整数数组heights,数组中的数字用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为1。求在该柱状图中,能够勾勒出来的矩形的最大面积。
题解 单调栈
如果枚举每一个柱,一个很自然的想法就是找到以该柱作为高的矩形左边界和右边界。维护一个单调栈存放下标,从栈底到栈顶的下标对应高严格单调递增。那么每次枚举一个柱子i,维护完单调栈在i未入栈时的栈顶top一定确定为小于左边界的最右边柱子。如何确定右边界呢?每当栈中有一个元素j出栈,可以知道的是height[j]>=height[i],我们就让i作为大于右边界的最左边柱子即可。之所以可以这样,是因为假如有若干个相同高度的柱子,最右边的柱子一定可以求得正确的右边界,每次都求一下maxSize,在求完最右边柱子后一定能够正确更新,只不过在这个柱子前重复求了高度相同柱子的非正确结果,并且这些非正确结果一定小于此时的maxSize。

public int largestRectangleArea(int[] heights) {
    int n = heights.length, stk[] = new int[n + 1], size = 0, maxSize = 0;
	stk[size++] = -1;
	for (int i = 0; i <= n; i++) {
		while (size > 1 && (i == n || heights[i] <= heights[stk[size - 1]])) {
			int h = heights[stk[--size]];
			maxSize = Math.max(maxSize, (i - stk[size - 1] - 1) * h);
		}
		stk[size++] = i;
	}
	return maxSize;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值