栈和队列
1)概述
和顺序表和链表一样,栈也是用来存储逻辑关系为“一对一”数据的线性存储结构
栈对数据存和去的过程有特殊的要求:
- 栈只能从表的一端存取数据,另一端是封闭的
- 在栈中,无论是存数据还是取数据,都必须遵循先进后出的原则,即最先进栈的元素最后出栈。
简单来说:栈是一种只能从表的一端存取数据且遵循“先进后出”原则的线性存储结构
栈顶:栈的开口端
栈底:栈的封口端
栈顶元素指的是距离栈顶最近的元素,而栈底元素指的是,位于栈最底部的元素
进栈和出栈:
基于栈结构的特点,在实际的应用中通常会对栈执行下面的两种操作:
- 向栈中添加元素,此过程被称为“进栈”(入栈或压栈)
- 从栈中提取出指定元素,此过程中被称为出栈(或弹栈)
栈的具体实现:
栈是一种特殊的线性存储结构,因此栈的具体实现有以下两种方式:
- 顺序栈:采用顺序存储结构,可以模拟栈存储数据的特点,从而实现栈存储结构
- 莲栈:采用链式存储结构实现栈
2)顺序栈
顺序栈,也就是用顺序表来实现栈存储结构(就是说用数组来实现)。
如果仔细观察顺序表和栈结构就会发现,他们存储数据的方式高度相似,只不过栈对数据的存取过程有特殊的限制,而顺序表没有。
在使用顺序表模拟栈存储结构常用的实现思路,就是在顺序表中设定一个实时指向栈顶元素的**变量(**top),top的初值为-1,表示栈中没有存储任何数据元素,也就是说栈是空栈,一旦数据元素进栈,top就 +1,如果元素出栈 top 就-1
理解:
top代表栈顶元素的索引值
元素“入栈”
最初的时候栈是空栈,即数组是空的,top的初始值为-1。
向栈中添加元素1,则top = 0然后依次存储
typedef struct SortStack{
int* array;
int top ;
}*sortStack;
int push(sortStack stack,int elem){
stack->array[++(stack->top)]=elem;
return stack->top;
}
分析:
从代码中定义了一个SortStack的数据结构,这个数据结构中一个属性用于存储一个数组,另一个用于存储top指针。
push方法用于向数组中添加一个数据。
这样的行为称之为“入栈”
元素“出栈”
top变量其实对模拟数据入栈的操作没有什么实际的帮助,但是它是为了实现数据出栈而准备的。
出栈时需要把数据一个个的扔出栈,直到取到对应位置的元素,而这里的对应的位置索引就是通过top来完成指向的。
/**
* 弹出栈的最后一个元素
* @param array
* @return
*/
int pop(sortStack stack){
if (stack->top == -1){
INFO_LOG("This stack is a empty stack");
return -1;
}
int tempData = stack->array[stack->top];
printf("The last elem is : %d",tempData);
stack->top--;
return tempData;
}
分析:
由于top总是指向栈顶元素,所以每次都会把栈定的元素弹出来。
如果top的值为-1则说明这个栈是一个空栈
3)链栈
链栈就是用链表实现栈存储结构
顺序栈是将顺序表的一端作为栈底,另一端作为栈顶,而链栈也是这样的,通常我们将链表的头部作为栈顶,尾部作为栈底。
对于这个也很显而易见,对于栈来说,我们无法随机存取,这和链表的顺序存取结构也是十分契合的。
这里以头为顶更便于栈的理解。
元素“入栈”
linkStackNode push(linkStackNode head,int element){
linkStackNode newNode = (linkStackNode)malloc(sizeof (struct LinkStackNode));
//让新结点的next指向head
newNode->next = head;
newNode->data = element;
head = newNode;
//把新结点作为head返回
return head;
}
元素“出栈”
linkStackNode pop(linkStackNode head){
if (head){
//保证栈内是有元素的
linkStackNode temp = head;
//然后把栈顶元素进行更新
head = head->next;
printf("出栈元素 :%d",temp->data);
if (head){
printf("新栈顶元素 %d\n",head->data);
} else{
printf("栈已空");
}
free(temp);
} else{
printf("栈内已无元素");
return head;
}
return head;
}
出栈的思想也很简单,就是把栈顶元素弹出,然后更新栈顶元素即可。
注意整个过程中避免栈内无数据还要做数据出栈的错误操作
4)用栈结构求表达式的值
如何用栈结构求一个表达式的值?
所谓表达式,就是由变量、常量以及运算符组合而成的式子,常用的运算符有(!,^,+,-,*,/,())这几种
用栈结构去求一个表达式的值,已经设计好了全新的表示表达式方法,称为后缀表达式或者逆波兰表达式。和普通表达式不同,后缀表达式习惯把运算符写在它的运算项之后,而且整个表达式的过程中不用() 表明运算的优先级关系
比如说:表达式-> !3 + 4*2/(1-5)^2
就可以转换为后缀表达式:
3 ! 4 2 * 1 5 - 2 ^ / +
虽然后缀表达式相比于普通的表达式来说,能轻松的借助栈存储结构求得值,但是却完全舍弃了表达式该有的可读性。
具体求值的过程:当用户给定一个后缀表达式时,按照从左到右的顺序依次扫描表达式中的各个运算项和运算符
- 遇到运算项时,直接入栈
- 遇到运算符时,将位于栈顶的运算项出栈,对于单目运算符去一个运算项,对于双目运算符去两个运算项,第一个取出来的运算向作为右运算项,另一个作为左运算项,求得表达式的值后将其入栈
- 直到栈中存在一个运算项为止,此运算项即为整个表达式的值
5)队列
队列和栈一样也是一种对数据的存取由严格要求的“线性存储结构”
和栈结构不同的是,队列的两端都开口,,要求数据只能从一端进,从另一端出
进数据的一端叫做队尾,出数据的一段叫做队头,数据元素进队列的过程叫做入队,出队列的过程叫做出队。
队列中数据的进出要求遵循,先进先出的原则,最先进队列的数据元素最先出队列,最后进队列的元素,最后出队列
区分:
对于栈来说是先进后出,而对于队列来说是先进先出。栈是一端开口,队列是两端开口
根据存储结构的实现可分为:
- 顺序队列:在顺序表的基础上实现的队列结构
- 链队列:在链表的基础上实现的队列结构
两者的区别仅是顺序表和链表的去呗,即在实际的物理空间中,如果数据几种存储就是用的顺序队列,如果数据分散存储就是用的链队列。
6)顺序队列
顺序队列使用的是数组,因此需要提前申请一块足够大的内存空间初始化顺序队列,初次之外,满足顺序队列中数据从队尾进,队头出且先进先出的要求,需要定义两个指针(top 和 rear) 分别指向队列的队头元素和队尾元素。
当数据元素进队列时,对应的操作应该是将其存储在rear指向的数组的位置,然后rear + 1,当需要出队列的时候,就需要做 top +1 的操作
方法一:
#include <stdio.h>
#define DEBUG_LOG
#define INFO
#include "log.h"
#include "malloc.h"
typedef struct SortQueue{
int* array;
int top;
int rear;
}*sortQueue;
/**
* 初始化具有阿巴个元素的队列
* @param n
* @return
*/
sortQueue init(){
sortQueue queue = (sortQueue)malloc(sizeof (struct SortQueue));
int array[] = {1,2,3,4};
queue->array = array;
queue->rear = 4;
queue->top = 0;
}
/**
* 进队列的方法
* @param queue
* @return
* 把data放到queue 里面
*/
sortQueue inQueue(sortQueue queue,int data){
int rearIndex = queue->rear;
queue->array[rearIndex] = data;
queue->rear += 1;
return queue;
}
/**
* 出队列的方法
* @param queue
* @return
*/
sortQueue outQueue(sortQueue queue){
int topIndex = queue->top;
int rearIndex = queue->rear;
if (topIndex == rearIndex){
INFO_LOG("There is not element!Can't out queue");
} else{
printf("The top element is >>>%d",queue->array[queue->top]);
}
queue->top+=1;
return queue;
}
这种方法穿在一个问题:
当数据全部出队后,top和rear的重合指向了a[4]而不是a[0],也就是说,整个顺序队列在数据的不断进队出队的过程中,在顺序表中的位置不断的后移
影响:
- 顺序队列之前的数组存储空间将无法再被使用,造成空间浪费
- 如果顺序表申请的空间不够大,就容易造成溢出,产生溢出错误
所以就有了方法二
方法二:
将顺序表打造成一个环状
代码修改:
/**
* 进队列的方法
* @param queue
* @return
* 把data放到queue 里面
*/
sortQueue inQueue(sortQueue queue,int data){
/*
* 新增判断语句,如果再增加的一个元素的下一个元素大于max,则从a[0]重新开始存储
* 如果rear +1 和 front 重合,则表明数组已满
*
*/
if (((queue->rear)+1)%sizeMax == queue->top){
/**
* 这里就表示空间已满,rear队尾指针和头指针重合
*/
printf("The space if full");
return queue;
}
int rearIndex = queue->rear;
queue->array[rearIndex % sizeMax] = data;
queue->rear = (rearIndex % sizeMax) + 1;
return queue;
}
/**
* 出队列的方法
* @param queue
* @return
*/
sortQueue outQueue(sortQueue queue){
int topIndex = queue->top;
int rearIndex = queue->rear;
//如果front == rear 表示队列为空
if (topIndex == rearIndex){
INFO_LOG("There is not element!Can't out queue");
} else{
printf("The top element is >>>%d",queue->array[queue->top]);
}
queue->top = (queue->top+1)%sizeMax;
return queue;
}
通过取余的方式来实现最后可以接到表头,这样其实是一种很巧妙的方式实现顺序队列的头尾相接,且不用再本身的物理结构上进行修改,因为对于这个顺序队列还是线性的,但是在逻辑上已经是环状的了。
注意:
- 当队列为空的时候,队列的头指针会等于队列的尾指针
- 当队列满员的时候,队列的头指针会等于队列的尾指针
7)链式队列
链式队列,通过链表实现队列的存储结构
通过链表实现队列的方式和顺序队列是一样的,都需要借助一个top指针和一个rear指针,一个指向队列头,一个指向队列尾。
#include <stdio.h>
#include"log.h"
#include <malloc.h>
/*
*定义链式队列的结点
*/
typedef struct LinkQueueNode{
struct LinkQueueNode * next;
int data;
}*linkQueueNode;
/*
* 向队列中插入数据应该向队尾插入
* 对于链式队列来说就不存在会有空间满了的情况了
* 传入链表的最后一个结点,直接把结点添加最后即可
*/
linkQueueNode inQueue(linkQueueNode rear,int newData){
linkQueueNode newNode = (linkQueueNode)malloc(sizeof(struct LinkQueueNode));
newNode->next = NULL;
newNode->data = newData;
rear->next = newNode;
rear = newNode;
return newNode;
}
/*
* 向队列中退出一个元素,将top指针所指向的首元素出队
* 出队之后要根据对列的长度保证 top 指针和 rear 指针指向正确
* 如果队列出队后的长度大于等于1,则rear 指针不用管
* 但是如果队列出队后的长度为0,这个时候就意味着rear指针的之前指向已经出队的node 这个时候就需要更新指针的指向了
*/
linkQueueNode outQueue(linkQueueNode top,linkQueueNode rear){
if (top == NULL || top->next == NULL)
{
printf("There is a empty queue");
}
linkQueueNode outNode = top;
printf("出栈元素是>>>>%d\n",outNode->data);
top = top->next;
/*
* 如果尾指针指向一开始的首元素,且队列的长度为1,那么让他指向新的空结点
*/
if (rear == outNode)
{
rear = top;
}
free(outNode);
}
在出队元素的时候一定要提前判断队列中是否还有元素,要提示用户无法做出出队操作,保证程序的健壮性
8)小结
首先:虽然栈和队列存储结构不同,但是两者都是线性结构
线性结构:用于存储逻辑关系为”一对一“数据的存储结构,比如说顺序存储结构和链式存储结构