数据结构-栈和队列(速通版本)

前言

栈(Stack)和队列(Queue)是两种基本的线性数据结构,它们在计算机科学中应用非常广泛,用于存储和管理数据。
**栈** 是一种后进先出(Last In First Out, LIFO)的数据结构。你可以将栈想象成一摞盘子,你只能在顶部添加或移除盘子。这意味着最后放入栈中的元素将是第一个被移除的。栈的基本操作包括:
- `push`:将一个元素放入栈顶。
- `pop`:移除栈顶元素。
- `peek`或`top`:查看栈顶元素而不移除它。
- `isEmpty`:检查栈是否为空。
栈常用于算法中的临时存储,比如逆序一个数据集、解决递归问题、以及实现后退功能等。
**队列** 是一种先进先出(First In First Out, FIFO)的数据结构。队列就像排队买票,先到的人会先得到服务,然后从队尾离开。队列的基本操作包括:
- `enqueue`或`offer`:在队列的末尾添加一个元素。
- `dequeue`或`poll`:移除队列的第一个元素。
- `front`或`peek`:查看队列的第一个元素而不移除它。
- `isEmpty`:检查队列是否为空。
队列在需要按照顺序处理元素的场景中非常有用,比如在任务调度、缓冲处理以及实现广度优先搜索算法中。
在实际应用中,这两种数据结构可能有不同的变体和实现方式,比如双端队列(Deque),它允许从两端添加或移除元素。
 

栈和队列本质上来讲就是:本质上其实就是顺序表和链表的再利用

 栈的讲解

栈的概念及结构

栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。

进行数据插入和删除操作的一端称为栈顶,另一端称为栈底(因为先进后出)。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。

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

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

后进先出,后进去的先出来(就像羽毛球桶)

图片实例(这里是我画了一个图,找了两个图)

实现栈的方式

栈的实现可以是顺序表,也可以是链表

入数据 1 2 3 4

出数据 4 3 2 1

顺序表(数组)实现

链表实现的话最好是双链表

当然单链表也是可以的

不过一般情况下我们都是采取单链表的方式进行实现

因为

C语言中栈的实现通常采用顺序表的方式而不是链表的方式,原因包括但不限于以下几点:

1. **简单性**:顺序表的实现相对简单,它是基于数组的,而数组是C语言的基本数据结构。链表的实现需要额外的指针操作,对于栈的基本操作(入栈和出栈)来说,这种复杂性通常是不必要的。

2. **性能**:顺序表(数组)在内存中是连续存储的,这意味着存取速度快,CPU缓存的效率也更高。而链表由于其非连续性,访问速度相对较慢,且不利于缓存优化。

3. **空间效率**:顺序表可以预先分配固定大小的内存空间,而链表则需要为每个元素分配独立的内存空间,并且每个节点都需要额外的指针空间。在栈的应用场景中,通常可以预估栈的最大深度,因此顺序表可以更有效地利用内存。

4. **栈特性**:栈是后进先出(LIFO)的数据结构,其操作主要集中在栈顶进行。顺序表可以通过索引直接访问栈顶元素,而链表则需要从头开始遍历,直到找到最后一个元素。

5. **边界检查**:顺序表可以方便地进行上溢检查,只需比较栈的当前元素数量和预分配的大小即可。链表则需要维护一个额外的计数器来记录元素数量,以进行上溢检查。

6. **缓存局部性**:由于顺序表的数据存储是连续的,它具有较好的时间局部性和空间局部性,这使得它更适合现代计算机系统的缓存机制。

7. **编译器优化**:现代编译器对数组和循环有很好的优化,可以生成高效的代码。而链表的优化则更为复杂,且效果可能不如顺序表。

8. **编程习惯**:C语言程序员通常习惯于使用数组来实现栈,因为数组在C语言中使用非常普遍,且容易理解。

然而,这并不意味着链表不能用于实现栈。在一些特定场景下,链表实现的栈也有其优势,例如当栈的大小频繁变化或者对内存的使用有更严格的要求时。链表可以根据需要动态分配内存,而不需要像顺序表那样预先分配一块较大的内存空间。

选择顺序表还是链表来实现栈,最终取决于具体的应用场景和性能要求。

存在CPU缓存的问题 所以数组其实还是比较好用的

计算机的存储体系-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/Jason_from_China/article/details/138589959

栈的实现

 创建文件

创建栈的结构

typedef int STDataType;
typedef struct Stack
{
	STDataType* _a; // 首元素的地址
	int _top;		// 栈顶,初始化为0,也就是等同于size,初始化为-1,等同于下标
	int _capacity;  // 容量 
}Stack;

这段代码定义了一个名为 Stack 的结构体,用于实现一个栈数据结构:

  1. STDataType:这是一个类型定义,它将 int 类型别名为 STDataType。这意味着在定义栈结构体时,可以使用 STDataType 来表示栈中存储的数据类型为整数。

  2. Stack 结构体:

    • STDataType* _a:这是一个指向 STDataType 类型数据的指针,用于指向栈的内存空间。在栈的上下文中,这个数组通常用来存储栈中的元素。_a 通常指向一个预先分配的数组,该数组的大小由 _capacity 成员指定。
    • int _top:这个整数用来表示栈顶的位置。在大多数栈的实现中,栈顶是一个非常重要的概念,因为它指向了最后一个入栈的元素的位置。在这段代码中,_top 初始化为0,意味着栈顶位于数组的第一个元素(在C语言中数组索引从0开始)。如果使用 _top 初始化为-1,则表示栈是空的,因为此时没有元素入栈,栈顶的下标为-1。
    • int _capacity:这个整数用来存储栈的最大容量,即栈能存储的最大元素数量。在栈操作中,这个值用来检查栈是否已满,以避免栈溢出。

这个 Stack 结构体的定义为栈的实现提供了基础框架。通常,还需要实现一些函数来操作这个栈,比如 Push(入栈)、Pop(出栈)、Top(获取栈顶元素)、IsEmpty(检查栈是否为空)等。接下来我们会逐个实现。

初始化栈

#include"Stack.h"
// 初始化栈 
void StackInit(Stack* ps)
{
	ps->_a = NULL;
	
	// 这里栈顶初始化为-1和初始化为0是不一样的
	//    0 0 0 0 0 0 0 0 数组
	// -1 0 0 0 0 0 0 0 0 初始化为-1
	//    0 0 0 0 0 0 0 0 初始化为0
    // 初始化为0,也就是等同于size,初始化为-1,等同于下标
	ps->_capacity = ps->_top = 0;
}

代码解释

  1. `void StackInit(Stack* ps)`:这是 `StackInit` 函数的定义开始,它接收一个指向 `Stack` 结构体的指针 `ps` 作为参数。
  2.  `ps->_a = NULL;`:这行代码将栈的数组指针 `_a` 初始化为 `NULL`。这意味着初始化时栈没有分配任何内存空间,数组为空。
  3. `ps->_capacity = ps->_top = 0;`:这里初始化了两个成员变量:
  4.    - `_capacity`:栈的容量,这里初始化为0,表示栈的最大容量为0,即栈没有任何空间可以存储元素。
  5.  - `_top`:栈顶的索引,这里初始化为0,表示栈顶位于数组的第一个位置。由于数组指针 `_a` 已经初始化为 `NULL`,此时栈是空的。

_top 初始化的不同方式:

  1. - **初始化为-1**:在一些栈的实现中,`_top` 被初始化为-1,用来表示栈为空。这种方式下,数组的索引从0开始,但 `_top` 的初始值-1意味着没有元素在栈中。当第一个元素被推入栈时,`_top` 会增加到0,表示数组的第一个元素现在包含数据。
  2. - **初始化为0**:在这段代码中,`_top` 被初始化为0,这同样表示栈为空。由于 `_a` 是 `NULL`,即使 `_top` 是0,栈实际上也是空的。当第一个元素被推入栈时,数组 `_a` 会被分配内存,且 `_top` 保持不变,直到有元素入栈。
  3. 选择哪种初始化方式取决于你的栈操作的具体实现。如果你的栈操作允许在 `_top` 为0时推入元素,那么初始化 `_top` 为0是合适的。如果你希望 `_top` 总是指向栈顶元素的下一个位置,那么初始化 `_top` 为-1可能更合适。
  4. 在这段代码中,由于 `_a` 被初始化为 `NULL`,栈的状态(空或非空)主要由 `_a` 的值决定。
  5. `_top` 的初始化值更多地是为将来元素入栈时的索引管理做准备。

这里我采取的是初始化为0,也就可以简介表示实际的个数

销毁栈

// 销毁栈 
void StackDestroy(Stack* ps)
{
	// 判断为不为空,判断里面有没有数值
	assert(ps && ps->_top);
	free(ps->_a);
	ps->_capacity = ps->_top = 0;
}
  1. free(ps->_a);:这行代码调用 free 函数来释放栈的数组 _a 所占用的内存。这是销毁栈的关键步骤,因为它避免了内存泄漏。

  2. ps->_capacity = ps->_top = 0;:在释放了栈的内存之后,将 _capacity_top 成员变量重置为0。_capacity 表示栈的容量,重置为0意味着栈不再具有存储任何元素的能力。_top 表示栈顶的位置,重置为0可以表示栈现在是空的(取决于你的栈实现中 _top 的使用方式)。

入栈

// 入栈 
void StackPush(Stack* ps, STDataType data)
{
	//首先判断容量是多少,然后进行扩容
	if (ps->_capacity == ps->_top)
	{
		//扩容
		int newapacity = ps->_capacity == 0 ? 4 : 2 * ps->_capacity;
		STDataType* tmp = (STDataType*)realloc(ps->_a, newapacity * sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("StackPush:tmp:error:");
			exit(1);
		}
		//改变容量大小,指向新空间的头第一个元素位置的地址
		ps->_capacity = newapacity;
		ps->_a = tmp;
	}
	//插入数值
	ps->_a[ps->_top] = data;
	ps->_top++;
}

C语言-realloc函数的使用-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/Jason_from_China/article/details/137074963注意,realloc涉及一个原地扩容和异地扩容的情况,所以才会出现这一行代码

ps->_a = tmp;

这段代码定义了一个名为 StackPush 的函数,用于向栈中添加一个新元素。

  1. void StackPush(Stack* ps, STDataType data):函数定义开始,StackPush 接收一个指向 Stack 结构体的指针 ps 和一个要入栈的数据 data

  2. if (ps->_capacity == ps->_top):这行代码检查栈是否已满。_capacity 是栈的最大容量,_top 是栈顶的索引。如果两者相等,表示栈已经达到最大容量。

  3. int newapacity = ps->_capacity == 0 ? 4 : 2 * ps->_capacity;:如果栈已满,则需要扩容。新的容量 newcapacity 是当前容量的两倍,如果当前容量为0(即栈是空的或尚未分配内存),则新容量设置为4。

  4. STDataType* tmp = (STDataType*)realloc(ps->_a, newapacity * sizeof(STDataType));:使用 realloc 函数为栈的数组 _a 分配新的内存空间。realloc 会处理原有内存块的复制,并返回指向新内存块的指针。

  5. if (tmp == NULL):检查 realloc 是否成功。如果 tmpNULL,表示内存分配失败。

  6. perror("StackPush:tmp:error:");:如果内存分配失败,使用 perror 函数输出错误信息。perror 会将参数字符串作为前缀,后跟一个冒号和空格,然后输出当前设定的 errno 描述的错误信息。

  7. exit(1);:如果内存分配失败,调用 exit 函数以非零状态码退出程序。

  8. ps->_capacity = newapacity;:如果内存分配成功,更新栈的容量为新的容量。

  9. ps->_a = tmp;:将栈的数组指针 _a 更新为指向新分配的内存空间。

  10. ps->_a[ps->_top] = data;:将传入的数据 data 存储到栈顶位置。由于栈是顺序存储的,这一步是直接通过数组索引来完成的。

  11. ps->_top++;:更新栈顶索引 _top,将其增加1,以指向数组中的下一个位置,为下一次入栈做准备。

这段代码实现了一个动态栈,它可以根据需要自动扩容。当栈满时,它会增加栈的容量并重新分配内存。需要注意的是,realloc 的使用可能会导致原有内存块被移动到新的内存位置,因此所有指向原内存块的指针都需要更新。此外,realloc 失败时的错误处理是必要的,以避免程序因内存分配问题而崩溃。

出栈

// 出栈 
void StackPop(Stack* ps)
{
	// 判断为不为空,判断里面有没有数值
	assert(ps && ps->_top);
	ps->_top--;
}
  1. void StackPop(Stack* ps):函数定义开始,StackPop 接收一个指向 Stack 结构体的指针 ps 作为参数。

  2. assert(ps && ps->_top);assert 是一个宏,用于在运行时测试一个条件是否为真。如果条件为假,则 assert 将产生一个断言失败,通常会导致程序异常终止。在这里,它用于确保传入的栈指针 ps 不是 NULL,并且 ps->_top 也是非空的,从而确保栈已经初始化过并且 _top 成员变量是有效的。

  3. ps->_top--;:这行代码将栈顶索引 _top 减一。由于栈是后进先出(LIFO)的数据结构,减少 _top 的值就相当于从栈顶部移除了一个元素。在栈的顺序存储实现中,通常不需要实际移动数组中的元素,只需要更新栈顶的索引即可。

出栈其实就很简答,只需要删除栈顶就可以

删除

获取栈顶元素 

// 获取栈顶元素 
STDataType StackTop(Stack* ps)
{
	assert(ps && ps->_top);
	return ps->_a[ps->_top - 1];
}
  1. STDataType StackTop(Stack* ps):函数定义开始,StackTop 接收一个指向 Stack 结构体的指针 ps 作为参数,并返回栈顶的元素。

  2. assert(ps && ps->_top);assert 是一个宏,用于在运行时测试一个条件是否为真。如果条件为假,则 assert 将产生一个断言失败,通常会导致程序异常终止。在这里,它用于确保传入的栈指针 ps 不是 NULL,并且 ps->_top 也是非空的,从而确保栈已经初始化过并且 _top 成员变量是有效的。

  3. return ps->_a[ps->_top - 1];:这行代码返回栈顶元素的值。由于栈是后进先出(LIFO)的数据结构,栈顶元素就是数组 _a 中索引为 _top - 1 的元素。这里假设 _top 初始化为0并且数组索引从0开始,所以栈顶元素的索引是 _top - 1

获取栈中有效元素个数 

int StackSize(Stack* ps)
{
	assert(ps && ps->_top);
	return ps->_top - 1;
}

检测栈是否为空,如果为空返回非零结果(1),如果不为空返回0 

int StackEmpty(Stack* ps)
{
	assert(ps);
	return ps->_top == 0;
	// 所以,ps->_top == 0 这个表达式的作用是比较栈顶位置 ps->_top 是否等于 0。
	// 如果相等,说明栈为空,表达式结果为 true(在C语言中用 1 表示);
	// 如果不相等,说明栈不为空,表达式结果为 false(在C语言中用 0 表示)
}

栈代码的总结

Stack.h

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int STDataType;
typedef struct Stack
{
	STDataType* _a; // 首元素的地址
	int _top;		// 栈顶,初始化为0,也就是等同于size,初始化为-1,等同于下标
	int _capacity;  // 容量 
}Stack;
// 初始化栈 
void StackInit(Stack* ps);
// 销毁栈 
void StackDestroy(Stack* ps);
// 入栈 
void StackPush(Stack* ps, STDataType data);
// 出栈 
void StackPop(Stack* ps);
// 获取栈顶元素 
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数 
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps);

Stack.c

#include"Stack.h"
// 初始化栈 
void StackInit(Stack* ps)
{
	ps->_a = NULL;
	
	// 这里栈顶初始化为-1和初始化为0是不一样的
	//    0 0 0 0 0 0 0 0 数组
	// -1 0 0 0 0 0 0 0 0 初始化为-1
	//    0 0 0 0 0 0 0 0 初始化为0
    // 初始化为0,也就是等同于size,初始化为-1,等同于下标
	ps->_capacity = ps->_top = 0;
}
// 销毁栈 
void StackDestroy(Stack* ps)
{
	// 判断为不为空,判断里面有没有数值
	assert(ps && ps->_top);
	free(ps->_a);
	ps->_capacity = ps->_top = 0;
}
// 入栈 
void StackPush(Stack* ps, STDataType data)
{
	//首先判断容量是多少,然后进行扩容
	if (ps->_capacity == ps->_top)
	{
		//扩容
		int newapacity = ps->_capacity == 0 ? 4 : 2 * ps->_capacity;
		STDataType* tmp = (STDataType*)realloc(ps->_a, newapacity * sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("StackPush:tmp:error:");
			exit(1);
		}
		//改变容量大小,指向新空间的头第一个元素位置的地址
		ps->_capacity = newapacity;
		ps->_a = tmp;
	}
	//插入数值
	ps->_a[ps->_top] = data;
	ps->_top++;
}
// 出栈 
void StackPop(Stack* ps)
{
	// 判断为不为空,判断里面有没有数值
	assert(ps && ps->_top);
	ps->_top--;
}
// 获取栈顶元素 
STDataType StackTop(Stack* ps)
{
	assert(ps && ps->_top);
	return ps->_a[ps->_top - 1];
}
// 获取栈中有效元素个数 
int StackSize(Stack* ps)
{
	assert(ps && ps->_top);
	return ps->_top - 1;
}
// 检测栈是否为空,如果为空返回非零结果(1),如果不为空返回0 
int StackEmpty(Stack* ps)
{
	assert(ps);
	return ps->_top == 0;
	// 所以,ps->_top == 0 这个表达式的作用是比较栈顶位置 ps->_top 是否等于 0。
	// 如果相等,说明栈为空,表达式结果为 true(在C语言中用 1 表示);
	// 如果不相等,说明栈不为空,表达式结果为 false(在C语言中用 0 表示)
}

队列的讲解

队列的概念

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

一端进另一端出

也就是可以做到,先进先出

队列的使用场景

队列的经典应用就是保持公平性

比如:抽号机

生产者消费者模型,先来先出去,多线程的话就可能涉及到一个锁的概念

广度优先遍历(概念)

直接好友:好友

间接好友:好友的好友

可以采取队列的方式推荐

队列如何实现

数组的无法实现的因为需要一边进一边出,所以一般是单链表实现队列

同时单链表也会更加省空间

队列的实现 

创建文件

首先我们需要知道单链表实现队列的时候,我们需要有头节点和尾结点,以及链表的实际的个数,所以我们不能在结构体里面创建那麽多结构体,所以我们采取结构体的嵌套

创建栈的结构

// 链式结构:表示队列 
typedef struct QListNode
{
	QDataType _data;
	struct QListNode* _next;
}QNode;
// 队列的结构 (因为队列是先进先出,后进后出,也就是和栈是相反的,此时会尾进头出,所以我们需要更新尾部和头部节点)
// (把头结点,尾结点,链表的实际的大小放里面,这样不需要每次使用的时候进行循环找尾,我们只需要每次更新尾结点就可以)
typedef struct Queue
{
	QNode* _phead;//头节点
	QNode* _ptail;//尾结点
	int size;
}Queue;

这段代码定义了两个结构体,QNodeQueue,分别用于表示队列中的节点和整个队列。下面是对每个部分的详细解释:

  1. QNode 结构体:这个结构体表示队列中的单个节点。

    • _data:这是 QNode 结构体中的一个成员变量,用来存储节点中的数据。QDataType 是数据类型的别名,它应该在相关的头文件中定义,代表节点存储的数据类型。
    • _next:这是一个指向 QNode 类型的指针,用来指向同一队列中的下一个节点。如果 _next 为 NULL,表示这是队列中的最后一个节点。
  2. Queue 结构体:这个结构体表示整个队列。

    • _phead:这是一个指向 QNode 类型的指针,用来指向队列的头节点。队列的头节点是最早被加入的节点,也是下一个将要被移除的节点。
    • _ptail:这是一个指向 QNode 类型的指针,用来指向队列的尾节点。队列的尾节点是最后被加入的节点,它用于在添加新元素时更新队列。
    • size:这是一个整数类型的变量,用来存储队列中当前的元素数量。

在队列的链式表示中,不需要像顺序表示(使用数组)那样进行动态内存分配和缩容。链式结构自然地允许队列的动态增长和缩减,因为每个节点独立维护其后继节点的指针。

队列的基本操作包括:

  • 入队(Enqueue):在队尾添加一个新节点。
  • 出队(Dequeue):移除队头的节点,并返回其数据。
  • 查看队头(Peek/Front):返回队头节点的数据但不移除它。
  • 检查队列是否为空(IsEmpty):检查队列的头节点是否为 NULL

初始化与销毁队列

// 初始化队列 
void QueueInit(Queue* ps)
{
	ps->_phead = NULL;
	ps->_ptail = NULL;
	ps->size = 0;
}
// 销毁队列 
void QueueDestroy(Queue* ps)
{
	assert(ps);
	QNode* cur = ps->_phead;
	while (cur)
	{
		//存下下一个节点的地址,不会出现找空的情况
		QNode* next = cur->_next;
		free(cur);
		cur =next;
	}
}
  1. QueueInit 函数

    • 这个函数接收一个指向 Queue 结构体的指针 ps
    • ps->_phead = NULL;:将队列的头节点指针设置为 NULL。这表示队列初始化时是空的,没有任何元素。
    • ps->_ptail = NULL;:将队列的尾节点指针也设置为 NULL。由于队列是空的,头尾节点都不存在。
    • ps->size = 0;:设置队列中元素的数量为0。
  2. QueueDestroy 函数

    • 这个函数同样接收一个指向 Queue 结构体的指针 ps
    • assert(ps);:使用 assert 宏来确保传入的 ps 不是 NULL。如果 ps 是 NULLassert 将触发断言失败,这有助于避免对空指针的无效操作。
    • QNode* cur = ps->_phead;:声明一个 QNode 类型的指针 cur 并初始化为队列的头节点。
    • while (cur):使用 while 循环来遍历队列,直到 cur 为 NULL,即队列中的所有节点都被处理完毕。
    • QNode* next = cur->_next;:在释放当前节点之前,先保存下一个节点的地址,以便在释放当前节点后能够继续遍历队列。
    • free(cur);:使用 free 函数释放当前节点 cur 所占用的内存。
    • cur = next;:将 cur 更新为下一个节点,继续循环直到所有节点都被释放。
    • 循环结束后,队列中的所有节点都被释放,此时队列被销毁。

队尾入队列 

// 队尾入队列 
void QueuePush(Queue* ps, QDataType x)
{
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("QueuePush:newnode:error:");
		exit(1);
	}
	//把需要插入的数值插入到节点里面
	newnode->_next = NULL;
	newnode->_data = x;
	
	// ps->_phead == ps->_ptail 的结果确实是 true,这表明队列中只有一个元素(头尾相接)。
	// 但是,通常情况下,我们不会使用 ps->_phead == ps->_ptail 
	// 来检查队列中是否只有一个元素。相反,我们通常会使用 ps->size == 1 或者 ps->_phead != NULL && ps->_ptail != NULL 
	// 来检查队列中是否只有一个元素。

	//插入节点 第一个节点,这里的判断是不能写成ps->_phead ==ps->_ptail;因为都是空指针不能进行比较
	//ps->_phead == NULL
	if (ps->size == 0)
	{
		ps->_phead = ps->_ptail = newnode;
	}
	else
	{
		//尾插节点
		ps->_ptail->_next = newnode;
		ps->_ptail= newnode;
	}
	ps->size++;
}

这段代码定义了一个名为 QueuePush 的函数,用于在队列的尾部添加一个新的元素。

  1. void QueuePush(Queue* ps, QDataType x):函数定义开始,QueuePush 接收一个指向 Queue 结构体的指针 ps 和一个要入队的新数据 x

  2. QNode* newnode = (QNode*)malloc(sizeof(QNode));:使用 malloc 函数为新节点分配内存。newnode 是指向新分配内存的指针。

  3. if (newnode == NULL):检查 malloc 是否成功分配了内存。如果 newnodeNULL,表示内存分配失败。

  4. perror("QueuePush:newnode:error:");:如果内存分配失败,使用 perror 函数输出错误信息。

  5. exit(1);:如果内存分配失败,调用 exit 函数以非零状态码退出程序。

  6. newnode->_next = NULL;:将新节点的 _next 指针设置为 NULL。这是新节点的尾部标识,表示在新节点之后没有更多的节点。

  7. newnode->_data = x;:将新数据 x 存储在新节点的 _data 成员中。

  8. if (ps->size == 0):检查队列是否为空(即队列中的元素数量为0)。

  9. ps->_phead = ps->_ptail = newnode;:如果是空队列,那么新节点同时是头节点和尾节点。因此,将头节点和尾节点指针都指向 newnode

  10. else:如果队列不是空的,执行以下操作:

    • ps->_ptail->_next = newnode;:将当前尾节点的 _next 指针指向新节点,从而将新节点添加到队列的末尾。
    • ps->_ptail = newnode;:更新尾节点指针,使其指向新节点。
  11. ps->size++;:队列中元素的数量增加1。

这段代码实现了队列的尾部入队操作。它首先检查队列是否为空,然后相应地将新节点添加到队列的末尾,并更新尾节点指针和队列的元素数量。如果内存分配失败,程序将输出错误信息并退出。

队头出队列 

// 队头出队列 
void QueuePop(Queue* ps)
{
	assert(ps && ps->size);
	// 改变头结点
	ps->_phead = ps->_phead->_next;
	ps->size--;
}

这段代码定义了一个名为 QueuePop 的函数,其目的是从队列头部移除一个元素。

  1. #include"Queue.h":这行代码应该位于文件的顶部,用于引入包含 Queue 结构体和 QNode 结构体定义以及 QDataType 类型定义的头文件。

  2. void QueuePop(Queue* ps):函数定义开始,QueuePop 接收一个指向 Queue 结构体的指针 ps 作为参数。

  3. assert(ps && ps->size);assert 是一个宏,用于在运行时测试一个条件是否为真。如果条件为假,则 assert 将产生一个断言失败,通常会导致程序异常终止。在这里,它用于确保传入的队列指针 ps 不是 NULL,并且队列中至少有一个元素(即 ps->size 大于0)。

  4. ps->_phead = ps->_phead->_next;:这行代码将队列的头节点指针 _phead 更新为指向下一个节点,从而移除当前队列头部的节点。由于队列是先进先出(FIFO)的数据结构,队头节点是将要被移除的节点。

  5. ps->size--;:队列中元素的数量减少1。

这段代码实现了队列的头部出队操作。它首先通过 assert 检查队列是否非空,然后将头节点指针移动到下一个节点,以此出队操作,最后更新队列的元素数量。

获取队列头部元素

// 获取队列头部元素 
QDataType QueueFront(Queue* ps)
{
	assert(ps && ps->size);
	return ps->_phead->_data;
}
  1. QueueFront 函数

    • 这个函数接收一个指向 Queue 结构体的指针 ps
    • assert(ps && ps->size);:使用 assert 宏确保传入的队列指针 ps 不是 NULL,并且队列中至少有一个元素(即 ps->size 大于0)。
    • return ps->_phead->_data;:返回队列头部节点的 _data 成员。由于队列是先进先出(FIFO)的数据结构,队头节点的 _data 成员包含了最早被加入的元素的值。

获取队列队尾元素 

// 获取队列队尾元素 
QDataType QueueBack(Queue* ps)
{
	assert(ps && ps->size);
	return ps->_ptail->_data;
}
  1. QueueBack 函数

    • 这个函数同样接收一个指向 Queue 结构体的指针 ps
    • assert(ps && ps->size);:使用 assert 宏确保传入的队列指针 ps 不是 NULL,并且队列中至少有一个元素。
    • return ps->_ptail->_data;:返回队列尾部节点的 _data 成员。在链式队列中,尾部节点是最后一个被加入的节点,其 _data 成员包含了最近一个被加入的元素的值。

获取队列中有效元素个数

// 获取队列中有效元素个数 
int QueueSize(Queue* ps)
{
	return ps->size;
}
  1. QueueSize 函数

    • 这个函数接收一个指向 Queue 结构体的指针 ps
    • return ps->size;:返回队列中有效元素的个数,即 size 成员的值。

检测队列是否为空,如果为空返回非零结果,如果非空返回0 

// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* ps)
{
	assert(ps);
	return ps->size == 0;
}

QueueEmpty 函数

  • 这个函数接收一个指向 Queue 结构体的指针 ps
  • assert(ps);:使用 assert 宏确保传入的队列指针 ps 不是 NULL
  • return ps->size == 0;:检查队列是否为空。如果 size 成员的值等于0,则表示队列为空,函数返回非零值(在C语言中,非零值被视为“真”),否则返回0(“假”)

队列代码的总结 

Queue.h

#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int QDataType;
// 链式结构:表示队列 
typedef struct QListNode
{
	QDataType _data;
	struct QListNode* _next;
}QNode;
// 队列的结构 (因为队列是先进先出,后进后出,也就是和栈是相反的,此时会尾进头出,所以我们需要更新尾部和头部节点)
// (把头结点,尾结点,链表的实际的大小放里面,这样不需要每次使用的时候进行循环找尾,我们只需要每次更新尾结点就可以)
typedef struct Queue
{
	QNode* _phead;//头节点
	QNode* _ptail;//尾结点
	int size;
}Queue;
// 这里采取一级指针进行实现代码逻辑,如果不创建队列的结构,我们就需要采取二级指针
// 初始化队列 
void QueueInit(Queue* ps);
// 销毁队列 
void QueueDestroy(Queue* ps);
// 队尾入队列 
void QueuePush(Queue* ps, QDataType data);
// 队头出队列 
void QueuePop(Queue* ps);
// 获取队列头部元素 
QDataType QueueFront(Queue* ps);
// 获取队列队尾元素 
QDataType QueueBack(Queue* ps);
// 获取队列中有效元素个数 
int QueueSize(Queue* ps);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* ps);

Queue.c

#include"Queue.h"
// 初始化队列 
void QueueInit(Queue* ps)
{
	ps->_phead = NULL;
	ps->_ptail = NULL;
	ps->size = 0;
}
// 销毁队列 
void QueueDestroy(Queue* ps)
{
	assert(ps);
	QNode* cur = ps->_phead;
	while (cur)
	{
		//存下下一个节点的地址,不会出现找空的情况
		QNode* next = cur->_next;
		free(cur);
		cur =next;
	}
}
// 队尾入队列 
void QueuePush(Queue* ps, QDataType x)
{
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("QueuePush:newnode:error:");
		exit(1);
	}
	//把需要插入的数值插入到节点里面
	newnode->_next = NULL;
	newnode->_data = x;
	
	// ps->_phead == ps->_ptail 的结果确实是 true,这表明队列中只有一个元素(头尾相接)。
	// 但是,通常情况下,我们不会使用 ps->_phead == ps->_ptail 
	// 来检查队列中是否只有一个元素。相反,我们通常会使用 ps->size == 1 或者 ps->_phead != NULL && ps->_ptail != NULL 
	// 来检查队列中是否只有一个元素。

	//插入节点 第一个节点,这里的判断是不能写成ps->_phead ==ps->_ptail;因为都是空指针不能进行比较
	//ps->_phead == NULL
	if (ps->size == 0)
	{
		ps->_phead = ps->_ptail = newnode;
	}
	else
	{
		//尾插节点
		ps->_ptail->_next = newnode;
		ps->_ptail= newnode;
	}
	ps->size++;
}
// 队头出队列 
void QueuePop(Queue* ps)
{
	assert(ps && ps->size);
	// 改变头结点
	ps->_phead = ps->_phead->_next;
	ps->size--;
}
// 获取队列头部元素 
QDataType QueueFront(Queue* ps)
{
	assert(ps && ps->size);
	return ps->_phead->_data;
}
// 获取队列队尾元素 
QDataType QueueBack(Queue* ps)
{
	assert(ps && ps->size);
	return ps->_ptail->_data;
}
// 获取队列中有效元素个数 
int QueueSize(Queue* ps)
{
	return ps->size;
}
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* ps)
{
	assert(ps);
	return ps->size == 0;
}

括号匹配(栈)

20. 有效的括号 - 力扣(LeetCode)

c++有栈 但是C语言没有

到那时我们可以自己造

这里的代码是直接调用栈,然后调用

等于三个左括号的任意一个 我们就入栈

左括号(入栈)

右括号

取出栈顶数据,出栈并且进行匹配,这里匹配的是不匹配的情况

这里进行方向的判断

因为不匹配可以直接拿结果

栈里面还有数据意味着数量不相等

‘也就是此时进行判断 此时栈是不是为空

图解

  1. 因为每次都是左括号入栈,然后然后才有右括号的,所以我们可以来一个判断,如果入栈半天只有左括号,那么说明也就是第四种情况,直接返回fash就可以
  2. 根据栈的性质,先进先出,我们可以把输入数值的左括号入栈,因为匹配的时候,我们不能去寻找与之匹配的右括号,因为这样可能存在漏掉某一个数组里面的数组就像第三个,所以我们需要进行遍历,所有的左括号入栈,因为的对称的,所以我们可以判定哪些不是与之匹配的右括号,不是就false(这期间记得每次匹配之后,我们需要进行出栈,也就是让栈的第一个数值进行更换,因为我们已经参与匹配并且成功了)
  3. 循环结束之后此时我们进行一个判空,如果是空的说明是,满足条件的,不是空的说明是不满足条件的

代码的实现

#pragma once
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
typedef char STDataType;
typedef struct Stack {
    STDataType* _a; // 首元素的地址
    int _top; // 栈顶,初始化为0,也就是等同于size,初始化为-1,等同于下标
    int _capacity; // 容量
} Stack;
// 初始化栈
void StackInit(Stack* ps);
// 销毁栈
void StackDestroy(Stack* ps);
// 入栈
void StackPush(Stack* ps, STDataType data);
// 出栈
void StackPop(Stack* ps);
// 获取栈顶元素
STDataType StackTop(Stack* ps);
// 获取栈中有效元素个数
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0
int StackEmpty(Stack* ps);
// 初始化栈
void StackInit(Stack* ps) {
    ps->_a = NULL;

    // 这里栈顶初始化为-1和初始化为0是不一样的
    //    0 0 0 0 0 0 0 0 数组
    // -1 0 0 0 0 0 0 0 0 初始化为-1
    //    0 0 0 0 0 0 0 0 初始化为0
    // 初始化为0,也就是等同于size,初始化为-1,等同于下标
    ps->_capacity = ps->_top = 0;
}
// 销毁栈
void StackDestroy(Stack* ps) {
    // 判断为不为空,判断里面有没有数值
    assert(ps && ps->_top);
    free(ps->_a);
    ps->_capacity = ps->_top = 0;
}
// 入栈
void StackPush(Stack* ps, STDataType data) {
    // 首先判断容量是多少,然后进行扩容
    if (ps->_capacity == ps->_top) {
        // 扩容
        int newapacity = ps->_capacity == 0 ? 4 : 2 * ps->_capacity;
        STDataType* tmp =
            (STDataType*)realloc(ps->_a, newapacity * sizeof(STDataType));
        if (tmp == NULL) {
            perror("StackPush:tmp:error:");
            exit(1);
        }
        // 改变容量大小,指向新空间的头第一个元素位置的地址
        ps->_capacity = newapacity;
        ps->_a = tmp;
    }
    // 插入数值
    ps->_a[ps->_top] = data;
    ps->_top++;
}
// 出栈
void StackPop(Stack* ps) {
    // 判断为不为空,判断里面有没有数值
    assert(ps && ps->_top);
    ps->_top--;
}
// 获取栈顶元素
STDataType StackTop(Stack* ps) {
    assert(ps && ps->_top);
    return ps->_a[ps->_top - 1];
}
// 获取栈中有效元素个数
int StackSize(Stack* ps) {
    assert(ps && ps->_top);
    return ps->_top - 1;
}
// 检测栈是否为空,如果为空返回非零结果(1),如果不为空返回0
int StackEmpty(Stack* ps) {
    assert(ps);
    return ps->_top == 0;
    // 所以,ps->_top == 0 这个表达式的作用是比较栈顶位置 ps->_top 是否等于 0。
    // 如果相等,说明栈为空,表达式结果为 true(在C语言中用 1 表示);
    // 如果不相等,说明栈不为空,表达式结果为 false(在C语言中用 0 表示)
}
bool isValid(char* s) {

    // 创建变量
    Stack ps;
    // 初始化变量
    StackInit(&ps);
    while (*s) {
        // 给出入栈条件,左边括号进行入栈
        if (*s == '[' || *s == '{' || *s == '(') {
            StackPush(&ps, *s);
        } else {
            if (StackEmpty(&ps)) {
                //StackDestroy(&ps);
                return false;
            }
            // 取栈顶
            STDataType ch = StackTop(&ps);
            // 判断不匹配的条件
            if (ch == '[' && *s != ']' || ch == '(' && *s != ')' ||
                ch == '{' && *s != '}') {
                //StackDestroy(&ps);
                return false;
            }
            // 出栈
            StackPop(&ps);
        }
        ++s;
    }
    // 检测栈是否为空,如果为空返回非零结果ture,如果不为空返回false
    bool ret = StackEmpty(&ps);
    return ret;
}

解释:

  1. 栈结构体定义

    • STDataType* _a:指向栈数组的指针,用于存储栈中的元素。
    • int _top:表示栈顶的位置。数组索引从0开始,栈顶位置 _top 初始化为0,表示栈为空。
    • int _capacity:栈的容量,用于存储栈中最多可以存放的元素数量。
  2. 栈操作函数

    • StackInit:初始化栈,将数组指针设置为 NULL,栈顶 _top 和容量 _capacity 都设置为0。
    • StackDestroy:释放栈数组的内存,并将栈顶和容量重置为0。
    • StackPush:入栈操作,将元素添加到栈顶,如果栈已满则先进行扩容。
    • StackPop:出栈操作,移除栈顶的元素。
    • StackTop:获取栈顶元素的值。
    • StackSize:获取栈中元素的数量。
    • StackEmpty:检查栈是否为空。
  3. 括号验证函数 isValid

    • 该函数接收一个字符数组 s 作为参数,用于验证字符串中的括号是否正确配对。
    • 在函数内部,首先创建并初始化了一个 Stack 类型的变量 ps
    • 然后,使用 while 循环遍历字符串 s
      • 如果当前字符是 [{ 或 ((左括号),则将其入栈。
      • 如果当前字符是 ]} 或 )(右括号):
        • 首先检查栈是否为空。如果栈为空,说明没有对应的左括号与之配对,返回 false
        • 否则,获取栈顶元素并与当前字符配对检查。如果配对不成功,返回 false
        • 如果配对成功,执行出栈操作。
    • 在循环结束后,检查栈是否为空。如果栈为空,则说明所有括号都正确配对,返回 true;否则返回 false
  4. 括号配对规则

    • 每种左括号([{()必须与其对应的右括号(]}))配对。
    • 括号的配对必须遵守正确的嵌套顺序。
  5. 注意事项

    • 在 StackPush 函数中,使用了 realloc 进行内存扩容。如果 realloc 失败,程序将输出错误信息并退出。
    • 在 StackDestroy 函数中,使用 assert 宏确保传入的栈指针 ps 不是 NULL,并且栈是非空的。
    • 在 isValid 函数中,没有释放局部栈 ps 的内存,因为 ps 是自动变量,其内存会在函数结束时自动释放。如果 ps 是动态分配的,那么需要在函数末尾调用 StackDestroy 并传递 ps 的地址。

 具备教学意义的实操(用队列实现栈)

题目

225. 用队列实现栈 - 力扣(LeetCode)icon-default.png?t=N7T8https://leetcode.cn/problems/implement-stack-using-queues/description/

 

实现逻辑

一个是先进先出(队列),一个是后进先出(栈)

这里用两个队列导入·一下数据

出来数据之后入数据

代码

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int QDataType;
// 链式结构:表示队列 
typedef struct QListNode
{
	QDataType _data;
	struct QListNode* _next;
}QNode;
// 队列的结构 (因为队列是先进先出,后进后出,也就是和栈是相反的,此时会尾进头出,所以我们需要更新尾部和头部节点)
// (把头结点,尾结点,链表的实际的大小放里面,这样不需要每次使用的时候进行循环找尾,我们只需要每次更新尾结点就可以)
typedef struct Queue
{
	QNode* _phead;//头节点
	QNode* _ptail;//尾结点
	int size;
}Queue;
// 这里采取一级指针进行实现代码逻辑,如果不创建队列的结构,我们就需要采取二级指针
// 初始化队列 
void QueueInit(Queue* ps);
// 销毁队列 
void QueueDestroy(Queue* ps);
// 队尾入队列 
void QueuePush(Queue* ps, QDataType data);
// 队头出队列 
void QueuePop(Queue* ps);
// 获取队列头部元素 
QDataType QueueFront(Queue* ps);
// 获取队列队尾元素 
QDataType QueueBack(Queue* ps);
// 获取队列中有效元素个数 
int QueueSize(Queue* ps);
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* ps);

// 初始化队列 
void QueueInit(Queue* ps)
{
	ps->_phead = NULL;
	ps->_ptail = NULL;
	ps->size = 0;
}
// 销毁队列 
void QueueDestroy(Queue* ps)
{
	assert(ps);
	QNode* cur = ps->_phead;
	while (cur)
	{
		//存下下一个节点的地址,不会出现找空的情况
		QNode* next = cur->_next;
		free(cur);
		cur =next;
	}
}
// 队尾入队列 
void QueuePush(Queue* ps, QDataType x)
{
	QNode* newnode = (QNode*)malloc(sizeof(QNode));
	if (newnode == NULL)
	{
		perror("QueuePush:newnode:error:");
		exit(1);
	}
	//把需要插入的数值插入到节点里面
	newnode->_next = NULL;
	newnode->_data = x;
	
	// ps->_phead == ps->_ptail 的结果确实是 true,这表明队列中只有一个元素(头尾相接)。
	// 但是,通常情况下,我们不会使用 ps->_phead == ps->_ptail 
	// 来检查队列中是否只有一个元素。相反,我们通常会使用 ps->size == 1 或者 ps->_phead != NULL && ps->_ptail != NULL 
	// 来检查队列中是否只有一个元素。

	//插入节点 第一个节点,这里的判断是不能写成ps->_phead ==ps->_ptail;因为都是空指针不能进行比较
	//ps->_phead == NULL
	if (ps->size == 0)
	{
		ps->_phead = ps->_ptail = newnode;
	}
	else
	{
		//尾插节点
		ps->_ptail->_next = newnode;
		ps->_ptail= newnode;
	}
	ps->size++;
}
// 队头出队列 
void QueuePop(Queue* ps)
{
	assert(ps && ps->size);
	// 改变头结点
	ps->_phead = ps->_phead->_next;
	ps->size--;
}
// 获取队列头部元素 
QDataType QueueFront(Queue* ps)
{
	assert(ps && ps->size);
	return ps->_phead->_data;
}
// 获取队列队尾元素 
QDataType QueueBack(Queue* ps)
{
	assert(ps && ps->size);
	return ps->_ptail->_data;
}
// 获取队列中有效元素个数 
int QueueSize(Queue* ps)
{
	return ps->size;
}
// 检测队列是否为空,如果为空返回非零结果,如果非空返回0 
int QueueEmpty(Queue* ps)
{
	assert(ps);
	return ps->size == 0;
}

//匿名结构体(我们需要两个队列,所以采取结构体嵌套的形式解决问题)
typedef struct 
{
    Queue Q1;
    Queue Q2;
} MyStack;

//初始化(关键在于,初始化的数值出去之后就销毁,所以我们需要创建节点进行初始化)
MyStack* myStackCreate() 
{
    MyStack* obj = (MyStack*)malloc(sizeof(MyStack));
    if(obj == NULL)
    {
        perror("obj:error:");
        exit(1);
    }
    QueueInit(&(obj->Q1));
    QueueInit(&(obj->Q2));
    return obj;
}
//入栈 
void myStackPush(MyStack* obj, int x) 
{
    if(QueueEmpty(&(obj->Q1)))//q1为空
    {
        QueuePush(&(obj->Q2), x);
    }
    else
    {
        QueuePush(&(obj->Q1), x);
    }
}

//删除栈顶元素(核心逻辑)
int myStackPop(MyStack* obj) 
{
    //判空(假设法假设q1为空)
    MyStack* Empty = &(obj->Q1);
    MyStack* noEmpty = &(obj->Q2);
    if(QueueEmpty(&(obj->Q2)))//如果q1不为空,q2为空,此时颠倒一下
    {
        Empty = &(obj->Q2);
        noEmpty = &(obj->Q1);
    }
    //循环导入
    while(QueueSize(noEmpty) > 1)//这里的关键点是你不知道谁是空,不是空
    {
        QueuePush(Empty, QueueFront(noEmpty));//这里 是把不空的数值头部元素,循环导入到空的数值里面去
        QueuePop(noEmpty);//并且进行出栈操作
    }
    //结束之后留下了一个,我们保存下来返回这个节点,并且最后进行删除
    int ret = QueueFront(noEmpty);
    QueuePop(noEmpty);
    return ret;
}

//取出栈顶元素
int myStackTop(MyStack* obj) 
{
    if(QueueEmpty(&(obj->Q1)))//q1为空
    {
        return QueueBack(&(obj->Q2));
    }
    else
    {
        return QueueBack(&(obj->Q1));
    }
}
//判空
bool myStackEmpty(MyStack* obj) 
{
    //q1 q2都为空的时候才为空
    return (QueueEmpty(&(obj->Q1)) && QueueEmpty(&(obj->Q2)));
}
//释放空间
void myStackFree(MyStack* obj) 
{
    QueueDestroy(&(obj->Q1));
    QueueDestroy(&(obj->Q2));
    free(obj);
}

/**
 * Your MyStack struct will be instantiated and called as such:
 * MyStack* obj = myStackCreate();
 * myStackPush(obj, x);
 
 * int param_2 = myStackPop(obj);
 
 * int param_3 = myStackTop(obj);
 
 * bool param_4 = myStackEmpty(obj);
 
 * myStackFree(obj);
*/

代码解释:

  1. 数据结构定义

    • QDataType:定义了队列中存储的数据类型,这里使用 int
    • QNode:定义了队列中的节点,包含数据 _data 和指向下一个节点的指针 _next
    • Queue:定义了队列结构,包含指向队列头部和尾部的指针 _phead 和 _ptail,以及记录队列大小的 size
  2. 队列操作函数

    • QueueInit:初始化队列,将头尾指针设置为 NULL,大小设置为0。
    • QueueDestroy:释放队列中所有节点的内存。
    • QueuePush:在队列尾部添加一个新节点。
    • QueuePop:移除队列头部的节点,并减少队列大小。
    • QueueFront:返回队列头部的元素。
    • QueueBack:返回队列尾部的元素。
    • QueueSize:返回队列中元素的数量。
    • QueueEmpty:检查队列是否为空。
  3. 栈操作函数

    • myStackCreate:创建一个新的 MyStack 实例,初始化两个队列 Q1 和 Q2
    • myStackPush:如果 Q1 为空,则将新元素推入 Q2;否则推入 Q1
    • myStackPop:如果 Q2 不为空且 Q1 为空,则交换两个队列的角色。然后,将 Q2 中的元素逐个移动到 Q1,除了最后一个元素。最后,返回并移除 Q2 中的最后一个元素。
    • myStackTop:返回栈顶元素。如果 Q1 不为空,返回 Q1 的尾部元素;否则返回 Q2 的尾部元素。
    • myStackEmpty:如果两个队列都为空,则返回 true,表示栈为空。
    • myStackFree:销毁 MyStack 实例,释放所有相关联的内存。
  4. 使用队列实现栈的逻辑

    • 通过两个队列 Q1 和 Q2 的交替使用,实现了栈的后进先出(LIFO)特性。当一个队列为空时,可以将另一个队列中的元素移动过来,最后一个元素就是栈顶元素。
    • myStackPush 函数根据 Q1 是否为空来决定将新元素推入哪个队列,这样做的目的是保持两个队列中的元素数量大致相等,从而在 myStackPop 操作中能够高效地将元素从一个队列移动到另一个队列。
  5. 注意事项

    • 在 myStackPop 和 myStackTop 函数中,需要检查两个队列的状态,以确定当前栈顶元素所在的队列。
    • myStackDestroy 函数中,确保先销毁队列,再释放 MyStack 实例的内存。

这种使用两个队列实现栈的方法利用了队列的先进先出特性来模拟栈的后进先出特性。这种方法的好处是可以在 O(1) 时间内完成栈的基本操作,同时避免了使用数组实现栈时可能需要的数组扩展和收缩操作。

 具备教学意义的实操(用栈实现队列)

 题目

 232. 用栈实现队列 - 力扣(LeetCode)

逻辑

核心逻辑

代码

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int STDataType;
typedef struct Stack
{
	STDataType* _a; // 首元素的地址
	int _top;		// 栈顶,初始化为0,也就是等同于size,初始化为-1,等同于下标
	int _capacity;  // 容量 
}Stack;
// 初始化栈 
void StackInit(Stack* ps);
// 销毁栈 
void StackDestroy(Stack* ps);
// 入栈 
void StackPush(Stack* ps, STDataType data);
// 出栈 
void StackPop(Stack* ps);
// 获取栈顶元素 
STDataType StackTop(Stack* ps);
// 获取栈尾元素
STDataType StackTail(Stack* ps);
// 获取栈中有效元素个数 
int StackSize(Stack* ps);
// 检测栈是否为空,如果为空返回非零结果,如果不为空返回0 
int StackEmpty(Stack* ps);
// 初始化栈 
void StackInit(Stack* ps)
{
	ps->_a = NULL;
	
	// 这里栈顶初始化为-1和初始化为0是不一样的
	//    0 0 0 0 0 0 0 0 数组
	// -1 0 0 0 0 0 0 0 0 初始化为-1
	//    0 0 0 0 0 0 0 0 初始化为0
    // 初始化为0,也就是等同于size,初始化为-1,等同于下标
	ps->_capacity = ps->_top = 0;
}
//销毁
void StackDestroy(Stack* ps) {
    if (ps != NULL) { // 检查指针是否非空
        free(ps->_a);
        ps->_a = NULL;
        ps->_capacity = 0;
        ps->_top = -1; // 将栈顶指针设置为-1,表示栈为空
    }
}
// 入栈 
void StackPush(Stack* ps, STDataType data)
{
	//首先判断容量是多少,然后进行扩容
	if (ps->_capacity == ps->_top)
	{
		//扩容
		int newapacity = ps->_capacity == 0 ? 4 : 2 * ps->_capacity;
		STDataType* tmp = (STDataType*)realloc(ps->_a, newapacity * sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("StackPush:tmp:error:");
			exit(1);
		}
		//改变容量大小,指向新空间的头第一个元素位置的地址
		ps->_capacity = newapacity;
		ps->_a = tmp;
	}
	//插入数值
	ps->_a[ps->_top] = data;
	ps->_top++;
}
// 出栈 
void StackPop(Stack* ps)
{
	// 判断为不为空,判断里面有没有数值
	assert(ps && ps->_top);
	ps->_top--;
}
// 获取栈顶元素 
STDataType StackTop(Stack* ps)
{
    assert(ps && ps->_top >= 0);
    return ps->_a[ps->_top-1];
}
// 获取栈尾元素 
STDataType StackTail(Stack* ps)
{
	assert(ps && ps->_top);
	return ps->_a[0];
}
// 获取栈中有效元素个数 
int StackSize(Stack* ps)
{
	assert(ps && ps->_top);
	return ps->_top - 1;
}
// 检测栈是否为空,如果为空返回非零结果(1),如果不为空返回0 
int StackEmpty(Stack* ps)
{
	assert(ps);
	return ps->_top == 0;
	// 所以,ps->_top == 0 这个表达式的作用是比较栈顶位置 ps->_top 是否等于 0。
	// 如果相等,说明栈为空,表达式结果为 true(在C语言中用 1 表示);
	// 如果不相等,说明栈不为空,表达式结果为 false(在C语言中用 0 表示)
}

typedef struct 
{   
    Stack Q1;
    Stack Q2;
} MyQueue;

//初始化
MyQueue* myQueueCreate() 
{     
    MyQueue* ps = (MyQueue*)malloc(sizeof(MyQueue));
    if(ps == NULL)
    {
        perror("ps");
        exit(1);
    }
    StackInit(&(ps->Q1));
    StackInit(&(ps->Q2));
    return ps;
}
//入数据
void myQueuePush(MyQueue* obj, int x) 
{   
    StackPush(&(obj->Q1),x); 
}

//获取栈顶数据
int myQueuePeek(MyQueue* obj)
{
    //队列是先进后出,栈是先进先出
    if(StackEmpty(&(obj->Q2)))
    {
        while(!StackEmpty(&(obj->Q1)))
        {
           StackPush(&(obj->Q2),StackTop(&(obj->Q1)));
           StackPop(&(obj->Q1));
        }
    }
    return StackTop(&(obj->Q2));
}
//删除栈顶数据
int myQueuePop(MyQueue* obj) 
{
    int ret = myQueuePeek(obj);
    StackPop(&(obj->Q2));
    return ret;
}
//判空
bool myQueueEmpty(MyQueue* obj)
{
    return StackEmpty(&(obj->Q1)) && StackEmpty(&(obj->Q2));
}
//释放
void myQueueFree(MyQueue* obj) 
{
    StackDestroy(&(obj->Q1));
    StackDestroy(&(obj->Q2));
    free(obj);
}
/**
 * Your MyQueue struct will be instantiated and called as such:
 * MyQueue* obj = myQueueCreate();
 * myQueuePush(obj, x);
 
 * int param_2 = myQueuePop(obj);
 
 * int param_3 = myQueuePeek(obj);
 
 * bool param_4 = myQueueEmpty(obj);
 
 * myQueueFree(obj);
*/

代码解释

  1. StackInit: 初始化一个栈,设置栈的容量和栈顶指针为0,同时初始化栈的数组为NULL。

  2. StackDestroy: 销毁栈,释放栈的内存,并将栈的指针和容量设置为0,栈顶指针设置为-1。

  3. StackPush: 向栈中压入一个元素。如果栈的容量等于栈顶指针(意味着栈已满),则先进行扩容,然后将数据压入栈顶。

  4. StackPop: 从栈中弹出一个元素。由于栈顶指针指向栈顶元素的下一个位置,所以直接减少栈顶指针的值。

  5. StackTop: 返回栈顶元素的值,但不移除它。

  6. StackTail: 返回栈尾元素的值,但不移除它。

  7. StackSize: 返回栈中元素的数量。

  8. StackEmpty: 判断栈是否为空,如果栈顶指针为0,表示栈为空。

  9. myQueueCreate: 创建并初始化队列,分配内存给 MyQueue 结构体,并初始化其两个栈 Q1Q2

  10. myQueuePush: 向队列中添加一个元素。由于队列使用栈 Q1 来模拟入队操作,所以直接将元素压入 Q1

  11. myQueuePeek: 返回队列的第一个元素,即队列的前端元素。由于队列是先进先出的,而栈是先进后出的,所以如果出队栈 Q2 为空,需要将 Q1 中的所有元素依次弹出并压入 Q2,这样 Q2 的底部就成为队列的前端。然后返回 Q2 的栈顶元素。

  12. myQueuePop: 移除并返回队列的第一个元素。它首先调用 myQueuePeek 获取队列前端的元素,然后调用 StackPopQ2 中移除该元素,并返回它。

  13. myQueueEmpty: 判断队列是否为空。如果 Q1Q2 都为空,则队列为空。

  14. myQueueFree: 释放队列占用的内存,销毁两个栈,释放 MyQueue 结构体的内存。

设计循环队列

 622. 设计循环队列 - 力扣(LeetCode)

实现逻辑

空间大小是固定的

这一道题用链表实现看起来是特别有优势

我们可以搞成循环链表

循环队列想可以重复使用这些空间

也就是到尾部,就绕回去了,也就是有限空间的无限利,保证先进先出,重复使用

就像图书馆,就四个座位,走一个人补一个人

什么时候是空,什么时候是满(此时我们怎么区分空和满)

方法1,增加size记录空还是满

size==0 也就是空

size==k也就是满

方法2:额外多开一个空间(永远空一个空间,也就是不存在满的时候,相等)

也就是五个空间放四个数据,多开一个空间解决空和满

当最后一个是空的时候,又不满足了

所以我们得到(什么用取模来解决回绕)

图解

代码

typedef struct 
{
    int* a;//首元素的地址
    int phead;//头
    int ptile;//尾
    int size;//空间的实际个数(不是开辟个数)
} MyCircularQueue;

// 构造器,设置队列长度为 k (初始化)
MyCircularQueue* myCircularQueueCreate(int k) 
{
    //开辟空间的时候 多开辟一个空间
    MyCircularQueue* obj=(MyCircularQueue*)malloc(sizeof(MyCircularQueue));
    obj->a = malloc(sizeof(int)*(k+1));
    obj->phead=0;
    obj->ptile=0;
    obj->size=k;
    return obj;
}
//进行插入
bool myCircularQueueEnQueue(MyCircularQueue* obj, int value) 
{   
    if((obj->ptile+1)%(obj->size+1) == obj->phead)
    {
        return false;//队列已满
    }
    //插入
    obj->a[obj->ptile] = value;
    obj->ptile = (obj->ptile+1)%(obj->size+1);
    return true;
}
//删除一个元素
bool myCircularQueueDeQueue(MyCircularQueue* obj) 
{
    if(obj->phead == obj->ptile)
    {
        return false;
    }
    //删除元素只需要头部减少一个就好、
    obj->phead = (obj->phead+1)%(obj->size+1);
    
    return true; 
}
//获取首元素
int myCircularQueueFront(MyCircularQueue* obj) 
{
    if(obj->phead == obj->ptile)
    {
        return -1;
    }
    return obj->a[(obj->phead)%(obj->size+1)];    
}
//获取队尾元素
int myCircularQueueRear(MyCircularQueue* obj) 
{
    if(obj->phead == obj->ptile)
    {
        return -1;
    }
    return obj->ptile == 0 ? obj->a[obj->size]:obj->a[obj->ptile-1];   
}
//进行判空
bool myCircularQueueIsEmpty(MyCircularQueue* obj) 
{
    return obj->phead == obj->ptile;
}
//判满
bool myCircularQueueIsFull(MyCircularQueue* obj) 
{
    return (obj->ptile+1)%(obj->size+1) == obj->phead;
}
//释放节点
void myCircularQueueFree(MyCircularQueue* obj) 
{
    free(obj->a);
    free(obj);
}

/**
 * Your MyCircularQueue struct will be instantiated and called as such:
 * MyCircularQueue* obj = myCircularQueueCreate(k);
 * bool param_1 = myCircularQueueEnQueue(obj, value);
 
 * bool param_2 = myCircularQueueDeQueue(obj);
 
 * int param_3 = myCircularQueueFront(obj);
 
 * int param_4 = myCircularQueueRear(obj);
 
 * bool param_5 = myCircularQueueIsEmpty(obj);
 
 * bool param_6 = myCircularQueueIsFull(obj);
 
 * myCircularQueueFree(obj);
*/

代码解释

这段代码定义了一个循环队列 MyCircularQueue 的数据结构及其操作函数。循环队列是一种使用固定大小的缓冲区并按循环方式使用它的队列。以下是对每个部分的详细解释:

  1. 循环队列结构体定义

    typedef struct 
    { int* a; // 指向队列元素数组的指针 int phead; // 队列头部的索引 
    int ptail; // 队列尾部的索引 
    int size; // 队列的容量 
    } 
    MyCircularQueue;

  2. 构造器 myCircularQueueCreate

    这个函数用于创建一个新的循环队列,分配一个大小为 k+1 的数组(多一个空间用于处理队列满的逻辑),并初始化队列头尾索引。

  3. 入队操作 myCircularQueueEnQueue

    这个函数用于在队列尾部添加一个新元素。如果队列已满(即 ptail 的下一个位置是 phead 的当前位置),则返回 false;否则,将新值存入尾部,然后按循环方式更新 ptail

  4. 出队操作 myCircularQueueDeQueue

    这个函数用于移除队列头部的元素。如果队列已空(即 phead 等于 ptail),则返回 false;否则,按循环方式更新 phead

  5. 获取队首元素 myCircularQueueFront

    这个函数用于获取队首元素的值。如果队列为空,则返回 -1

  6. 获取队尾元素 myCircularQueueRear

    这个函数用于获取队尾元素的值。如果队列为空,则返回 -1。由于循环队列的尾部可能在数组的开始处,所以需要特殊处理。

  7. 判断队列是否为空 myCircularQueueIsEmpty

    这个函数用于判断队列是否为空,即检查 pheadptail 是否相等。

  8. 判断队列是否为满 myCircularQueueIsFull

    这个函数用于判断队列是否为满,即检查 ptail 的下一个位置是否是 phead 的当前位置。

  9. 释放队列 myCircularQueueFree

    这个函数用于释放循环队列所占用的内存。

  • 65
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值