栈
栈的概念
- 栈是一种只允许在一端进行插入和删除操作的线性表。它是一种操作受限的线性表。在表中允许进行插入和删除的一端称为“栈顶”,另一端称为“栈底”。
- 栈的插入操作通常称为“入栈”或“进栈”(push),而栈的删除操作则称为“出栈”或“退栈”(pop)。当栈中无数据元素时称为“空栈”。
- 栈具有“后进先出”的特性。
栈的结构分析
栈只能在栈顶进行操作,最后进栈的元素第一个出栈 将栈转化成一个顺序表或链表,压栈即为对该线性表进行尾插,而入栈即为对该线性表进行尾删,而完成该操作最具优势的结构自然是顺序表。
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top; // 栈顶
int capacity; // 容量
}Stack;
栈的初始化
top可能会有两种取值:0和-1。 对于top取0,即top表示的是栈顶的下一个元素,其功能相当于size。 而若top取-1,则top表示的是栈顶的元素,当栈没有元素时,对a进行解引用a[top]则会报错。
void StackInit(Stack* ps)
{
assert(ps);
ps->a = NULL;
ps->top = 0;
ps->capacity = 0;
}
判断栈是否为空
由于top就是当前元素个数,所以判断栈是否为空,就是判断top是否为0
bool StackEmpty(Stack* ps)
{
assert(ps);
return ps->top == 0;
}
栈顶插入数据
插入数据之前首先要判断空间容量的问题,如果空间不足,就需要进行扩容,top标记当前栈中有几个元素,capacity标记当前栈最多容纳几个元素。
void StackPush(Stack* ps, STDataType data)
{
assert(ps);
if (ps->top == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, sizeof(STDataType) * newCapacity);
if (tmp == NULL)
{
perror("realloc fail");
return ;
}
ps->a = tmp;
ps->capacity = newCapacity;
}
ps->a[ps->top] = data;
ps->top++;
}
栈顶删除数据
删除栈顶元素即出栈,出栈前需要断言,判断当前是否有元素可以删除,调用StackEmpty函数。
void StackPop(Stack* ps)
{
assert(ps);
assert(!StackEmpty(ps));
ps->top--;
}
获取栈顶元素
top所指向数组的下表总是比实际栈顶元素下标大1,所以栈顶元素的下标是top-1。
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(!StackEmpty(ps));
return ps->a[ps->top - 1];
}
获取栈中有效元素个数
因为数组元素下标从0开始,因此top就表示栈中有效元素个数。
int StackSize(Stack* ps)
{
assert(ps);
return ps->top;
}
栈的销毁
销毁栈,无非就是将ps->a指向的空间释放掉。这块空间释放后,所有数据也就会跟着销毁。此时的top和capacity就要一起变为0。而ps->a由于被释放,变为了野指针,那么也是需要进行置空。
void StackDestroy(Stack* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = 0;
ps->capacity = 0;
}
队列
队列的概念
只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出FIFO(First In First Out) 的特性。
队列结构分析
我们需要定义的是链式结构的队列,即单链表为底层实现的。所以需要定义单链表结构来存储数据。然后,定义队列,队列里需要定义两个指向单链表的指针,一个是指向单链表头结点的指针,另一个则用来保存尾结点地址的指针。最后,还需定义一个记录当前队列元素个数的变量,用于遍历队列和判空。
//链式队列节点
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* q)
{
assert(q);
q->phead = NULL;
q->ptail = NULL;
q->size = 0;
}
队列判空
由于在定义队列时,增加了额外的记录当前有效数据个数的变量,所以这里直接返回该变量与0比较的值即可。
bool QueueEmpty(Queue* q)
{
assert(q);
return q->size == 0;
}
队列入数据
首先还是需要进行断言判断指针有效性。其次,就是动态开辟结点,并对新节点初始化。然后,进行头插操作,记得第一次插入时,需要单独判断两个指针同时指向新结点。最后,记得让ptail指针指向最后一个结点和size++。
void QueuePush(Queue* q, QDataType x)
{
assert(q);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail\n");
return;
}
newnode->data = x;
newnode->next = NULL;
if (q->phead == NULL)
{
assert(q->ptail == NULL);
q->phead = q->ptail = newnode;
}
else
{
q->ptail->next = newnode;
q->ptail = newnode;
}
q->size++;
}
队列出数据
当队列为空时,不能进行删除操作,所以先断言判断一下是否为空队列。然后,就是头删链表的操作,需要注意的是当只剩一个结点时,对ptail进行特殊处理避免野指针。最后,就是让记录有效元素的size--一下。
void QueuePop(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
if (q->phead->next == NULL)
{
free(q->phead);
q->phead = q->ptail = NULL;
}
else
{
QNode* next = q->phead->next;
free(q->phead);
q->phead = next;
}
q->size--;
}
获取队头数据
由于在定义队列时,定义了一个记录当前有效元素个数的变量,所以直接返回该变量当前的值即可。
QDataType QueueFront(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->phead->data;
}
获取队尾数据
直接返回第一个结点的data即可。
QDataType QueueBack(Queue* q)
{
assert(q);
assert(!QueueEmpty(q));
return q->ptail->data;
}
获取队列中有效元素个数
在定义队列时,定义了一个记录当前有效元素个数的size变量,所以这里直接返回该变量当前的值即可。
int QueueSize(Queue* q)
{
assert(q);
return q->size;
}
队列销毁
首先,断言一下指针有效性。然后,依次释放每个结点。最后释放最后一个结点时,把ptail和phead置空,size置零。
void QueueDestroy(Queue* q)
{
assert(q);
QNode* cur = q->phead;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
q->phead = q->ptail = NULL;
q->size = 0;
}
总结
- 栈是限定仅在表尾进行插入和删除的线性表,又称后进先出的线性表。有顺序栈和链栈。栈的主要操作有进栈和出栈,对于顺序栈的进栈和出栈要注意判断栈满和栈空。
- 队列是一种先进先出的线性表。只允许在一段进行插入,一段进行删除。有循环队列和链队。队列的主要操作就是进队和出队,对于顺序表示的循环队列的进队和出队操作要注意判断队空和队满。
- 队列和栈都不支持随机访问和随机插入。