LeetCode - Longest Substring Without Repeating Characters
技术
最近启动了刷 LeetCode 的进程,Accepted 了几道题,但两天不到就忘了,即使是留了注释,想想写写笔记还是蛮有必要的,但我不希望不经思考整理就贴一堆代码,把博客搞的乱糟糟的,像 XSDN、XX园、X书 一样,所以也只是想把一些印象深刻的部分,留个笔记。
过去的我过于年少轻狂,把某些高级语言当成了全部,并未好好去用 C/C++ 写过点什么。忽视了那些底层的基础的后果便是,现在在写一些很简单的东西,在需要保证代码质量的时候,要纠结许久去思考某一段代码需要消耗比较大的代价才能下决定。用 C 语言写代码有个好,通过它可以让你弄清楚,计算机底层可以给你什么基本能力,什么能力是需要通过这些基本的东西通过封装的方式实现的,也相当于顺便强化计算机基础了,所以我也打算就用 C 来刷这一套算法题。
题目:
3.Longest Substring Without Repeating Characters
Examples:
Given
"abcabcbb"
, the answer is"abc"
, which the length is 3.Given
"bbbbb"
, the answer is"b"
, with the length of 1.Given
"pwwkew"
, the answer is"wke"
, with the length of 3. Notethat the answer must be a substring, "pwke" is a subsequence and not asubstring.
程序需要统计输入字符串的最长无重复子串的长度。最初想到的是,从第一个字符开始,遍历这个字符的字符,直到出现重复的字符,统计此时的字符串长度,然后从第二个字符开始,重复这个过程,取最大的一个,直到最后一个字符为止,其中因为边界的问题的处理,折腾了许久,不过还是实现了下面的第一版的代码:
int lengthOfLongestSubstring(char* s) {
int p = 0;
char c;
int maxLen = 0, tempLen = 0;
while (s[p] != '\0') {
int q = p + 1;
while ((c = s[q]) != '\0') {
int hasRepeat = false;
for (int i = p; i < q; i++) {
if (s[i] == c) {
hasRepeat = true;
break;
}
}
if (hasRepeat) {
break;
}
q++;
}
if ((tempLen = q - p) > maxLen) {
maxLen = tempLen;
}
p++;
}
return maxLen;
}
这段代码在 leetcode-cn.com OJ 上的运行时间是 180ms,只战胜了 29% 的 C 语言提交,显然这个 O(n^3) 的算法并没有什么竞争力。
这段算法有一个地方比较浪费 CPU 的是,在一个字符失败以后,又重叠判断了一部分相同的元素,看题解提示说用一种 “Sliding Window” 的方法(名字听起来好像 TCP 协议的发送窗口~),通过这种方法,我们只需要把其中不重复的部分保持起来,出现重复的话,就把开头的字符排除掉,这样,这部分字符仍然保持不重复,但是往前移动了一位,就不需要再回头比较元素是否重复了。
题解是用 Java 来写的,用了哈希表去保存字符的状态,C语言上哪找哈希表啊,不过不急,先改改看看什么情况,就有了下面的代码:
int lengthOfLongestSubstring(char* s) {
if (s[0] == '\0') return 0;
int p = 0, q = 1;
char c;
int maxLen = 1, tempLen = 0;
while ((c = s[q]) != '\0') {
bool hasRepeat = false;
for (int i = p; i < q; i++) {
if (s[i] == c) {
hasRepeat = true;
break;
}
}
if (hasRepeat) {
p++;
} else {
q++;
}
if ((tempLen = q - p) > maxLen) {
maxLen = tempLen;
}
}
return maxLen;
}
其中补充了空白字符的情况,复杂度降到了 O(n^2),OJ 显示花了 32ms,战胜了 49% 的提交,想想,还是有进步空间啊。
剩下的问题就是这个判断重复的循环了,像题解的哈希表一样,把一个个的遍历,换成一键查询得到结果,可以变得更快!但是,C语言并没有原生实现哈希表数据结构,要实现一个,显然,哈希函数跑起来的开销估计可能比这个循环还大吧。
仔细分析需要做一对一映射的数据。每次循环需要查询的只是一个 char ,大小是一个字节,本身只有 256 种情况,而可见字符都遵循 ASCII 编码,也就是说,本题目给的输入中,最多只是前 128 种情况。
面对这样的需求,在C语言里面,开个 128 长度的数组,显然就能覆盖所有的情况了,其中的冗余,在内存不要钱的今天,显然也是可以接受的。
稍微改改,把这一个查找代码
bool hasRepeat = false;
for (int i = p; i < q; i++) {
if (s[i] == c) {
hasRepeat = true;
break;
}
}
换成利用 C 数组实现的 char->boolean 的字典:
int lengthOfLongestSubstring(char* s) {
// only zero
if (s[0] == '\0') return 0;
int p = 0, q = 1;
char c;
int table[128] = {0};
table[s[p]] = 1;
int maxLen = 1, tempLen = 0;
while ((c = s[q]) != '\0') {
if (table[c]) {
table[s[p++]] = 0;
} else {
table[s[q++]] = 1;
}
// calc
if ((tempLen = q - p) > maxLen) {
maxLen = tempLen;
}
}
return maxLen;
}
时间复杂度降到 O(2n) = O(n),运行时间降到了 12ms,愉快地击败了 100% 的 C 语言提交