数据结构与算法—栈详解

原创不易 还请一键三连支持java

什么是栈

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

栈是这么定义的:算法

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

稍微介绍一下关键名词:数据结构

运算受限:也就是这个表你不能随便的删除插入。只能按照它的规则进行插入删除。好比栈就只能在一端进行插入和删除。一样,队列也是运算受限,只能在两头操做。函数

线性表:栈也是一种线性表,前面详细介绍过线性表,它表达的是一种数据的逻辑关系。也就是在栈内各个元素是相邻的。固然在具体实现上也分数组和链表实现,他们的物理存储结构不一样。可是逻辑结构(实现的目的)相同。学习

栈顶栈底: 这个描述是偏向于逻辑上的内容,由于你们知道数组在末尾插入删除更容易,而单链表一般在头插入删除更容易。因此数组能够用末尾作栈顶,而链表能够头作栈顶。优化

image-20210421182034079

栈的应用: 栈的应用普遍,好比你的程序执行查看调用堆栈、计算机四则加减运算、算法的非递归形式、括号匹配问题等等。因此栈也是必须掌握的一门数据结构。最简单你们都经历过,你拿一本书上下叠在一块儿,就是一个后进先出的过程,你能够把它当作一个栈。下面咱们介绍数组实现的栈和链表实现的栈。this

数组实现

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

结构设计

对于数组来讲,咱们模拟栈的过程很简单,由于栈是后进先出,咱们很容易在数组的末尾进行插入和删除。因此咱们选定末尾为栈顶。因此对于一个栈所须要的基础元素是 一个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;栈满。

image-20210421170312904

pop弹出并返回首位

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

image-20210421170904604

其余操做

例如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;

image-20210421171338480

pop弹出

与单链表头删除一致,若是不太了解请先看笔者队线性表介绍的。

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

image-20210421171722989

其余操做

其余例如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;
}

总结

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

你们若是想交流可加我vxq1315426911交流,也可关注个人公众号:bigsai第一手学习知识,最后不要吝啬你的一键三连,原创求支持,谢谢

相关文章
相关标签/搜索