文章目录
- 栈与队列对比
- 栈和队列的存储结构
- 栈的存储结构
- 1.栈的顺序存储结构
- 2. 栈的链式存储结构
- 队列的存储结构
- 1. 队列的顺序存储结构
- 2. 循环队列
- 3. 队列的链式存储
栈与队列对比
1. 栈
什么是栈呢?小时候我们玩过玩具枪就知道,栈就是类似于枪的弹夹,每装入一颗子弹,子弹就会往弹夹底部下去,但当你打子弹出来的时候发现,是从你最后放入弹夹的子弹开始依次打出的。还有网页的后退、word、ps中的“撤销”操作都运用了栈实现。
- 栈的定义
栈是限定仅在表尾进行插入和删除的线性表。有先进后出的特点。
- 特征
允许插入和删除的一端叫栈顶(top),另一端叫栈底。插入叫压栈或入栈,删除叫出栈。
2. 队列
什么叫队列呢?生活中类似于车站排队检票,检完票就离开,其他人在后面依次排队。又如键盘进行字母或数字的输入,在显示器记事本中的输出。
- 队列的特征
只允许在队尾进行插入操作,而在队头进行删除操作。有先进先出的特点。插入叫入队,删除叫出队。
3. 栈与队列
- 相同点
1、都是线性结构,有一对一的逻辑关系
2、插入操作都在表尾进行
3、时间空间代价相同
- 不同点
对比它们各自的特征
补充:顺序栈可实现多栈空间共享,但顺序队列不同。
栈和队列的存储结构
栈的存储结构
1.栈的顺序存储结构
栈的顺序存储结构用数组来实现,这里的 top 指向栈顶元素存储位置的下一个存储单元位置。
- 判空操作
public boolean isElempty() {
return top == 0;
//top的定义有两种方式:
//1、设置指向栈顶元素存储位置的下一个存储单元位置。就是说,top指向数组位置0时,没有数据元素,当插入数据元素时,top会指向栈顶元素的下一个存储位置,此时的栈就不为空了。
//2、也可以定义top == -1时栈为空
}
- 返回栈顶元素操作
注:top指向的是栈顶元素的下一个位置,所以返回栈顶元素时的位置是:top-1 位置。
//返回栈顶元素:首先要判断栈是否为空
public Object peek() {
if(!isElempty()) {
return stackElem[top-1];
}else {
return null;
}
}
- 入栈操作
入栈操作:入栈是在栈顶一端进行操作的,栈每次进入一个数据元素,top+1。
例:
代码实现:
public void push(Object x) throws Exception {
if(top == stackElem.length) { //stackElem.length为定义存储栈的长度
throw new Exception("栈已满");
}else {
stackElem[top++] = x; //x压栈top再加1
//相当于stackElem[top] = x; top = top+1;
}
}
- 出栈操作
出栈操作:出栈也是在栈顶元素一端进行操作的,每当出栈一个栈顶元素,top 就会先 -1。
代码实现:
//输出栈顶元素
public Object pop() {
if(isElempty()) {
return null;
}else {
return stackElem[--top]; //top指向的是栈顶元素的下一个位置,所以top要先-1,返回的才是栈顶元素
}
}
2. 栈的链式存储结构
栈顶放在单链表的头部。不需要像单链表一样定义头结点。栈顶指针即为头指针。
注:top指向的为栈顶结点,插入和删除都在表头(栈顶)进行。
- 先定义一个Node结点类
package stack;
/**
* 创建结点类的描述
* @author 懒惰的小黑
*
*/
public class Node {
//定义数据、下一个结点的引用(指针)
public Object date;
public Node next;
//定义有参、无参构造方法
public Node() {
this.date = null;
this.next = null;
}
public Node(Object date) {
this.date = date;
this.next = null;
}
public Node(Object date,Node next) {
this.date = date;
this.next = next;
}
}
- 判断栈空和置空操作
public class LinkStack implements IStack{
public Node top;
public void clear() {
top = null;
}
public boolean isElempty() {
return top == null; //相当于:return top==null?true:false;
}
- 返回栈的长度操作
与单链表一样,对每一个结点进行遍历即可。
//长度
public int length() {
Node p = top;
int length = 0;
while(p!=null) {
p = p.next; //指针后移
++length; //长度+1
}
return length;
}
- 取栈顶元素
//取栈顶元素
public Object peek() {
if(isElempty()) {
return null;
}else {
return top.date;
}
}
- 入栈操作
- 说明:定义入栈的新结点 p ,把栈顶指针(top)赋值给新结点的直接后继(p.next),再将新结点 p 赋值给 栈顶指针 top就行了。
代码实现:
//压栈
public void push(Object x) {
Node p = new Node(x);
p.next = top;
top = p;
}
- 出栈操作
说明:用一个 p 结点存放出栈的结点,然后栈顶指针 top 指向后一结点即可。
代码实现:
//出栈
public Object pop() {
if(isElempty()) {
return null;
}else {
Node p = top; //结点p存储出栈的结点
top = top.next; //指针后移
return p.date;
}
}
注:
顺序栈和链栈在时间复杂度都是O(1)。
队列的存储结构
1. 队列的顺序存储结构
队列的顺序存储结构跟线性表的顺序存储结构相同。但是,在线性表顺序存储结构中,如果要去删除元素(也就是说队列出队)的话,那么队列后的元素都要向前移动,时间复杂度为O(n),限制了一个条件就是队列必须存储在数组的前n个单元。如果没有这一限制条件,性能是不是就会大大提升呢?所以队列是用以下这种方式存储的。
- 队列的顺序存储
引用两个指针,front指针指向队头元素,rear指针指向队尾元素的下一个位置。
- “假溢出”问题
如上假设 这个队列总个数不超过5个,但在图d中,目前如果接着入队的,因数组元素队尾已被 a5 占用,再加入则会发生数组下标越界而引发溢出,可实际上,我们发现队列中下标0、1位置还空闲,但不能进行入队操作而产生的溢出,我们称这种溢出现象称为“假溢出”。
如果在一辆公交车上,你发现后面的座位都坐满了,前面还有座位你会选择不去坐而下车去等下一辆公交车吗?这不单单是浪费,我还想拿小锤锤锤你呢!所以为了解决“假溢出”问题,最好的办法就是把顺序队列所使用的存储空间看成是一个逻辑上首尾相连的循环队列。
2. 循环队列
- 队满与队空的判断
所以这时的 rear 应该指向数组0位置。
此时,a6、a7在入队:
此时发现:
队列为空时,
front == rear;
队满时也是
front == rear;
那怎么判断呢?
解决这一问题有很多方法:
方法一:
设置一个标识变量flag:
队列空:当front == rear 且 flag = 0;
队列满:当front == rear 且flag = 1;
方法二:
当队列为空时:front == rear;
队满时:队列满时,数组保留一个空间单元。
所以队满时会出现这两种情况:front与rear可能相差一整圈或一个位置。若定义队列的最大尺寸为QueueSize,
队满条件为:(rear+1)% QueueSize == front
- 队列长度
通用的计算队列长度公式为:
(rear - front + QueueSize)% QueueSize
可以列出 front>rear、front<rear 时的 全部4种(分别为队满、队未满情况) 情况,再通过分别分析出 front>rear时的队列长度、front<rear时的队列长度,分析得出队列长度公式。 - 读取队首元素操作
//读取队首元素
public Object peek() {
if(front == rear) { //队列为空
return null;
}else {
return queueElem[front];
}
}
- 入队操作
注:rear 始终指向队尾元素的下一存储位置。
public void offer(Object x) throws Exception {
if((rear + 1)%queueElem.length == front ) { //队满
throw new Exception("队已满");
}else {
queueElem[rear] = x; //入队
rear = (rear +1)%queueElem.length; //修改队尾指针
}
}
- 出队操作
注:先取出队首元素的值,再将 front 循环 +1。
//出队
public Object poll() {
if(front == rear) { //队空
return null;
}else {
Object t = queueElem[front];
front = (front + 1)%queueElem.length; //front值循环加1
return t;
}
}
时间复杂度分析:
入队与出队操作的算法时间复杂度都为O(1)。
3. 队列的链式存储
队列的链式存储结构用 不带头结点 的单链表实现。
front::指向队首元素结点
rear: 指向队尾元素结点
- 结点类
同上,只不过在此处本人在此处封装了属性,增加了get、set方法,方便后面使用。! - 判空与置空操作
public class LinkQueue implements IQueue {
private Node front;
private Node rear;
public LinkQueue() {
front = rear = null;
}
public void clear() {
front = rear = null;
}
public boolean isEmpty() {
return front == null;
}
- 计算长度操作
通过遍历每一个结点。
public int length() {
Node p = front;
int length = 0;
while(p != null) {
p = p.getNext(); //指针下移
++length;
}
return length;
}
- 取队首元素
//取队首元素
public Object peek() {
if(front!=null) {
return front.getDate();
}else {
return null;
}
}
- 入队操作
基本思想和不带头结点的单链表相似,不同之处只是链队列限制了在表尾插入。
代码实现:
//入队
public void offer(Object x) {
Node p = new Node(x); //初始化新结点
if(front != null) { //队列不为空
rear.setNext(p); //把p结点赋值给rear指向结点的next域
rear = p; //改变队尾位置,把p赋给rear
}else {
front = rear = p;
}
}
- 出队操作
//出队
public Object poll() {
if(front != null) {
Node p = front; //p存放出队的首结点
front = front.getNext(); //把front指向下一结点
return p.getDate(); //返回p结点数据域
}else {
return null;
}
}
参考书籍:《大话数据结构》、《数据结构—java语言描述(第2版)-清华大学出版社》