第一章:栈
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 ,判断字符串是否有效。
有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 每个右括号都有一个对应的相同类型的左括号。
注意:此题需要用到上面栈实现的各函数
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)的栈,并支持普通栈的全部四种操作(push
、top
、pop
和 empty
)。(此题需要用到上方队列实现的各函数)
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. 用栈实现队列
请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(push
、pop
、peek
、empty
)。(此题需要用到上方栈实现的各函数)
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