给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1。求在该柱状图中,能够勾勒出来的矩形的最大面积。
- 解题思路:利用栈来辅助,一旦我们发现当前的高度要比堆栈顶端所记录的高度要矮,就可以开始对堆栈顶端记录的高度计算面积了。在这里,我们巧妙地处理了当 i 等于 n 时的情况。同时在这一步里,我们判断一下当前的面积是不是最大值。
- 如果当前的高度比堆栈顶端所记录的高度要高,就压入堆栈。
class Solution {
public int largestRectangleArea(int[] heights) {
int n = heights.length;
int max = 0;
Stack<Integer> stack = new Stack<>();
for(int i = 0; i <= n; i++){
//在栈非空的前提下,如果i==n或者当前柱子高度比栈顶指向的低,需不断计算面积并且视情况更新最大值
while(!stack.isEmpty() && (i == n || heights[i] < heights[stack.peek()])){
int height = heights[stack.pop()];
//这里宽度的范围是[stack.peek() + 1, i - 1]
//所以宽度为i - 1 - stack.peek() - 1 + 1 =
int width = stack.isEmpty() ? i : i - 1 - stack.peek();
max = Math.max(max, width * height);
}
stack.push(i);
}
//返回最大面积
return max;
}
}
复杂度分析
时间复杂度是 O(n),因为从头到尾扫描了一遍数组,每个元素都被压入堆栈一次,弹出一次。
空间复杂度是 O(n),因为用了一个堆栈来保存各个元素的下标,最坏的情况就是各个高度按照从矮到高的顺序排列,需要将它们都压入堆栈。
解题思路:利用KMP算法
KMP(Knuth-Morris-Pratt)是由三人联合发表的一个算法,目的就是为了在一个字符串 haystack 中找出另外一个字符串 needle 出现的所有位置。它的核心思想是避免暴力法当中出现的不必要的比较。
举例:
haystack = "ABC ABCDAB ABCDABCDABDE"
needle = "ABCDABD"
解法 1:暴力法,当比较到上图所示位置的时候,发现 D 和空格不一样。接下来,needle 往前挪动一小步,然后继续和 haystack 比较。i和j都重新从0开始
解法 2:KMP,直接让 needle 挪动到如上图所示的位置。i保持不变,只单独移动j
此处有两个常见的问题:
- 为什么 KMP 无需慢慢移动比较,可以跳跃式比较呢?不会错过一些可能性吗?
- 如何能知道 needle 跳跃的位置呢?
LPS
为了说明这两个问题,必须先讲解 KMP 里的一个重要数据结构——最长的公共前缀和后缀,英文是 Longest Prefix and Suffix,简称 LPS。
LPS 其实是一个数组,记录了字符串从头开始到某个位置结束的一段字符串当中,公共前缀和后缀的最大长度。所谓公共前缀和后缀,就是说字符串的前缀等于后缀,并且,前缀和后缀不能是同一段字符串。
以上题中 needle 字符串,它的 LPS 数组就是:{0, 0, 0, 0, 1, 2, 0}。
needle = "ABCDABD"
LPS = {0000120}
LPS[0] = 0,表示字符串"A"的最长公共前缀和后缀的长度为 0。
注意:虽然"A"的前缀和后缀都等于 A,但前缀和后缀不能是同一段字符串,因此,”A”的 LPS 为 0。
LPS[1] = 0,表示字符串”AB”的最长公共前缀和后缀长度为 0。
因为它只有一个前缀 A 和后缀 B,并且它们不相等,因此 LPS 为 0。
LPS[4] = 1,表示字符串 ABCDA 的最长公共前缀和后缀的长度为 1。
该字符串有很多前缀和后缀,前缀有:A,AB,ABC,ABCD,后缀有:BCDA,CDA,DA,A,其中两个相同并且长度最长的就是 A ,所以 LPS 为 1。
LPS[5] = 2,表示字符串 ABCDAB 的最长公共前缀和后缀的长度为 2。
该字符串有很多前缀和后缀,前缀有:A,AB,ABC,ABCD,ABCDA,后缀有:BCDAB,CDAB,DAB,AB,B,其中两个相同并且长度最长的就是 AB,所以 LPS 为 2。
代码实现
假如已经求出了 LPS 数组,如何实现上述跳跃策略?代码实现如下。
class Solution {
public int strStr(String haystack, String needle) {
int m = haystack.length();
int n = needle.length();
if(n == 0){
return 0;
}
int[] lps = getLPS(needle);
int i = 0;
int j = 0;
while(i < m){
if(haystack.charAt(i) == needle.charAt(j)){
i ++;
j ++;
if(j == n){
//已经匹配到最后一个位置了,返回i的起始位置
return i - n;
}
}
else if( j > 0){
//只是移动j
j = lps[j- 1];
}
else{
//因为j重新从0开始,所以i的值也需要发生变动
i ++;
}
}
//如果尝试完了所有的努力,还是找不到结果的话,直接返回-1;
return -1;
}
}
代码解释:
- 分别用变量 m 和 n 记录 haystack 字符串和 needle 字符串的长度。
- 若 n=0,返回 0,符合题目要求。
- 求出 needle 的 LPS,即最长的公共前缀和后缀数组。
- 分别定义两个指针 i 和 j,i 扫描 haystack,j 扫描 needle。
- 进入循环体,直到 i 扫描完整个 haystack,若扫描完还没有发现 needle 在里面,就跳出循环。
- 在循环体里面,当发现 i 指针指向的字符与 j 指针指向的字符相等的时候,两个指针一起向前走一步,i++,j++。
- 若 j 已经扫描完了 needle 字符串,说明在 haystack 中找到了 needle,立即返回它在 haystack 中的起始位置。
- 若 i 指针指向的字符和 j 指针指向的字符不相同,进行跳跃操作,j = LPS[j - 1],此处必须要判断 j 是否大于 0。
- j=0,表明此时 needle 的第一个字符就已经和 haystack 的字符不同,则对比 haystack 的下一个字符,所以 i++。
- 若没有在 haystack 中找到 needle,返回 -1。
复杂度分析
KMP 算法需要 O(n) 的时间计算 LPS 数组,还需要 O(m) 的时间扫描一遍 haystack 字符串,整体的时间复杂度为 O(m + n)。这比暴力法快了很多。
如何求出 needle 字符串的最长公共前缀和后缀数组?
解法:对于给定的字符串 needle,用一个 i 指针从头到尾扫描一遍字符串,并且用一个叫 len 的变量来记录当前的最长公共前缀和后缀的长度。举例说明如下。
当 i 扫描到这个位置的时候,len=4,表明在 i 之前的字符串里,最长的前缀和后缀长度是 4,也就是那 4 个绿色的方块。
现在 needle[i] 不等于 needle[4],怎么计算 LPS[i] 呢?
既然无法构成长度为5的最长前缀和后缀,那便尝试构成长度为 4,3,或者 2 的前缀和后缀,但做法并非像暴力法一样逐个尝试比较,而是通过 LPS[len - 1] 得知下一个最长的前缀和后缀的长度是什么。举例说明如下。
- LPS[len - 1] 记录的是橘色字符串的最长的前缀和后缀,假如 LPS[len - 1]=3,那么前面 3 个字符和后面的 3 个字符相等
- 绿色的部分其实和橘色的部分相同。
- LPS[len - 1] 记录的其实是 i 指针之前的字符串里的第二长的公共前缀和后缀(最关键点)。
- 更新 len = LPS[len - 1],继续比较 needle[i] 和 needle[len]。
代码实现
//计算needle的前缀字符串
int[] getLPS(String str){
int[] lps = new int[str.length()];
//lps的第一个值一定是0,因为长度为1的字符串的最长公共前缀后缀的长度为0
int i = 1, len = 0;
//指针i遍历整个输入字符串
while(i < str.length()){
//i指针能延续前缀和后缀,则更新lps值为len + 1
if(str.charAt(i) == str.charAt(len)){
lps[i++] = ++len;
}
//否则,判断len是否大于0,尝试第二长的前缀和后缀,是否能继续延续下去
else if(len > 0){
len = lps[len - 1];
}
//所有的前缀和后缀都不符合,则当前的lps为0(默认值为0))), i ++
else{
i ++;
}
}
return lps;
}