嵌入式 - 数据结构:栈和队列
目录
一、栈:后进先出
1.1 栈的核心概念
1.2 栈的分类与实现
1.2.1 顺序栈
创建栈:需要同时申请存放栈结构和数据元素的内存空间
销毁栈:需按顺序释放数据空间和栈结构空间
判断栈空 / 栈满:通过栈针位置判断
压栈与出栈:
1.2.2 链式栈
创建
判空:检查链表是否只有空白节点压栈:采用头插法将新元素插入链表头部
入栈
出栈:删除链表第一个有效节点并返回其值
销毁:释放所有节点空间
1.3 栈的典型应用
二、队列:先进先出的数据通道
2.1 队列的核心概念
2.2 队列的分类与实现
2.2.1 循环队列
2.2.2 链式队列
2.3 队列的典型应用
三、栈与队列的对比与选择
3.1 相同点
3.2 不同点
3.3 实现选择建议
四、总结
栈和队列是两种看似简单却至关重要的线性数据结构。它们以独特的方式管理着数据的进出顺序,在算法设计和程序开发中扮演着不可或缺的角色。本文将深入剖析栈和队列的本质、实现方式及应用场景。
一、栈:后进先出
1.1 栈的核心概念
栈是一种遵循 "后进先出"(LIFO, Last In First Out)原则的线性数据结构。想象一摞叠在一起的盘子,最后放上去的盘子总是最先被取走,这就是栈的工作方式。在栈中,数据元素的插入和删除操作只能在同一端进行,这一端被称为栈顶,与之相对的另一端则被称为栈底。
栈的基本操作包括:
- 入栈(压栈):将元素放入栈顶位置
- 出栈(弹栈):将栈顶元素取出
- 栈针:一个记录栈顶位置的标记,用于快速定位栈顶元素
1.2 栈的分类与实现
根据存储方式的不同,栈可以分为顺序栈和链式栈两种。
1.2.1 顺序栈
顺序栈使用连续的内存空间(通常是数组)来存储数据元素,其实现简单高效。
类型定义:
typedef int datatype; // 存放数据的类型
typedef struct stack {datatype *pdata; // 存放数据空间首地址int top; // 栈顶元素位置int tlen; // 最多存放元素个数
} seqstack;
基本操作实现:
-
创建栈:需要同时申请存放栈结构和数据元素的内存空间
/*创建栈*/
seqstack *create_seqstack(int len)
{seqstack *ptmpstack = NULL;//申请标签空间ptmpstack = malloc(sizeof(seqstack));if(NULL == ptmpstack){perror ("fail to malloc");return NULL;}//申请存放数据的空间ptmpstack->pdata = malloc(sizeof(datatype)*len);if(NULL == ptmpstack->pdata){perror("fail to malloc");return NULL;}ptmpstack->top = 0; //top指针指向存放的最后一个数据的下一个//由于top是从0 开始,其数值跟存放的长度tlen一样ptmpstack->tlen = len;//return ptmpstack;
}
-
销毁栈:需按顺序释放数据空间和栈结构空间
/*销毁栈*/
int destroy_seqstack(seqstack **pptmpstack)
{//先释放元素free((*pptmpstack)->pdata);//再释放指向栈的指针 //不然会找不到free(*pptmpstack);*pptmpstack = NULL;return 0;
}
-
判断栈空 / 栈满:通过栈针位置判断
// 栈针为0即为空栈
int is_empty_seqstack(seqstack *ptmpstack) {return 0 == ptmpstack->top;
}// 栈针与最大容量相等即为满栈
int is_full_seqstack(seqstack *ptmpstack) {return ptmpstack->tlen == ptmpstack->top;
}
-
压栈与出栈:
// 压栈:将元素放入栈顶,栈针加1
int push_seqstack(seqstack *ptmpstack, datatype tmpdata) {if (is_full_seqstack(ptmpstack)) {return -1;}ptmpstack->pdata[ptmpstack->top++] = tmpdata;return 0;
}// 出栈:栈针减1,返回栈顶元素
datatype pop_seqstack(seqstack *ptmpstack) {if (is_empty_seqstack(ptmpstack)) {return -1;}return ptmpstack->pdata[--ptmpstack->top];
}
顺序栈还可以根据增长方向和栈针指向分为多种类型
- 增栈:栈的方向自低地址向高地址增长
- 减栈:栈的方向自高地址向低地址增长
- 空栈:栈针指向要入栈的位置
- 满栈:栈针指向栈顶元素的位置
1.2.2 链式栈
链式栈使用链表结构实现,避免了顺序栈的容量限制问题。其节点定义与单向链表类似,压栈操作类似于链表的头插法,出栈操作则是删除链表的第一个有效节点。
基本操作:
-
创建
/*链式栈的创建*/
linknode *create_empty_linkstack(void)
{//创建空白节点linknode *ptmpstack = NULL;ptmpstack = malloc(sizeof(linknode));if(NULL == ptmpstack){perror("fail to malloc");return NULL; //linknode 类型的返回值返回null}//初始化节点中的值ptmpstack->pdata = 0;ptmpstack->pnext = NULL;//返回空白节点地址return ptmpstack;
}
-
判空:检查链表是否只有空白节点压栈:采用头插法将新元素插入链表头部
/*判断栈空*/
int is_empty_linkstack(linknode *phead)
{//判断空白节点后面还有没有节点 return NULL == phead->pnext;
}
-
入栈
/*入栈*/
int push_linkstack(linknode *phead, datatype tmpdata)
{//头插法 //没有元素上限限制linknode *ptmpstack = NULL;//一定要记得申请空间!!!ptmpstack = malloc(sizeof(linknode));if(NULL == ptmpstack){perror("fail to malloc");return -1;//int 类型的返回值返回数字}ptmpstack->pdata = tmpdata;ptmpstack->pnext = phead->pnext;phead->pnext = ptmpstack;return 0;
}
-
出栈:删除链表第一个有效节点并返回其值
/*出栈*/
datatype pop_linkstack(linknode *phead)
{linknode *ptmpstack = NULL;datatype popdata = 0;if(is_empty_linkstack(phead))return -1;//删除第一个有效元素ptmpstack = phead->pnext; //是链表! 不可以不负责任的free掉//先把头跟后面的连起来 ,再freephead->pnext = ptmpstack->pnext; popdata = ptmpstack->pdata;free(ptmpstack);//返回数据return popdata;
}
-
销毁:释放所有节点空间
/*销毁*/
int destroy_linkstack(linknode **pphead)
{//销毁链表linknode *ptmpstack = NULL;linknode *pfreenode = NULL;ptmpstack = *pphead;while(ptmpstack != NULL){pfreenode = ptmpstack;ptmpstack = ptmpstack->pnext;free(pfreenode);}*pphead = NULL;return 0;
}
链式栈的优势在于可以动态调整大小,避免了顺序栈的容量限制,但由于需要额外存储指针,会消耗更多内存。
1.3 栈的典型应用
- 函数调用栈:程序执行时,函数调用信息通过栈来管理,确保函数返回时能正确回到调用位置
- 表达式求值:用于解析和计算数学表达式,特别是处理括号匹配和运算符优先级
- 深度优先搜索(DFS):在图论算法中,栈常用于实现深度优先搜索
- 撤销操作:许多应用程序的撤销功能通过栈来实现,记录操作历史
二、队列:先进先出的数据通道
2.1 队列的核心概念
队列是一种遵循 "先进先出"(FIFO, First In First Out)原则的线性数据结构。如同日常生活中的排队场景,先进入队列的元素会先被处理。队列有两个端点:队头(允许删除元素的一端)和队尾(允许插入元素的一端)。
队列的基本操作包括:
- 入队:将元素添加到队尾
- 出队:从队头移除并返回元素
- 判空 / 判满:判断队列是否为空或已满
2.2 队列的分类与实现
队列主要有两种实现方式:循环队列(顺序队列)和链式队列。
2.2.1 循环队列
循环队列使用数组实现,通过巧妙的下标计算使队列空间可以循环利用,避免了 "假溢出" 问题。
类型定义:
typedef int datatype; // 存放数据的类型
typedef struct queue {datatype *pdata; // 存放数据空间的首地址int head; // 头下标(队头)int tail; // 尾下标(队尾)int tlen; // 最多存放元素个数
} seqqueue;
关键技术:
循环队列通过取余运算实现下标循环,当队尾指针达到数组末尾时,会自动绕回数组开头。为避免队列空满状态判断冲突,通常牺牲一个存储空间,以(tail + 1) % tlen == head
作为队满条件。
基本操作实现:
- 创建与销毁:
// 创建循环队列
seqqueue *create_seqqueue(int len) {seqqueue *ptmpqueue = malloc(sizeof(seqqueue));if (NULL == ptmpqueue) {perror("fail to malloc");return NULL;}ptmpqueue->pdata = malloc(sizeof(datatype) * len);if (NULL == ptmpqueue->pdata) {perror("fail to malloc");return NULL;}ptmpqueue->head = 0;ptmpqueue->tail = 0;ptmpqueue->tlen = len;return ptmpqueue;
}// 销毁循环队列
int destroy_seqqueue(seqqueue **pptmpqueue) {free((*pptmpqueue)->pdata);free(*pptmpqueue);*pptmpqueue = NULL;return 0;
}
- 判断空满:
// 队头与队尾下标相等即为空
int is_empty_seqqueue(seqqueue *ptmpqueue) {return ptmpqueue->head == ptmpqueue->tail;
}// 队尾加1取余后等于队头即为满
int is_full_seqqueue(seqqueue *ptmpqueue) {return ((ptmpqueue->tail + 1) % ptmpqueue->tlen) == ptmpqueue->head;
}
- 入队与出队:
// 入队:将元素放入队尾,队尾下标加1取余
int enter_seqqueue(seqqueue *ptmpqueue, datatype tmpdata) {if (is_full_seqqueue(ptmpqueue)) {return -1;}ptmpqueue->pdata[ptmpqueue->tail] = tmpdata;ptmpqueue->tail = (ptmpqueue->tail + 1) % ptmpqueue->tlen;return 0;
}// 出队:返回队头元素,队头下标加1取余
datatype quit_seqqueue(seqqueue *ptmpqueue) {datatype retval;if (is_empty_seqqueue(ptmpqueue)) {return -1;}retval = ptmpqueue->pdata[ptmpqueue->head];ptmpqueue->head = (ptmpqueue->head + 1) % ptmpqueue->tlen;return retval;
}
2.2.2 链式队列
链式队列使用链表实现,通常由一个头指针和一个尾指针分别指向队头和队尾。入队操作在链表尾部插入元素,出队操作在链表头部删除元素。
基本操作
- 创建
/*创建*/
linknode *create_empty_linkqueue(void)
{//单向链表创建linknode *ptmpqueue = NULL;ptmpqueue = malloc(sizeof(linknode)); //为啥写两遍: 非链式的就得写两遍if(ptmpqueue == NULL){perror("fail to malloc");return NULL;}ptmpqueue->data = 0;ptmpqueue->pnext = NULL;return ptmpqueue;
}
- 判断空
/*判断空*/
int is_empty_linkqueue(linknode *phead)
{//顺序栈判断是否为NULLreturn phead->pnext == NULL;
}
- 入队
/*入队*/
int enter_linkqueue(linknode *phead, datatype tmpdata)
{//尾插法 //没有元素上限限制//不用判断满linknode *ptmpqueue = NULL;linknode *ptail = phead;//链式的入栈 一定要记得申请空间啊`!!!ptmpqueue = malloc(sizeof(linknode));if(NULL == ptmpqueue){perror("fail to malloc");return -1;}while(ptail->pnext != NULL){ptail = ptail->pnext;}ptmpqueue->pnext = NULL;ptmpqueue->data = tmpdata;ptail->pnext = ptmpqueue;return 0;
}
- 出队
/*出队*/
datatype quit_linkqueue(linknode *phead)
{//顺序栈的出栈linknode *ptmpqueue = NULL;datatype que = 0;//记得判断空!!! //是把phead 放进去! 不是ptmpqueue!if(is_empty_linkqueue(phead))return -1;ptmpqueue = phead->pnext;que = ptmpqueue->data;phead->pnext = ptmpqueue->pnext;//需要free掉出栈元素吗 是的!!!free(ptmpqueue);return que;
}
- 销毁
/*销毁*/
int destroy_linkqueue(linknode **pphead)
{//单向链表销毁linknode *ptmpqueue = NULL;linknode *pfreenode = NULL;ptmpqueue = *pphead;while(NULL != ptmpqueue){pfreenode = ptmpqueue;ptmpqueue = ptmpqueue->pnext;free(pfreenode); }*pphead = NULL;return 0;}
链式队列的优势是可以动态调整大小,不存在容量限制问题,但同样需要额外存储指针信息。
2.3 队列的典型应用
- 缓冲处理:在数据传输(如网络通信)中,队列常用于缓冲不同速率的数据源和数据处理模块
- 广度优先搜索(BFS):在图论算法中,队列是实现广度优先搜索的核心数据结构
- 任务调度:操作系统中的任务调度器常用队列管理等待执行的任务
- 打印队列:多个程序共享打印机时,通过队列管理打印任务
三、栈与队列的对比与选择
3.1 相同点
- 都是线性数据结构,元素之间存在一对一的逻辑关系
- 都可以通过数组或链表实现
- 插入和删除操作都有明确的位置限制
3.2 不同点
- 操作顺序:栈是后进先出,队列是先进先出
- 操作位置:栈只在一端操作,队列在两端操作
- 应用场景:栈适用于需要回溯的场景,队列适用于需要公平处理的场景
3.3 实现选择建议
- 当数据量固定且已知时,顺序实现(顺序栈、循环队列)效率更高
- 当数据量不确定时,链式实现更灵活
- 顺序实现需注意容量限制和溢出问题
- 链式实现需权衡额外的指针存储开销
四、总结
栈和队列作为两种基础的线性数据结构,虽然简单却蕴含着深刻的设计思想。它们通过限制数据操作的方式,为特定问题提供了高效的解决方案。理解栈和队列的工作原理及其实现方式,是每个程序员必备的基础知识。
在实际开发中,选择栈还是队列,取决于具体问题的需求:当需要 "回溯" 功能时选择栈,当需要 "公平处理" 时选择队列。而顺序实现与链式实现的选择,则需要在效率和灵活性之间做出权衡。