关于数据结构和算法:栈这种数据结构不就后进先出

33次阅读

共计 6779 个字符,预计需要花费 17 分钟才能阅读完成。

什么是栈

栈在咱们日常编码中遇到的十分多,很多人对栈的接触可能仅仅局限在 递归应用的是栈StackOverflowException,栈是一种后进先出的数据结构(能够设想生化金字塔的牢房和生化角斗场的狗洞)。

栈是这么定义的:

栈(stack)又名堆栈,它是一种 运算受限 的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,绝对地,把另一端称为栈底。向一个栈插入新元素又称作进栈、入栈或压栈,它是把新元素放到栈顶元素的下面,使之成为新的栈顶元素;从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。

略微介绍一下要害名词:

运算受限 :也就是这个表你不能轻易的删除插入。只能依照它的规定进行插入删除。比方栈就 只能在一端进行插入和删除。同样,队列也是运算受限,只能在中间操作。

线性表 :栈也是一种线性表,后面具体介绍过线性表,它表白的是一种 数据的逻辑关系 。也就是在栈内各个元素是相邻的。当然在具体实现上也分数组和链表实现,他们的物理存储构造不同。然而逻辑构造(实现的目标) 雷同。

栈顶栈底: 这个形容是偏差于逻辑上的内容,因为大家晓得 数组在开端插入删除 更容易,而 单链表通常在头插入删除 更容易。所以数组能够用开端做栈顶,而链表能够头做栈顶。

栈的利用: 栈的利用宽泛,比方你的程序执行查看调用堆栈、计算机四则加减运算、算法的非递归模式、括号匹配问题等等。所以栈也是必须把握的一门数据结构。最简略大家都经验过,你拿一本书高低叠在一起,就是一个后进先出的过程,你能够把它看成一个栈。上面咱们介绍 数组 实现的栈和 链表 实现的栈。

数组实现

数组实现的栈用的比拟多,咱们常常刷题也会用数组去实现一个简略的栈去解决简略的问题。

结构设计

对于数组来说,咱们模仿栈的过程很简略,因为栈是后进先出,咱们很容易在数组的开端进行插入和删除。所以咱们选定开端为栈顶。所以对于一个栈所须要的根底元素是 一个 data[] 数组 和一个 top(int) 示意栈顶地位。

那么初始化函数代码为:

private T data[];
private int top;
public seqStack() {data=(T[]) new Object[10];
    top=-1;
}
public seqStack(int maxsize)
{data=(T[]) new Object[maxsize];
    top=-1;
}

push 插入

栈的外围操作之一 push():入栈操作。

  • 如果 top< 数组长度 -1。入栈,top++;a[top]=value;
  • 如果 top== 数组长度 -1;栈满。

pop 弹出并返回首位

  • 如果 top>=0, 栈不为空,能够弹出。return data[top--];
  • 如下图,原本栈为 1,2,3,4,5,6(栈顶), 执行 pop 操作,top 变为 3 的地位并且返回 4;

其余操作

例如 peek 操作时返回栈顶不弹出. 所以只需满足要求时候 return data[top] 即可。

数组实现:

package 队栈;

public class seqStack<T> {private T data[];
    private int top;
    public seqStack() {data=(T[]) new Object[10];
        top=-1;
    }
    public seqStack(int maxsize)
    {data=(T[]) new Object[maxsize];
        top=-1;
    }
    boolean isEmpty()
    {return top==-1;}
    int length()
    {return top+1;}
    
    boolean push(T value) throws Exception// 压入栈
    {if(top+1>data.length-1)
        {throw new Exception("栈已满");
        }
        else {data[++top]=value;
            return true;
        }
    }
    T peek() throws Exception// 返回栈顶元素不移除
    {if(!isEmpty())
        {return data[top];
        }
        else {throw new Exception("栈为空");
        }
    }
    T pop() throws Exception
    {if(isEmpty())
        {throw new Exception("栈为空");
        }
        else {return data[top--];
        }
    }
    public String toString()
    {if(top==-1)
        {return "";}
        else {
            String va="";
            for(int i=top;i>=0;i--)
            {va+=data[i]+" ";
            }
            return va;
        }
    }
}

链表实现

有数组实现,链表当然也能实现。对于栈的设计,大抵能够分为两种思路:

  • 像数组那样在尾部插入删除 。大家都晓得链表效率低在查问,而查问到尾部效率很低, 就算用了尾指针 ,能够 解决尾部插入效率 ,然而仍然 无奈解决删除效率 (删除须要找到前驱节点),还 须要双向链表 。后面尽管具体介绍过双向链表,然而这样未免 太简单
  • 所以咱们采纳 带头节点的单链 表在头部插入删除,把头当成栈顶,插入间接在头节点后插入,删除也间接删除头节点后第一个节点即可,这样就能够完满的满足栈的需要。

结构设计

设计上和链表很类似,长话短说,短话不说,间接上代码就懂。
链表的节点

static class node<T>
{
    T data;
    node next;
    public node() {}
    public node(T value)
    {this.data=value;}
}

根本构造:

public class lisStack <T>{
    int length;
    node<T> head;// 头节点
    public lisStack() {head=new node<>();
        length=0;
    }
    // 其余办法
}

push 插入

与单链表头插入统一,如果不太理解能够看看后面写的线性表有具体解说过程。

和数组造成的栈有个区别,链式实现的栈实践上栈没有大小限度(不冲破内存零碎限度),不须要思考是否越界,而数组则须要思考容量问题。

如果一个节点 team 入栈:

  • 空链表入栈head.next=team;
  • 非空入栈team.next=head.next;head.next=team;

pop 弹出

与单链表头删除统一,如果不太理解请先看笔者队线性表介绍的。

和数组同样须要判断栈是否为空,如果节点 team 出栈:head 指向 team 后驱节点。

其余操作

其余例如 peek 操作时返回栈顶不弹出. 所以只需判空满足题意时候 return head.next.data 即可。而 length 你能够遍历链表返回长度,也能够动静设置 (本文采取) 追随栈长变动。

链表实现:

package 队栈;

public class lisStack <T>{
    static class node<T>
    {
        T data;
        node next;
        public node() {}
        public node(T value)
        {this.data=value;}
    }
    int length;
    node<T> head;// 头节点
    public lisStack() {head=new node<>();
        length=0;
    }
    boolean isEmpty()
    {return head.next==null;}
    int length()
    {return length;}
    public void push(T value) {// 近栈
       node<T> team=new node<T>(value);
       if(length==0)
       {head.next=team;}
       else {
        team.next=head.next;
        head.next=team;}
       length++;
    }
    public T peek() throws Exception {if(length==0) {throw new Exception("链表为空");}
        else {// 删除
            return (T) head.next.data;
        }
  }
    public T pop() throws Exception {// 出栈
      if(length==0) {throw new Exception("链表为空");}
      else {// 删除
        T value=(T) head.next.data;
              head.next=head.next.next;//va.next
              length--;
              return value;
            }
    }
    public String toString(){if(length==0) {return "";}
        else {
              String va="";
            node team=head.next;
            while(team!=null)
            {
                va+=team.data+" ";
                team=team.next;
            }
            return va;
         }    
    }
}

栈能这么玩

既然下面具体解说设计栈,这里来两道栈十分经典十分经典的例题(十分高频,很容易忘,又很重要,一般问题就不放的)

力扣 20 无效的括号:

题意:给定一个只包含 '(',')','{','}','[',']' 的字符串,判断字符串是否无效。

无效字符串需满足:

左括号必须用雷同类型的右括号闭合。
左括号必须以正确的程序闭合。
留神空字符串可被认为是无效字符串。

示例 :

输出: "()[]{}"
输入: true

示例 :

输出: "([)]"
输入: false

剖析:
括号类的问题是经典栈类问题,必定要想到用栈解决。判断一个字符串满不满足一个无效的字符串,就要看它是不是都能组成对。

从单个括号对来说,((,))都是不满足的,只有 () 才可满足,即一左一右。

从多个括号对来说 {[(字符串还可承受任意有限 ([,{ 的括号。然而如果向左的括号只能先接管 ) 括号(变成{[)。

从下面能够看作一种相打消的思维。例如 (({[()()]})) 字符串遍历时候能够这样解决:

  • (({[(下一个 ) 消掉成(({[
  • (({[(下一个 ) 消掉成(({[
  • (({[下一个 ] 消掉成(({
  • (({下一个 } 消掉成((
  • ((下一个 ) 消掉成(
  • (下一个 ) 消掉成 这样就满足题意

每次操作的时候都判断残余无效括号最顶部那个括号是否可能和遍历的相打消,这个过程利用栈判断以后是退出栈还是打消顶部,到最初如果栈为空阐明满足,否则不满足,当然具体括号要对应,具体实现代码为:

public boolean isValid(String s) {Stack<Character>stack=new Stack<Character>();
     for(int i=0;i<s.length();i++)
     {char te=s.charAt(i);
         if(te==']')
         {if(!stack.isEmpty()&&stack.pop()=='[')
                 continue;
             else {return false;}
         }
         else if(te=='}')
         {if(!stack.isEmpty()&&stack.pop()=='{')
                 continue;
             else {return false;}
         }
         else if(te==')')
         {if(!stack.isEmpty()&&stack.pop()=='(')
                 continue;
             else {return false;}
         }
         else
             stack.push(te);
     }
     return stack.isEmpty();}

当然,JDK 自带的栈用起来不快,能够用数组优化:

public boolean isValid(String s) {char a[]=new char[s.length()];
    int index=-1;
     for(int i=0;i<s.length();i++)
     {char te=s.charAt(i);
         if(te==']')
         {if(index>=0&&a[index]=='[')
                 index--;
             else {return false;}
         }
         else if(te=='}')
         {if(index>=0&&a[index]=='{')
                 index--;
             else {return false;}
         }
         else if(te==')')
         {if(index>=0&&a[index]=='(')
                 index--;
             else {return false;}
         }
         else
             a[++index]=te;
     }
     return index==-1; 
 }

力扣 32 最长无效括号(艰难)

题目形容:给定一个只蕴含 ‘(‘ 和 ‘)’ 的字符串,找出最长的蕴含无效括号的子串的长度。

示例 :

输出: “(()”
输入: 2
解释: 最长无效括号子串为 “()”

示例 :

输出: “)()())”
输入: 4
解释: 最长无效括号子串为 “()()”

计划一暴力

这种题核心思想就是应用 栈模仿 。本题的话更简略一点因为只有()两种括号,使用暴力的时候就能够循环每次找到最长的无效括号。而括号匹配的时候能够 间接终止 的状况是 ) 右括号多出无奈匹配。

例如 ())( 到第三个不可能和后面相连。如果来 ( 只须要期待前面可能来 ),一个) 能够和一个 ( 组成一对,打消栈中的一个(

当然,在具体的实现上,咱们用数组模仿栈,实现代码为:

public  int longestValidParentheses(String s) {char str[]=s.toCharArray();// 字符数组
    int max=0;
    for(int i=0;i<str.length-1;i++)
    {
        int index=-1;
        if(max>=str.length-i)
            break;
        for(int j=i;j<str.length;j++)
        {if(str[j]=='(')
                index++;
            else {if(index<0)
                {
                    i=j;
                    break;
                }
                else {index--;}
            }
            if(index==-1&&(j-i+1>max))
            {max=j-i+1;}
        }
    }    
    return max;
}

这个复杂度太高,咱们看看如何用栈优化。

计划二栈优化

如何将这道题从一个 O(n2)的工夫复杂度优化到 O(n)?很容易,咱们须要留神他的过程。咱们先轻易看几个可能的最大状况。

  • ()) () ( () ()) 最大为前面局部(空格离开)
  • () () (( () 最大为后面局部
  • (( ( ( ( () () () () 最大为前面局部

对于这么一次获取你会发现不同括号会有些区别:
(:左括号一旦呈现那么他就期待一个 ) 进行匹配,但它的前面可能有 ) 并且在这两头有很多其余括号对。
): 右扩号有两种状况:

  • 一种是以后曾经超过左括号后面曾经不可能间断了。例如 ()) () 第三个括号呈现曾经使得整个串串不可能间断,最大要么在其右面 要么再其右面。你能够了解其为一种清零初始机制。
  • 另一种状况 ) 就是指标栈中存在 ( 可与其进行匹配。匹配之后要叠加到打消后平级的数量上,并且判断是否是最大值。(上面会解释)

具体实现 的思路上,就是应用一个 int 数组标记以后层级 (栈深) 有正确的括号数量。模仿一次栈行为从左向右,遇到 ) 太多 (以后栈中不存在( 进行匹配)就将数据清零从新开始。这样始终到最初。你能够把它看成台接,遇到 ( 就上一个台阶 并清零该新台阶 ,遇到) 就下一个台阶并且把 数量加到降落后的台阶上。具体能够看上面图片模仿的过程:
() ( () () ( () ) )

认真看看这张图,具体实现代码为:

 public static int longestValidParentheses(String s) {
        int max=0;    
        int value[]=new int[s.length()+1];
        int index=0;
        for(int i=0;i<s.length();i++)
        {if(s.charAt(i)=='(')
            {
                index++;
                value[index]=0;
            }
            else {//")"
                if(index==0)
                {value[0]=0;
                }
                else {value[index-1]+=value[index--]+2;// 叠加
                    if(value[index]>max)// 更新
                        max=value[index];
                }
            }
        }
        return max;
 }

用栈也能够实现,然而效率比数组略低:

public int longestValidParentheses(String s) {
  int maxans = 0;
  Stack<Integer> stack = new Stack<>();
  stack.push(-1);
  for (int i = 0; i < s.length(); i++) {if (s.charAt(i) == '(') {//(将以后的 
      stack.push(i);
    } else {stack.pop();
      if (stack.empty()) {stack.push(i);
      } else {//i-stack.peek 就是 i 是呈现的总个数 peek 是还没匹配的个数
        maxans = Math.max(maxans, i - stack.peek());
      }
    }
  }
  return maxans;
}

总结

到这里,本文对栈的介绍就完结了,置信你能够手写个栈并且能够小试牛刀解决括号匹配问题!当然栈能解决的问题还有很多比方接雨水问题、二叉树非递归遍历等等,有些重要的还会再总结。

欢送关注我的公众号:bigsai第一手学习常识,最初不要 悭吝你的一键三连,原创求反对,谢谢

正文完
 0