1、栈的定义
栈(stack):是限定仅在表尾进行插入和删除操作的线性表。
我们把允许插入和删除的一端称为栈顶(top),另一端称为栈底(bottom),不含任何数据元素的栈称为空栈。
栈又称为后进先出(Last In First Out),简称LIFO结构。
栈的插入操作(push),叫作进栈,也称压栈、入栈。类似子弹入弹夹。
栈的删除操作(pop),叫作出栈,也有的叫作弹栈。类似子弹出弹夹。
来张图认识一下这些概念:
2、栈的两种存储结构
栈也是线性表,前一章我们介绍了线性表,所以栈依然拥有顺序存储和链式存储两种结构。
每种结构我们需要重点关注其插入和删除操作的算法实现。也是我们说的进栈和出栈操作,一般用push和pop进行表示。
2.1、顺序存储结构
来一张图认识一下顺序存储结构栈:
2.1.1、进栈
/**
* 进栈
*/
private static ResultEnum push(SqStack stack, String e) {
if (stack.top == MAXSIZE - 1) {//栈满
return ResultEnum.ERROR;
}
stack.top++;
stack.data[stack.top] = e;
return ResultEnum.OK;
}
2.1.2、出栈
/**
* 出栈 将栈顶那个有值的数据元素删除。
*/
private static ResultEnum pop(SqStack stack) {
if (stack.top == -1) {//空栈,无元素可删
return ResultEnum.ERROR;
}
stack.data[stack.top] = null;//被删除的元素置空
stack.top--;
return ResultEnum.OK;
}
2.1.3、一个完整案例
java语言实现:
public class TestStack {
public static void main(String[] args) {
//第一步:创建栈对象的实例
SqStack sqStack = buildSqlStack(2);
if (sqStack == null) {
System.out.println("创建栈失败");
return;
}
printStackInformation(sqStack);
//第二步:把e这个数据元素压入栈中(进栈)
System.out.println("---------插入数据e(进栈)-----------");
ResultEnum insertResultEnum = push(sqStack, "e");
if (insertResultEnum == ResultEnum.ERROR) {
System.out.println("error:栈满溢出");
} else if (insertResultEnum == ResultEnum.OK) {
printStackInformation(sqStack);
}
//第三步:把栈顶的有值数据从栈中移除(出栈)
System.out.println("---------删除栈顶有值元素(出栈)-----------");
ResultEnum deleteResult = pop(sqStack);
if (deleteResult == ResultEnum.ERROR) {
System.out.println("error:栈满溢出");
} else if (deleteResult == ResultEnum.OK) {
printStackInformation(sqStack);
}
}
private static void printStackInformation(SqStack sqStack) {
for (int i = sqStack.data.length - 1; i >= 0; i--) {
System.out.println("data:" + sqStack.data[i]);
}
System.out.println("栈顶 top:" + sqStack.top);
}
private static int MAXSIZE = 5;//栈的最大可存储空间
/**
* 构建一个为顺序存储结构的栈对象的实例
*
* @param dataCount
* @return
*/
private static SqStack buildSqlStack(int dataCount) {
if (dataCount == 0) {
return null;
}
String[] data = new String[MAXSIZE];
for (int i = 0; i < dataCount; i++) {
data[i] = "a" + i;
}
int top = dataCount - 1;
return new SqStack(data, top);
}
/**
* 进栈
*/
private static ResultEnum push(SqStack stack, String e) {
if (stack.top == MAXSIZE - 1) {//栈满
return ResultEnum.ERROR;
}
stack.top++;
stack.data[stack.top] = e;
return ResultEnum.OK;
}
/**
* 出栈 将栈顶那个有值的数据元素删除。
*/
private static ResultEnum pop(SqStack stack) {
if (stack.top == -1) {//空栈,无元素可删
return ResultEnum.ERROR;
}
stack.data[stack.top] = null;//被删除的元素置空
stack.top--;
return ResultEnum.OK;
}
/**
* 栈的结构定义,可以理解为栈这个对象
*/
static class SqStack {
String[] data;
int top;
public SqStack(String[] data, int top) {
this.data = data;
this.top = top;
}
}
public enum ResultEnum {
OK, ERROR;
}
}
案例主要包含三步内容:
第一步:创建栈对象的实例;
第二步:把e这个数据元素压入栈中(进栈);
第三步:把栈顶的有值数据从栈中移除(出栈)。
输出结果
data:null
data:null
data:null
data:a1
data:a0
栈顶 top:1
---------插入数据e(进栈)-----------
data:null
data:null
data:e
data:a1
data:a0
栈顶 top:2
---------删除栈顶有值元素(出栈)-----------
data:null
data:null
data:null
data:a1
data:a0
栈顶 top:1
这种代码,只有动手去写一写,才能更好的理解和消化。
2.2、链式存储结构
来一张图认识一下链式存储结构栈:
2.1.1、进栈
private static ResultEnum push(LinkStack linkStack, String e) {
StackNode stackNode = new StackNode();
stackNode.data = e;
stackNode.next = linkStack.top;
linkStack.top = stackNode;
linkStack.count++;
return ResultEnum.OK;
}
2.1.2、出栈
private static ResultEnum pop(LinkStack linkStack) {
if (linkStack.isEmpty()) {
return ResultEnum.ERROR;
}
linkStack.top.data = null;
linkStack.top = linkStack.top.next;
linkStack.count--;
return ResultEnum.OK;
}```
### 2.1.3、完整案例
java语言实现:
```java
public class TestLinkStack {
public static void main(String[] args) {
LinkStack linkStack = new LinkStack();
push(linkStack, "a0");
push(linkStack, "a1");
push(linkStack, "e");
printStackInformation(linkStack);
pop(linkStack);
printStackInformation(linkStack);
}
private static void printStackInformation(LinkStack linkStack) {
System.out.println("--------------栈顶到栈底依次输出--------------");
for (StackNode stackNode = linkStack.top; stackNode != null; stackNode = stackNode.next) {
System.out.println(stackNode.data);
}
System.out.println("栈顶 top:" + linkStack.getTop());
}
//入栈
private static ResultEnum push(LinkStack linkStack, String e) {
StackNode stackNode = new StackNode();
stackNode.data = e;
stackNode.next = linkStack.top;
linkStack.top = stackNode;
linkStack.count++;
return ResultEnum.OK;
}
//出栈
private static ResultEnum pop(LinkStack linkStack) {
if (linkStack.isEmpty()) {
return ResultEnum.ERROR;
}
linkStack.top.data = null;
linkStack.top = linkStack.top.next;
linkStack.count--;
return ResultEnum.OK;
}
//结点对象
static class StackNode {
String data;
StackNode next;
}
//链式存储结构的栈对象
static class LinkStack {
StackNode top;
int count;
public String getTop() {
return top.data;
}
public boolean isEmpty() {
return top == null;
}
}
enum ResultEnum {
OK, ERROR;
}
}
输出结果:
--------------栈顶到栈底依次输出--------------
e
a1
a0
栈顶 top:e
--------------栈顶到栈底依次输出--------------
a1
a0
栈顶 top:a1
3、栈的应用-斐波那契的递归函数
3.1、迭代法
迭代函数
private static void useIteration(int count) {
int[] arr = new int[count];
arr[0] = 0;
System.out.println("第0个月:" + arr[0] + " 对兔子");
arr[1] = 1;
System.out.println("第1个月:" + arr[1] + " 对兔子");
for (int j = 2; j < count; j++) {
arr[j] = arr[j - 1] + arr[j - 2];
System.out.println("第" + j + "个月:" + arr[j] + " 对兔子");
}
}
调用:
public static void main(String[] args) {
useIteration(7);
}
输出:
第0个月:0 对兔子
第1个月:1 对兔子
第2个月:1 对兔子
第3个月:2 对兔子
第4个月:3 对兔子
第5个月:5 对兔子
第6个月:8 对兔子
3.2、递归法
递归函数:
private static int useFbi(int i) {
if (i < 2) {
return i == 1 ? 1 : 0;
}
return useFbi(i - 1) + useFbi(i - 2);
}
private static void printFbi(int count) {
for (int i = 0; i < count; i++) {
System.out.println("第" + i + "个月:" + useFbi(i) + " 对兔子");
}
}
调用:
public static void main(String[] args) {
printFbi(7);
}
输出:
第0个月:0 对兔子
第1个月:1 对兔子
第2个月:1 对兔子
第3个月:2 对兔子
第4个月:3 对兔子
第5个月:5 对兔子
第6个月:8 对兔子
3.3、对比两种实现方法
递归法相较于迭代法的代码,要简洁的多。
递归法的推导过程其实也是栈的操作过程,我们都需要找到最顶上的之后开始依次往回计算。
4、队列的定义
队列(queue):是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队列是一种先进先出(First In Frist Out)的线性表,简称FIFO。
允许插入的一端称为队尾,允许删除的一端称为队头。
例如队列(a1,a2,a3,…,an),那么a1就是队头元素,an是队尾元素。这时我们删除时,总是从a1开始,而插入时,在列在an之后。
来一张图认识一下,队列:
4、队列的两种存储结构
2.1、顺序存储结构
包含一种叫作循环队列,队列的头尾相接的顺序存储结构。
引入两个指针:front指针指向队头元素,rear指针指向队尾元素的下一个位置,rear指针也可以指向剩余的队头位置。
两个计算公式:
如果队列的最大尺寸为QueueSize,那么队列已满的条件是:(rear+1)% QueueSize == front;
通用队列计算长度公式=:(rear-front +QueueSize)%QueueSize;不过笔者简单的推导了一下,|rear-front|%QueueSize 也能得出这个队列的长度。
来一张图看看循环队列的模样:
2.1.1、循环队列出队和入队完整案例
public class TestQueue {
public static void main(String[] args) {
SqQueue sqQueue = new SqQueue();
initQueue(sqQueue, 3);
System.out.println("-----------入队列后的结果如下-----------");
sqQueue.enQueue(sqQueue, "e");
printQueueInformation(sqQueue);
System.out.println("队列有值数据元素个数(队列长度):"+sqQueue.getLength());
System.out.println("-----------出队列后的结果如下-----------");
sqQueue.deQueue(sqQueue);
printQueueInformation(sqQueue);
System.out.println("队列有值数据元素个数(队列长度):"+sqQueue.getLength());
}
private static void printQueueInformation(SqQueue sqQueue) {
for (int i = sqQueue.data.length - 1; i >= 0; i--) {
System.out.println("data:" + sqQueue.data[i]);
}
System.out.println("队列 front:" + sqQueue.front+" , rear:"+sqQueue.rear);
}
private static void initQueue(SqQueue sqQueue, int dataCount) {
if (dataCount <= 0) {
return;
}
sqQueue.front = 0;
sqQueue.rear = dataCount - 1;
String[] arr = new String[MAXSIZE];
for (int i = 0; i < dataCount; i++) {
arr[i] = "a" + i;
}
sqQueue.data = arr;
}
//队列的最大容量
private static final int MAXSIZE = 5;
static class SqQueue {
String[] data;
int front;//头指针
int rear;//尾指针,若队列不空,指向队列尾元素的下一个位置
int getLength() {
return (rear - front + MAXSIZE) % MAXSIZE;
}
ResultEnum enQueue(SqQueue sqQueue, String e) {
if ((sqQueue.rear + 1) % MAXSIZE == sqQueue.front) {//队列满
return ResultEnum.ERROR;
}
sqQueue.data[rear] = e;//将元素e赋值给队尾
sqQueue.rear = (sqQueue.rear + 1) % MAXSIZE;//rear指针指向后移一位
return ResultEnum.OK;
}
ResultEnum deQueue(SqQueue sqQueue) {
if (sqQueue.front == sqQueue.rear) {//队列为空
return ResultEnum.ERROR;
}
sqQueue.data[front] = "";
sqQueue.front = (sqQueue.front + 1) % MAXSIZE;
return ResultEnum.OK;
}
}
enum ResultEnum {
OK, ERROR;
}
}
输出结果:
-----------入队列后的结果如下-----------
data:null
data:null
data:e
data:a1
data:a0
队列 front:0 , rear:3
队列有值数据元素个数(队列长度):3
-----------出队列后的结果如下-----------
data:null
data:null
data:e
data:a1
data:
队列 front:1 , rear:3
队列有值数据元素个数(队列长度):2
从输出结果中我们可以看到队列的操作,删除从队头开始,新增从队尾插入。
2.2、链式存储结构
队列的链式存储结构,其实就是线性表的单链表,只不过它只能尾进头出而已,我们把它简称为链队列。
为了操作上更方便,我们将队头指针指向链队列的头结点,而队尾指针指向终端结点,如下图所示:
空队列时,front和rear都指向头结点,如下图所示:
2.1.1、出队和入队完整案例
这段代码调试了一定时间,尤其是入队的时候要当心null。
public class TestLinkQueue {
public static void main(String[] args) {
LinkQueue linkQueue = new LinkQueue();
linkQueue.enQueue("a0");
linkQueue.enQueue("a1");
linkQueue.enQueue("e");
printStackInformation(linkQueue);
linkQueue.deQueue(linkQueue);
printStackInformation(linkQueue);
}
private static void printStackInformation(LinkQueue linkQueue) {
System.out.println("--------------队列依次输出--------------");
for (QNode stackNode = linkQueue.front; stackNode != null; stackNode = stackNode.next) {
System.out.println(stackNode.data);
}
}
static class LinkQueue {
QNode front = new QNode(); // 头指针
QNode rear = new QNode(); // 尾指针
ResultEnum enQueue(String e) {
QNode qNode = new QNode();
qNode.data = e;
qNode.next = null;
if (rear.next == null) { // 队列为空
rear.next = qNode; // 更新队尾和队首指针
front.next = qNode;
return ResultEnum.OK;
}
rear.next.next = qNode;//把拥有元素e的新结点qNode赋值给原队尾结点的后继。
rear.next = qNode;//把当前的qNode设置为队尾结点,rear指向qNode
return ResultEnum.OK;
}
ResultEnum deQueue(LinkQueue linkQueue) {
QNode qNode;
if (linkQueue.front == linkQueue.rear) {
return ResultEnum.ERROR;
}
qNode = linkQueue.front.next;//将要删除的队头结点暂存给qNode
qNode.data = null;
linkQueue.front.next = qNode.next;//将原队头结点的后继qNode->next赋值给头结点后继
if (linkQueue.rear == qNode) {//若队头是队尾,则删除后将rear指向头结点
linkQueue.rear = linkQueue.front;
}
return ResultEnum.OK;
}
}
static class QNode {
String data;
QNode next;
}
enum ResultEnum {
OK, ERROR;
}
}
输出结果:
--------------队列依次输出--------------
null
a0
a1
e
--------------队列依次输出--------------
null
a1
e
5、总结
栈和队列它们都是特殊的线性表,只不过对插入和删除操作做了限制。
栈(stack)是限定仅在表尾进行插入和删除操作的线性表。
队列(queue)是只允许在一端进行插入操作,在另一端进行删除操作的线性表。
它们均可以用线性表的顺序存储结构和链式存储结构来实现,但都在着顺序存储的一些弊端,因此它们各自有各自的技巧来解决这个问题。
对于栈来说,如果两个相同的数据类型的栈,则可以用数组的两端作栈底的方法来让两个栈共享数据,这就可以最大化的利用数组空间。
对于队列来说,为了避免数组插入和删除时需要移动数据,于是就引入了循环队列,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗,时间复杂度从O(n)变成了O(1).
原创不易,求个关注。
微信公众号: 一粒尘埃的漫旅
里面有很多想对大家说的话,就像和朋友聊聊天。
写代码,做设计,聊生活,聊工作,聊职场。
我见到的世界是什么样子的?
搜索关注我吧。