Hexo

点滴积累 豁达处之

0%

leetcode算法-字符串

leetcode算法

(LeetCode-20) 有效的括号

题目

给定一个只包括 ‘(‘,’)’,’{‘,’}’,’[‘,’]’ 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  • 左括号必须用相同类型的右括号闭合。
  • 左括号必须以正确的顺序闭合。
  • 每个右括号都有一个对应的相同类型的左括号。

示例 1:

1
2
输入:s = "()"
输出:true

示例 2:

1
2
输入:s = "()[]{}"
输出:true

示例 3:

1
2
输入:s = "(]"
输出:false

分析

方法

仔细分析,可以看到,每当遇到一个右括号,不管是’)’,’}’,’]’ ,表示需要前面一个有匹配的左括号,而且如果左右括号没有相邻在一起,就需要左右括号之间的其他括号也符合上一个原则。

通过分析过程,我们是否可以得到什么提示?需要按顺序往前回溯曾经处理的数据,是不是有点先进后出的感觉。所以可以利用栈来处理这个题目。这个题目其实和我们一期——栈与队列——(LeetCode-394)字符串解码这个题目有非常大的相似之处,只是这个题目更简单。

Leetcode_96

在具体的代码实现上,是入栈左括号还是右括号都是可以的,这个没有什么区别。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class IsValidStr {

public static void main(String[] args) {
String s = "(]";
System.out.println(isValid(s));
}

public static boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for(char c : s.toCharArray()){
if(c == '(' ){
stack.push(')');
}else if (c == '{'){
stack.push('}');
}else if(c == '['){
stack.push(']');
}else if(c == ')' || c == ']' || c == '}' ) {
if(stack.isEmpty() || !stack.pop().equals(c)){
return false;
}
}
}
return stack.isEmpty();
}
}

**(LeetCode-415) **字符串相加

题目

给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和并同样以字符串形式返回。

你不能使用任何內建的用于处理大整数的库(比如 BigInteger), 也不能直接将输入的字符串转换为整数形式。

示例 1:

1
2
输入:num1 = "11", num2 = "123"
输出:"134"

示例 2:

1
2
输入:num1 = "456", num2 = "77"
输出:"533"

示例 3:

1
2
输入:num1 = "0", num2 = "0"
输出:"0"

分析

方法

这个问题虽然题目要求不能直接将输入的字符串转换为整数形式,但是并没有禁止把字符串拆开成一个个的字符后,我们手动累加。

所以我们可以仿照小时候学习整数相加列竖式的办法,一个个字符对齐位以后再相加,同时在相加的顺序上注意需要逆序处理,因为我们整数的相加就是从个十百位这样的顺序相加的。因为可能有进位的情况,所以我们还需要一个变量专门用来记录是否进位以及进位的数量。理解上面的意思,这个代码就很简单了。

Leetcode_97

至于获得字符串中每个字符,怎么把他转为数字呢?可以利用acsii码表,将字符减去’0’,就是这个字符所代表的数字

Leetcode_98

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AddStrings {

public static void main(String[] args) {
String num1 = "456", num2 = "77";
System.out.println(addStrings(num1, num2));
}

public static String addStrings(String num1, String num2) {
StringBuilder sb = new StringBuilder();
// 记录进位的变量
int carry = 0;
for(int i = num1.length() - 1, j = num2.length() -1;
i >= 0 || j >= 0 || carry == 1; i--, j--){
int x = i < 0 ? 0 : num1.charAt(i) - '0';
int y = j < 0 ? 0 : num2.charAt(j) - '0';
sb.append((x + y + carry) % 10);
carry = (x + y + carry) / 10;
}
return sb.reverse().toString();
}
}

字符串匹配之BF算法

在一个字符串中寻找另一字符串,最容易想到的,也是最简单的办法是:取主串和模式串/搜索串中的每一位依次比较,如果匹配则同时后移一位继续比较,直至匹配到模式串的最后一位;如果出现不匹配的字符,则模式串向后移动一位,继续比较。这种解决问题的思路简单暴力,也是这个算法被叫做BF(Brute Force)的原因。

整个匹配的过程可以参考下图,我们假设主串为“abdea”,搜索串为“dea”:

Leetcode_99

这个算法的复杂度还是比较好分析的,我们假设主串的长度是 m,模式串的长度是 n,在最好的情况下,在第一个字符处的匹配就能够成功,例如主串是 a b c d ,模式串是a b c,这时只遍历了模式串的长度,因为时间复杂度是 O(n);

在最坏的情况下,每次都需要遍历整个模式串,但是又未能匹配成功,例如主串是 a a a a a …a,模式串是 a a a a b,所以需要遍历 m - n + 1 次,时间复杂度是 O(m * n) 。

BF算法的最坏时间复杂度虽然不好,但它易于理解和编程,在实际应用中,一般还能达到近似于O(m+n)的时间度(最坏情况不是那么容易出现的),因此,还在被大量使用。

字符串匹配之BM算法

各种文本编辑器的”查找”功能(Ctrl+F),大多采用Boyer[ˈbɔɪə]-Moore[mʊə]算法。

Boyer-Moore算法不仅效率高,而且构思巧妙,容易理解。1977年,德克萨斯大学的Robert S. Boyer教授和J Strother Moore教授发明了这种算法。BM算法里的总体思路是:对于每一次失败的匹配尝试,算法都能够使用这些信息来排除尽可能多的无法匹配的位置。

下面根据Moore教授自己的例子来解释这种算法。

假定字符串为”HERE IS A SIMPLE EXAMPLE”,搜索词为”EXAMPLE”。

Leetcode_100

首先,”字符串”与”搜索词”头部对齐,从尾部开始比较。

这是一个很聪明的想法,因为如果尾部字符不匹配,那么只要一次比较,就可以知道前7个字符(整体上)肯定不是要找的结果。

我们看到,”S”与”E”不匹配。这时,”S”就被称为”坏字符”(bad character),即不匹配的字符。我们还发现,”S”不包含在搜索词”EXAMPLE”之中,这意味着可以把搜索词直接移到”S”的后一位。

Leetcode_101

依然从尾部开始比较,发现”P”与”E”不匹配,所以”P”是”坏字符”。但是,”P”包含在搜索词”EXAMPLE”之中。所以,将搜索词后移两位,两个”P”对齐。

Leetcode_102

我们由此总结出”坏字符规则”:

1
后移位数 = 坏字符的位置 – 模式串中的上一次出现位置

如果”坏字符”不包含在搜索词之中,则上一次出现位置为 -1。

以”P”为例,它作为”坏字符”,出现在主串的第6位(从0开始编号),也就是“空格,A,空格,S,I,M,P”,在模式串中的上一次出现位置为4,所以后移 6 - 4 = 2位。

Leetcode_103

再以前面第二步的”S”为例,它出现在第6位,上一次出现位置是 -1(即未出现),则整个模式串后移 6 - (-1) = 7位。

Leetcode_104

依然从尾部开始比较,”E”与”E”匹配。

Leetcode_105

比较前面一位,”LE”与”LE”匹配。

Leetcode_106

比较前面一位,”PLE”与”PLE”匹配。

Leetcode_107

比较前面一位,”MPLE”与”MPLE”匹配。我们把这种情况称为”好后缀”(good suffix),即所有尾部匹配的字符串。注意,”MPLE”、”PLE”、”LE”、”E”都是好后缀。

Leetcode_108

比较前一位,发现”I”与”A”不匹配。所以,”I”是”坏字符”。

Leetcode_109

根据”坏字符规则”,此时模式串应该后移 2 - (-1)= 3 位。问题是,此时有没有更好的移法?

Leetcode_110

我们知道,此时存在”好后缀”。所以,可以采用”好后缀规则”:

1
后移位数 = 好后缀的位置 – 模式串中的上一次出现位置

举例来说,如果字符串”ABCDAB”的后一个”AB”是”好后缀”。那么它的位置是5(从0开始计算,取最后的”B”的值),在模式串中的上一次出现位置是1(第一个”B”的位置),所以后移 5 - 1 = 4位,前一个”AB”移到后一个”AB”的位置。

再举一个例子,如果字符串”ABCDEF”的”EF”是好后缀,则”EF”的位置是5 ,上一次出现的位置是 -1(即未出现),所以后移 5 - (-1) = 6位,即整个模式串移到”F”的后一位。

这个规则有三个注意点:

1
2
3
(1)"好后缀"的位置以最后一个字符为准。假定"ABCDEF"的"EF"是好后缀,则它的位置以"F"为准,即5(从0开始计算)。
(2)如果"好后缀"在模式串中只出现一次,则它的上一次出现位置为 -1。比如,"EF"在"ABCDEF"之中只出现一次,则它的上一次出现位置为-1(即未出现)。
(3)如果"好后缀"有多个,如果最长的那个"好后缀"在模式串中只出现一次,其他"好后缀"的上一次出现位置必须在头部。比如我们的例子中所有的"好后缀"(MPLE、PLE、LE、E)之中,只有"E"在"EXAMPLE"还出现在头部,所以后移 6 - 0 = 6位。

Leetcode_111

可以看到,”坏字符规则”只能移3位,”好后缀规则”可以移6位。所以,Boyer-Moore算法的基本思想是,每次后移这两个规则之中的较大值。

更巧妙的是,这两个规则的移动位数,只与模式串有关,与原字符串无关。因此,可以预先计算生成《坏字符规则表》和《好后缀规则表》。使用时,只要查表比较一下就可以了

Leetcode_112

继续从尾部开始比较,”P”与”E”不匹配,因此”P”是”坏字符”。根据”坏字符规则”,后移 6 - 4 = 2位。

Leetcode_113

从尾部开始逐位比较,发现全部匹配,于是搜索结束。如果还要继续查找(即找出全部匹配),则根据”好后缀规则”,后移 6 - 0 = 6位,即头部的”E”移到尾部的”E”的位置。

最坏情况下找到模式所有出现的时间复杂度为O(mn),在最好情况下执行匹配找到模式所有出现的时间复杂度为O(n/m)。

所以,BM算法里的总体思路就是:对于每一次失败的匹配尝试,算法都能够使用这些信息来排除尽可能多的无法匹配的位置。同时可以预先计算生成《坏字符规则表》和《好后缀规则表》,大大加快查找匹配的速度。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public class BMStr {
public static void main(String[] args) {
String main = "HERE IS A SIMPLE EXAMPLE";
// String main = "HEREXAMPLE";
String ptn = "EXAMPLE";
// String ptn = "abcab";
System.out.println(bm(main, ptn));
}
/**
* 生成好后缀数组
*/
private static void goodSuffixRule(String str, int[] suffix, boolean[] prefix){
Arrays.fill(suffix, -1);
Arrays.fill(prefix, false);

int n = str.length();
for (int i = 0; i < n - 1; i++){
int j = i;
int k = 0;

while (j >= 0 && str.charAt(j) == str.charAt(n - k - 1)){
--j;
++k;
suffix[k] = j + 1;
}
if (j == -1){
prefix[k] = true;
}
}
}

private static final int[] badChar = new int[256];

public static int bm(String main, String ptn){
if (main == null || ptn == null){
return -1;
}

int m = main.length();
int n = ptn.length();
badCharRule(ptn, badChar);

int[] suffix = new int[n];
boolean[] prefix = new boolean[n];
goodSuffixRule(ptn, suffix, prefix);

int i = n - 1;
while (i <= m - 1) {
int j = n - 1;
while (j >= 0 && main.charAt(i) == ptn.charAt(j)){
--i;
if (--j == -1){
return i + 1;
}
}

//计算坏字符规则下移动的位数
int moveWithBC = j - badChar[main.charAt(i)];

//计算好后缀规则下移动的位数
int moveWithGS = Integer.MIN_VALUE;
if (j < n - 1){
moveWithGS = moveWithGS(n, j, suffix, prefix);
}
i += Math.max(moveWithBC, moveWithGS);
}

return -1;
}

/**
* 生成坏字符数组
*/
private static void badCharRule(String str, int[] badChar){
Arrays.fill(badChar, -1);
for (int i = 0; i < str.length(); i++) {
badChar[str.charAt(i)] = i;
}
}

/**
* 计算好后缀情况下的移动位数
*/
private static int moveWithGS(int n, int j, int[] suffix, boolean[] prefix){
int k = n - j - 1;
if (suffix[k] != -1){
return j - suffix[k] + 1;
}
for (int i = k - 1; i >= 0; i--) {
if (prefix[i]){
return n - i;
}
}
return n;
}
}

(LeetCode- 3) 无重复字符的最长子串

题目

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

1
2
3
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

1
2
3
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

1
2
3
4
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

分析

思路:
这道题主要用到思路是:滑动窗口

什么是滑动窗口?

其实就是一个队列,比如例题中的 abcabcbb,进入这个队列(窗口)为 abc 满足题目要求,当再进入 a,队列变成了 abca,这时候不满足要求。所以,我们要移动这个队列!

如何移动?

我们只要把队列的左边的元素移出就行了,直到满足题目要求!

一直维持这样的队列,找出队列出现最长的长度时候,求出解!

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class LengthOfLongestSubstring {

public static void main(String[] args) {
String str = "pwwkew";
int max = new LengthOfLongestSubstring().lengthOfLongestSubstring(str);
System.out.println();
}

public int lengthOfLongestSubstring(String s) {
Map<Character, Integer> map = new HashMap<>();
int start = 0, max = 0;
for(int end = 0; end < s.length(); end++){
Integer index = map.get(s.charAt(end));
if(index != null){
start = Math.max(index + 1, start);
}
map.put(s.charAt(end), end);
max = Math.max(max, end - start + 1);
}
return max;
}

// 从头开始判断当前最多有几个不重复串
public int lengthOfLongestSubstring(String s) {
// 记录每个字符是否出现过
Set<Character> occ = new HashSet<Character>();
int n = s.length();
int rk = -1, ans = 0;
for(int i = 0; i < n; ++i){
if(i != 0){
occ.remove(s.charAt(i - 1));
}
while (rk + 1 < n && !occ.contains(s.charAt(rk + 1))){
occ.add(s.charAt(rk + 1));
++rk;
}
ans = Math.max(ans, rk - i + 1);
}
return ans;
}

public int lengthOfLongestSubstring1(String s) {
if(s.length() <= 1) {
return s.length();
}
StringBuilder builder = new StringBuilder();
int max = 0;
int end = 1;
builder.append(s.charAt(0));
while (end < s.length()){
int num = builder.indexOf(s.charAt(end) + "");
if(num == -1){
builder.append(s.charAt(end)) ;
}else {
// 移动滑动窗口
builder.append(s.charAt(end));
builder.delete(0, num + 1);
}
max = Math.max(max, builder.length());
end++;
}
return max;
}
}

(LeetCode- 5) 最长回文子串

题目

给你一个字符串 s,找到 s 中最长的回文子串。

示例 1:

1
2
3
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:

1
2
输入:s = "cbbd"
输出:"bb"

分析

方法二:中心扩展算法

我们仔细观察一下状态转移方程:

Leetcode2_17

找出其中的状态转移链

Leetcode2_18

可以发现,所有的状态在转移的时候的可能性都是唯一的。也就是说,我们可以从每一种边界情况开始「扩展」,也可以得出所有的状态对应的答案。

边界情况即为子串长度为 1 或 2 的情况。我们枚举每一种边界情况,并从对应的子串开始不断地向两边扩展。如果两边的字母相同,我们就可以继续扩展,例如从 P(i+1,j−1) 扩展到 P(i,j);如果两边的字母不同,我们就可以停止扩展,因为在这之后的子串都不能是回文串了。

聪明的读者此时应该可以发现,「边界情况」对应的子串实际上就是我们「扩展」出的回文串的「回文中心」。方法二的本质即为:我们枚举所有的「回文中心」并尝试「扩展」,直到无法扩展为止,此时的回文串长度即为此「回文中心」下的最长回文串长度。我们对所有的长度求出最大值,即可得到最终的答案。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class LongestPalindrome {

public static void main(String[] args) {
String s = "abcdefgfedcba";
String aa = new LongestPalindrome().longestPalindrome( s);
System.out.println("");
}

public String longestPalindrome(String s) {
int maxStart = 0, maxEnd = 0;
for(int i = 0; i < s.length(); i++){
// 以当前字母为中心,进行左右扩展
int charCenterLen = expandAroundCenter( s, i, i);
int blackCenterLen = expandAroundCenter( s, i, i + 1);
int len = Math.max(charCenterLen, blackCenterLen);
if(len > maxEnd - maxStart){
maxStart = i - (len - 1) / 2;
maxEnd = i + len / 2;
}
}
return s.substring(maxStart, maxEnd + 1);
}

private int expandAroundCenter(String s, int left, int right){
while (left >= 0 && right < s.length() && s.charAt(left) == s.charAt(right)) {
left--;
right++;
}
return right - left - 1;
}
}

(LeetCode-8) 字符串转换整数 (atoi)

请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C++ 中的 atoi 函数)。

函数 myAtoi(string s) 的算法如下:

  • 读入字符串并丢弃无用的前导空格
  • 检查下一个字符(假设还未到字符末尾)为正还是负号,读取该字符(如果有)。 确定最终结果是负数还是正数。 如果两者都不存在,则假定结果为正。
  • 读入下一个字符,直到到达下一个非数字字符或到达输入的结尾。字符串的其余部分将被忽略。
  • 将前面步骤读入的这些数字转换为整数(即,”123” -> 123, “0032” -> 32)。如果没有读入数字,则整数为 0 。必要时更改符号(从步骤 2 开始)。
  • 如果整数数超过 32 位有符号整数范围 [−231, 231 − 1] ,需要截断这个整数,使其保持在这个范围内。具体来说,小于 −231 的整数应该被固定为 −231 ,大于 231 − 1 的整数应该被固定为 231 − 1 。
  • 返回整数作为最终结果。

注意:

1
2
本题中的空白字符只包括空格字符 `' '` 。
除前导空格或数字后的其余字符串外,请勿忽略 任何其他字符。

示例 1:

1
2
3
4
5
6
7
8
9
10
11
输入:s = "42"
输出:42
解释:加粗的字符串为已经读入的字符,插入符号是当前读取的字符。
第 1 步:"42"(当前没有读入字符,因为没有前导空格)
^
第 2 步:"42"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
^
第 3 步:"42"(读入 "42")
^
解析得到整数 42 。
由于 "42" 在范围 [-231, 231 - 1] 内,最终结果为 42 。

示例 2:

1
2
3
4
5
6
7
8
9
10
11
输入:s = "   -42"
输出:-42
解释:
第 1 步:" -42"(读入前导空格,但忽视掉)
^
第 2 步:" -42"(读入 '-' 字符,所以结果应该是负数)
^
第 3 步:" -42"(读入 "42")
^
解析得到整数 -42 。
由于 "-42" 在范围 [-231, 231 - 1] 内,最终结果为 -42 。

示例 3:

1
2
3
4
5
6
7
8
9
10
11
输入:s = "4193 with words"
输出:4193
解释:
第 1 步:"4193 with words"(当前没有读入字符,因为没有前导空格)
^
第 2 步:"4193 with words"(当前没有读入字符,因为这里不存在 '-' 或者 '+')
^
第 3 步:"4193 with words"(读入 "4193";由于下一个字符不是一个数字,所以读入停止)
^
解析得到整数 4193 。
由于 "4193" 在范围 [-231, 231 - 1] 内,最终结果为 4193 。

分析

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public int myAtoi(String s) {
int len = s.length();
char[] charArray = s.toCharArray();
int idx = 0;
// 检查是否有空格
while (idx < len && charArray[idx] == ' '){
idx++;
}
// idx 和字符串的长度相等, 说明全是空格
if(idx == len){
return 0;
}
// 此时idx指向为第一个非空格字符,idx前面字符均可忽略 如果出现符号字符,用sign标记
int sign = 1;
char firstChar = charArray[idx];
if(firstChar == '+'){
idx++;
}else if(firstChar == '-') {
sign = -1;
idx++;
}
int result = 0;
while (idx < len){
char currentChar = charArray[idx];
// 出现了 '0' -- '9' 之外的字符则终止
if(currentChar > '9' || currentChar < '0'){
break;
}
// 只能存储32位大小的有符号整数
if(result > Integer.MAX_VALUE / 10 || (result == Integer.MAX_VALUE / 10 && (currentChar - '0') >
(Integer.MAX_VALUE % 10) )){
return Integer.MAX_VALUE;
}
if(result < Integer.MIN_VALUE / 10 || (result == Integer.MIN_VALUE / 10 && (currentChar - '0') >
-(Integer.MIN_VALUE % 10) )){
return Integer.MIN_VALUE;
}
// 转换
result = result * 10 + sign * (currentChar - '0');
idx++;
}
return result;
}

(LeetCode- 151) 翻转字符串里的单词

题目

给你一个字符串 s ,请你反转字符串中 单词 的顺序。

单词 是由非空格字符组成的字符串。s 中使用至少一个空格将字符串中的 单词 分隔开。

返回 单词 顺序颠倒且 单词 之间用单个空格连接的结果字符串。

注意:输入字符串 s中可能会存在前导空格、尾随空格或者单词间的多个空格。返回的结果字符串中,单词间应当仅用单个空格分隔,且不包含任何额外的空格。

示例 1:

1
2
输入:s = "the sky is blue"
输出:"blue is sky the"

示例 2:

1
2
3
输入:s = "  hello world  "
输出:"world hello"
解释:反转后的字符串中不能存在前导空格和尾随空格。

示例 3:

1
2
3
输入:s = "a good   example"
输出:"example good a"
解释:如果两个单词间有多余的空格,反转后的字符串需要将单词间的空格减少到仅有一个。

分析

方法1

这个问题可以偷懒取巧,使用JDK为我们提供的方法来实现,比如String类中的trim()、split()方法来去除空格和分解字符串,也可以使用Collections类中的reverse方法来反转。不过性能不是特别好,而且这种解法对不起题目的难度:中等,如果面试官不允许使用上面说的方法怎么办?

其实,考察题目的示例,”the sky is blue”转换为”blue is sky the”,如果我们以单词来看,可以这样说,先读取到的单词最后出现,符合栈的定义,所以这个题目完全可以使用栈或者双端队列来实现,比如LeetCode的官方解法,时间复杂度:O(n),空间复杂度:O(n)。

方法2

除此之外,我们还可以倒着遍历原始字符串s,用空格作为单词中间的分割,每遇到空格视为读完一个单词,然后将该单词输出。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class ReverseWords {

public static void main(String[] args) {
// String s = "a good example";
// String s = " hello world ";
// String s = "the sky is blue";
String s = " hello world ";
String result = new ReverseWords().reverseWords(s);
System.out.println("");
}

public String reverseWords(String s) {
char[] charArr = s.toCharArray();
int left = 0, right = s.length() - 1;
// 排除字符串前后空格
while (charArr[left] == ' '){
left++;
}
while (charArr[right] == ' '){
right--;
}
StringBuilder sb = new StringBuilder();
while (left <= right){
// 倒序遍历
int index = right;
while (index >= left && charArr[index] != ' '){
index--;
}
// 遇到单词前的空格, 取出单词,单词的范围就是index + 1 -- right
for(int i = index + 1; i <= right; i++){
sb.append(charArr[i]);
}
if(index > left){
sb.append(' ');
}
// 单词之间可能有多个空格,排除
while (index >= left && charArr[index] == ' '){
index--;
}
// 遇见了新单词,重复开始处理
right = index;
}

return sb.toString();
}

// public String reverseWords(String s) {
// StringBuilder result = new StringBuilder();
// Stack<Character> stack = new Stack<>();
// for(int i = s.length()-1; i >= 0; i--){
// char c = s.charAt(i);
// if(c == ' '){
// if(!stack.isEmpty() ){ // 单词结束
// // 清空栈
// clearnStack(result, stack);
// }
// }else {
// stack.push(c);
// }
// }
// // 清空栈
// clearnStack(result, stack);
// return result.toString();
// }
//
// private void clearnStack(StringBuilder result, Stack<Character> stack) {
// if(stack.isEmpty()){
// return;
// }
// if(result.length() == 0){
// while (!stack.isEmpty()){
// result.append(stack.pop());
// }
// }else {
// result.append(' ');
// while (!stack.isEmpty()){
// result.append(stack.pop());
// }
// }
// }
}

(LeetCode- 165) 比较版本号

题目

给你两个版本号 version1 和 version2 ,请你比较它们。

版本号由一个或多个修订号组成,各修订号由一个 ‘.’ 连接。每个修订号由 多位数字 组成,可能包含 前导零 。每个版本号至少包含一个字符。修订号从左到右编号,下标从 0 开始,最左边的修订号下标为 0 ,下一个修订号下标为 1 ,以此类推。例如,2.5.33 和 0.1 都是有效的版本号。

比较版本号时,请按从左到右的顺序依次比较它们的修订号。比较修订号时,只需比较 忽略任何前导零后的整数值 。也就是说,修订号 1 和修订号 001 相等 。如果版本号没有指定某个下标处的修订号,则该修订号视为 0 。例如,版本 1.0 小于版本 1.1 ,因为它们下标为 0 的修订号相同,而下标为 1 的修订号分别为 0 和 1 ,0 < 1 。

返回规则如下:

  • 如果 version1 > version2 返回 1,
  • 如果 version1 < version2 返回 -1,
  • 除此之外返回 0。

示例 1:

1
2
3
输入:version1 = "1.01", version2 = "1.001"
输出:0
解释:忽略前导零,"01" 和 "001" 都表示相同的整数 "1"

示例 2:

1
2
3
输入:version1 = "1.0", version2 = "1.0.0"
输出:0
解释:version1 没有指定下标为 2 的修订号,即视为 "0"

示例 3:

1
2
3
输入:version1 = "0.1", version2 = "1.1"
输出:-1
解释:version1 中下标为 0 的修订号是 "0",version2 中下标为 0 的修订号是 "1" 。0 < 1,所以 version1 < version2

分析

方法1

这个问题也使用JDK为我们提供的方法来实现。具体看代码,性能也不算低。

方法2

既然是两个字符串需要比较,我们使用两个指针i和j分别指向两个字符串的开头,然后向后遍历,当遇到小数点’.’时停下来,并将每个小数点’.’分隔开的修订号解析成数字进行比较,越靠近前边,修订号的优先级越大。根据修订号大小关系,返回相应的数值。

因此时间复杂度为 O(n + m)。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class CompareVersion {
public static void main(String[] args) {
String version1 = "1.01", version2 = "1.001";
int a = new CompareVersion().compareVersion( version1, version2);
System.out.println("");
}
public int compareVersion(String version1, String version2) {
int ii = 0, j = 0;
int n = version1.length(), m = version2.length();
while (ii < n || j < m){
int num1 = 0, num2 = 0;
while (ii < n && version1.charAt(ii) != '.') {
num1 = num1 * 10 + version1.charAt(ii++) - '0';
}
while (j < m && version2.charAt(j) != '.') {
num2 = num2 * 10 + version2.charAt(j++) - '0';
}
if(num1 > num2 ) return 1;
else if(num1 < num2) return -1;
ii++;j++;
}
return 0;
}
}

(LeetCode- 76) 最小覆盖子串

题目

给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 ""

注意:

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

示例 1:

1
2
输入:s = "ADOBECODEBANC", t = "ABC"
输出:"BANC"

示例 2:

1
2
输入:s = "a", t = "a"
输出:"a"

示例 3:

1
2
3
4
输入: s = "a", t = "aa"
输出: ""
解释: t 中两个字符 'a' 均应包含在 s 的子串中,
因此没有符合条件的子字符串,返回空字符串。

分析

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
public class MinWindow {

public static void main(String[] args) {
String s = "a", t = "b";
String s1 = new MinWindow().minWindow(s, t);
System.out.println("");
}

public String minWindow(String s, String t) {
String temp = "";
Map<Character, Integer> oci = new HashMap<>();
// 滑动窗
Map<Character, Integer> oct = new HashMap<>();
// 初始化
for(int i = 0; i < t.length(); i++){
Character c = t.charAt(i);
oci.put(c, oci.getOrDefault(c, 0) + 1);
}
int n = s.length();
int left = 0, right = 0;
while (left <= right && right < n){
while (right < n && oci.get(s.charAt(right)) == null){
right++;
}
if(right >= n) return temp;
oct.put(s.charAt(right), oct.getOrDefault(s.charAt(right), 0) + 1);
// 检查是否满足条件
while (check(oci, oct)){
// 满足条件
while (oci.get(s.charAt(left)) == null){
left++;
}
if(temp == "" || temp.length() > right - left + 1){
temp = s.substring(left, right + 1);
}
oct.put(s.charAt(left), oct.getOrDefault(s.charAt(left), 0) - 1);
left++;
}
right++;
}
return temp;
}

private boolean check(Map<Character, Integer> oci, Map<Character, Integer> oct) {
for (Map.Entry<Character, Integer> entry : oci.entrySet()) {
if(oct.getOrDefault(entry.getKey(), 0) < entry.getValue() ){
return false;
}
}
return true;
}


public String minWindow1(String s, String t) {
if (s == null || s.length() == 0 || t == null || t.length() == 0){
return "";
}
/*s 和 t 由英文字母组成,所以128个够用了*/
int[] tChars = new int[128];
/*用字符串t的字符初始化字典tChars,记录每个字符出现的次数*/
for (int i = 0; i < t.length(); i++) {
tChars[t.charAt(i)]++;
}
/*left是窗口当前左边界,right是窗口当前右边界*/
int left = 0, right = 0;
/*size记录满足条件的窗口大小,count是需求的字符个数*/
int size = Integer.MAX_VALUE, count = t.length();
/*start是最小覆盖串开始的index*/
int start = 0;
/*遍历所有字符*/
while (right < s.length()) {
char c = s.charAt(right);
if (tChars[c] > 0) {/*需要当前字符c*/
count--;
}
/*当前字符在tChars中的次数减一,这个次数可以为负数
* 比如当前字符在s中存在,在t中不存在
* 或者当前窗口范围内,在s中出现的次数大于t中出现的次数*/
tChars[c]--;
/*窗口中已经包含所有字符*/
if (count == 0) {
/*左边界向右递增,滑动窗口开始缩减,字典tChars要进行复原
* 缩减的停止处就是窗口内的字符集合刚好包含了t的全部字符,类似于示例 1中窗口包含了"BANC"
* 如果字典tChars的某个字符个数小于0,说明要么当前字符在s中存在,在t中不存在
* 要么当前窗口范围内,在s中出现的次数大于t中出现的次数*/
while (left < right && tChars[s.charAt(left)] < 0) {
tChars[s.charAt(left)]++;
left++;
}
/*更新size的最小值*/
if (right - left + 1 < size) {
size = right - left + 1;
start = left;/*记录下最小值时候的开始位置,最后返回覆盖串时候会用到*/
}
/*left向右移动后窗口肯定不能满足了重新开始循环*/
tChars[s.charAt(left)]++;
left++;
count++;
}
right++;
}
return size == Integer.MAX_VALUE ? "" : s.substring(start, start + size);
}
}