柱状图中最大的矩形+KMP算法

LeetCode 第 84 题柱状图中最大的矩形

给定 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),因为用了一个堆栈来保存各个元素的下标,最坏的情况就是各个高度按照从矮到高的顺序排列,需要将它们都压入堆栈。

LeetCode 第 28 题

解题思路:利用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;
    }

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值