栈与队列Java

目录

实现栈

实现思路

什么是栈

​栈的用处

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

代码展示

官方标准库(栈)Java 

代码讲解

实现队列

实现思路

什么是队列(queue) 

普通队列

循环队列

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

代码展示

 官方标准库(队列)Java 

代码讲解

enQueue()方法的细节

用栈实现队列

实现思路

代码展示

代码讲解

dump()

栈的应用:求最小栈

实现思路

代码展示

代码讲解

总结


 

实现栈

实现思路

什么是栈

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

4770340e2b2197b74f07620ceb0dfd28.png栈的用处

那为什么要用栈呢?栈有很广的应用场景。我们常用的“撤销”功能,浏览器的历史记录,以及底层的递归,判断有效括号,表达式求值和数制转换等问题 均能见到他的身影。建议阅读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) 

ec3c4af986afa2211d67f3cb6ff1f205.png

队列基本型

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

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

1.队列遵循先进先出的规则(FIFO->first in first out),也就是说先入队的元素先出队,即先被添加的元素先被删除

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

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

那为什么偏要用指针呢?因为有了指针才能确保先进先出——毕竟队列由数组实现,数组可没有先进先出的习惯。于是此时需要两个指针,来指明添加和删除元素的方向。在基于数组实现的队列中,指针就相当于数组索引一样的存在,事实上,在我们接下来的实现程序中指针值就被当做索引使用

普通队列

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

d3952e5f792f62707f67179be4c0fee4.png

普通队列:空队列

此时我们在空队列中添加一个元素2,prear指向第一个元素2;接下来添加一个4,prear指向第二个元素4;最后添加一个6,prear指向第三个元素6。此时队列满了,无法再添加元素。看来只能删除元素释放内存了:我们删除队列第一个元素2,此时p0指向第二个元素4,如下图所示。

8b693e7d05bfc277a36544bc3f0f7d92.png

普通队列:满队列

现在的问题是,即便我们删除了一个的元素,也无法再在队列中添加元素了,因为prear指针指向了队列的最后一个元素,无法再向后挪,相当于是添加新元素的入口被堵死了。这样还是很浪费存储空间的,因为p0前面的队列空间都用不了了。

循环队列

这个时候我们就需要循环队列的加持。我们不是希望能够利用p0之前的队列空间么?在循环队列中,每次添加,删除操作后,指针都会取余队列长度。这样,prear在满的时候就会指向第一个元素的位置,如下图所示。此时新元素就可以被添加,多聪明!循环队列和普通队列的不同之处在于,循环队列的两个指针可以通过取余循环在队列中行进自如,而普通队列,prear指向最后一个元素时就被卡死了。

2c874878ee6925f047649a4c5bf7bc03.png

循环队列

现在内存的问题解决了,我们现在还有一个未解之谜,即如何判断队列是否为空和队列是否为满——这两个判断不论是在入队,出队,还是打印值等功能中都起到了尤为重要的作用。那如何判断他们呢?

对于满,就是p0指向队列索引0的位置,prear指向队列索引最大值位置。注:重复一下,我们为了方便,以下程序会在给队列添加/删除元素前让指针指向该元素位(指针就相当于该元素的索引),所以,在每次在执行添加后,我们要把指针自增1指向下一个元素位。言归正传,在添加最后一个元素的时候,prear+1等于队列长度,取余后得到prear此时在元素位0,也就是说在列表为满的时候,有p0==prear

对于空,我们要分两种情况。1.当队列还未添加元素时,p0==prear==0。2.当队列满后清空时:(清空过程) p0==prear (满) ->p0==prear+1 ->p0==prear+2->p0==prear+3,队列被清空。但是此时prear==3,队列长度为3,prear%3==0,prear还是等于p0。当然,对于任意{p0∈N|p0∈[0,queue.length-1]},都有p0==prear,剩下的情况大家可以自行推算。

72fd7e2bb7c49f5f6c1185d4e3b97458.png

循环列表从满到空的全过程

我们现在惊讶的发现,判断空和满的条件竟然都是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() {//返回p0指向的元素
		if (isEmpty()) {
			return -1;
		}
		return arr[p0];
	}

	public int rear() {// 返回最prear指向的元素
		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个有效元素了?

我们演示一遍原代码(空元素位用0表示):queue=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指向下一个元素位。使用改良后的enqueue()代码效果为:10,1,2,3,4。完美的实现!


用栈实现队列

实现思路

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

20ba69ecaf9bfeadc1151a487ed6dbb2.png

双栈实现队列

为什么要搞得这么复杂呢?我们这么做是有根据的。我们注意到,由于栈是后进后出,也就是说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<>();// 储存最小值

	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中。如果stackMin中有数据,而输入数据e又比该数据小,那就先把stackMin中的数据先删掉,然后再将当前最小数据e压入stackMin栈中,如果e比该数据大,则不变——不管怎么样,一定要保证stackMin里面存储的是最小值。

输入结束后,就调用getMin()方法找到最小值。事实上,我们可以看到,用栈求最小值其实和使用数组求最小值的原理如出一辙。


总结

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

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值