【数据结构】—— 栈与队列

news2024/9/23 5:24:47

目录

  • 前言
  • 一、栈
    • 1.1 堆栈原理
    • 1.2 栈的实现
  • 二、队列
    • 2.1 队列的概念
    • 2.2 队列结构
      • 2.2.1 顺序队列
      • 2.2.2 链队
    • 2.3 队列的实现
  • 三、堆与栈的区别
    • 3.1 内存中的堆与栈
    • 3.2 数据结构中的堆与栈
  • 结语


前言

  在单片机数据处理的时候,如果在中断里添加太多函数,可能会影响整个程序的运行。这时利用数据结构栈或者队列,先将缓存放进去,等主程序空闲时再处理,可以变相提高代码稳定性。栈与队列是一种特殊操作的线性表,本文主要介绍栈与队列的原理以及单片机C语言实现。


一、栈

1.1 堆栈原理

  1. 栈的类型定义
      栈Stack又名堆栈,是限定只能在表的一端(表尾)进行插入或删除操作的线性表。允许进行插入或删除的这一端称为栈顶(Top);另一端则称栈底(Bottom),不能进行插入或删除。当栈中没有包含数据元素时,称为空栈。栈非空时,处于栈顶位置的元素称为栈顶元素。向一个栈插入新的元素称为入栈或选栈(Push)、此时,插入的元素成为新的栈顶元素;从栈中删除一个元素时,只能删除当前的栈顶元素,称为出栈或退栈(Pop)。
      由于栈的插入和删除只能在栈顶进行,最先入栈的元素必定最后出栈,最后入栈的元素最先出栈,因此栈又叫做后进先出(Last In First Out,LIFO)线性表。

  2. 顺序栈的表示
      与顺序表类似,顺序栈就是用一组地址连续的存储单元依次存放自栈底到栈顶的数据元素。同时,为了指示当前的栈顶元素位置,需另设一个指针变量top,称为栈顶指针,通常 top指向栈中下一个入栈位置,当栈顶指针指向第一个存储单元时表示空栈。入栈时,首先将新元素的值存入当前的栈顶位置,然后使栈顶指针top+1;出栈时则相反,先使栈顶指针 top一1,然后取出当前位置的元素的值。顺序栈的结构及基本操作如图所示:
    请添加图片描述
      顺序栈的实现方式可以采用预设的固定长度的一维数组来实现,也可以通过预定义两个常量INIT_SIZE 和INCREMENT分别表示顺序栈的初始长度和增量,以动态分配的顺序存储结构进行描述。

1.2 栈的实现

  1. 定义结构体
#define	ERROR	0
#define OK 		1
#define INIT_SIZE	10	//存储空间初始分配量
#define INCREMENT	5	//分配增量

//定义栈结构体
typedef struct
{
	char*	base;  
	int*	top; 
	int 	stacksize;		//当前已分配的存储空间
}Stack;
  1. 顺序栈初始化
int InitStack(Stack* S)
{
	S->base = (char*)malloc(INIT_SIZE * sizeof(char));
	if(!S->base)	return ERROR;	//申请失败返回0
	
	S->top = S->base; 				//设置初始值
	S->stacksize = INIT_SIZE;
	return OK;
}
  1. 判断栈是否为空
bool EmptyStack(Stack* S)
{
	return S->top == S->base;
}
  1. 入栈
int Push(Stack* S, char x)
{
	// 检查空间够不够,不够就增容
	if (ps->top - S->base >= S->stacksize)
	{
		S->base = (char*)realloc(S->base,(S->stacksize + INCREMENT) * sizeof(char));
		if(!S->base)	return ERROR;			//申请失败返回0
	
		S->top = S->base + S->stacksize;		//重新设置栈顶指针
		S->stacksize += INCREMENT;
	}

	*S->top++ = x;		//新元素入栈,栈顶指针加1
	return OK;
}
  1. 出栈
int Pop(Stack* S,char* x)
{
	if(S->top == S->base) return ERROR;		//栈空则返回失败
	--S->top;								//修改栈顶指针减1
	*x = *S->top;							//出栈元素给X
	return OK;
}

二、队列

2.1 队列的概念

  队列Queue是限定只能在表的两端分别进行插入或删除操作的线性表。在队列结构中,数据元素只能从一端(表尾)插入,从另一端(表头)删除。允许插入的一端称为队尾(Rear),允许删除的一端称为队头(Front)。当队列中没有包含数据元素时,称为空队。向一个队列插入新的元素称为人队(Enqueue),此时,插入的元素成为新的队尾元素;从队列中删除一个元素时,只能删除当前的队头元素,称为出队(Dequeue)。基于列的这种“先进先出”的结构特点,因而也称为先进先出(First In First Out,FIFO)线性表。
请添加图片描述

2.2 队列结构

  队列的表示与实现也可以采用顺序存储结构和链式存储结构描述

2.2.1 顺序队列

  采用顺序存储结构表示队列时,可以用C语言中的一维数组进行描述。由于对于队列的操作只在队头和队尾进行,因此,需要设置两个指针 front和rear分别指示队头和队尾的位置,并可约定队尾指针rear指向当前队尾元素后的下一个位置,队头指针front指向当前的队头元素。顺序队列的表示形式如下,

#define MAXSIZE 100			//队列的最大容量
typedef struct SqQueue
{
	int data[MAXSIZE];		//队列的存储空间
	int rear,front;			//对头队尾指针
}SqQueue;

(1)初始化队列。队尾指针和队头指针均指向下标为0的单元,令front=rear=0。
(2)入队操作。新元素将存放到当前队尾指针所指的单元,并使队尾指针rear增1指示下一次插入的位置,则rear=rear+1。
(3)出队操作。将当前队头指针所指单元的值返回,并将队头指针front增1即可,则 front=front+1。
请添加图片描述

  入队和出队操作的过程如上图所示。可以看出,队列为空,不能做出队操作,因此判断队列为空的条件是front==rear;经过多次插入和删除操作以后,队列的队尾指针和队头指针将逐步向后移动,最终出现当队尾指针已指向所定义空间中的最后单元时,即rear=MAXSIZE-1时,队列满,无法再入队。虽然此时队头指针所指位置之前可能存在若干单元空闲,却无法进行入队操作的问题。这种现象称为“假溢出”,可以借助两种方法解决顺序队列的“假溢出”问题:

方法一:采用“移动队列”的方法,即每当执行一次出队操作,则依次将队头和队尾指针向数组的起始位置移动,始终保持队头在数组的起始位置。这种方法的代价是产生大量的元素移动,显然不是一种好方法。
方法二:采用循环队列,将一维数组的最后一个单元和第一个单元连接起来构成循环数组,此时称为“循环队列”。当队尾指针已指向数组的最后时,在进行人队操作过程中,可将队尾指针rear 移至数组的起始位置,表示下一次入队操作时的队尾。(居指针移动的方式是rear=(rear+1)%MAXSIZE,队头指针的移动也一样front=(front+1)%MAXSIZE,如图所示:
请添加图片描述

  循环队列初始化空队时,front=rear=0;当队列经过多次出队入队操作后,会出现队头指针front 和队尾指针rear 再次指向同一单元的情况,此时有可能队列空也有可能队列满。为了区分队满和队空的情况,规定当队空时,front==rear;队满时,队尾指针指向队头指针的前一个位置即为满 (rear+1)%MAXSIZE==front ,实际循环队列队满时,所容纳的元素个数为MAXSIZE-T;通过空留一个存储单元的方式区分循环队列队满和队空的情况。下面第三小节详细说明循环队列的存储表示和基本操作的实现方法。

2.2.2 链队

  采用链式存储结构表示队列,称为链队。显然,利用与线性链表类似的方法可以很容易实现链队结构,并分别设置队尾和队头指针指示链队的位置。在链队结构中增加一个附加的头结点,并使队头指针front指向它。此时,判断空队的标志是,链队的队尾指针rear和队头指针front均指向头结点。带头结点的链队结构示意图如图所示:
请添加图片描述

2.3 队列的实现

  这边以ringbuffer环形缓冲区为例,环形缓冲区是队列的一个应用,这里采用顺序队来实现

  1. ringbuffer.h头文件
#ifndef __RINGBUFFER_H
#define __RINGBUFFER_H

#ifdef __cplusplus
 extern "C" {
#endif 

#include "stm32f4xx.h"
#include <stdio.h>

//队列结构体声明
typedef struct {
	uint8_t* pBuff;			//保存缓存数据
	uint8_t* pEnd;  		// pBuff + legnth
	uint8_t* wp;    		// Write Point
	uint8_t* rp;    		// Read Point
	uint16_t length;	
	uint8_t  flagOverflow; // set when buffer overflowed
} RingBuffer;

void rbInitialize(RingBuffer* pRingBuff, uint8_t* buff, uint16_t length);
void rbClear(RingBuffer* pRingBuff);
void rbPush(RingBuffer* pRingBuff, uint8_t value);
uint8_t rbPop(RingBuffer* pRingBuff);
uint16_t rbGetCount(const RingBuffer* pRingBuff);
int8_t rbIsEmpty(const RingBuffer* pRingBuff);
int8_t rbIsFull(const RingBuffer* pRingBuff);

#ifdef __cplusplus
}
#endif

#endif 
  1. ringbuffer.c源文件
#include "ringbuffer.h"

//初始化队列结构体
void rbInitialize(RingBuffer* pRingBuff, uint8_t* buff, uint16_t length)
{
	pRingBuff->pBuff = buff;
	pRingBuff->pEnd  = buff + length;
	pRingBuff->wp = buff;
	pRingBuff->rp = buff;
	pRingBuff->length = length;
	pRingBuff->flagOverflow = 0;
}

//清除缓存溢出标志位
//正常情况下不会过流,只有在IAP情况下可能会出现虚假过流
void rbClear(RingBuffer* pRingBuff)
{
 	pRingBuff->wp = pRingBuff->pBuff;
	pRingBuff->rp = pRingBuff->pBuff;
	pRingBuff->flagOverflow = 0;
}

//入队
//首先判断队列有没有满,满就退回一段数据,未满就写数据
void rbPush(RingBuffer* pRingBuff, uint8_t value)
{
	uint8_t* wp_next = pRingBuff->wp + 1;
	if( wp_next == pRingBuff->pEnd ) {
		wp_next -= pRingBuff->length; 	// Rewind pointer when exceeds bound
	}
	if( wp_next != pRingBuff->rp ) {
		*pRingBuff->wp = value;
		pRingBuff->wp = wp_next;
	} else {
		pRingBuff->flagOverflow = 1;
	}
}

//出队,空队返回,否则读取数据
uint8_t rbPop(RingBuffer* pRingBuff)
{
	if( pRingBuff->rp == pRingBuff->wp ) return 0; // empty
  
	uint8_t ret = *(pRingBuff->rp++);
	if( pRingBuff->rp == pRingBuff->pEnd ) {
		pRingBuff->rp -= pRingBuff->length; 		// Rewind pointer when exceeds bound
	}
	return ret;
}

//返回未读数据长度
uint16_t rbGetCount(const RingBuffer* pRingBuff)
{
	return (pRingBuff->wp - pRingBuff->rp + pRingBuff->length) % pRingBuff->length;
}

//判断队列数据是否为空
//1:空;0:非空
int8_t rbIsEmpty(const RingBuffer* pRingBuff)
{
	return pRingBuff->wp == pRingBuff->rp; 
}

//判断队列空间是否满,没啥意义,可以不用
int8_t rbIsFull(const RingBuffer* pRingBuff)
{
 	return (pRingBuff->rp - pRingBuff->wp + pRingBuff->length - 1) % pRingBuff->length == 0;
}
  1. 函数调用
    首先对空间初始化申请队列缓存
#define RX_BUFF_SIZE	4096
static RingBuffer  RX_RingBuff;

//初始化
void RcvFrom_Init()
{
  static uint8_t RX_Buff[RX_BUFF_SIZE];
  rbInitialize(&RX_RingBuff, RX_Buff, sizeof(RX_Buff));
}

//入队
void Push_RcvData(u8 dat)
{
  rbPush(&RX_RingBuff, dat);
}

  然后在中断压入队列,以串口中断为例

void USART1_IRQHandler(void)
{
    u8 rcvdata;

    if (USART_GetFlagStatus(USART1, USART_FLAG_ORE) != RESET)
    {
        USART_ClearFlag(USART1,USART_FLAG_ORE);
        USART_ReceiveData(USART1);
        //The RXNE flag can also be cleared by a read operation to the USART_DR register(USART_ReceiveData()).
    }
    if(USART_GetITStatus(USART1,USART_IT_RXNE) != RESET)
    {
        rcvdata = USART_ReceiveData(USART1);
        Push_RcvData(rcvdata);
    }
}

  最后在主函数里对数据处理

u8 cur;

void main(void)
{
	/*初始化*/
	RcvFrom_Init();
	while(1)
	{
		cur = rbPop(&RX_RingBuff);
		/*具体处理*/
	}
}

三、堆与栈的区别

  首先,堆与栈是两个不同的概念,但是相同点是,他们都是内存里的一块空间。下面介绍它们最主要的区别

3.1 内存中的堆与栈

  1. 栈区stack
      栈由操作系统自动分配释放 ,用于存放函数的参数值、局部变量等。当函数被调用时,其参数会被push到堆栈内存中,待调用函数执行完毕后,被压入堆栈的参数就会被pop出来。另外函数体中定义的临时变量会被存放于堆栈中,堆栈是由操作系统分配的,内存的申请与回收都是由OS决定的。
      栈的内存地址生长方向由高到底,所以后定义的变量地址低于先定义的变量。栈中存储的数据的生命周期随着函数的执行完成而结束。

  2. 堆区heap
      用于存放进程运行中被动态分配的内存段,大小并不固定,可动态扩张或缩减,当进程调用malloc等函数分配时,新分配的内存就被动态的添加到堆上,当调用free等函数释放内存时,被释放的内存就会从堆中被剔除(堆被缩减)。堆由开发人员分配和释放, 若开发人员不释放,程序结束时由OS回收,分配方式类似于链表。

int main()
{
    // C 中用 malloc() 函数申请
    char* p1 = (char *)malloc(10);
    
    // 用 free() 函数释放
    free(p1);

	char *p2;			//栈
	
	while(1)
	{
		//
	}
}

  从上面的示例代码可以看出,由于单片机正常情况下无操作系统,所以在代码里定义的全局变量就相当于堆,如p1,因为它不会自动销毁。而一些暂时性的数据如局部变量就相当于栈,如p2,因为函数被调用结束就会被系统自动释放

3.2 数据结构中的堆与栈

  栈的数据结构在1.1小节有介绍,是一种运算受限的线性表,其限制是指只仅允许在表的一端进行插入和删除操作。而堆是一种常用的树形结构,是一种特殊的完全二叉树,当且仅当满足所有节点的值总是不大于或不小于其父节点的值的完全二叉树被称之为堆。堆的这一特性称之为堆序性。因此,在一个堆中,根节点是最大(或最小)节点。如果根节点最小,称之为小顶堆(或小根堆),如果根节点最大,称之为大顶堆(或大根堆)。堆的左右孩子没有大小的顺序。


结语

  作为一种数据结构,在单片机软件开发中并不常用,更多的是使用队列。比如单片机很多实时操作系统RTOS基本有内置的消息队列API,却没有栈API。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2100011.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

使用mobaxterm连接linux出现连接中断问题

1.问题描述 使用mobaxterm在连接到远程服务器时&#xff0c;如果隔一段时间不进行操作的话&#xff0c;会出现中断连接的现象。 2.解决 为了增强Linux系统的安全性&#xff0c;我们需要在用户输入空闲一段时间后自动断开&#xff0c;这个操作可以由设置TMOUT值来实现。将以下…

netty编程之基于websocket实现聊天功能

写在前面 源码 。 本文看下netty如何通过websocket实现聊天功能。 类似于实现http server,netty实现websocket也很简单&#xff0c;同样使用对应的编码器和解码器就行了&#xff0c;相关的有HttpServerCodec,HttpObjectAggregator,ChunkedWriteHandler。 1&#xff1a;编码 …

已解决:VS2022启动闪退,错误模块名称: clr.dll,版本: 4.8.9261.0,时间戳: 0x667a1925的问题

本问题已得到解决&#xff0c;请看以下小结&#xff1a; 关于《VS2022启动闪退》的解决方案 记录备注报错时间2024年报错版本VS2022报错复现下载某款VPN软件后&#xff0c;打开VS2022闪退&#xff0c;事件查看器输出如下报错描述错误应用程序名称: devenv.exe&#xff0c;版本:…

DAY 2 - 3 : 线性表—顺序存储

线性表—顺序表 问题引入&#xff1a; 线性表 定义 若干数据元素的一个线性序列。 表示 L (D,R) (即线性表L包含数据元素集合D和关系集合R&#xff09; D{ ai | ai∈datatype ,i0,1,2...n-1 ,n≥0} R{ <ai,ai1> | ai,ai1∈D, 0 ≤ i ≤ n - 2} < ai,ai1 >在这里称…

数据结构代码集训day14(适合考研、自学、期末和专升本)

题目均来自b站up&#xff1a;白话拆解数据结构&#xff01; 今日题目如下&#xff1a;&#xff08;1&#xff09;试写一个算法判断给定字符序列是否是回文。 &#xff08;2&#xff09;给定一个算法判断输入的表达式中括号是否匹配。假设只有花、中、尖三种括号。 题1 回文序列…

学习笔记 | 一文搞懂MySQL体系架构!!!(day22)

本文章的内容会在后面文章中慢慢讲解&#xff0c;该文章主要给各位博友zaipin提供学习思路&#xff0c;也希望大家在评论区发言表述&#xff0c;觉得文章有不足指出也可点评&#xff0c;希望大家多多支持&#xff01;&#xff01;&#xff01; 目录 一、MySQL 1.1 数据库概述 …

【项目日记】高并发内存池---实现页缓存

放纵自己的欲望是最大的祸害&#xff1b; 谈论别人的隐私是最大的罪恶&#xff1b; 不知自己过失是最大的病痛。 --- 亚里士多德 --- 高并发内存池---实现页缓存 1 页缓存整体设计思路2 框架搭建3 NewSpan函数4 请求Span联动 1 页缓存整体设计思路 首先我们来看页缓存的设…

windows手工杀毒-寻找可疑进程之进程名称

上篇回顾&#xff1a;windows手工杀毒-寻找可疑进程之进程图标-CSDN博客 上篇中我们简单介绍了什么是电脑病毒&#xff0c;也介绍了一种发现可疑进程的方法即根据进程图标确认是否是病毒&#xff0c;这种方法存在的理论基础是&#xff0c;通过图标可以很容易在电脑上找…

遥控器新手操作指南!!!

一、准备工作 检查电量&#xff1a;确保无人机和遥控器的电池电量充足&#xff0c;以避免在飞行过程中因电量不足而导致意外。 安装与连接&#xff1a;确保无人机的螺旋桨安装正确且牢固&#xff0c;同时检查无人机存储卡是否插入&#xff0c;以及遥控器与无人机之间的连接是…

论文笔记:2023顶会SIGIR - Strategy-aware Bundle Recommender System

论文笔记&#xff1a;2023顶会SIGIR - Strategy-aware Bundle Recommender System

【位运算】--- 初阶题目赏析

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; 算法Journey 根据上一篇位运算的总结&#xff0c;我们来体会几道初阶题目。 &#x1f3e0; 判定字符是否唯一 &#x1f4cc; 题目解析 判定字符是否唯一…

通义千问AI PPT初体验:一句话、万字文档、长文本一键生成PPT!

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;专注于分享AI全维度知识&#xff0c;包括但不限于AI科普&#xff0c;AI工…

leecode刷题经典算法套路模版笔记【递归回溯篇】--根本逻辑,快速掌控

刷题套路总结&#xff1a; 双指针&#xff1a; 单调性&#xff1b;对两端按照规律进行操作移动&#xff1b; 常见移法&#xff0c;右指针右移扩大范围&#xff0c;左指针左移缩小范围&#xff1b; 先举例模拟&#xff0c;然后推导公式&#xff1b; 递归&#xff0c;回溯 &am…

线程间同步的方式有哪些?

Linux 系统提供了五种用于线程间同步的方式&#xff1a;互斥锁、读写锁、自旋锁、信号量、条件变量 互斥锁 主要用于保护共享数据&#xff0c;确保同一时间内只有一个线程访问数据。 互斥量本质上来说就是一把锁&#xff0c;在访问共享资源前对互斥量进行加锁&#xff0c;访…

【go-zero】win启动rpc服务报错 panic: context deadline exceeded

win启动rpc服务报错 panic: context deadline exceeded 问题来源 在使用go-zero生成的rpc项目后 启动不起来 原因 这个问题原因是wndows没有启动etcd 官方文档是删除了etcd配置 而我自己的测试yaml配置有etcd&#xff0c;所以需要启动etcd 下载安装好etcd后&#xff0…

Java Full GC 的常见原因及优化策略

Java Full GC 的常见原因及优化策略 1、导致Full GC的常见原因1.1 新生代设置过小1.2 新生代设置过大1.3 Survivor区设置不当 2、优化GC策略2.1 吞吐量优先2.2 暂停时间优先 3、结论 &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在Java应…

自控原理-传递函数(闭环 扰动 偏差 前馈 复合 顺馈)

都知道闭环传递函数定义为&#xff1a;G1G2/(1G1G2H) 但是当碰到复杂的系统&#xff0c;比如复合顺馈&#xff0c;前馈扰动等&#xff0c;就不知道分子到底要不要乘上G2了。 这个公式是如何推导出来的&#xff0c;今天看到一个公式图片&#xff1a; 过程非常详细。 由此我也…

C语言遇见的一些小问题

问题如下&#xff1a; 1&#xff1a;为什么这样的代码为报错 #define _CRT_SECURE_NO_WARNINGS #include <iostream> #include <algorithm> #include <cstdio> #include<string> #include<stdlib.h> using namespace std; int main() {int i …

C语言 ——— #define定义标识符

目录 #define 定义常变量 #define 定义字符串 #define 定义一条代码 #define 定义的标识符是否需要加分号 #define 定义常变量 代码演示&#xff1a; #define M 100 //定义常变量 代码用途&#xff1a; int a M; int arr[M] { 0 }; 此时的 M 具有常属性&#xff0c…

什么是UART?

1.什么是UART&#xff1f; 通用异步收发传输器&#xff08;Universal Asynchronous Receiver/Transmitter)&#xff0c;通常称作UART。UART 表示通用异步接收机/发射机&#xff0c;定义用于在两个设备之间交换串行数据的协议或一组规则。UART 非常简单&#xff0c;仅在发射机和…