文章目录
栈
什么是栈?
栈是线性表的一种。
它只允许在固定的一端进行插入和删除元素操作
。
而进行数据插入和删除操作的一端称为栈顶,另一端称为栈底
。
所以栈增删元素就有了我们常说的先进后出(Last In First Out)
:
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
栈的实现
栈的实现有两个选择,一个是顺序表,一个是链表。
这里是用顺序表去实现的。
而为什么不用链表,原因如下:
顺序表尾端为栈顶,每次入栈或出栈都是一次尾插或尾删,实现起来比较简单,且时间复杂度为O(1)。
如果选用单链表去实现,尾插或尾删的操作比较复杂,且时间复杂度为O(N)。
如果选用带头双向循环链表,虽然时间复杂度也是O(1),但结构不如顺序表简单。
综上,栈用顺序表来实现。
代码实现
由于顺序表有动态静态之分,
显然动态版本更灵活,
所以下面是用的动态可扩容的顺序表。
栈的声明
与普通顺序表一样,
取代 size,
看起来更直观。
data 指向后续动态开辟的数组。
typedef int DataType;
typedef struct Stack
{
DataType* data;
int top; //栈顶
int capacity; //容量
}Stack;
初始化栈
初始先给四个空位,
后续不够方便扩容。
栈顶初始化为 0。
void StackInit(Stack* ps)
{
assert(ps);
ps->data = (DataType*)malloc(sizeof(DataType) * 4);
ps->top = 0;
ps->capacity = 4;
}
判断栈是否为空
为提高代码可读性,
所以返回值采用布尔类型。
为空则返回真,
非空则返回假。
bool StackEmpty(Stack* ps)
{
return ps->top == 0;
}
入栈
入栈操作实际上就是顺序表尾插。
插入数据之前要先判断栈是否已满,
满了则扩容,
这里容量扩为原来的二倍。
因为栈的操作只有入栈插入数据,
所以增容只在入栈操作中用到,
无需单拎出来弄成一个函数。
void StackPush(Stack* ps, DataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
DataType* tmp = (DataType*)realloc(ps->data, sizeof(DataType) * ps->capacity * 2);
if (tmp == NULL)
{
perror("StackPush");
exit(-1);
}
ps->data = tmp;
ps->capacity *= 2;
}
ps->data[ps->top] = x;
ps->top++;
}
出栈
出栈就是顺序表尾删。
无需操作数据。
而当栈为空的时候无需删除,
所以用 assert 断言一下是否非空。
void StackPop(Stack* ps)
{
assert(ps);
assert(!StackEmpty(ps));
ps->top--;
}
返回栈顶的值
同样需要判断栈是否为空。
DataType StackTop(Stack* ps)
{
assert(ps);
assert(!StackEmpty(ps));
return ps->data[ps->top - 1];
}
获取栈中有效元素的个数
int StackSize(Stack* ps)
{
return ps->top;
}
销毁栈
由于 data 指向的数组是动态开辟出来的,
所以最后不要忘了释放掉。
void StackDestroy(Stack* ps)
{
free(ps->data);
ps->data = NULL;
ps->capacity = 0;
ps->top = 0;
}
队列
什么是队列?
队列其实是很形象的。就比如排队做核酸的队伍就是一个队列,排队来得早的人先做,来的晚的人后做。又比如医院挂号,挂得早会诊就早,所以医院的挂号机的原理就是依托队列实现的。
所以与栈进行比较,栈是后进先出,
而队列则是先进先出(*First In First Out*)
。
所以队列就是只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表
。
队列也有两种基本操作:
-
入队列:进行插入操作的一端称为队尾
-
出队列:进行删除操作的一端称为队头
队列的实现
摆在这里的还是那个问题,
队列是用顺序表去实现还是用链表去实现?
这里用的是链表,而且是单链表,原因很简单:
用顺序表的话出队列相当于头删,头删则需要后边的元素向前覆盖,时间度复杂度为O(N)。
而如果用链表,出队列即头删的时间复杂度仅为O(N),且单链表本身就已经足够实现队列,没必要用结构更复杂的带头双向循环链表。
代码实现
队列的声明
一个完整的队列有头有尾,
其中的 head 指向链表的头,
tail 指向链表的尾。
//链表节点
typedef int QDataType;
typedef struct QueueNode
{
QDataType data;
struct QueueNode* next;
}QueueNode;
//队列
typedef struct Queue
{
struct QueueNode* head;
struct QueueNode* tail;
}Queue;
初始化队列
队列初始为空,
所以将首尾置空即可。
void QueueInit(Queue* pq)
{
pq->head = NULL;
pq->tail = NULL;
}
判断队列是否为空
为提高代码的可读性,
这里的返回值用布尔类型。
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL;
}
队尾入队列
入队列需要注意:
当队列为空时,
由于 head 和 tail 都是空指针,
所以需要 head 和 tail 同时指向新节点。
如果队列非空,
则直接动 tail 即可。
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QueueNode* newnode = (QueueNode*)malloc(sizeof(QueueNode));
if (newnode == NULL)
{
perror("QueuePush");
exit(-1);
}
newnode->next = NULL;
newnode->data = x;
if (QueueEmpty(pq))
pq->head = pq->tail = newnode;
else
{
pq->tail->next = newnode;
pq->tail = newnode;
}
}
队头出队列
队头出队列的前提是队列非空,
所以前面需要断言一下。
当队列只有一个元素时,
此时出队列后就变成空队列了。
所以 free 掉队头元素时 head 和 tail 都变成了空指针。
当队列有两个或两个以上的元素时,
直接进行头删操作。
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
if (pq->head->next == NULL)
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QueueNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
}
取队头的数据
取队头的数据前提是队列非空,
所以前面用 assert 断言一下。
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
取队尾的数据
取队尾的数据前提是队列非空,
所以前面用 assert 断言一下。
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
返回队列的数据数
这个就不用判空了,
直接遍历一遍。
int QueueSize(Queue* pq)
{
int size = 0;
QueueNode* cur = pq->head;
while (cur)
{
++size;
cur = cur->next;
}
return size;
}
销毁队列
队列是链表,
是一个个动态开辟出来的节点,
所以需要遍历链表,
分别释放掉每一个节点。
void QueueDestroy(Queue* pq)
{
assert(pq);
QueueNode* cur = pq->head;
while (cur)
{
QueueNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = NULL;
pq->tail = NULL;
}