【数据结构】栈和队列


1.栈

1.1栈的认识

栈是一种线性表,栈中每个元素只有一个前驱和一个后继。栈只能在表尾插入和删除数据,遵循 LIFO(Last In First Out,后进先出)。进行数据插入和删除的一端叫做栈顶(top),另一端叫做栈底(bottom)

在这里插入图片描述

注意

  1. 如果栈用数组实现,栈底指向栈中第一个数据的位置,栈顶指向栈中最后一个数据的下一个位置,如上图。
  2. 栈中没有数据叫做空栈。

1.2栈的基本操作

  1. 栈可以用数组和链表来实现,但数组的结构实现更优一些。栈是后进先出,如果用链表实现,我们得再链表尾部插入数据,删除数据,用数组来直接访问栈顶位置,插入数据或删除数据即可。所以数据在插入和删除数据更优。
  2. 选择数组来实现栈,又会出现静态栈(空间大小是固定的)和动态栈顶(空间是可扩容的)。静态栈还是会出现老问题:空间不够或则空间浪费。所以最终选择动态栈。

数组实现

  1. 定义
typedef int STElemType;//方便更换数据类型
typedef struct Stack
{
	STElemType* a;
	int top;//栈顶元素的下一个元素的下标
	int capacity;//容量
}ST;
  1. 初始化
void STInit(ST* ps)
{
	assert(ps);
	ps->a = (STElemType*)malloc(sizeof(STElemType) * 5);//假设初始容量是5
	if (ps->a == NULL)
	{
		perror("malloc fail");
		return;
	}
	ps->capacity = 5;
	ps->top = 0;//top是栈顶元素的下一个位置的下标
}

  1. 销毁
//销毁
void STDestroy(ST* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->capacity = 0;
	ps->top = 0;
}
  1. 压栈
void STPush(ST* ps, STElemType x)
{
	assert(ps);
	//考虑栈是否满了
	if (ps->top == ps->capacity)//top指向栈顶下一个元素的下标,当栈满后,top等于容量
	{
		//扩容(容量为原来的两倍)
		STElemType* tmp = (STElemType*)realloc(sizeof(STElemType) * ps->capacity * 2);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		ps->a = tmp;
		ps->capacity *= 2;
	}
	ps->a[ps->top] = x;
	ps->top++;
}

注意
用tmp接受新空间的地址而不用ps->a接受,是因为防止空间申请失败,返回NULL,如果用ps->a接受新空间地址且申请失败,就失去旧空间的地址,会造成内存泄漏。

  1. 判断栈空
bool STEmpty(ST* ps)
{
	return ps->top == 0;//top等于0,返回真,说明栈空
}
  1. 出栈
void STPop(ST* ps)
{
	assert(ps);
	//考虑栈是否空
	if (STEmpty(ps))
	{
		printf("栈空\n");
		return;
	}
	ps->top--;//这就是用数组实现栈的好处,只需将top往前挪一位,不用删除数据
}
  1. 栈的大小
int STSize(ST* ps)
{
	assert(ps);
	return ps->top;
}
  1. 栈顶元素
STElemType STTop(ST* ps)
{
	assert(ps);
	if (STEmpty(ps))
	{
		printf("栈空\n");
		return 0;
	}
	return ps->a[ps->top-1];
}
  1. 例子
    在这里插入图片描述

2.队列

2.1队列的认识

队列也是一种线性表,队列中每个元素只有一个前驱和一个后继。不同于栈,队列只在表尾插入,在表头删除,遵循FIFO(First In First Out,先进先出)。进行数据插入的一端叫做队尾(rear),进行数据删除的一端叫做队头(front)

在这里插入图片描述

2.2队列的基本操作

队列也可以用数组和链表实现,但是用链表的结构实现更优。用数组删除数据,其实是删除数组的第一个元素,出队列后如果不挪动数据,那么只是把队头位置往后移,那么这个位置就会浪费,如果挪动数据,那么就需要将后面的数据往前移,这样的话效率太低了,但后面的循环队列就可以用数组实现且完美解决这个问题。现在我选择用链表实现队列。

链表实现

  1. 定义
typedef int QElemType;//队列数据类型
typedef struct QueueNode//队列的结点
{
	QElemType data;
	struct QueueNode* next;
}QNode;
typedef struct Queue//队列
{
	QueueNode* front;//用于头删
	QueueNode* rear;//用于尾插
	int size;//记录队列元素个数
}Queue;
  1. 初始化
void QueueInit(Queue* pq)
{
	assert(pq);
	pq->front = pq->rear = NULL;
	pq->size = 0;
}
  1. 销毁
void QueueDestroy(Queue* pq)
{
	assert(pq);
	QNode* cur = pq->front;
	while (cur)
	{
		QNode* tmp = cur->next  ;
		free(cur);
		cur = tmp;
	}
	pq->front = pq->rear = NULL;
	pq->size = 0;
}
  1. 入队
QNode* BuyNode(QElemType x)
{
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}
void QueuePush(Queue* pq,QElemType x)
{
	assert(pq);
	//申请一个新的节点
	QNode* newnode = BuyNode(x);
	//还要考虑第一次入队时,front和rear都指向NULL的情况
	if (pq->size == 0)
	{
		pq->front = pq->rear = newnode;
		pq->size++;
	}
	else
	{
		pq->rear->next = newnode;
		pq->rear = pq->rear->next;
		pq->size++;
	}
}
  1. 出队
void QueuePop(Queue* pq)
{
	assert(pq);
	//先判断队列是否为空
	assert(!QueueEmpty(pq));
	//这里还要考虑特殊情况:如果队列只有一个元素,释放掉队头后,front会指向NULL,而rear还是指向已释放的空间
	if (pq->front == pq->rear)
	{
		free(pq->front);
		pq->front = pq->rear = NULL;
	}
	else
	{
		QNode* next = pq->front->next;
		free(pq->front);
		pq->front = next;
	}
	pq->size--;
}
  1. 队列大小
//队列大小
int QueueSize(Queue* pq)
{
	assert(pq);
	return pq->size;
}
  1. 取队头元素
QElemType QueueFront(Queue* pq)
{
	assert(pq);
	//若队列为空,报错,防止越界访问
	assert(!QueueEmpty(pq));
	return pq->front->data;
}
  1. 取队尾元素
QElemType QueueBack(Queue* pq)
{
	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->rear->data;
}

2.3循环队列(LeetCode622)

循环队列是用数组实现,可以充分利用储存空间。OJ链接
在这里插入图片描述
代码

//定义
typedef struct
{
	int* a;
	int front;
	int rear;
	int k;//要存储的数据个数
}MyCircularQueue;

//初始化
MyCircularQueue* myCircularQueueCreate(int k) 
{
	MyCircularQueue* q = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));
	q->a = (int*)malloc(sizeof(int) * (k + 1));//得多申请一个,用来分辨队满和队空
	if (q->a == NULL)
	{
		perror("malloc");
		return;
	}
	q->front = q->rear = 0;
	q->k = k;
	return q;
}

//判断是否已满
bool myCircularQueueIsFull(MyCircularQueue* obj)
{
	return (obj->rear + 1) % (obj->k + 1) == obj->front;
}

//入队
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) 
{
	//先判断队列是否已满
	if (myCircularQueueIsFull(obj))
	{
		return false;
	}
	obj->a[obj->rear] = value;
	obj->rear = (obj->rear + 1) % (obj->k + 1);
	return true;
}

//判断队空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) {
	return obj->front == obj->rear;
}

//出队
bool myCircularQueueDeQueue(MyCircularQueue* obj) {
	//先判断队空
	if (myCircularQueueIsEmpty(obj))
	{
		return false;
	}
	obj->front = (obj->front + 1) % (obj->k + 1);//直接往前挪一步就可以了
	return true;
}

//取队头元素
int myCircularQueueFront(MyCircularQueue* obj) 
{
	//先判断对列是否为空
	if (myCircularQueueIsEmpty(obj))
	{
		return -1;
	}
	return obj->a[obj->front];
}

//取得队尾元素
int myCircularQueueRear(MyCircularQueue* obj) 
{
	//先判断队列是否为空
	if (myCircularQueueIsEmpty(obj))
	{
		return -1;
	}
	return obj->a[(obj->rear - 1 + obj->k + 1)%(obj->k +1)];//要考虑rear=0的情况
}

//释放
void myCircularQueueFree(MyCircularQueue* obj) 
{
	free(obj->a);
	obj->front = obj->rear = obj->k = 0;
	free(obj);
}

3.练习

学习了这么多栈和队列的基本操作,是时候让它们上场。

3.1判断回文

回文是指正读、反读均相同的字符序列,如"abba"和"abdba"均是回文,但"good"不是回文。试设计算法判定给定的字符序列是否为回文。(提示:将一半字符入栈)
代码实现

int IsPalin(ST*ps,char* s)
{
	int len = strlen(s);
	int half = len / 2;
	//压栈
	int i = 0;
	for (i = 0; i < half; i++)
	{
		STPush(ps, *s);
		s++;
	}
	//判断字符串长度是否是奇数
	if (len % 2)
	{
		s++;//是奇数就跳过中间的字符
	}
	//判断回文
	while (*s)
	{
		STDataType tmp = STTop(&ps);
		STPop(&ps);
		if (tmp != *s)
		{
			return 0;
		}
		s++;
	}
	return 1;
}

3.2有效的括号(LeetCode 20)

给定一个只包括 ‘(’,‘)’,‘{’,‘}’,‘[’,‘]’ 的字符串 s ,判断字符串是否有效。OJ链接
有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号
    在这里插入图片描述

思路
遇到左括号就入栈,遇到右括号就将左括号出栈,判断左括号是否和右括号匹配。
代码实现

bool isValid(char* s)
{
	ST st;
	STInit(&st);
	while (*s)//当字符为/0时,停止
	{
		if (*s == '(' || *s == '[' || *s == '{')
		{
			STPush(&st, *s);//栈中元素类型是char,记得修改STElemType
		}
		else
		{
			//先判断是否为空,如果栈为空,说明一上来就右括号或者左括号已经匹配完了,那就不匹配
			if (STEmpty(&st))
			{
				STDestroy(&st);//记得将栈释放,防止内存泄漏
				return false;
			}
			QElemType top = STTop(&st);
			STPop(&st);
			//判断是否匹配
			if ((*s == ')' && top != '(') || (*s == ']' && top != '[') || (*s == '}' && top != '{'))
			{
				STDestroy(&st);
				return false;
			}
		}
		s++;//记得将字符指向下一个
	}
	//还要考虑如果栈中还有多余的左括号,那么就说明不匹配
	if (!STEmpty(&st))
	{
		STDestroy(&st);
		return false;
	}
	STDestroy(&st);
	return true;
}

3.3用队列实现栈(LeetCode 225)

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop 和 empty)。

实现 MyStack 类:

  1. void push(int x) 将元素 x 压入栈顶。
  2. int pop() 移除并返回栈顶元素。
  3. int top() 返回栈顶元素。
  4. bool empty() 如果栈是空的,返回 true ;否则,返回 false 。

思路

  1. 用两个队列来实现栈,保持一个队列为空,一个队列存放数据
  2. 因为队列遵循“先进先出”,栈遵循“后进先出”。入栈时,将数据压入有数据的队列(如果两个队列都没有数据,就任选一个)。出栈时,将前面的所有数据压入另一个队列,只保留一个元素在当前队列中,此时出队就相当于出栈,满足后进先出。

在这里插入图片描述

代码实现

//定义
typedef struct {
    Queue q1;
    Queue q2;
} MyStack;

//初始化
MyStack* myStackCreate() {
	MyStack* S = (MyStack*)malloc(sizeof(MyStack));
	if (S == NULL)
	{
		perror("malloc fail");
		return;
	}
	QInit(&S->q1);
	QInit(&S->q2);
	return S;
}

//插入
void myStackPush(MyStack* obj, int x) {
	//将数据压入有数据的队列中
	//所以要找出有数据的队列
	if (!QEmpty(&obj->q1))
	{
		QPush(&obj->q1, x);
	}
	else
	{
		QPush(&obj->q2, x);
	}
}

//判断栈是否为空
bool myStackEmpty(MyStack* obj) 
{
	return QEmpty(&obj->q1) && QEmpty(&obj->q2);//两个队列都为空,栈才为空
}
//出栈
int myStackPop(MyStack* obj) 
{
	//先判断两个队列是否都为空,为空就报错
	assert(!myStackEmpty(obj));
	//找出两个队列中的空队列和非空队列
	Queue* EmptyQ = &obj->q1;//假设q1为空,q2非空
	Queue* NoEmptyQ = &obj->q2;
	//接下来确定空队列和非空队列
	if (QEmpty(&obj->q2))
	{
		EmptyQ = &obj->q2;
		NoEmptyQ = &obj->q1;
	}
	//将非空队列的前n-1个元素入队到空队列
	while (QueueSize(NoEmptyQ) > 1)//非空队列只剩下一个元素就停止
	{
		QPush(EmptyQ, QueueFront(NoEmptyQ));
		QueuePop(NoEmptyQ);
	}
	//接下来取非空队列的最后一个元素,就相当于取栈顶的元素,顺便出栈
	int top = QueueFront(NoEmptyQ);
	QueuePop(NoEmptyQ);
	return top;
}

//取栈顶元素
int myStackTop(MyStack* obj) 
{
	//先判断两个队列是否都为空
	assert(!MyStackEmpty(obj));
	//取栈顶元素就相当于取非空队列的队尾元素
	Queue* EmptyQ = &obj->q1;
	Queue* NoEmptyQ = &obj->q2;
	if (QEmpty(&obj->q2))
	{
		EmptyQ = &obj->q2;
		NoEmptyQ = &obj->q1;
	}
	return QueueBack(NoEmptyQ);
}

//释放
void myStackFree(MyStack* obj) {
	QueueDestroy(&obj->q1);
	QueueDestroy(&obj->q2);
	free(obj);
	obj = NULL;
}

3.4用栈实现队列(LeetCode232)

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push、pop、peek、empty):

实现 MyQueue 类:

  1. void push(int x) 将元素 x 推到队列的末尾
  2. int pop() 从队列的开头移除并返回元素
  3. int peek() 返回队列开头的元素
  4. boolean empty() 如果队列为空,返回 true ;否则,返回 false

思路

  1. 一个栈(PushStack)用来压入数据,一个栈(PopStack)用来弹出数据。
  2. 入队时,选择PushStack压入数据。当需要出队时,如果PopStack为空栈,就将PushStack的数据压入PopStack,然后PopStack出栈就相当于出队;如果PopStack不为空栈,那么就直接出栈。

在这里插入图片描述

代码实现

//定义
typedef struct
{
	ST pushst;
	ST popst;
}MyQueue;

//初始化
MyQueue* myQueueCreate()
{
	MyQueue* mq = (MyQueue*)malloc(sizeof(MyQueue));
	if (mq == NULL)
	{
		perror("malloc fail");
		return;
	}
	STInit(&mq->popst);
	STInit(&mq->pushst);
	return mq;
}

//压栈
void myQueuePush(MyQueue* obj, int x) 
{
	STPush(&obj->pushst, x);
}

//判断栈空
bool myQueueEmpty(MyQueue* obj)
{
	return STEmpty(&obj->popst) && STEmpty(&obj->pushst);//两个栈为空,队列才为空
}
//出栈
int myQueuePop(MyQueue* obj) 
{
	//先判断两个栈是否为空
	assert(!myQueueEmpty(obj));
	//如果popst为空,那么将pushst的数据压入popst,再在popst出栈
	//如果popst非空,那么直接在popst出栈
	if (STEmpty(&obj->popst))
	{
		while (!STEmpty(&obj->pushst))
		{
			STPush(&obj->popst, STTop(&obj->pushst));
			STPop(&obj->pushst);
		}
	}
	int front = STTop(&obj->popst);
	STPop(&obj->popst);
	return front;
}

//返回队头元素
int myQueuePeek(MyQueue* obj)
{
	//先判断队列是否为空
	assert(!myQueueEmpty(obj));
	//如果popst没有数据就将pushst的数据导入popst
	if (STEmpty(&obj->popst))
	{
		while (!STEmpty(&obj->pushst))
		{
			STPush(&obj->popst, STTop(&obj->pushst));
			STPop(&obj->pushst);
		}
	}
	return STTop(&obj->popst);
}

//释放队列
void myQueueFree(MyQueue* obj) {
	STDestroy(&obj->popst);
	STDestroy(&obj->pushst);
	free(obj);
	obj = NULL;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值