leetcode 456. 132 Pattern 132模式 题解(栈)

【例子1】132 Pattern

  https://leetcode.com/problems/132-pattern/description/

  Given a sequence of n integers a1, a2, ..., an, a 132 pattern is a subsequence ai, aj, ak such that i < j < k and ai < ak < aj. Design an algorithm that takes a list of n numbers as input and checks whether there is a 132 pattern in the list.

  Note: n will be less than 15,000.

 思路:求一个list中是否有132模式。利用一个栈,从后往前遍历数组,再设置一个second变量。栈中记录的是当前遍历到的最大值,而second则用来记录目前第二大的数。只要目前遍历的数,小于栈顶的最大值和second,这时候就刚好满足132模式。

 代码:

    bool find132pattern(vector<int>& nums) {
        if(nums.size()<3)return false; 
         stack<int> s;
        int second=INT_MIN;
        for(int i=nums.size()-1;i>=0;i--){
            if(nums[i]<second)return true;
            while(!s.empty()&&nums[i]>s.top()){
                second = s.top();
                s.pop();
            }
            s.push(nums[i]);
        }
        return false;
    }
View Code

 

 

【例子2】直方图中的最大矩形

  https://leetcode.com/problems/largest-rectangle-in-histogram/description/

  Given n non-negative integers representing the histogram's bar height where the width of each bar is 1, find the area of largest rectangle in the histogram.

                   

思路:这道题目有多种解法,例如:动态规划、分治法、线段树等等。这里主要讲一下用栈解决的思路。那么,最大矩形什么时候出现呢?

首先,最大矩形必定恰好包含了图中的一个直方,例如上面的右图。因此,我们只要计算出恰好包含第一个直方的最大矩形、恰好包含第二个直方的.最大矩形..然后得到其中的最大值就行了。

接着,考虑如何计算这些矩形的面积,它们的高就是恰好包含的直方的高h,只要计算各个矩形的跨度(L,R)即可,L表示矩形开始的位置,R表示结束位置。这时候的矩形面积就是h*(R-L+1)。如上面的右图的阴影部分,R=3,L=2。

最后,最大矩形的右边界很容易确定,只要出现一个直方h[j]小于当前直方h[i],就说明恰好包含h[i]的最大矩阵的右边界是j-1。但是开始的位置在哪里呢?这就需要用到栈,用栈来记录恰好包含第i个直方的最大矩阵的左边界。具体的计算过程,从左至右先将直方的下标逐一入栈s,如果遇到h[i]<s.top(),那么,就说明恰好包含h=s.top()的最大矩阵在R=i-1的位置结束,而它的左边界就是在s.pop()之后,L=s.top()+1,这时候的面积就是h*(R-L+1)。计算的过程中,与之前的结果比较大小,得到直方图的最大矩阵面积。

注意:按照上面的思路,我们在遍历h数组之后,最后的栈中并不一定为空。这是因为,存在直方的高度小于最右边的直方。在开始阶段,往数组h最右边插入一个0作为结束,就能够计算到每一个直方的最大矩阵面积了。

代码:

int largestRectangleArea(vector<int>& h) {
       h.push_back(0);
        int res = 0;
        vector<int> pos;
        for(int i=0;i<h.size();i++){
            while(!pos.empty()&&h[i]<=h[pos.back()]){
                int cur = h[pos.back()];
                pos.pop_back();//出栈
                int sidx = pos.empty() ? -1:pos.back();
                res = max(res, cur*(i-sidx-1));
            }
            pos.push_back(i);
        }
        return res;
    }
View Code

 

 【例子3】最大全1子矩阵

https://leetcode.com/problems/remove-duplicate-letters/description/

Given a 2D binary matrix filled with 0's and 1's, find the largest rectangle containing only 1's and return its area.

For example, given the following matrix:

1 0 1 0 0
1 0 1 1 1
1 1 1 1 1
1 0 0 1 0

Return 6.  

思路:这个题目本质上和上一题是相同的,可以相互转化,上一题中可以转化为一个0-1矩阵,求最大全1子矩阵的大小即可。这一个问题也可以将各行分别看作直方图的x坐标,求出当前的最大子矩阵,然后比较得到各行当中的最大值即可。例如,第1-1行可以看作是一个直方图,第1-2行可以看作是一个直方图....矩阵中的最大全1子矩阵就从各个直方图的最大矩阵中去找。不多说了,上代码:

代码:

    int maximalRectangle(vector<vector<char>>& m) {
        if(m.empty()) return 0;
        vector<int> h(m[0].size(),0);
        int res = 0;
        for(int i=0;i<m.size();i++){
            for(int j=0;j<m[0].size();j++){
               if(m[i][j]=='0') h[j] = 0;
               else h[j]++;
            }
            res = max(res,largestRectangleArea(h));
        } 
        return res;
    }
    
    int largestRectangleArea(vector<int>& h) {
       h.push_back(0);
        int res = 0;
        vector<int> pos;
        for(int i=0;i<h.size();i++){
            while(!pos.empty()&&h[i]<=h[pos.back()]){
                int cur = h[pos.back()];
                pos.pop_back();
                int sidx = pos.empty() ? -1:pos.back();
                res = max(res, cur*(i-sidx-1));
            }
            pos.push_back(i);
        }
        return res;
    }
View Code

 

【例子4】Verify Preorder Serialization of a Binary Tree

  https://leetcode.com/problems/verify-preorder-serialization-of-a-binary-tree/description/

One way to serialize a binary tree is to use pre-order traversal. When we encounter a non-null node, we record the node's value. If it is a null node, we record using a sentinel value such as #.

     _9_
    /   \
   3     2
  / \   / \
 4   1  #  6
/ \ / \   / \
# # # #   # #

For example, the above binary tree can be serialized to the string "9,3,4,#,#,1,#,#,2,#,6,#,#", where # represents a null node.

Given a string of comma separated values, verify whether it is a correct preorder traversal serialization of a binary tree. Find an algorithm without reconstructing the tree.

思路:验证是否为二叉树的先序序列。从左到右将各字符入栈,如果遇到栈顶字符是“#”,当前待压入栈中的字符是“#”,就说明栈顶的前一个字符是叶子节点,这时候,连续两个pop(),消去这个叶子节点。这时候,叶子节点的位置就是null,需要将“#”压入栈中。如果满足前面的条件,继续消去。如果遍历完整个字符串之后,栈中留下的只有一个“#”,就说明是先序序列。不过,如果出现连续三个“#”或者栈中只剩两个“#”的情况,这时候肯定不是先序序列,这要在栈的操作过程中加以判断。

另外,参考别人的discuss,二叉树只有度为0和度为2的两种节点,而根据公式:n0 = n2 + 1。如果在遍历的过程中遇到了n0>=n2+1(这时候遍历还未结束),就说明这个序列有问题了。同样,遍历完之后,如果n0!= n2 + 1,这也说明不是先序序列。

代码:

    bool isValidSerialization(string pre) {
        char tmp;
        bool isNum = false;
        pre += ',';
         vector<char> s;
        for(auto tmp:pre){
            if(tmp=='#'){
               while(!s.empty()&&s.back()=='#'){
                    s.pop_back();
                    if(s.empty()||s.back()=='#')return false;//case“###”
                    s.pop_back();
                }
                s.push_back('#');
            }else if(tmp==','){
                if(isNum)s.push_back('n');
                isNum = false;
            }else{
                isNum = true;
            }
        }
        return s.back()=='#'&&s.size()==1;   
    }
View Code

 

 

【例子5】二叉树的后序遍历(用栈)

 思路:二叉树的用栈后序遍历,比用栈前序、中序遍历都要复杂,方法也比较多。

  思路一,根据(左右中)的顺序,先将r压栈,再出栈,将r->left压栈,将r->right压栈,再将栈顶(r->right)出栈....这样就能够遍历到二叉树的每个节点,最后栈为空。可以发现,这样的出栈序列和二叉树的后序序列刚好相反。所以,只要在出栈的时候讲val存入vector数组,再反转即可,也可以采用insert函数,将每一次的val插入vector的首位。

  思路二,如果目的仅仅是输出后序序列,那么,是否输出当前节点,只需要看看它的左右节点是否输出,或者左右节点是否为空。所以,添加一个pre指针,指向前一次输出的节点。在栈不为空的情况下,按照s.top()->right、s.top()->left的方式依次入栈。当目前栈顶元素的左右节点都未null,或者,pre节点是左节点且右子树为null,pre节点是右节点的时候,就输出栈顶val,再出栈,pre就等于当前出栈节点。这样循环下去,直到栈为空。

  思路三,先从p节点一直往左下走,直到p->left=NULL,这时候,再判断p->right,是否为NULL,如果是则将p出栈,否则,需要将p->right入栈。如果p->right是叶子节点,则出栈,这时候需要添加一个指针为q,指向p->right,代表此节点已经被访问。这时候回退到了p节点,如果p->right==q,就知道它的右子节点已经被访问了,故继续出栈,访问p节点,这时候的q指针就指向p节点。

       思路四,和前面两种思路差不多,都用到pre前驱节点,对于每一个栈顶节点(初始是root),看看是否需要继续添加它的子节点入栈,不需要的话就输出这个节点,并出栈,否则就分别就左右子节点入栈。判断是否需要的关键在于当前的pre节点是哪个。

  总结起来,可以用q来表示已经访问过的指针,q初始值为NULL,只有在visit(p->right)之后,q=p->right,所以,当p->right = q时,这时候就需要访问p节点了。

//思路一:
vector<int> postorderTraversal(TreeNode* r) {
        vector<int> v;
        if (!r) return v;
        stack<TreeNode*> s;
        s.push(r);
        while(!s.empty()){
            TreeNode* cur = s.top();
            v.insert(v.begin(),cur->val);
            s.pop();
            if(cur->left)s.push(cur->left);
            if(cur->right)s.push(cur->right);
        }
        return v;
    }

//思路二:
vector<int> postorderTraversal(TreeNode* r) {
        vector<int> v;
        if (!r) return v;
        stack<TreeNode*> s;
        TreeNode* pre;
        s.push(r);
        while(!s.empty()){
            TreeNode* cur = s.top();
            if((!cur->left&&!cur->right)||(pre == cur->left&&!cur->right)||(pre&&pre==cur->right)){
                v.push_back(cur->val);
                s.pop();
                pre = cur;
            }else{
                if(cur->right)s.push(cur->right);
                if(cur->left)s.push(cur->left);
            }
        }
        return v;
    }
//思路三:
    vector<int> postorderTraversal(TreeNode* r) {
        vector<int> v;
        if (!r) return v;
        stack<TreeNode*> s;
        TreeNode* p = r;
        do{
            while(p){     //0,最开始将左节点全部入栈
                s.push(p);
                p=p->left;
            }
            TreeNode* pre = NULL;
            while(!s.empty()){
                p = s.top();
                if(p->right==pre){//1,访问过、或者右子树为空才出栈
                    v.push_back(p->val);
                    pre = p;
                    s.pop();
                }else{//2,没有访问过、跳出继续将p->right的一路左子树入栈,执行0
                    p = p->right;
                    break;
                }
            }
        }while(!s.empty());
        return v;
    }
View Code
 vector<int> postorderTraversal(TreeNode* r) {
        vector<int> v;
        if (!r) return v;
        stack<TreeNode*> s;
        s.push(r);
        //pre表示后序序列中节点的前驱
        //最开始的pre不能为空,要将pre和cur的左右子节点进行比较
        //当访问最左边节点时,如果pre=NULL且右节点==NULL,这一点就不能够入栈
        TreeNode *pre =new TreeNode(0);
        while(!s.empty()){
            TreeNode* cur = s.top();
            //有左节点,且左右节点都还没访问过,入栈
            if(cur->left&&pre!=cur->left&&pre!=cur->right){
                s.push(cur->left);
            //有右节点,且没有访问过,而左节点访问过
            }else if(cur->right&&pre!=cur->right&&(!cur->left||cur->left == pre)){
                s.push(cur->right);
            }else{
            //其他情况,由于栈后进先出,栈顶出栈即可,这时候pre就指向cur
                v.push_back(cur->val);
                pre = cur;
                s.pop();
            }
        }
        return v;
    }
思路四

 

【例子6】中缀表达式求值

思路:中缀就是我们平常所见的表达式序列。直接进行中缀求值需要用两个栈,一个栈存操作数,一个栈存放操作符,包括“+-*/()”。如果遍历的操作符优先级不比栈顶操作符大,就将栈顶操作符出栈,同时将两个操作数出栈,计算得到的结果再入栈。

为了减少操作数压栈入栈的次数,设置一个cur变量,代表当前得到的操作数。在需要计算的时候,只需将一个操作数出栈,计算得到的结果仍然是cur,直到目前的操作符优先级大于当前栈顶的优先级,才将cur压入栈中。

另外,考虑到遍历之后,两个栈中仍然有数据需要计算,这样就会有比较重复的代码了。为了解决这个问题,只需在最开始的时候,给表达式加上括号,这样在遍历之后,两个栈全部清空,这时候表达式的值就是cur。

#include<iostream>
#include<cstring>
#include<stack>
#include<unordered_map>
#include<unordered_set>
#include<functional>
using namespace std;

unordered_map<char, function<int  (int, int )>> op_map = {
            { '+' , [] (int  a, int  b) { return a + b; } },
            { '-' , [] (int  a, int  b) { return a - b; } },
            { '*' , [] (int  a, int  b) { return a * b; } },
            { '/' , [] (int  a, int  b) { return a / b; } }
        };
unordered_map<char, int> priority = {
    {'+',1},{'-',1},{'*',2},{'/',2},{'(',0}
};

/* 利用两个栈进行模拟计算 */
int Compute(string s)
{
    stack<char> opstk;    //操作符栈
    stack<int> numstk;  //操作数栈
    int pos = 0;
    s = "(" + s + ")";
    int cur;
    while(pos < s.length())
    {
        if (s[pos] == '('){
            opstk.push('(');
            if(s[++pos]=='-')cur = 0;//负号必定出现在'('的后面,变成减法
        }else if (s[pos] == ')'){
            //括号内计算得到的cur不需要入栈
            while (opstk.top() != '('){
                cur = op_map[opstk.top()](numstk.top(), cur);//只需拿cur与栈顶数字进行计算
                numstk.pop();
                opstk.pop();
            }
            opstk.pop();  //删除'('
            pos++;
        }else if (s[pos] >= '0' && s[pos] <= '9'){
            int integer = 0;
            while (s[pos] >= '0' && s[pos] <= '9'){
                integer = integer*10 + (s[pos++] - '0');
            }
            cur = integer;
        }else{
            while(priority[s[pos]] <= priority[opstk.top()]){
                cur = op_map[opstk.top()](numstk.top(), cur);
                numstk.pop();
                opstk.pop();
            }
            numstk.push(cur);
            opstk.push(s[pos++]);
        }
    }
    return cur;
}

int main()
{
    string s = "-2+3*(-2+3*4)-(-(-2))";
    cout << "结果为:" << Compute(s) << endl;//输出结果为26
    return 0;
}
View Code

【例子7】 用栈模拟汉诺塔问题

  就是将一个递归问题用栈写出来。

代码:

#include <queue>
#include<iostream>
#include<stack>
#include<cstdio>
using namespace std;

/*定义状态*/
typedef struct Statute{
    char left,right,mid;
    int n;
    Statute(){};
    Statute(char L,char m,char r,int n):left(L),mid(m),right(r),n(n){};
}Statute;

/*用栈*/
void HanNouWei(Statute s){
   stack<Statute> stk;
   stk.push(s);
   int cnt = 0;
   while(!stk.empty()){
       Statute cur = stk.top();
       stk.pop();
       if(cur.n==1){
          cout << " from "<< cur.left << " to " << cur.right  << endl;
       }else{
            stk.push(Statute(cur.mid,cur.left,cur.right,cur.n-1));
            //此处并不能够直接输出结果,而应该将这一步的状态放入栈中
            //cout << " from "<< cur.left << " to " << cur.right  << endl;
            stk.push(Statute(cur.left,cur.mid,cur.right,1));
            stk.push(Statute(cur.left,cur.right,cur.mid,cur.n-1));
       }
   }

}

/*递归*/
void hanoi(Statute s){
    Statute cur = s;
    if(cur.n == 1) {
        cout << " from "<< cur.left << " to " << cur.right  << endl;
        return;
    } else {
        hanoi(Statute(cur.left,cur.right,cur.mid,cur.n-1));
        cout << " from "<< cur.left << " to " << cur.right  << endl;
        hanoi(Statute(cur.mid,cur.left,cur.right,cur.n-1));
    }

}

int main(){
    Statute s = Statute('A','B','C',4);
    HanNouWei(s);
    cout << "==============================="<< endl;
    hanoi(s);
    return 0;
}
View Code

 

总结:如果问题的结构类似于(A(B)(C(D(E)))),其中A代表原问题,B、C、D分别代表嵌套的子问题,一个括号表示一个完整子问题的左右边界,完整意味着这个子问题的解可能是最后的最优解,或者说是最终解中的一部分。在这里原问题的解决需要先解决嵌套的子问题,这时候就可以用栈来解决问题。这里我总结出一个不成熟的方法,既然括号的匹配是最典型的的栈问题,那么,我们可以用括号来划分那些完整的子问题。就拿例子2来说,2,1,5,6,2,3,可以划分为( (2), 1, ((5, (6)), 2, (3))),每一个括号里面只有一个单独的数字,括号就刚好代表该高度的矩形的跨度。即使不知道能否用栈,只要我们能够标明括号,来划分完整的子问题,这就很容易得到用栈的解决方法了。左括号入栈,右括号出栈,其他操作.....这样执行下去,直到找到答案。另外,拿例子1来说,1, 3, 6, 4, 2,用栈的话就是(((2), 4), 6), 3, 1,当然,能够这样做的前提就是,我们需要意识到数组从右至左最大值和次大值的重要性。

 【例子8】[LeetCode] Closest Binary Search Tree Value II 二分搜索树中离target最近的k个值

Given a non-empty binary search tree and a target value, find k values in the BST that are closest to the target.

Note:

  • Given target value is a floating point.
  • You may assume k is always valid, that is: k ≤ total nodes.
  • You are guaranteed to have only one unique set of k values in the BST that are closest to the target.

 

Follow up:
Assume that the BST is balanced, could you solve it in less than O(n) runtime (where n = total nodes)?

思路:这道题是寻找二分搜索树中和target最接近的k个点(突然想起了最近邻法)。这里的思路还是比较简单的,二分搜索树的中序序列,恰好是从小到大,所以,我们只要中序遍历,用一个vector(在这里其实是充当队列)来存储得到的k个点,如果遍历到第k+1个位置,就看看k该节点和vector中第一个节点,哪个里target最近。。。所以,这里其实就是在中序遍历上面加点visit()代码即可,既可以用到栈,也可以用递归。这里时间复杂度o(n),空间复杂度是o(k)。

这里还有一个lgn的思路,用到两个栈,一个栈用来存储比tartget小的若干个数,另一个存储比target大的若干个数。先init两个栈,找到离target最近的两个值(一个小于或者等于,一个大于)分别入栈。接着,归并这两个栈,如果还没有达到k个,则需要从这两个栈中的节点出发,再取寻找那些“稍远”的点入栈,越到栈顶,离target越近。继续归并。

如果简化这道题,只用寻找一个点的话,则可以用到二分法,时间复杂度是lgn。

 

代码:

 

【例子9】Ternary Expression Parser三元表达式解析

Given a string representing arbitrarily nested ternary expressions, calculate the result of the expression. You can always assume that the given expression is valid and only consists of digits 0-9?:T and F (T and Frepresent True and False respectively).

Note:

  1. The length of the given string is ≤ 10000.
  2. Each number will contain only one digit.
  3. The conditional expressions group right-to-left (as usual in most languages).
  4. The condition will always be either T or F. That is, the condition will never be a digit.
  5. The result of the expression will always evaluate to either a digit 0-9T or F.

Example 1:

Input: "T?2:3"

Output: "2"

Explanation: If true, then result is 2; otherwise result is 3.

 

Example 2:

Input: "F?1:T?4:5"

Output: "4"

 思路:这里从后往前遍历,将数全部入栈,忽略冒号,如果遇到?,则看看前面的字符是T还是F,有选择地进行出栈即可。如果要从左往右遍历的话,那么,加得等到所有?的index都入栈之后,才能够从最后一个?开始计算。

这里还有一个比较巧妙的递归解法,考虑到合法表达式中?和:的数目总是相等的,所以,可以设置一个cnt1计算?的个数,cnt2计算:的个数,当cnt1 == cnt2时,根据?前面的‘T’或‘F’选择:前面的子串或者后面的子串进行计算。

还有一个使用STL中内置函数find_last_of的简单方法,每次只需找到字符串中最后一个?,这时候后面必定紧跟着A:B,计算出这一步的结果,再放回字符串中,循环下去就能够得到整个式子的结果了。

 

【例子10】Verify Preorder Sequence in Binary Search Tree

验证一个list是不是一个BST的preorder traversal sequence。

Given an array of numbers, verify whether it is the correct preorder traversal sequence of a binary search tree.

You may assume each number in the sequence is unique.

Follow up:
Could you do it using only constant space complexity?

思路:二分数例如,123  4  567,它的先序序列是421 3 65 7,所以可以从左开始依次将数字入栈,如果遇到比top大的数字,则pop,将比当前数字小的都出栈。这时将这个节点坐标的节点都出栈了,同时记住最后一个出栈的数字,为min,min就是这个数字的父母节点。再将当前数字入栈,如果按照先序序列的话,后面的数字必定都比min大。例如,4 2 1  3 -> 4 3(min 2)-> 4 3 6 -> 6 (min 4) -> 6 5 7 -> 7 (min 6)。

另一种思路,使用常量空间。直接在序列上面进行修改,其实也只需改掉遍历时的前一个数字即可。另外,这个问题同样可以用递归来解决。

posted on 2017-08-06 23:24  1592653  阅读(852)  评论(0编辑  收藏  举报