剑指offer之栈

一、杂话:不知不觉中就来到了栈了。哈哈,不过还是进度缓慢呀。栈之后的数据结构还有很多,有些自己都还没看完呢。路漫漫其修远兮。今天查了下,腾讯招聘的结果,不出预料,哈哈,又挂了。确实是能力不足呀。一方面编程能力不太行,另一方面相关基础知识又没学过,自个还得自学。难呀。不过现在,先不管那么多,先把数据结构与算法搞好,然后搞下操作系统,数据库,顺带再看看STL。加油吧,少年。
二、栈的相关知识点:
①首先要明白什么是栈。简单来说,就是符合先进后出的一种数据结构,就像往桶里面放东西,最先放进去的,最后才能拿出来,而最后放进去的,会在最上面,就会是最先拿出来的。
②栈的实现:学过了之前的数组和链表,就知晓了。其实无非两种方式,一种由数组实现,一种由链表实现。下面就展开讲下这两种实现方式:
1.数组实现栈:
图片说明
代码如下:

const int MAXSIZE = 1000;
//定义一个栈,用结构体定义
template <class T>
struct myStack {

private:    
    int top=-1;//栈顶
    T data[MAXSIZE];//自定义最大长度为1000

public:    
    //定义入栈函数
    void push(T n ){
        data[++top]= n;

    }
    //定义出栈函数
    void pop() {
        --top;
    }
    //获取栈顶元素函数
    T mytop() {
        return data[top];
    }
    //定义判空函数
    bool isEmpty() {
        return top == -1;
    }
    //定义长度大小函数
    int mysize() {
        return top+1;
    }
};

2.链表实现栈:
图示如下:
图片说明
代码如下:

//定义链表
template<class T>
struct ListNode {
    T val;
    struct ListNode* next;
    ListNode() :val(0), next(nullptr){}
    ListNode(T x) :val(x),next(nullptr ){}
};

//定义由链表构成的栈
template<class T>
struct myStack {
private:
    ListNode<T>* top=nullptr;
public:
    void push(T n) {
        ListNode<T>* newnode = new ListNode<T>(n);
        newnode->next = top;
        top = newnode;
    }
    void pop() {
        ListNode<T>* tmp = top;
        top = tmp->next;
        free(tmp);
    }
    T mytop() {
        return top->val;
    }
    bool isEmpty() {
        return !top;
    }
    int mysize() {
        if (top) {
            ListNode<T>* tmp = top; int sum = 0;
            while (tmp) {
                ++sum;
                tmp = tmp->next;
            }
            return sum;
        }
        else   return 0;
    }
};

③双栈共享空间:
即用一个数组存两个栈,这样就会一个问题,怎么判断栈满了呢,还有怎么判断出入的是哪个栈呢。
1.对于第一个问题,是通过用top1+1==top2来判断栈是否满的,因为有假设栈为空时,左边栈top1=-1,右边栈top2=n,即使左栈满了,右栈为空top2=0,top1=n-1,此时仍有top1+1=top2。所以,上面构造栈的两种方式中,要注意栈为空时top要么为-1位置,要么为空指针。
2.对于第二个问题,可以引入一个标志位,作为push,pop,isEmpty,mysize,等函数的参数,进而区分使用的是哪个栈。代码略。
④栈的应用:
理解递归:通过看了知乎大佬的讲解后,有了新的认识。其实递归就是递推加回归。其实现的基础就是栈。
递归主要掌握三点:
第一,要知道函数实现什么功能,这就要求编写者实现知道可以通过递归来解决问题,从而才能定义递归函数。
第二,明确回归点,就是递归结束的条件,容易遗漏导致死循环,可以通过递归关系式来反验证。
第三,找到递归关系式,怎么找,大佬指出,这个关系式都是将原范围一步步缩小的。因此,可以尝试缩小范围,缩小范围后需要执行什么操作才能达到结果,这些操作往往就是递归关系。
除此之外,由于递归有很多重复计算,因此可以通过记录已算值得方式减少不必要得重复,可以用数组/哈希表,一般用数组。只要额外判断下是否计算过即可。具体例子见下方链接的知乎文章。
三、刷题总结
①JZ5 用两个栈实现队列---简单
图片说明
思路:容易想到,一个栈存数据,一个栈抛出数据。进入栈1,再将栈1进到栈2,通过两次反转,这时,栈2就符合队列的弹出顺序了。进栈好说,但是出栈就会有一个问题,怎么保证栈2内的元素符合队列先进先出的规则呢。例如下图,如果栈2还没pop完,栈1就往栈2放入元素,就会导致顺序变乱。为此,要注意两点:第一,把栈1元素push到栈2前需要判断,栈2是否为空。第二,必须一次性把栈1的元素全部放进去。
图片说明

代码如下:
class Solution
{
public:
    void push(int node) {
        stack1.push(node);
    }

    int pop() {//注意只有当stack2为空时,才可以将stack1中的元素全部push进去
        if(stack2.empty()){
           while(!stack1.empty()){
                stack2.push(stack1.top());
                stack1.pop();
            }
         }
            int p= stack2.top();
            stack2.pop();
            return p; 
    }

private:
    stack<int> stack1;
    stack<int> stack2;
};

②JZ20 包含min函数的栈---较难
图片说明
思路:用两个栈,栈1负责存数据,栈2个负责存最小的数。每一次,进入新数据,如果栈2为空就直接放进去,否则就比较新数与栈2栈顶的数哪个更小,如果新数小就放入,否则继续放栈顶的数。这样做的目的是与栈1同步,确保栈1pop完后栈2存在栈顶的元素就是栈1的最小元素。见图:
图片说明

代码如下:

class Solution {
public:
    stack<int>stack1,stack2;
    void push(int value) {
        stack1.push(value);
        if(stack2.empty()||value<stack2.top())  stack2.push(value);
        else    stack2.push(stack2.top());
    }
    void pop() {
        stack1.pop();stack2.pop();
    }
    int top() {
         return stack1.top();
    }
    int min() {
         return stack2.top();
    }
};

③JZ21 栈的压入、弹出序列---中等
图片说明
思路:用一个栈去模拟入栈出栈的过程。同时用两个指针i,j分别指向入栈向量和出栈向量。如果说,pushV[i]==popV[j],证明一入栈就出栈,++i和++j,否则就说明该元素入栈。如果是入栈即出栈,要注意一点,就是可能此时栈里有元素,所以得判断下popV[j]是否等于栈顶元素,如果是,就要把栈顶元素pop掉,同时++j,这里还要注意,这个过程是不断while的,直到,popV[j]不等于栈顶元素了才停止。可以尝试自行图解体会。原理大致如此。这是一个很奇妙的题目,想到了就能做出来,所以要部分记忆。
代码如下:

    bool IsPopOrder(vector<int> pushV,vector<int> popV) {
        //双指针+栈模拟
        //特殊情况
        if(pushV.size()!=popV.size())    return 0;
        stack<int>pushstack;
        int i=0,j=0;
        while(i<pushV.size()&&j<popV.size()){
            if(pushV[i]!=popV[j]){//说明没有一入栈就pop掉
                //此时要把元素放进pushstack中
                pushstack.push(pushV[i++]);
            }
            else{
                ++i;++j;
                while(!pushstack.empty()&&popV[j]==pushstack.top()){//发现popV是栈顶元素,则应该把栈顶元素pop掉
                    pushstack.pop();
                    ++j;
                }
            }
        }
        return pushstack.empty();

④JZ23 二叉搜索树的后序遍历序列---较难
图片说明
思路:这个题与二叉树的后序遍历有关,因此在开始前需要了解下,二叉树的后序遍历的实现方式。二叉树的后序遍历实现方式有两种,一种是直接用递归实现,一种是用非递归实现。下面分别讲这两种方式。
1.二叉树后序遍历的递归实现
先遍历左右树,在输出当前结点,递归函数即可。

//递归实现二叉树的后序遍历
void back_search(tree *head) {
    if (!head)    return;
    back_search(head->left);
    back_search(head->right);
    cout << head->val<<"  ";
}

2.非递归实现二叉树的后序遍历:
比较难。需要创建两个指针,一个指向当前结点,一个记录前一个遍历的结点,还得注意每次当前结点都必须是栈顶的结点,这一点很关键,如果没有这一点的话,函数会死循环的。原理就是,每次结点输出之前都是左右树结点先输出完了,自身才能输出。因此需要记录前一个输出的结点是否是当前结点的左右结点,或者当前结点无左右结点,如果是,则可以输出当前结点。否则,需要把结点两端的左右结点入栈。注意循环的条件是,存储树结点的栈不为空,因此需要一开始就把根节点放入栈中。还有,放入左右树时,注意必须是先放入右树,在放入左树,原因是,遍历时是先左后右,而入栈后是先进后出,所以先放右再放左。

//非递归实现二叉树的后序遍历---费解
void back_search2(tree *head) {
    stack<tree *>s; tree* cur=head;
    tree* pre=nullptr;//用来标记已经遍历的结点
    s.push(cur);
    while (!s.empty() ) {
        cur = s.top();//cur一定要始终指向栈顶
        if ((!cur->left && !cur->right)||(pre&&(pre==cur->left||pre==cur->right))) {
            cout << cur->val << "  ";
            s.pop();
            pre = cur;
        }
        else {
            if (cur->right)    s.push(cur->right);
            if (cur->left)    s.push(cur->left);
        }
    }
}

3.另外,题目提到了一种特殊的树,叫二叉搜索树,这种树与普通的二叉树不同。这种树的左子树结点的值都比根节点值小,右子树结点的值都比根节点值大,且左右子树也是二叉搜索树。
4.解题方法:
(1)递归的方式---有点费解:
有点类似快排,二分查找,归并排序,都是找到中间的位置,然后递归考虑两边的情况。鉴于二叉搜索树的特点,我们可以先就一个结点,分析它是否符合左树小于,右树大于的特点,然后再递归分析左树和右树是否也符合这个特点。引入两个指针,left和right,让探针probe=left,让其在左边找到区分左右树的临界点,然后,让probe继续移动,判断右边的数是否都大于根节点值,如果时就让其继续移动,否则就不动。这样,如果probe==right就证明该结点符合左树小于,右树大于的特点,接下来继续递归判断左树和右树即可。注意两点:第一,回归点是,left>=right,原因是要确保左边指针始终在左边,右边指针始终在右边,当出现left>=right就证明此时只剩<=1个数了,不用再判断了,肯定是true。第二,判断右树时,需要right--,因为,第一个根节点已经判断过了,所以要判断下一个,需要--right。
代码如下:

    bool VerifySquenceOfBST(vector<int> sequence) {
        //借助搜索树的特点,我们可以定位到左右树的区间
        return BST(sequence,0,sequence.size()-1);
    }
    bool BST(vector<int> v,int left,int right){
         //特例
        if(!v.size())    return 0;
        if(left>=right)    return 1;//left>righe代表没有数了
         int probe=left,mid;
        while(v[probe]<v[right])    ++probe;
        mid=probe;
        while(v[probe]>v[right])    ++probe;
        return probe==right&&BST(v,left,mid-1)&&BST(v,mid,right-1);
    }

(2) 利用单调栈解决---没太懂
首先了解下什么是单调栈。顾名思义,单调栈就是,栈内元素是有序单调的,有两种,单调增和单调减,不过它指的单调是从栈顶到栈底的,比如入栈顺序[1,2,3,4,5]那么就是一个单调递减栈。
这种解法很绕,暂且先放着:
代码如下:

bool VerifySquenceOfBST(vector<int> sequence) {
       if(!sequence.size())    return 0;
       stack<int>s;int parent=INT32_MAX;
       for(int i=sequence.size()-1;i>=0;--i){
           if(sequence[i]>parent)    return 0;//如果比父节点大,证明非搜索树 
           while(!s.empty()&&sequence[i]<s.top()){//如果小于,证明是左节点
               parent=s.top();
               s.pop();
           }

           s.push(sequence[i]);
       }
       return 1;
}

⑤JZ59 按之字形顺序打印二叉树---较难
图片说明
思路:可以说自个是完全没有思路的,目前已知的有,前序遍历,中序遍历,后序遍历的递归实现,对前序遍历,中序遍历和后序遍历的非递归实现有了解,但是不熟悉,将在涉及这一板块内容时,再进一步详述。至于层序遍历,就只知道一个用队列实现,而像题目要求的之字形这种遍历方法就更不懂了。原因主要是对树的遍历并不熟悉。
因此,只好借鉴大佬的思路,尝试理解下:其实这个题用的是层序遍历的思想,而层序遍历的思想其实就是广度优先搜索,即BFS,BFS有下面两种模板,一种是不记录层数,一种是记录层数的:

//不记录层数的BFS
void BFS(){
    vist[]={0};
    quene<int>q(start_val);
    while(!q.empty()){
        int cur=q.front;q.pop();
        for(遍历cur所有相邻结点next){
            if(next存在&&vist[next]==0){
                vist[next]=1;
                q.push(next);
            }
        }
    }
}
//记录层数的BFS
void BFS(){
    vist[]={0};int level=0;
    quene<int>q(start_val);
    while(!q.empty()){
        int turns=q.size();
        while(turns--){
            int cur=q.front;q.pop();
            for(遍历cur所有相邻结点next){
                if(next存在&&vist[next]==0){
                    vist[next]=1;
                    q.push(next);
                }
            }
        }
        ++level;
    }
}

因此,有方法一,用BFS实现之字形遍历:

    vector<vector<int> > Print(TreeNode* pRoot) {
        //用BFS解
        vector<vector<int>>res;//用来存结果;
        if(!pRoot)    return res;//特殊情况
        queue<TreeNode *> q;//用队列来存每一层的结点
        int level=0;//记录遍历层数
        q.push(pRoot);//先要把根节点放入队列中
        while(!q.empty()){
            int turns=q.size();
            vector<int>ans;//用来记录每一层的遍历结果
            while(turns--){
                TreeNode* node=q.front();
                q.pop();
                ans.push_back(node->val);
                if(node->left)    q.push(node->left);
                if(node->right)    q.push(node->right);
            }
            ++level;
            //判断是否为偶数,如是,则翻转
            if(!(level&1))
                reverse(ans.begin(), ans.end());
            res.push_back(ans);

        }
        return res;
    }

还有方法二:利用两个栈来实现

稍微总结下:树的遍历中前序,中序,后序都与栈有关,而层序遍历往往与队列有关。在学树这一板块时,需要重新总结下,栈和队列的应用。

试题链接:
用两个栈实现队列
包含min函数的栈
栈的压入、弹出序列
二叉搜索树的后序遍历序列
按之字形顺序打印二叉树
参考文章:
知乎大佬的好文:对于递归有没有什么好的理解方法

全部评论

相关推荐

点赞 评论 收藏
分享
点赞 收藏 评论
分享
牛客网
牛客企业服务