数据结构之栈和队

数据结构————栈

我们在这里对于栈进行一个整理,这里说的栈,是指的顺序栈,链表栈等我后期将链表整理好了在一起发上来。

这里栈我只是简述一下关于栈的知识,具体的一些细的知识自己百度吧,我写这一篇文章的目的是为了能够手写栈。
首先,栈基本介绍,栈是什么呢?栈是一个数据结构,类似的话就是一个水瓶,你有ABC三个物品,装入栈里面,就是ABC,这是入栈的熟悉,入栈就是将元素放进栈中,而如果我们需要将元素拿出栈呢,我们直接全部拿出来,拿出来的顺序就是CBA,这就是栈的数据结构LIFO,先进先出,顾名思义,如果你一口气将其拿出来,就是谁先进去谁先出来。
我在这里在啰嗦一点,有几个专业名词,
第一个是入栈(push),上面提到了就是进入栈
第二个是出栈(pop),同理就是从栈中出来
第三个叫做栈顶,就是如果你需要出栈,先出来的元素
系统中我们也在使用栈,例如说递归,就是栈的体现

我们要自己制作栈的话,首先要完成一个顶级接口,这是规范,在Java中约定大于配置,特别是在做项目的时候,规范是很重要的。
package Stackk;

public interface Stacks<E> {
		int getsize();
		boolean isEmpty();
		void push(E e);
		E pop();
		E peek();
}

完成接口了,我们要完成的是,顺序栈所以需要去借助我们上一章完成的数据结构,顺序表也就是数组,我们要完成这个类,继承我们之前写的顺序表,并且实现栈这个接口。

自己完成的栈

首先是栈中的元素,存在了哪里?
栈中存的元素其实是存在了之前我们完成的数组中,所以当我们完成栈的构造的时候,不要忘记使用我们之前的动态数组。
如果忘记了我写的数组
下面是链接https://blog.csdn.net/weixin_44077141/article/details/102638246

栈的构造,有一个空白的构造函数,还有一个有参构造可以传进来数组的大小
Array<E> arrays;
	public ArrayStack( int capacity) {
		arrays=new Array<E>(capacity);
	}
	public ArrayStack() {
		arrays=new Array<E>();
	}
下面是一些的基本的方法,继承我们之前的写的动态数组
显示栈的长度
/**
	 * 显示栈的长度
	 */
	@Override
	public int getsize() {
		// TODO Auto-generated method stub
		return arrays.getSize();
	}
判断栈是否为空
/**
	 * 判断栈是否为空
	 */
	@Override
	public boolean isEmpty() {
		// TODO Auto-generated method stub
		return arrays.isEmpty();
	}
元素入栈
/**
	 * 添加都最后一个元素
	 * 入栈
	 */
	@Override
	public void push(E e) {
		// TODO Auto-generated method stub
		arrays.addList(e);
	}
元素出栈
/**
	 * 移除最后一个元素
	 * 出栈
	 */
	@Override
	public E pop() {
		// TODO Auto-generated method stub
		
		return arrays.removeLast();
	}
拿到栈顶元素
/**
	 * 拿到最后一个元素
	 */
	@Override
	public E peek() {
		// TODO Auto-generated method stub
		return arrays.getLast();
	}

#####重写toString方法

/**
	 * 重写toString
	 */
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		StringBuilder sb=new StringBuilder();
		sb.append("Stack:");
		sb.append("[");
		for (int i = 0; i < arrays.getSize(); i++) {
			sb.append(arrays.get(i));
			if(i!=arrays.getSize()-1) {
				sb.append(", ");
			}
		}
		sb.append("] top");
		return sb.toString();
	}

PS:写一句题外话,我希望大家能够多写,尽管有些东西很简单,但是我也希望大家能够加上一两句备注,使用备注当你将鼠标放在方法和变量上边时可以直接看到这个方法的备注,省去大量的时间。

我们去写一个main函数去使用一下
package Stackk;

public class Mm {
public static void main(String[] args) {
	ArrayStack<Integer> stack =new ArrayStack<Integer>();
	for (int i = 0; i < 5; i++) {
		stack.push(i);
		System.out.println(stack);
	}
	stack.pop();
	System.out.println(stack);
}
}

在这里插入图片描述

复杂度分析

在进行push(E e)也就是入栈操作的时候因为是所以是先进后出,只是在数组的末尾进行操作,除非扩容否则他的复杂度是O(1),如果考虑扩容,只有扩容的时候他的复杂度是O(n),但是也只是在到达扩容的条件的时候,所以其实均摊来说他的复杂度还是O(1)。
在进行出栈的操作的时候,pop因为出的是最后一个所以他的复杂度也是O(1),如果不到达临界值的话,是不会变成O(n),但是如果均摊一下,他的复杂度也是O(1)。

数据结构————队

我在这里将队的基本概念重述一遍,队就是一种数据结构更加的去接近我们的生活,就是跟我们平时遇到的队是一样的,先进先出,跟我们排队是一样的效果,我在队中入队ABC,在队中出队,出的顺序也是ABC,我在这里讲述几个概念。
第一,这里的出队指的也是将元素从队中出去。
第二,入队,指的的是将元素放进队中。
第三,队的出入顺序是FIFO,先入先出,First In First Out

队的顶级接口

写一个队也是的,需要去完成他们这种的顶级接口,我这里指的队的也是顺序表的队,底层是数组。

package Queue;

public interface Queue<E> {
	int getsize();
	boolean isEmpty();
	void enqueue(E e);
	E dequeue();
	E getfront();
	
}
队的一些基本的方法,也是继承我之前写的动态数组,
队的构造,因为我们这个队的底层是动态数组,所以将动态数组的类型设置为private,这是队的底层我们不需要让用户知道,完成两个构造,一个可以传入一个数组的容量。
private Array<E> arr;
	
	public  ArrayQueue(){
		arr=new Array<>();
		
	}
	public  ArrayQueue(int capacity){
		arr=new Array<>(capacity);
		}
	
继承我们写的队的顶级接口
拿到队的长度
	/**
	 * 拿到一个队列的长度
	 */
	@Override
	public int getsize() {
		// TODO Auto-generated method stub
		return arr.getSize();
	}
	
队判断是否为空
/**
	 * 判断是否为空
	 */
	@Override
	public boolean isEmpty() {
		// TODO Auto-generated method stub
		return arr.isEmpty();
	}
入队
**
	 * enqueue入对
	 */
	@Override
	public void enqueue(E e) {
		// TODO Auto-generated method stub
		arr.addList(e);
	}
出队
/**
	 * 出队
	 */
	@Override
	public E dequeue() {
		// TODO Auto-generated method stub
		return arr.removeFirst();
	}
拿到队的第一个元素
/**
	 * 得到队的头部的第一个元素
	 */
	@Override
	public E getfront() {
		// TODO Auto-generated method stub
		return arr.getFirst();
	}
重写toString方法
/**
	 * 重写toString
	 */
	@Override
	public String toString() {
		// TODO Auto-generated method stub
		StringBuilder sb=new StringBuilder();
		sb.append("Queue:");
		sb.append(" front [");
		for (int i = 0; i < arr.getSize(); i++) {
			sb.append(arr.get(i));
			if(i!=arr.getSize()-1) {
				sb.append(", ");
			}
		}
		sb.append("] tail");
		return sb.toString();
	}
我们来完成一个main方法来将我们书写的数据结构进行验证
	public static void main(String[] args) {
		ArrayQueue<Integer> arr=new ArrayQueue<Integer>();
		for (int i = 0; i < 10; i++) {
			arr.enqueue(i);
			System.out.println(arr);
			if(i%3==2) {
				arr.dequeue();
				System.out.println(arr);
			}
		}
	}

执行的结果
在这里插入图片描述

复杂度分析

enqueue(入队)这个操作是有的时候如果不扩容的话他的复杂度是O(1),如果他扩容的话他的复杂度是O(n),所以其实如果是均摊的复杂度的话,他的复杂度其实是O(1)
dequeue(出队),因为我们采用的动态数组,出队就是意味着第一个元素从数组中出去,也就是空间复杂度是O(n),然后就是元素不停地出队入队,数组前边的空间会空出来,会有大量的空间无法被利用,我们在下一个节解决这个问题。

循环队列

这个东西其实是和队的定义是并列的,但是这个是队的一种,什么叫做循环队列呢?

循环队列就是一个队列形成一个圈,队的头和尾变成了数组中的两个指向的位置,如果是在C语言中可以用指针来标识,我们在这里只能用两个int型的数据,两个数据存放数组中手和尾的位置。

在这里插入图片描述这个图是我们平时使用的队列,
在这里插入图片描述这个图是循环队列
在这里关于循环队列有几个概念
首先之前我们判断队是空的,是看底层的数组,但是现在这个数据结构不能在使用我们之前的完成的动态数组了。
队空的条件也就变成了判断首==尾
判断队满的条件那也是一样,不能够使用数组了,判断队满的条件是(尾+1)%队的长度是否等于首
现在我们将首的定义为front这个变量,尾的话是tail,后面在提到这两个变量就是用front,tail来代替了。
但是循环队列解决了队列中因为频繁的入队出队的问题,导致一些空间利用不上的问题
的,但是队满和队空的问题难以抉择,于是我们将一个空间有意识的浪费掉造成,队满的条件其实是尾和首差一个元素,这就是我们使用循环队列的代价。这种情况也叫做假溢出。

循环队列中的构造

在循环队列中,因为是循环所以需要需要去记录首和尾的位置,存储数据的也是数组但是不需要去用自己完成的类,所以我们在最初的定义的时候就是可以定义出首和尾,还有我们自己跌数组,其实在这里循环队列的长度我们是可以不用写的,这里的size值是可以推测出来的,但是我在这里先不去说,希望大家思考一下。

/**
	 * 没有意外的话就是默认的10 
	 * 但是如果是只能够存储到9个元素
	 */
	public LoopQueue() {
		this(10);
	}
	
	
	/**
	 * 这里+1是为了将用户给定的数组全部存入
	 * @param capacity
	 */
	public LoopQueue(int capacity) {
			data=(E[])new Object[capacity+1];
			front=0;
			tail=0;
			size=0;
			
	}
	
拿到队列长度
/**
	 * 拿到一个队列的长度
	 */
	@Override
	public int getsize() {
		// TODO Auto-generated method stub
		return size;
	}
判断队是否为空
/**
	 * 判断是否为空
	 */
	@Override
	public boolean isEmpty() {
		// TODO Auto-generated method stub
		return front==tail;
	}
入队
/**
	 * 队满的条件
	 * 为了扩容进行考虑
	 */
	@Override
	public void enqueue(E e) {
		// TODO Auto-generated method stub
		if((tail+1)%data.length==front) {
			resize(getCapacity()*2);
		}
		data[tail]=e;
		tail=(tail+1)%data.length;
		size++;
	}
出队
/**
	 * 出队
	 * 将大小变得小
	 */
	@Override
	public E dequeue() {
		// TODO Auto-generated method stub
		if(isEmpty()) {
			throw new IllegalArgumentException("栈为空");
			}
		E ret=data[front];
		front=(front+1)%data.length;
		size--;
		if(size==getCapacity()/4&&getCapacity()!=0) {
			resize(getCapacity()/2);
		}
		return ret;
	}
得到队首元素
@Override
	public E getfront() {
		// TODO Auto-generated method stub
		if(isEmpty()) {
			throw new IllegalArgumentException("栈为空");
			}
		return data[front];
	}
重写toString方法
@Override
	public String  toString() {
		
		StringBuilder res=new StringBuilder();
		res.append(String.format("LoopQueue: size= %d , capacity= %d\n",size,getCapacity()));
		res.append("front [");
		for (int i = front; i !=tail; i=(i+1)%data.length) {
			res.append(data[i]);
			if((i+1)%data.length !=tail) {
				res.append(", ");
			}
		}
			res.append("] tail");
			return res.toString();
	}

###我们来一个main函数实现一下我们所写的队的结构

	public static void main(String[] args) {
		LoopQueue<Integer> arr=new LoopQueue<Integer>();
		for (int i = 0; i < 10; i++) {
			arr.enqueue(i);
			System.out.println(arr);
			if(i%3==2) {
				arr.dequeue();
				System.out.println(arr);
			}
		}
	}

在这里插入图片描述
其实个人感觉和普通的队列的区别其实挺明显的,只不过在上文中打印队列没有打印capacity,这个变量是用来显示数组容量的,我们之所以使用的是循环队列就是为了解决数组中有元素却显示数组满了的情况,使用循环队列用“浪费”一个空间的条件完成了灵活使用队的目的。

复杂度分析

对于循环队列来说,入队enqueue,在不牵扯扩容的基础上,他的复杂度是O(1),只有到达临界值的时候,他的复杂度才是O(n),但是如果是看平均的复杂度来说其实还是O(1)。
出队dequeue,之前队列的问题是因为是从前边的元素进行出队,所以导致前边的元素会被浪费或是复杂度处于O(n)消耗系统资源,但是采用了循环队列通过记录位置,并且可以直接累加进行求余,更加便利的在队中对于元素进行操作。

性能差距

我们试验一下看看是普通的队列操作比较的快捷还是循环队列的操作比较快一点

package Queue;

import java.util.Random;

public class Main {
	private static double  test(Queue<Integer> arr,int op) {
		
		long start = System.nanoTime();
		Random ran=new Random();
		for (int i = 0; i < op; i++) {
			arr.enqueue(ran.nextInt(Integer.MAX_VALUE));
		}
		for (int i = 0; i < op; i++) {
			arr.dequeue();
		}
		long end = System.nanoTime();
		
		return(start-end)/1000000000.0;
	}
public static void main(String[] args) {
	int op=100000;
	ArrayQueue<Integer> arraysQueue=new ArrayQueue<Integer>();
	double test = test(arraysQueue, op);
	System.out.println(test);
	
	LoopQueue<Integer> LoopQueue=new LoopQueue<Integer>();
	double test1 = test(LoopQueue, op);
	System.out.println(test1);
	

 }
}

实验的结果
在这里插入图片描述
试验结果证明其实循环队列的效率远远大于普通的队列,这个数据结构更好地去优化的了队这个数据结构,但是具体的倍数是和我们使用的JVM是有关的呢,我们在这里就不仔细的分析了。
PS:上文中循环队列中size可以使用首和尾直接算出的答案是(尾-首+长度)%长度
注意这个数要是正数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

又是重名了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值