目录
栈
1.栈的结构
栈中的数据遵循先进后出(Last in First Out)的原则。我们常见的出/入数据的操作,在栈中常成为出栈/压栈。

2.栈的实现
栈一般可以使用数组或链表来实现,而数组的方式实现较优,因为数组在尾插数据的处理上的代价较小。

使用链表的方式实现时,若进行出栈操作,每次都需要遍历链表一次找尾进行内存释放实现出栈的操作;相反,使用数组的方式实现时,只需要关注数组的容量是否存满,实现出栈操作时只需要将top(存放在数组内的数据量)自减1即可。出栈操作在两种实现方式的时间复杂度上,数组为O(1),链表为O(N)。可见,使用数组实现栈更优。
以下是使用C语言实现栈的代码:
typedef int STDataType;
//定义栈
typedef struct Stack
{
STDataType* a;
int capacity; //栈的容量
int top; //栈内的数据个数
}ST;
void STInit(ST* ps); //初始化栈
void STDestroy(ST* ps); //销毁栈
void STPush(ST* ps, STDataType x); //入栈
void STPop(ST* ps); //出栈
int STSize(ST* ps); //获取栈内的数据大小
bool STEmpty(ST* ps); //检查栈内数据是否为空
STDataType STTop(ST* ps); //获取栈顶数据元素
这些是栈常见的一些基本操作,入栈、出栈、栈的初始化/销毁、栈内数据量、栈顶元素和栈是否为空。
首先,初始化栈:
//初始化
void STInit(ST* ps)
{
assert(ps);
ST* phead = (ST*)malloc(sizeof(ST) * 4);
if (phead == NULL)
{
perror("malloc fail");
exit(-1);
}
ps->a = phead;
ps->capacity = 4;//初始化的容量
ps->top = 0; //top指栈顶元素的下一个位置
}
这里需要注意栈内top的取值,若top初始化取值为0,则top代表的是栈内的数据量和指栈顶元素的下一个位置;若top初始化取值为-1,则top代表的是指向栈顶元素的位置。两种取值都是可行的,但是不同的取值在后面的代码实现也有一些细微的差别,而接下来的代码中我会使用top初始化取值为0。
既然使用了动态内存申请的栈空间,自然在程序退出时需要释放掉,所以需要销毁栈:
//销毁
void STDestroy(ST* ps)
{
assert(ps);
free(ps->a);
}
接下来,需要实现入栈和出栈的操作:
//入栈
void STPush(ST* ps, STDataType x)
{
assert(ps);
if (ps->capacity == ps->top)
{
ST* phead = (ST*)realloc(ps->a, sizeof(ST) * ps->capacity * 2);
if (phead == NULL)
{
perror("realloc fail");
exit(-1);
}
ps->a = phead;
ps->capacity *= 2;
}
ps->a[ps->top] = x;
ps->top++;
}
这里的操作类似顺序表的插入数据,原理其实都差不多。
//出栈
void STPop(ST* ps)
{
assert(ps);
assert(!STEmpty(ps));
ps->top--;
}
这里的出栈操作就可以看出来使用数组实现的好处了。加上在进行出栈操作的过程中是不能对空的数组进行出栈的,这样会令top的值为负数,导致数组越界报错,所以在进行出栈的过程中需要对传入的指针进行判空。
剩下的一些操作都较为简单,获取栈中的有效数据个数:
//获取栈中有效数据元素个数
int STSize(ST* ps)
{
assert(ps);
return ps->top;
}
检查栈内数据是否为空:
//检查栈内数据数量是否为空
bool STEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
获取栈顶元素:
//获取栈顶元素
STDataType STTop(ST* ps)
{
assert(ps);
assert(!STEmpty(ps));
return ps->a[ps->top - 1];
}
这里有个小细节,返回值中需要对top-1才能访问到栈顶元素,因为一开始的top的设定就是指向下一个栈顶元素的位置。而且,这里也需要对栈进行判空,因为空的栈是无法获取栈顶元素的。
以上就是实现栈的代码,接下来我会继续讲述队列的实现。
队列
1.队列的结构
队列与栈不同的是,它遵循先入先出(First in First Out)的原则,即在队尾插入数据,称入队;在对头删除数据,称出队。

2.队列的实现
队列与栈相同,都可以使用数组或链表进行实现,但是队列使用链表却较优。原因是链表在删除队头数据操作时较为容易。


可见,我们只需要增加一个指向队尾的指针就可以快速进行入队的操作。出队时,也只需要将head指针指向下一个即可,反观数组的头删操作比较复杂。
以下是使用C语言实现队列的代码:
typedef int QueueDataType;
typedef struct QListNode
{
QueueDataType data;
struct QListNode* next;
}QListNode;
typedef struct Queue
{
QListNode* head; //指向队头的指针
QListNode* tail; //指向队尾的指针
int size; //队列内的数据数量
}Queue;
//初始化队列
void QueueInit(Queue* pq);
//队尾入队
void QueuePush(Queue* pq, QueueDataType x);
//队头出队
void QueuePop(Queue* pq);
//获取队头元素
QueueDataType QueueFront(Queue* pq);
//获取队尾元素
QueueDataType QueueBack(Queue* pq);
//获取队列中有效元素个数
int QueueSize(Queue* pq);
//检测队列是否为空,为空返回true/非零,为非空返回false/0
int QueueEmpty(Queue* pq);
//销毁队列
void QueueDestroy(Queue* pq);
以上是队列的基本操作,初始化/销毁队列、数据入队/出队、获取队头/队尾元素、获取队列数据元素个数和检查队列是否为空。这里使用了结构体的嵌套,目的是简化传参的值。
首先是初始化队列:
//初始化队列
void QueueInit(Queue* pq)
{
pq->head = pq->tail = NULL;
pq->size = 0;
}
销毁队列:
//销毁队列
void QueueDestroy(Queue* pq)
{
assert(pq);
QListNode* cur = pq->head;
while (cur)
{
pq->head = cur->next;
free(cur);
cur = pq->head;
}
pq->tail = NULL;
}
入队:
//队尾入队
void QueuePush(Queue* pq, QueueDataType x)
{
QListNode* newnode = (QListNode*)malloc(sizeof(QListNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
if (!QueueEmpty(pq))
{
pq->tail->next = newnode;
pq->tail = newnode;
}
else
{
pq->head = pq->tail = newnode;
}
pq->size++;
}
入队时会出现两种情况,一种是队列为空,此时需要将头指针和尾指针赋值;另一种是队列不为空,这种情况可以直接在队尾尾插数据。
出队:
//队头出队
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
if (pq->head->next == NULL)
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QListNode* next = pq->head;
pq->head = next->next;
free(next);
}
pq->size--;
}
在出队时也会出现两种情况,一种是队列只剩一个数据时,需要注意同时将尾指针置空,避免出现野指针的访问问题;另一种是队列还有数据,此时只需要进行删除操作即可。同时需要注意传入的指针需要对队列进行判空,因为出队是无法对空队列进行删除的。
获取队头元素:
//获取队头元素
QueueDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->head->data;
}
获取队尾元素:
//获取队尾元素
QueueDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
在获取队头和队尾元素时,都需要对队列进行判空,因为空队列是没有数据可以获取的。
检查队列数据是否为空:
//检测队列是否为空,为空返回true/非零,为非空返回false/0
int QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
获取队列中有效元素个数:
//获取队列中有效元素个数
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
以上就是C语言实现队列的代码。
3.循环队列
循环队列也称为环形队列是一种特殊的队列,它既可以使用数组实现,也可以使用通过循环链表实现。

在实际的使用中,一般会为队列多开一个数据空间来表示环形队列满了的状态,若front和rear同时相等,会有两种情况,第一种是队列为空两数相等;另一种是队列已满两数相等,因此为避免冲突,多开出一个空间,此时rear+1==front才能表示满了的状态。
环形队列如何实现呢?我会在下一篇文章进行讲解,谢谢大家的细心阅读。
文章详细介绍了栈和队列这两种数据结构,包括它们的结构特性、实现方式以及在C语言中的具体代码实现。栈遵循先进后出原则,通常用数组或链表实现,文中推荐使用数组。队列遵循先入先出原则,链表实现更优。同时,文章还提到了循环队列的概念,指出环形队列在满状态判断上的特殊性。
1200

被折叠的 条评论
为什么被折叠?



