数据结构-第3节-栈和队列

目录

1.栈

1.1.栈的概念及结构

1.2.栈的实现

2.队列

2.1.队列的概念及结构

2.2.队列的实现

3.栈和队列面试题

4.概念选择题


1.栈

1.1.栈的概念及结构

栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶
出栈:栈的删除操作叫做出栈。出数据也在栈顶

注:

1.本章介绍的栈和前面内存中的栈区是不同的,本章介绍的栈是数据结构中栈,是一个数据结构,前面内存中的栈是操作系统中内存划分的一个区域,叫做栈,用来函数调用时建立栈帧

2.两种栈都符合后进先出原则。后进先出是相对同时在栈里面的数据而言的,例如入栈顺序为1 2 3 4,出栈顺序不一定为4 3 2 1,也有可能是1 2 3 4(入1出1入2出2入3出3入4出4)、2 4 3 1等等

1.2.栈的实现

栈的实现一般可以使用顺序表数组)或者链表实现,相对而言数组的结构实现更优一些。因为数组在尾上插入数据的代价比较小

数组栈:

链式栈:

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Stack.h"

void TestStack()
{
	ST st;
	StackInit(&st);
	StackPush(&st, 1);
	StackPush(&st, 2);

	printf("%d ", StackTop(&st));
	StackPop(&st);

	StackPush(&st, 3);
	StackPush(&st, 4);


	while (!StackEmpty(&st))
	{
		printf("%d ", StackTop(&st));
		StackPop(&st);
	}
	printf("\n");

	StackDestory(&st);
}

int main()
{
	TestStack();

	return 0;
}

Stack.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

//struct Stack
//{
//	int a[N];
//	int top; // 栈顶的位置
//};

typedef int STDataType;

typedef struct Stack
{
	STDataType* a;
	int top;		// 栈顶的位置
	int capacity;	// 容量
}ST;

//初始化
void StackInit(ST* ps);

//销毁
void StackDestory(ST* ps);

//入栈
void StackPush(ST* ps, STDataType x);

//出栈
void StackPop(ST* ps);

//判断栈是否为空
bool StackEmpty(ST* ps);

//栈的元素个数
int StackSize(ST* ps);

//输出栈顶元素
STDataType StackTop(ST* ps);

Stack.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Stack.h"

void StackInit(ST* ps)
{
	assert(ps);
	ps->a = NULL;
	ps->top = 0;
	ps->capacity = 0;
}

void StackDestory(ST* ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->capacity = ps->top = 0;
}

void StackPush(ST* ps, STDataType x)
{
	assert(ps);
	// 
	if (ps->top == ps->capacity)
	{
		int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		ps->a = (STDataType*)realloc(ps->a, newCapacity * sizeof(STDataType));
		if (ps->a == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}

		ps->capacity = newCapacity;
	}

	ps->a[ps->top] = x;
	ps->top++;
}

void StackPop(ST* ps)
{
	assert(ps);
	assert(ps->top > 0);
	--ps->top;
}

bool StackEmpty(ST* ps)
{
	assert(ps);

	//方法1
	/*if (ps->top > 0)
	{
		return false;
	}
	else
	{
		return true;
	}*/

	//方法2
	return ps->top == 0;
}

STDataType StackTop(ST* ps)
{
	assert(ps);
	assert(ps->top > 0);

	return ps->a[ps->top - 1];
}

int StackSize(ST* ps)
{
	assert(ps);
	return ps->top;
}

注:

1.若成员变量top初始化为0,那么top指向栈顶元素的后一个位置;若成员变量top初始化为-1,那么top指向栈顶元素。上面的实现我们采用第一种方式,top指向栈顶元素的后一个位置


2.队列

2.1.队列的概念及结构

队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out)
入队列:进行插入操作的一端称为队尾
出队列:进行删除操作的一端称为队头
注:如果有一个入队顺序,则出队顺序唯一

栈的用处:解决括号匹配、逆波兰表达式求解、递归改非递归......

队列的用处:公平排队、广度优先遍历......

2.2.队列的实现

队列也可以用数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。使用单链表头插比较容易,如果再记录并更新一个尾指针则十分方便。

Test.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Queue.h"

void TestQueue()
{
	Queue q;
	QueueInit(&q);

	QueuePush(&q, 1);
	QueuePush(&q, 2);
	printf("%d ", QueueFront(&q));
	QueuePop(&q);

	QueuePush(&q, 3);
	QueuePush(&q, 4);

	while (!QueueEmpty(&q))
	{
		printf("%d ", QueueFront(&q));
		QueuePop(&q);
	}
	printf("\n");
}

int main()
{
	TestQueue();

	return 0;
}

Queue.h文件:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>

typedef int QDataType;

typedef struct QueueNode
{
	QDataType data;
	struct QueueNode* next;
}QNode;

typedef struct Queue
{
	QNode* head;
	QNode* tail;

}Queue;

//初始化
void QueueInit(Queue* pq);

//销毁
void QueueDestory(Queue* pq);

//队尾节点插入
void QueuePush(Queue* pq, QDataType x);

//队头节点删除
void QueuePop(Queue* pq);

//判断队列是否为空
bool QueueEmpty(Queue* pq);

//返回队列节点个数
size_t QueueSize(Queue* pq);

//返回队头元素
QDataType QueueFront(Queue* pq);

//返回队尾元素
QDataType QueueBack(Queue* pq);

Queue.c文件:

#define _CRT_SECURE_NO_WARNINGS 1
#include "Queue.h"

void QueueInit(Queue* pq)
{
	assert(pq);
	pq->head = pq->tail = NULL;
}

void QueueDestory(Queue* pq)
{
	assert(pq);
	QNode* cur = pq->head;
	while (cur)
	{
		QNode* next = cur->next;
		free(cur);
		cur = next;
	}

	pq->head = pq->tail = NULL;
}

void QueuePush(Queue* pq, QDataType x)
{
	assert(pq);
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	assert(newnode);

	newnode->data = x;
	newnode->next = NULL;

	if (pq->tail == NULL)
	{
		assert(pq->head == NULL);
		pq->head = pq->tail = newnode;
	}
	else
	{
		pq->tail->next = newnode;
		pq->tail = newnode;
	}
}

void QueuePop(Queue* pq)
{
	assert(pq);
	assert(pq->head && pq->tail);

	if (pq->head->next == NULL)
	{
		free(pq->head);
		pq->head = pq->tail = NULL;
	}
	else
	{
		QNode* next = pq->head->next;
		free(pq->head);
		pq->head = next;
	}
}

bool QueueEmpty(Queue* pq)
{
	assert(pq);

	//return pq->head == NULL && pq->tail == NULL;
	return pq->head == NULL;
}

size_t QueueSize(Queue* pq)
{
	assert(pq);
	QNode* cur = pq->head;
	size_t size = 0;
	while (cur)
	{
		size++;
		cur = cur->next;
	}

	return size;
}

QDataType QueueFront(Queue* pq)
{
	assert(pq);
	assert(pq->head);

	return pq->head->data;
}

QDataType QueueBack(Queue* pq)
{
	assert(pq);
	assert(pq->tail);

	return pq->tail->data;
}

注:

1.当QueuePop函数头删删除到只剩最后一个节点时,下面代码就有问题了,因为只剩最后一个节点时,pq-head->next为空给到next,然后next赋值给pq->head,此时head为空tail不为空,而tail指向的位置已经被释放掉了,tail为野指针,程序有问题。因此需要判断,如果只剩一个节点的话,那么释放空间并且head和tail同时置为空,如果有两个或以上节点的话那么使用下面代码


3.栈和队列面试题

练习1:

题目描述:

题目链接:20. 有效的括号 - 力扣(LeetCode) (leetcode-cn.com)

思路:

1.遇到左括号就进栈

2.遇到右括号就出栈,观察出栈的元素和这个右括号是否匹配,不匹配就报错

代码:

注:

1.本题用c语言实现的话,需要先造轮子实现一个栈,然后用栈解决该问题,上面代码没有贴栈的实现,栈的实现前面有

2.如果只输入一个左括号,应该输出为false,因此最后要判断栈是否为空,如果栈为空,说明所有左括号都匹配了,才输出true;如果栈不为空,说明还有左括号没有匹配,应该输出false,所以代码如下

3.如果只输入一个右括号,那么没有左括号,栈为空,当把栈顶元素取出来与右括号比较时,因为栈为空所以无法取出,取出栈顶元素函数那里的断言会报错。所以在取出栈顶元素前应该判断,如果栈为空,直接输出false,代码如下

练习2:

题目描述:

 题目链接:225. 用队列实现栈 - 力扣(LeetCode) (leetcode-cn.com)

思路:

1.入栈:push数据到不为空的队列

2.出栈:把不为空的队列的数据前N-1导入另一个空队列,最后剩下的一个删掉

结构:

代码:

注:

1.创建栈的时候,不能用如下方式,因为这里面创建的st在栈上面,是一个局部变量,出了函数该局部变量就会被销毁,返回的&st就是一个野指针,入下图左所示。因此使用malloc动态开辟内存在堆上可以解决该问题,入下图右所示

 

2.如果这种多接口的题有问题需要调试,可以根据报错接口,找该接口或者调用该接口的地方是否出错,下图一;如果看不出来可以将代码复制到vs编译器中,然后根据无法通过的案例在vs主函数中使用此案例并进行调试找出存在的问题,下图二三;还有一种高效的方法(排除法)可以找出到底是哪个接口有问题,如下图四五所示,将错误案例复制到力扣的测试用例中,将一个接口和对应的参数删除,删除后运行如果仍然有错就说明不是该接口的问题,继续删除其他接口进行测试,如果删除了某一个接口和对应的参数后程序运行成功,说明是该接口的问题

 

 

3.如果报了某个接口出错(下面是断言出错了),如果接口没错,那一定是调用该接口时有问题,在代码中找调用该接口的地方,想要验证是否是此处调用该接口出错了,可以在调用接口前后分别打印某些值,如果都打印出来了,说明不是此处调用出错;如果只打印了前面后面没打印,说明此处调用出错(因为断言出错程序直接退出,不会打印后面的值),如下图所示,第二次打印中后面的33333333333没有打印出来,说明是第二次QueueFront调用出错了

练习3:

题目描述:

题目链接:232. 用栈实现队列 - 力扣(LeetCode) (leetcode-cn.com)

思路:

一个进队列栈一个出队列栈,每次进队列给栈pushST进栈,每次出队列给栈popST出栈,当要出栈而popST为空时,将pushST倒置放入popST中

代码:

注:

1.如下图左所示,用ST* pushST创建的是结构体指针,ST pushST创建的是结构体,后面StackInit函数初始化的是结构体的内容,无法对一个指针初始化,此时用ST* pushST创建的指针没有内容,是一个野指针,因此创建结构体应该用ST pushST。如果一定要用ST* pushST,应该给该指针malloc一个栈出来,如下右图所示

  

练习4:

题目描述:

 

题目链接:622. 设计循环队列 - 力扣(LeetCode) (leetcode-cn.com)

思路:

1.push一个数时,将该数赋值给tail指向的节点并让tail指向下一个节点(要判断循环队列是否已满,只有未满的时候才能插入数据

2.每次pop一个数时,不改变节点的数据,只让front指向下一个节点。当tail已经指向最后一个节点再让tail指向下一个节点时,tail应该指向第一个节点(不是front指向的节点)(要判断如果front和tail相等了就为空不能再删了

代码:

注:

1.实际中我们有时还会使用一种队列叫循环队列。如操作系统课程讲解生产者消费者模型时可以就会使用循环队列。环形队列可以使用数组实现,也可以使用循环链表实现。

2.循环队列的节点数是固定的,循环队列中开始两个指针front和tail指向同一个节点。每次push一个数时,将该数赋值给tail指向的节点并让tail指向下一个节点;每次pop一个数时,不改变节点的数据,只让front指向下一个节点。当tail已经指向最后一个节点再让tail指向下一个节点时,tail应该指向第一个节点(不是front指向的节点)。在循环队列中front与tail相等时队列为空,当tail指向节点的下一个节点是front指向的节点时队列为满。每次开循环队列如果需要a个空间,实际需要开辟a+1个空间(因为如果需要a个空间而实际只开辟a个空间的话,存入最后一个数据之后tail会指向下一个节点,而该节点必定是front指向的节点,会认为是空队列,所以应该多申请一个空间)

3.循环队列在插入数据时,要判断循环队列是否已满,只有未满的时候才能插入数据;删除数据的时候,如果front和tail相等了就为空不能再删了

4.如果用单项循环链表实现循环队列,容易判空和判满并且不用判尾部因为可以使用循环链表,但是链表想要找尾不好找,因为tail永远指向的是最后一个节点的下一个节点,单向链表取前一个结点的数据是不好取的(除非使用双向链表才能很好的解决这个问题),因此我们用数组进行实现


4.概念选择题

1.一个栈的初始状态为空。现将元素1、2、3、4、5、A、B、C、D、E依次入栈,然后再依次出栈,则元素出
栈的顺序是( )。
A 12345ABCDE
B EDCBA54321
C ABCDE12345
D 54321EDCBA

答案:B

2.若进栈序列为 1,2,3,4 ,进栈过程中可以出栈,则下列不可能的一个出栈序列是()
A 1,4,3,2
B 2,3,4,1
C 3,1,4,2
D 3,4,2,1

答案:C

3.循环队列的存储空间为 Q(1:100) ,初始状态为 front=rear=100 。经过一系列正常的入队与退队操作后, front=rear=99 ,则循环队列中的元素个数为( )
A 1
B 2
C 99
D 0

答案:D

4.以下( )不是队列的基本运算?
A 从队尾插入一个新元素
B 从队列中删除第i个元素
C 判断一个队列是否为空
D 读取队头元素的值

答案:B

5.现有一循环队列,其队头指针为front,队尾指针为rear;循环队列长度为N。其队内有效长度为?(假设队头不存放数据)
A (rear - front + N) % N + 1
B (rear - front + N) % N
C ear - front) % (N + 1)
D (rear - front + N) % (N - 1)

答案:B

注: 

:rear-front

:rear+N-front

 合起来就是(rear+N-front)%N

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

随风张幔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值