初阶数据结构—栈和队列

第一章:栈

1.1栈的概念及结构

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

压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶

出栈:栈的删除操作叫做出栈。出数据也在栈顶

1.2 栈的实现

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

栈头文件各函数声明
cpp 复制代码
#pragma once
#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef int STDataType;
typedef struct Stack//数组栈
{
	STDataType* a;
	int top;//栈顶
	int capacity;//容量
}ST;

void STInit(ST* pst);//初始化栈
void STDestroy(ST* pst);//销毁栈
void STPush(ST* pst, STDataType x);//入栈
void STPop(ST* pst);//出栈
STDataType STTop(ST* pst);//获取栈顶元素
bool STEmpty(ST* pst);//栈是否为空
int STSize(ST* pst);//栈中有效元素个数
栈各函数实现
初始化栈
cpp 复制代码
void STInit(ST* pst)//初始化栈
{
	assert(pst);
	pst->a = NULL;
	pst->top = 0; //该初始化方式,栈顶指向【栈顶元素的后面】。先插入,再++
	//pst->top = -1;//该初始化方式,栈顶指向【栈顶元素】。先++,再插入
	pst->capacity = 0;
}
栈是否为空
cpp 复制代码
bool STEmpty(ST* pst)//栈是否为空
{
	assert(pst);
	return pst->top == 0;//为空返回true;不为空返回false
}
入栈
cpp 复制代码
void STPush(ST* pst, STDataType x)//入栈
{
	assert(pst);
	if (pst->top == pst->capacity)//检查容量(如果栈顶等于容量)
	{
		int newCapacity = pst->capacity == 0 ? 4 : 2 * pst->capacity;//栈容量为0就开辟4个单位,否则开辟原容量2倍
		STDataType* tmp = (STDataType*)realloc(pst->a, sizeof(STDataType) * newCapacity);
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		pst->a = tmp;
		pst->capacity = newCapacity;
	}
	pst->a[pst->top] = x;
	pst->top++;
}
出栈
cpp 复制代码
void STPop(ST* pst)//出栈
{
	assert(pst);
	assert(!STEmpty(pst));
	pst->top--;
}
获取栈顶元素
cpp 复制代码
STDataType STTop(ST* pst)//获取栈顶元素
{
	assert(pst);
	assert(!STEmpty(pst));
	return pst->a[pst->top - 1];
}
栈中有效元素个数
cpp 复制代码
int STSize(ST* pst)//栈中有效元素个数
{
	assert(pst);
	return pst->top;
}
销毁栈
cpp 复制代码
void STDestroy(ST* pst)//销毁栈
{
	assert(pst);
	free(pst->a);
	pst->a = NULL;
	pst->top = pst->capacity = 0;
}
栈的测试
cpp 复制代码
void TestStack1()
{
	ST st;
	STInit(&st);
	STPush(&st, 1);
	STPush(&st, 2);
	printf("%d ", STTop(&st));
	STPop(&st);
	STPush(&st, 3);
	STPush(&st, 4);
	while (!STEmpty(&st))
	{
		printf("%d ", STTop(&st));
		STPop(&st);
	}

	STDestroy(&st);
}

int main()
{
    TestStack1();//2 4 3 1
    return 0;
}

第二章:队列

2.1 队列的概念及结构

队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 。

入队列:进行插入操作的一端称为队尾

出队列:进行删除操作的一端称为队头

2.2 队列的实现

队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。

队里头文件各函数声明
cpp 复制代码
#pragma once

#include <stdlib.h>
#include <assert.h>
#include <stdbool.h>

typedef int QDataType;
typedef struct QueueNode //队列节点
{
	struct QueueNode* next;
	QDataType data;
}QNode;

typedef struct Queue //储存队列节点头尾指针及节点个数的结构体
{
	QNode* phead;
	QNode* ptail;
	int size;
}Queue;

void QueueInit(Queue* pq);//队列初始化
void QueueDestroy(Queue* pq);//队列释放
void QueuePush(Queue* pq, QDataType x);//队列插入
void QueuePop(Queue* pq);//队列删除
QDataType QueueFront(Queue* pq);//取队头数据
QDataType QueueBack(Queue* pq);//取队尾数据
int QueueSize(Queue* pq);//队列元素个数
bool QueueEmpty(Queue* pq);//队列是否为空
队列各函数实现
初始化队列
cpp 复制代码
void QueueInit(Queue* pq)//初始化队列
{
	assert(pq);
	pq->phead = NULL;
	pq->ptail = NULL;
	pq->size = 0;
}
队列是否为空

队列的插入、删除、获取队头队尾数据需要判断是否为空

cpp 复制代码
//队列的插入、删除、获取队头队尾数据需要判断是否为空
bool QueueEmpty(Queue* pq)//队列是否为空
{
	assert(pq);
	return pq->phead == NULL && pq->ptail == NULL;
}
队尾入队列
cpp 复制代码
void QueuePush(Queue* pq, QDataType x)//队列插入
{
	assert(pq);
	//创建队列节点
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("malloc newnode fail");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;

	//插入
	if (QueueEmpty(pq) == true)
		pq->phead = pq->ptail = newnode;
	else //非空队列
	{
		pq->ptail->next = newnode;
		pq->ptail = newnode;
	}
	pq->size++;
}
队头出队列
cpp 复制代码
void QueuePop(Queue* pq)//队列删除
{
	assert(pq);
	assert(!QueueEmpty(pq));

	if (pq->phead->next == NULL) //1.一个节点
	{
		free(pq->phead);
		pq->phead = pq->ptail = NULL;
	}
	else //2.多个节点
	{    //头删
		QNode* next = pq->phead->next;
		free(pq->phead);
		pq->phead = next;
	}
	pq->size--;
}
获取队列头部元素
cpp 复制代码
QDataType QueueFront(Queue* pq)//取队头数据
{
	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->phead->data;
}
获取队列队尾元素
cpp 复制代码
QDataType QueueBack(Queue* pq)//取队尾数据
{
	assert(pq);
	assert(!QueueEmpty(pq));
	return pq->ptail->data;
}
获取队列中有效元素个数
cpp 复制代码
int QueueSize(Queue* pq)//队列元素个数
{
	assert(pq);
	return pq->size;
}
释放队列
cpp 复制代码
void QueueDestroy(Queue* pq)//释放队列
{
	assert(pq);
	QNode* cur = pq->phead;
	while (cur)
	{
		QNode* del = cur;
		cur = cur->next;
		free(del);
	}
	pq->phead = pq->ptail = NULL;
	pq->size = 0;
}
队列测试
cpp 复制代码
#include "Queue.h"
#include <stdio.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);
	printf("Size:%d\n", QueueSize(&q));

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

	QueueDestroy(&q);
}

int main()
{
	TestQueue();
	return 0;
}

第三章:栈和队列面试题

1. 括号匹配问题

给定一个只包括 '(' ,')' ,'{' ,'}' ,'[' ,']' 的字符串 s ,判断字符串是否有效。

有效字符串需满足:

  1. 左括号必须用相同类型的右括号闭合。
  2. 左括号必须以正确的顺序闭合。
  3. 每个右括号都有一个对应的相同类型的左括号。

注意:此题需要用到上面栈实现的各函数

cpp 复制代码
//1.括号匹配问题 https://leetcode.cn/problems/valid-parentheses/description/
// 这道题需要用到栈,所以直接复用栈的实现,避免重复造轮子
// 遍历数组,数组指针遇到左括号入栈,然后数组指针++
// 获取栈顶元素,并且出栈(即弹出元素),跟数组指针当前指向元素比较,看是否为匹配的括号。
// 如果该数组都是匹配括号,那么每次都能入栈,出栈。
// 如果不是,那么就会出现没有入栈的情况,即栈为空,那么说明不匹配
bool isValid(char* s) {
    ST st;// 创建栈       
    STInit(&st); // 初始化栈

    // 1.左括号入栈
    // 2.出栈元素与右括号匹配
    while (*s) { // 遍历数组                                
        if (*s == '(' || *s == '[' || *s == '{') // 左括号入栈
            STPush(&st, *s);
        else { // 出栈元素与右括号匹配                    
            // 在出栈之前要判断栈是否为空,如果为空,说明没有左括号,直接返回假
            if (STEmpty(&st)) {
                STDestroy(&st);
                return false;
            }

            char top = STTop(&st); // 获取栈顶元素
            STPop(&st); // 将栈顶元素出栈
            //if ((*s == ')' && top == '(') || (*s == ']' && top == '[') || (*s == '}' && top == '{'))
            //这里不能用上方条件判断正确后直接返回true,因为最后一个元素为左括号时也满足,但不正确 
            //下方如果栈顶元素与数组指针指向元素不匹配返回假
            if ((*s == ')' && top != '(') || (*s == ']' && top != '[') || (*s == '}' && top != '{')) {
                STDestroy(&st);
                return false;
            }
        }
        s++;
    }
    //这里判断栈里是否还有元素,
    // 如果有,说明有一个单独的左括号,不匹配
    // 如果没有,说明都匹配完成
    bool ret = STEmpty(&st);
    STDestroy(&st);
    return ret;
}

2. 用队列实现栈

请你仅使用两个队列实现一个后入先出(LIFO)的栈,并支持普通栈的全部四种操作(pushtoppopempty)。(此题需要用到上方队列实现的各函数)

cpp 复制代码
// q1队列用来插入数据,当需要弹出时,将q1的部分数据插入到q2队列,此时需要弹出的数据就在q1最前端。反之亦然
typedef struct {
    // Queue为储存队列节点头尾指针及节点个数的结构体
    Queue q1, q2;//创建2个用于维护两个队列

    // Queue *q1, *q2;
    //这样创建不好,因为未初始化,所以是野指针,当下方调用QueueInit函数初始化时,就会造成对野指针的解引用
    //除非使用malloc开辟Queue结构体的空间。而且后续使用完还需要释放。
    // 不如创建2个Queue结构体封装到MyStack结构体中,直接malloc开辟MyStack需要的空间
} MyStack;

// 两个队列在操作完数据后,一定是某一个为空,或者两个都为空
MyStack* myStackCreate() {
    MyStack* obj = (MyStack*)malloc(sizeof(MyStack));
    if (obj == NULL) {
        perror("malloc MyStack* obj fail");
        return NULL;
    }
    QueueInit(&obj->q1);
    QueueInit(&obj->q2);

    return obj;
}

void myStackPush(MyStack* obj, int x) {
    if (!QueueEmpty(&obj->q1)) //当q1不为空时,往q1入数据
        QueuePush(&obj->q1, x);
    else
        QueuePush(&obj->q2, x); //只要q1有数据,就可以向q2插入数据
}

int myStackPop(MyStack* obj) { //栈的pop是后进先出
    // 弹出数据时就是在两个队列相互导数据,所以要判断哪个队列为空
    // 1.创建名为空和非空的变量。假设q1为空,q2不为空,如果假设错误,互换。
    // 2.将非空队列的数据导入空队列。非空队列数据剩余1时,该数据就是要弹出的数据

    //第一步
    Queue* pEmptyQ = &obj->q1;
    Queue* pNonEmptyQ = &obj->q2;
    if (!QueueEmpty(&obj->q1)) {
        pEmptyQ = &obj->q2;
        pNonEmptyQ = &obj->q1;
    }

    // 第二步
    while (QueueSize(pNonEmptyQ) > 1) { //非空队列数据还剩1个时就是要弹出的数据
        QueuePush(pEmptyQ, QueueFront(pNonEmptyQ));//非空队列取队头数据插入到空队列
        QueuePop(pNonEmptyQ);//弹出非空队列数据
    }
    int top = QueueFront(pNonEmptyQ);//此时非空队列还剩1个数据,取非空队列队头数据
    QueuePop(pNonEmptyQ);//再弹出非空队列的数据
    return top;
}

int myStackTop(MyStack* obj) {
    if (!QueueEmpty(&obj->q1)) //如果q1队列不为空,那么返回q1队列的队尾数据
        return QueueBack(&obj->q1);
    else
        return QueueBack(&obj->q2);//如果q2队列不为空,那么返回q2队列的队尾数据
}

bool myStackEmpty(MyStack* obj) {
    return QueueEmpty(&obj->q1) && QueueEmpty(&obj->q2);
}

void myStackFree(MyStack* obj) {
    QueueDestroy(&obj->q1);//q1或q2队列是否为空都需要释放。为空只是没有数据,但队列已经申请了空间
    QueueDestroy(&obj->q2);
    free(obj);
    //不能直接释放obj,因为obj里面是两个维护【队列头尾指针和节点个数】的结构体。
    // 释放obj只是释放了这两个结构体的空间,但是队列的节点还未释放
}

3. 用栈实现队列

请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpoppeekempty)。(此题需要用到上方实现的各函数)

cpp 复制代码
typedef struct {
	ST pushst;
	ST popst;
} MyQueue;


MyQueue* myQueueCreate() {
	MyQueue* obj = (MyQueue*)malloc(sizeof(MyQueue));
	if (obj == NULL) {
		perror("malloc MyQueue* obj fail");
		return NULL;
	}
	STInit(&obj->pushst);
	STInit(&obj->popst);
	return obj;
}

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

int myQueuePeek(MyQueue* obj) {
	if (STEmpty(&obj->popst)) {
		while (!STEmpty(&obj->pushst)) {
			STPush(&obj->popst, STTop(&obj->pushst));
			STPop(&obj->pushst);
		}
	}
	return STTop(&obj->popst);
}

int myQueuePop(MyQueue* obj) {
	int front = myQueuePeek(obj);
	STPop(&obj->popst);
	return front;
}

bool myQueueEmpty(MyQueue* obj) {
	return STEmpty(&obj->pushst) && STEmpty(&obj->popst);
}

void myQueueFree(MyQueue* obj) {
	STDestroy(&obj->pushst);
	STDestroy(&obj->popst);
	free(obj);
}

4. 设计循环队列

思路:

用数组队列,front指向头元素,rear指向尾元素的后面。

假设有k个数据,那么开辟k+1个空间,因为rear需要指向尾元素后面

注意:rear不能指向尾元素,否则只有一个元素时,或满元素时,front和rear都指向同一位置,无法判断满还是空

cpp 复制代码
typedef struct {
    int front; // 队列头下标
    int rear;  // 队列尾下标
    int k;     // 队列空间(比数据多一个)
    int* a;    // 数组
} MyCircularQueue;

bool myCircularQueueIsEmpty(MyCircularQueue* obj) { //队列是否为空
    return obj->front == obj->rear;//头尾下标相等即为空
}

bool myCircularQueueIsFull(MyCircularQueue* obj) { //队列是否满
    //rear 指针指向队列最后一个元素后面的空间,加 1 就将其移动到队列开头
    //k是元素个数,k+1才是队列空间个数。
    //所以rear+1 模 队列空间个数 等于头元素的位置就是满
    return (obj->rear + 1) % (obj->k + 1) == obj->front;
}

MyCircularQueue* myCircularQueueCreate(int k) { //创建队列
    MyCircularQueue* obj = (MyCircularQueue*)malloc(sizeof(MyCircularQueue));//开辟维护队列信息结构体的空间
    if (obj == NULL) {
        perror("malloc MyCircularQueue* obj fail");
        return NULL;
    }
    obj->a = (int*)malloc(sizeof(int) * (k + 1));//开辟队列
    obj->front = obj->rear = 0;//头尾下标指向同一位置(也就是空队列)
    obj->k = k;//队列元素个数
    return obj;
}

bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) { //插入数据
    if (myCircularQueueIsFull(obj))//队列满返回false
        return false;

    obj->a[obj->rear++] = value;//在rear位置处插入数据,rear++
    obj->rear %= (obj->k + 1);//因为是循环队列,所以rear位置需要取模。(k是元素个数,k+1是队列空间个数)
    return true;
}

bool myCircularQueueDeQueue(MyCircularQueue* obj) { //删除数据
    if (myCircularQueueIsEmpty(obj))//队列为空返回false
        return false;

    obj->front++;//移动头下标即可
    obj->front %= (obj->k + 1);//因为循环队列,头下标同上方为下标同理,需要取模处理
    return true;
}

int myCircularQueueFront(MyCircularQueue* obj) { //获取队首元素
    if (myCircularQueueIsEmpty(obj))//队列为空返回-1
        return -1;
    return obj->a[obj->front];//队列不为空,直接返回队首元素
}

int myCircularQueueRear(MyCircularQueue* obj) { //获取队尾元素
    if (myCircularQueueIsEmpty(obj))//队列为空返回-1
        return -1;

    // if (obj->rear == 0)
    //     return obj->a[obj->k];
    // else
    //     return obj->a[obj->rear - 1];

    //因为是循环队列,尾下标分2种情况
    //1.尾下标在队列头。 2.尾下标不在队列头
    return obj->a[(obj->rear - 1 + obj->k + 1) % (obj->k + 1)];
    //obj->rear-1是队尾元素,obj->k+1是队列空间个数。队尾元素+队列空间个数相当于绕了一圈,但还在原来的位置。
    //之所以要加上obj->k+1,是因为如果rear恰好指向队列头,那么-1操作就会rear就会变成负数。
    //所以该操作是为了确保在模运算中得到正确的结果
}

void myCircularQueueFree(MyCircularQueue* obj) { //释放队列
    free(obj->a);//释放队列数组
    free(obj);//释放维护队列信息的结构体
}

第四章:概念选择题

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或者100

答案:D

4.以下( )不是队列的基本运算?

A. 从队尾插入一个新元素

B. 从队列中删除第i个元素

C. 判断一个队列是否为空

D. 读取队头元素的值

答案:B

5.现有一循环队列,其队头指针为front,队尾指针为rear;循环队列长度为N。其队内有效长度为?(假设队头不存放数据)

A. (rear - front + N) % N + 1

B. (rear - front + N) % N

C. (rear - front) % (N + 1)

D. (rear - front + N) % (N - 1)

答案:B

相关推荐
懒惰才能让科技进步11 分钟前
从零学习大模型(十二)-----基于梯度的重要性剪枝(Gradient-based Pruning)
人工智能·深度学习·学习·算法·chatgpt·transformer·剪枝
DARLING Zero two♡15 分钟前
关于我、重生到500年前凭借C语言改变世界科技vlog.16——万字详解指针概念及技巧
c语言·开发语言·科技
Ni-Guvara24 分钟前
函数对象笔记
c++·算法
love_and_hope26 分钟前
Pytorch学习--神经网络--搭建小实战(手撕CIFAR 10 model structure)和 Sequential 的使用
人工智能·pytorch·python·深度学习·学习
Chef_Chen29 分钟前
从0开始学习机器学习--Day14--如何优化神经网络的代价函数
神经网络·学习·机器学习
芊寻(嵌入式)39 分钟前
C转C++学习笔记--基础知识摘录总结
开发语言·c++·笔记·学习
泉崎1 小时前
11.7比赛总结
数据结构·算法
你好helloworld1 小时前
滑动窗口最大值
数据结构·算法·leetcode
QAQ小菜鸟1 小时前
一、初识C语言(1)
c语言
hong1616881 小时前
跨模态对齐与跨领域学习
学习