目录
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:
题目描述:
思路:
1.遇到左括号就进栈
2.遇到右括号就出栈,观察出栈的元素和这个右括号是否匹配,不匹配就报错
代码:
注:
1.本题用c语言实现的话,需要先造轮子实现一个栈,然后用栈解决该问题,上面代码没有贴栈的实现,栈的实现前面有
2.如果只输入一个左括号,应该输出为false,因此最后要判断栈是否为空,如果栈为空,说明所有左括号都匹配了,才输出true;如果栈不为空,说明还有左括号没有匹配,应该输出false,所以代码如下
3.如果只输入一个右括号,那么没有左括号,栈为空,当把栈顶元素取出来与右括号比较时,因为栈为空所以无法取出,取出栈顶元素函数那里的断言会报错。所以在取出栈顶元素前应该判断,如果栈为空,直接输出false,代码如下
练习2:
题目描述:
思路:
1.入栈:push数据到不为空的队列
2.出栈:把不为空的队列的数据前N-1导入另一个空队列,最后剩下的一个删掉
结构:
代码:
注:
1.创建栈的时候,不能用如下方式,因为这里面创建的st在栈上面,是一个局部变量,出了函数该局部变量就会被销毁,返回的&st就是一个野指针,入下图左所示。因此使用malloc动态开辟内存在堆上可以解决该问题,入下图右所示
2.如果这种多接口的题有问题需要调试,可以根据报错接口,找该接口或者调用该接口的地方是否出错,下图一;如果看不出来可以将代码复制到vs编译器中,然后根据无法通过的案例在vs主函数中使用此案例并进行调试找出存在的问题,下图二三;还有一种高效的方法(排除法)可以找出到底是哪个接口有问题,如下图四五所示,将错误案例复制到力扣的测试用例中,将一个接口和对应的参数删除,删除后运行如果仍然有错就说明不是该接口的问题,继续删除其他接口进行测试,如果删除了某一个接口和对应的参数后程序运行成功,说明是该接口的问题
3.如果报了某个接口出错(下面是断言出错了),如果接口没错,那一定是调用该接口时有问题,在代码中找调用该接口的地方,想要验证是否是此处调用该接口出错了,可以在调用接口前后分别打印某些值,如果都打印出来了,说明不是此处调用出错;如果只打印了前面后面没打印,说明此处调用出错(因为断言出错程序直接退出,不会打印后面的值),如下图所示,第二次打印中后面的33333333333没有打印出来,说明是第二次QueueFront调用出错了
练习3:
题目描述:
思路:
一个进队列栈一个出队列栈,每次进队列给栈pushST进栈,每次出队列给栈popST出栈,当要出栈而popST为空时,将pushST倒置放入popST中
代码:
注:
1.如下图左所示,用ST* pushST创建的是结构体指针,ST pushST创建的是结构体,后面StackInit函数初始化的是结构体的内容,无法对一个指针初始化,此时用ST* pushST创建的指针没有内容,是一个野指针,因此创建结构体应该用ST pushST。如果一定要用ST* pushST,应该给该指针malloc一个栈出来,如下右图所示
练习4:
题目描述:
思路:
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 12345ABCDEB EDCBA54321C ABCDE12345D 54321EDCBA
答案:B
2.若进栈序列为 1,2,3,4 ,进栈过程中可以出栈,则下列不可能的一个出栈序列是()A 1,4,3,2B 2,3,4,1C 3,1,4,2D 3,4,2,1
答案:C
3.循环队列的存储空间为 Q(1:100) ,初始状态为 front=rear=100 。经过一系列正常的入队与退队操作后, front=rear=99 ,则循环队列中的元素个数为( )A 1B 2C 99D 0
答案:D
4.以下( )不是队列的基本运算?A 从队尾插入一个新元素B 从队列中删除第i个元素C 判断一个队列是否为空D 读取队头元素的值
答案:B
5.现有一循环队列,其队头指针为front,队尾指针为rear;循环队列长度为N。其队内有效长度为?(假设队头不存放数据)A (rear - front + N) % N + 1B (rear - front + N) % NC ear - front) % (N + 1)D (rear - front + N) % (N - 1)
答案:B
注:
:rear-front
:rear+N-front
合起来就是(rear+N-front)%N