栈与队列Java

目录

实现栈

实现思路

什么是栈

​栈的用处

实现栈所需的基本功能一览

代码展示

官方标准库(栈)Java 

代码讲解

实现队列

实现思路

什么是队列(queue) 

普通队列

循环队列

实现队列所需要写的基本功能

代码展示

 官方标准库(队列)Java 

代码讲解

enQueue()方法的细节

用栈实现队列

实现思路

代码展示

代码讲解

dump()

栈的应用:求最小栈

实现思路

代码展示

代码讲解

总结


实现栈

实现思路

什么是栈

我们在写实现栈的代码之前要先了解一下什么是栈。栈首先是一种数据结构,数据先进后出。就是说第一个被添加到空栈里面的元素,只能最后一个被遍历到或被删除。这种数据结构其实我们在生活中很常见到。你一般会不会用柜子装一些书?没错,这就是典型的栈结构——最先放进去的书要扒开所有后面放的书才能拿出来,这种模式叫做LIFO(Last In First Out),即后进先出。

栈的用处

那为什么要用栈呢?栈有很广的应用场景。我们常用的“撤销”功能,浏览器的历史记录,以及底层的递归,判断有效括号,表达式求值和数制转换等问题 均能见到他的身影。建议阅读CSDN博主 KLeonard《栈的应用》这篇文章来加深对栈的应用的印象。

我们这次栈的功能的实现是基于动态列表的,如果大家看不懂可以直接滑到下面,这里有Java的Stack官方库代码供大家直接使用。事不宜迟,我们现在就开始正式介绍实现栈所需的一些功能。

实现栈所需的基本功能一览

首先,我们需要一个方法来添加元素,这个过程叫做压栈,我们姑且把这个方法起名为push()把。

其次我们需要去删除元素,即出栈——那我们给这个方法起名为pop()。但是由于这个pop()是boolean类型的,因此虽然最后实现了删除的效果,但返回的是一个布尔值。由于我们在后续需要用到双栈,即将一个栈中元素删除后又将其添加到另一个栈里,我们就需要再写一个有返回值的方法,我们给该方法起名为popR()吧,popReturn的缩写。

另外,在应用栈时,我们难免会需要取元素,即取栈最后添加的元素,我们给他起名为topE()方法,topElement的缩写。

不仅如此,我们还急需一个判断栈是否为空的方法isEmpty(),这样pop()方法在删除元素前先判断栈是否为空,不为空再进行删除操作,避免了一些不必要的麻烦。

还有一个print()打印方法,这个功能可以直接打印栈中的所有元素,和topE()方法应用上各有殊途。print()的一大优势就是,允许不用Debug的情况下都可以查看到栈的状态。

最后,为了快速生成一个栈,我们通常会想——先初始化一个数组,添加一些值,然后通过循环遍历数组并把元素压入该空栈中,这样就避免了一长串的push()方法的使用。那我们为何不把这个循环操作封装成一个方法,然后调用该方法,把数组作为参数传入,然后立马就得到了一个新鲜的栈……这想想都美滋滋!所以我们再写一个cvtArr()的方法(convertArray的缩写),将数组内所有元素快速压入栈中。

注:remove(),add()等5个方法皆是动态列表里面的内置方法


代码展示

package advance.dataStructure;

import java.util.ArrayList;
import java.util.List;

class MyStack {
	private List<Integer> data=new ArrayList<>();// 动态列表
    //添加元素(压栈)
	public void push(int e) {
		data.add(e);
	}
    //判断栈是否为空
	public boolean isEmpty() {
		return data.isEmpty();
	}

	// 返回栈顶元素
	public int topE() {
		return data.get(data.size() - 1);
	}
    //删除元素(出栈)
	public boolean pop() {
		if (isEmpty())
			return false;
		data.remove(data.size() - 1);
		return true;
	}//删除元素,并返回该元素
	public int popR() {
		if (isEmpty())
			return 0;
		data.remove(data.size() - 1);
		return topE();
	}
    //打印栈内所有元素
	public void print() {
		for (int e : data)
			System.out.println(e);
	}
	//将数组转换成栈
	public void cvtArr(int[] arr) {
		for (int e : arr)
			push(e);
	}
};

public class MyStack_Main {
    //测试
	public static void main(String[] args) {
		MyStack obj = new MyStack();
		int[] arr = { 3, 2, 5, 1 };
		obj.cvtArr(arr);
		obj.pop();
		obj.print();
	}
}

官方标准库(栈)Java 

package advance.dataStructure;

import java.util.Stack;

public class Stack_library {
	public static void main(String[] args) {
		//1.创建栈对象
		Stack<Integer> stack = new Stack<>();
		int[] arr = { 1, 4, 2, 5 };
		//2.添加元素(压栈)
		for (int e : arr)
			stack.push(e);
		// 3.判断栈是否为空
        stack.empty()
		// 4. 删除元素(出栈)
		stack.pop();
		// 5. 返回栈顶元素(stack.peek())
		System.out.println("The top element is: " + stack.peek());
		// 6.返回栈的长度.(stack.size())
		System.out.println("The size is: " + stack.size());

	}
}

代码讲解

相信在看完《实现栈所需的基本功能一览》这个模块后,对栈的Java代码已经不难理解了。这里我写了两块代码,一块是自己实现的,一个是官方写的。个人建议如果真要写项目了,还是选择官方写的比较好,毕竟自己写的还是担心有点靠不住。不过平常写代码自娱自乐的时候,调用自己实现的栈也未尝不可。

另外,在第一段代码中,动态列表data的remove()和get()方法里面的参数时列表索引。其他……没了


实现队列

实现思路

什么是队列(queue) 

我们直接上定义:队列是限制仅在一端进行插入操作,在另一端进行删除操作的线性表。我看到队列这种结构总有种莫名的熟悉感。事实上,当年在学习电流的时候就用到了这个模型,电子们从电路的一端向另一端“滚动”,且进去的个数总等于出来的个数。。。好吧,言归正传。

队列 (queue) 相对栈 (stack) 来说实现的要复杂一些,毕竟栈有栈底,只有一个出入口,是单向的;而队列有一个入口,一个出口,是双向的。这就给了它几条性质:

1.队列先进先出(FIFO),也就是说先入队的元素先出队,即先被添加的元素先被删除

2.队列的长度是有限的,并且其数据在数据库中的分布是连续的 

3.队列需要用到两个指针,头指针p0 (pointerToTheFront的缩写) 和尾指针prear (pointerToTheRear的缩写),分别指向队列的第一个元素(队头)和最后一个元素(队尾)

那为什么偏要指针呢?因为有了指针才能确保先进先出,毕竟队列是由数组实现的,数组可没有先进先出的习惯。于是此时需要两个指针,来指明添加和删除元素的方向。

队列基本形

注:以上图片来源于data.biancheng.net

普通队列

普通队列说起来就有点鸡肋了,但这也是队列的雏形。假设队列长度为3,那么此时p0和prear重合并指向第0个元素。我们每添加一个元素,prear就会自增1,指向下一个元素;我们每删除一个元素,p0就会自增1,指向下一个元素。

此时我们在空队列中添加一个元素2,prear指向第1个元素2;又添加一个4,prear指向第2个元素;最后添加一个6,prear指向第3个元素。此时队列满了,无法再添加元素。看来只能删除元素释放内存了,我们删除第一个元素2,p0指向第2个元素。现在的问题是,即便我们删除了一个的元素,也无法在添加元素了,因为prear指针指向了队列的最后一个元素,无法再向后挪,相当于是添加新元素的入口被堵死了,这样还是很浪费存储空间的。

循环队列

这个时候我们就需要循环队列的加持。有了循环队列,尾指针prear取余该队列长度,这样它就指向了第一个元素的位置,这样新元素就可以被添加,多聪明!p0也是同理。循环队列和普通队列的不同之处在于,循环队列的两个指针可以通过取余循环在队列中行进自如,而普通队列,prear指向最后一个元素时就被卡死了。

现在内存的问题解决了,我们现在还有一个未解之谜,即如何判断队列是否为空和队列是否为满——这两个判断不论是在入队,出队,还是打印值等功能中都起到了尤为重要的作用。而我们此时却发现,判断空和满的条件是一样的,都是p0==prear!于是为了区别这两个条件,我们不得已在队列末尾留出一空元素位,里边什么也不存储,这样判断队列是否为满的条件就变成了:(prear+1)%length==p0,与判断是否为空的条件p0==prear区分开来。

大家可以自行推算一下,这种方法不管遇到了什么情况——不管是p0在prear前面,还是prear经过一轮添加操作跑到了p0的前面,队列都运转的稳如泰山,不出一点岔子。

实现队列所需要写的基本功能

首先我们需要一个构造器,给类里面的属性赋值。

其次是入队,我们给这个功能起名为enQueue(),如果队列不为满就执行添加元素的指令,且指针索引自增一,为下一次添加指令作好基础。

然后是出队,我们给这个功能起名为deQueue(),如果队列不为空就执行删除元素的指令,同时指针索引自增一,移到下一个元素的位置

较为重要的判断队列是否为空和是否为满的两个功能,分别起名为isEmpty() isFull()。第一个判断队列是否为空的功能主要语句是p0==prear。

最后就是三个不太重要的小功能,一个是返回队列最后一个元素,一个是返回列表第一个元素,另外一个是打印队列。我们把这三个小功能分别起名为:front()rear(),和print()


代码展示

package advance.dataStructure;

class MyQueue {
	private int p0;//头指针的位置
	private int prear;//尾指针的位置
	private int capacity;//队列的实际长度
	private int[] arr;//队列的载体
	// private ArrayList<Integer> data;

	public MyQueue(int len) {//len是队列的名义长度
		capacity = len + 1;//capacity是加上了空元素位后的队列长度
		arr = new int[capacity];
		p0 = prear = 0;
	}
    //打印队列
	public void print() {
        int last=this.arr.length-1;
		for (int i=0;i<last;i++) {
			System.out.print(arr[i]+", ");
		}
        System.out.println(arr[last]);
	}//打印效果:a, b, c, d
    //入队
	public boolean enQueue(int value) {
		if (isFull()) {
			return false;
		}
		if(prear==arr.length-1){//如果指针在空元素位就回到索引0
			prear=(prear+1)%capacity;
        }
		arr[prear] = value;
		prear = (prear + 1) % capacity;// 指针移到下一个
		return true;
	}
    //出队
	public boolean deQueue() {
		if (isEmpty()) {
			return false;
		}
		arr[p0]=0;
		p0 = (p0 + 1) % capacity;// 指针移到下一个
		return true;
	}

	public int front() {//返回第一个元素
		if (isEmpty()) {
			return -1;
		}
		return arr[p0];
	}

	public int rear() {// 返回最后一个元素
		if (isEmpty()) {
			return -1;
		}
		return arr[(prear - 1) % capacity];
	}// 指针 prear 由于之前已经指向了下一个元素, 所以指向当前元素是->prear-1

	public boolean isEmpty() {
		return p0 == prear;
	}
    // 为了将isFull()方法的条件判断 和 isEmpty()的分开来, 我们需要占用一下队列的最后一个位置
	public boolean isFull() {
		return (prear + 1) % capacity == p0;
	}
};


public class Main{
    //测试
	public static void main(String[] args) {
		int len = 5;//队列名义长度
		MyQueue q = new MyQueue(len);
		int[] arr = { 5, 1, 2, 3, 4 };
		for (int e : arr) {//循环添加元素至队列
			q.enQueue(e);
		}
		q.deQueue();
		q.deQueue();
		System.out.println(q.front() + " and " + q.rear() + "\n");
		q.print();
	}
}

 官方标准库(队列)Java 

package advance.dataStructure;

import java.util.LinkedList;
import java.util.Queue;

public class Queue_library {
	public static void main(String[] args) {
		Queue<Integer> q = new LinkedList();
        //打印第一个元素
		System.out.println("the first element: "+q.peek());
		//添加新元素
		q.offer(5);
		q.offer(4);
		//删除元素
		q.poll();
		
		System.out.println("the first element: "+q.peek()+" and size:"+q.size());
		
	}
}

代码讲解

我们在代码中使用的是顺序存储,还有一种链式存储的方法这里不予介绍。在构造方法MyQueue中,我们看到了len参数,该参数允许我们直接在实例化的时候把len(名义长度) 赋给类,使其能够在类MyQueue中被使用。

enQueue()方法的细节

我们讲一下enQueue() 方法模块中的第二个if条件判断语句的由来。当第一轮添加操作结束后,此时列队为满。在删除第一个元素后,头指针p0指向了索引1的位置。尾指针prear指向空元素位(不记得空元素位的作用的读者可以回到《循环队列》模块再看一下)。

如果此时进行添加操作,按照一般的逻辑,应该先添加元素,然后(prear+1)%capacity 使 prear指向索引0的位置,这样元素就添加到了空元素位。但问题是,这个空元素位是需要被置空的,不是用来放元素的,因为在Main类里初始化队列的时候,长度填的是5,如果样的话,实际上这个队列里岂不是有6个有效元素了?

我们演示一遍原代码:q=5,1,2,3,4,0。deQueue() -> 0,1,2,3,4,0。enQueue(10)   -> 0,1,2,3,4,10?!想必大家现在看出问题来了,原本10应该添加到第一个元素的位置,但是现在却错误的出现在了空元素位。

为了避免这种情况,我在enQueue()方法中添加了一个if条件语句来判断此时prear所在的索引是否为capacity-1,是的话就先 (prear+1)%capacity,使prear指向索引0的位置,再添加元素到此处,然后prear再+1指向下一个索引的位置。现在的队列为:10,1,2,3,4。


用栈实现队列

实现思路

栈也可以实现队列。这个时候我们需要用到双栈,一个栈控制入,一个栈控制出。我们姑且把控制入的栈叫做stackIn,控制出的栈叫做stackOut。比如我们在stackIn压入1,2,3,5。如果要删除操作或者是取第一个元素,要先把stackIn清空,放到另一个栈stackOut中。

双栈实现队列

为什么要搞得这么复杂呢?我们这么做是有根据的。我们注意到,由于栈是后进后出,也就是说stackIn中循环删除的顺序是:5,3,2,1。在stackIn删除一个元素,在stackOut就添加一个元素,因此添加顺序和删除顺序一致。操作完成后,我们惊奇的发现,在stackOut中,5刚好在栈底,而1作为最后添加进的元素处在栈顶。这么一来删的就是1,刚好就实现了队列的效果。

我们把清空stackIn然后又添加至stackOut的这一过程封装成dump()方法。其他方法都比较简单,老熟人了,这里我们不展开介绍,大家可以看一下代码。


代码展示

package advance.dataStructure;

public class Queue_StackBase {
    public MyStack stackIn;
    public MyStack stackOut;

    //初始化
    public Queue_StackBase() {
        stackIn = new MyStack(); 
        stackOut = new MyStack(); 
    }
    //入队
    public void push(int e) {
        stackIn.push(e);
    }
    //出队
    public int pop() {    
        dump();
        return stackOut.popR();
    }
    //返回第一个元素
    public int peek() {
        dump();
        return stackOut.topE();
    }
    //判读双栈是否为空
    public boolean empty() {
        return stackIn.isEmpty() && stackOut.isEmpty();
    }
    //清空stackIn的元素并转移至stackOut栈中
    private void dump(){
        if (!stackOut.isEmpty()) return; 
        while (!stackIn.isEmpty()){
                stackOut.push(stackIn.popR());
        }
    }
}

代码讲解

这段代码的逻辑在读者们看了前面两章之后应该就不难了,我们主要讨论一下dump()中为什么有if条件语句if (!stackOut.isEmpty()) return。

dump()

在dump() 添加1,2,3,5到stackOut之后,执行删除方法 pop() ,然后调用push()方法添加元素4。那么此时队列理应的出队顺序是:2,3,5,4。因此再次执行删除方法pop()的时候。删除的是2。如果我们不加上if语句,那么4会压到2之前,这样下一次删除的元素就是4?!这是错误的队列逻辑,因此只要stackOut不为空,stackIn就不会清栈。

另外补充一下,stackOut.push(stackIn.popR()) 这个 popR() 方法是之前再实现栈时写的方法,它会在删除栈顶元素后返回该元素。这样stackOut.push()就可以把该元素压入栈中,很合乎其理!


栈的应用:求最小栈

实现思路

求最小栈的意思就是使用栈去找最小值。我们都对使用数组寻找最小值了如指掌,但是栈要如何寻找最小值呢?我们可以使用双栈,一个栈用来收取数据,另外一个则收取数据中最小的那个。这样不就转化成我们使用数组求最小值的情况了么?


代码展示

package advance.dataStructure;

import java.util.Stack;

public class MinStack {
	Stack<Integer> stack = new Stack<>();
	Stack<Integer> stackMin = new Stack<>();// store minvalue

	public void pushE(int e) {
		stack.push(e);
		if (stackMin.isEmpty() || e < getMin())
			if (!(stackMin.isEmpty()))
				stackMin.pop();
			stackMin.push(e);
	}

	public int getMin() {
		return stackMin.peek();
	}
}

package advance.dataStructure;
//测试
public class MinStack_Main {
	public static void main(String[] args) {
		MinStack obj=new MinStack();
		int[] arr= {3,2,5,1};
		for(int e:arr)
			obj.pushE(e);
		System.out.println(""+obj.getMin());
	}
}

代码讲解

实操起来也很简单。我们先创建两个栈,一个叫stack,存储着所有数据;一个叫stackMin,存储着最小数据。如果stackMin中什么都没有,那就直接压栈。如果stackMin中有数据,输入数据e又比该数据小,那就先把stackMin中的数据先删掉,然后再将当前最小数据e压入stackMin栈中——不管怎么样,一定要保证stackMin里面存储的是最小值。

输入结束后,就调用getMin()方法找到最小值。用栈求最小值其实和使用数组求最小值如出一辙。


总结

栈与队列是无处不在的,十分常见的两种数据结构,他们各有优势,又可以互相转换。虽然有官方库可以直接调用,但是了解一下他的实现原理,以后学起更加抽象的栈和队列的知识时更能够举一反三。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


学习编程是顺着互联网的发展潮流,是一件好事。新手如何学习编程?其实不难,不过在学习编程之前你得先了解你的目的是什么?这个很重要,因为目的决定你的发展方向、决定你的发展速度。
IT行业是什么工作做什么?IT行业的工作有:产品策划类、页面设计类、前端与移动、开发与测试、营销推广类、数据运营类、运营维护类、游戏相关类等,根据不同的分类下面有细分了不同的岗位。
女生学Java好就业吗?女生适合学Java编程吗?目前有不少女生学习Java开发,但要结合自身的情况,先了解自己适不适合去学习Java,不要盲目的选择不适合自己的Java培训班进行学习。只要肯下功夫钻研,多看、多想、多练
Can’t connect to local MySQL server through socket \'/var/lib/mysql/mysql.sock问题 1.进入mysql路径
oracle基本命令 一、登录操作 1.管理员登录 # 管理员登录 sqlplus / as sysdba 2.普通用户登录
一、背景 因为项目中需要通北京网络,所以需要连vpn,但是服务器有时候会断掉,所以写个shell脚本每五分钟去判断是否连接,于是就有下面的shell脚本。
BETWEEN 操作符选取介于两个值之间的数据范围内的值。这些值可以是数值、文本或者日期。
假如你已经使用过苹果开发者中心上架app,你肯定知道在苹果开发者中心的web界面,无法直接提交ipa文件,而是需要使用第三方工具,将ipa文件上传到构建版本,开...
下面的 SQL 语句指定了两个别名,一个是 name 列的别名,一个是 country 列的别名。**提示:**如果列名称包含空格,要求使用双引号或方括号:
在使用H5混合开发的app打包后,需要将ipa文件上传到appstore进行发布,就需要去苹果开发者中心进行发布。​
+----+--------------+---------------------------+-------+---------+
数组的声明并不是声明一个个单独的变量,比如 number0、number1、...、number99,而是声明一个数组变量,比如 numbers,然后使用 nu...
第一步:到appuploader官网下载辅助工具和iCloud驱动,使用前面创建的AppID登录。
如需删除表中的列,请使用下面的语法(请注意,某些数据库系统不允许这种在数据库表中删除列的方式):
前不久在制作win11pe,制作了一版,1.26GB,太大了,不满意,想再裁剪下,发现这次dism mount正常,commit或discard巨慢,以前都很快...
赛门铁克各个版本概览:https://knowledge.broadcom.com/external/article?legacyId=tech163829
实测Python 3.6.6用pip 21.3.1,再高就报错了,Python 3.10.7用pip 22.3.1是可以的
Broadcom Corporation (博通公司,股票代号AVGO)是全球领先的有线和无线通信半导体公司。其产品实现向家庭、 办公室和移动环境以及在这些环境...
发现个问题,server2016上安装了c4d这些版本,低版本的正常显示窗格,但红色圈出的高版本c4d打开后不显示窗格,
TAT:https://cloud.tencent.com/document/product/1340