一、栈
1.栈的概念
栈是一种特殊的线性表,其只允许在固定的一端进行元素插入和删除操作。
我们将进行元素插入和删除操作的一段成为栈顶,另一端成为栈底,其栈中的元素出入顺序我们遵循先进先出的原则。
对于栈我们通常有两种操作:进栈/压栈/入栈 与 出栈。
我们将栈的插入操作叫做进栈/压栈/入栈,将栈的删除操作成为出栈。
我们用一个图来形象的表示一下栈。
在图中我们可以看到栈顶位置永远在最上面的那个元素所在的位置,我们也是在栈顶位置进行插入删除操作的,我们形象的把栈画作一个开一口的杯子,我们对杯子无法对底部进行操作,我们只能在杯顶进行操作。
2.栈的实现
对于栈我们通常可以通过数组和链表实现,相对于链表,我们使用数组在其尾部插入数据的代价更小,因此数组会更好一些。
栈我们也分为定长的栈和不定长的栈,在使用的过程中,定长的很显然不那么的适用,当我们需要插入的数据很多时我们就会因为长度固定而无法使用,因此我们使用动态栈。下面是我们实现动态栈的一些代码:
typedef int STDataType;
typedef struct Stack
{
STDataType* _a;
int _top; // 栈顶
int _capacity; // 容量
}Stack;
// 初始化栈
void StackInit(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 StackDestroy(Stack* ps);
void STInit(ST* pst)
{
assert(pst);
pst->a = NULL;
pst->top = 0;
pst->capacity = 0;
}
void STDestroy(ST* pst)
{
assert(pst);
free(pst->a);
pst->a = NULL;
pst->top = pst->capacity = 0;
}
void STPush(ST* pst, STDataType x)
{
assert(pst);
if (pst->top == pst->capacity)
{
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
pst->a = tmp;
pst->capacity = newcapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
void STPop(ST* pst)
{
assert(pst);
pst->top--;
}
STDataType STTop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
return pst->a[pst->top - 1];
}
bool STEmpty(ST* pst)
{
assert(pst);
return pst->top == 0;
}
int STSize(ST* pst)
{
assert(pst);
return pst->top;
}
可以看到我们创建了一个结构体,他包含了一个指针数组与栈顶元素位置还有整个数组的容量。我们创建指针数组有效的避免了普通数组长度固定的问题,用栈顶元素的位置实时记录数组内元素的个数,与容量相结合来判断数组空间是否足够。
接着我们来逐条解析一下代码是如何编写的:
我们将每一个对栈的操作写成函数,我们拥有了结构体,但是我们并没有创建栈,于是我们就初始化一个栈空间,我们将指针数组a设为空,将栈顶元素位置与容量都设置为0,这是我们最开始设置的一个栈,显然什么都没有的栈没什么用,于是我们就需要进行栈操作,我们先来进行插入操作的实现,我们优先进行判空,当不是空时我们就进行下一步操作,我们优先进行判断top与容量的大小,如果二者大小一样,我们就可以判断出来空间不够,然后进行扩展空间的操作,一般情况下我们扩展空间是将空间大小提升两倍,但是我们如果是在初始化后第一次进行扩展空间呢,我们还要乘以二吗,显然是不可行的,0乘以2依旧是0,所以我们分为两种情况,在第一次进行空间扩展的时候我们直接将空间扩展到4数据大小,当我们不是第一次扩展的时候我们依旧进行两倍空间的扩展,这样就能有效的解决问题,如果我们直接使用if语句进行判断对于我们精益求精的同学们而言显然显得有些许笨拙,于是我们就可以使用我们已经学习过的唯一一个三目操作符来简化代码。接着我们便可以使用realloc来进行空间大小的修改。在修改后我们可以对空间进行判断是否成功修改,一般情况下没有这个必要,但是我们严谨一点也不为过。接着我们便可以将数据插入进去,记得top要加一哦。
void STInit(ST* pst)
{
assert(pst);
pst->a = NULL;
pst->top = 0;
pst->capacity = 0;
}
void STPush(ST* pst, STDataType x)
{
assert(pst);
if (pst->top == pst->capacity)
{
int newcapacity = pst->capacity == 0 ? 4 : pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
pst->a = tmp;
pst->capacity = newcapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
接着我们来实现栈的元素删除操作,这个相较而言十分简单,我们只需要将top减一即可,因为我们对栈中访问数据的上限就是top,我们不会去访问top之后的数,即使他在那里也不会访问,后面就算插入我们也每次会将其覆盖新值,不用担心数据被错误使用:
void STPop(ST* pst)
{
assert(pst);
pst->top--;
}
栈操作中我们经常需要调用栈顶元素,于是我们写出了这样一个可以帮助我们获取栈顶元素的函数,我们首先要判空,如果栈为空自然就没有栈顶元素,同时我们还要确认好有效元素个数是否大于0,如果连元素都没有,我们也是占不到栈顶元素的,当我们检查都没问题后我们就进行栈顶元素的返回,注意大小是【】内的位置是top-1哦。
STDataType STTop(ST* pst)
{
assert(pst);
assert(pst->top > 0);
return pst->a[pst->top - 1];
}
有时候我们也需要知道栈内的元素个数,我们就需要一个函数帮助我们知道,在同样的判空操作之后我们直接将top返回即可。
int STSize(ST* pst)
{
assert(pst);
return pst->top;
}
在这段代码实现中我们并没有需求判断栈是否为空而实现不同的操作的操作,但是我们在别的程序上一般是需要使用的,于是我们将其也写出来,还是同样的判空操作,我们返回pst->top == 0,当二者相等的时候,返回true(空),不相等就返回false(非空),我们在这个函数中使用的是bool类型,于是我们在头文件中记得包含stdbool.h库函数。
bool STEmpty(ST* pst)
{
assert(pst);
return pst->top == 0;
}
现在栈的操作学完我们也要学会销毁栈,于是我们写下这样的代码,在判空过后我们将指针数组进行释放,使用free,并将其指向NULL,同时我们将top与capacity都赋值为0即可进行栈的销毁。
void STDestroy(ST* pst)
{
assert(pst);
free(pst->a);
pst->a = NULL;
pst->top = pst->capacity = 0;
}
栈是先进后出的特点,那有没有先进先出的呢?当然有!接下来我们进行队列的学习。
二、队列
我们刚才已经学习完了栈的想关知识,接下来我们学习队列:
1.队列的概念
队列是只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表。
我们画个图来简单展示一下:
在图中我们可以看到,队列就像一个管道,一段进水,另一端出水,遵循先进先出的原则。
2.队列的实现
队列也和栈一样可以使用数组和队列实现,但是使用链表会使其在队头出数据的效率更高,所以我们使用链表来进行实现。
既然我们使用了链表,那么我们就需要创建一个链式结构来帮助我们,于是我们有:
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType val;
}QNode;
我们通过next来连接下一个数据。
接下来我来给出我们需要进行实现的一些函数:
// 初始化队列void QueueInit(Queue* q);// 队尾入队列void QueuePush(Queue* q, QDataType data);// 队头出队列void QueuePop(Queue* q);// 获取队列头部元素QDataType QueueFront(Queue* q);// 获取队列队尾元素QDataType QueueBack(Queue* q);// 获取队列中有效元素个数int QueueSize(Queue* q);// 检测队列是否为空,如果为空返回非零结果,如果非空返回0int QueueEmpty(Queue* q);// 销毁队列void QueueDestroy(Queue* q);
typedef struct Queue
{
QNode* phead;
QNode* ptail;
int size;
}Queue;
void QueueInit(Queue* pq)
{
assert(pq);
pq->phead = NULL;
pq->ptail = NULL;
pq->size = 0;
}
接着我们来进行队列的插入操作,记住是从尾部插入,我们如果要将数据插入,显然是插入一个新的节点,于是我们要创建一个QNode*类型的节点来保存所插入的数据我们将其取名为newNode,我们将其的next指向NULL,将他的val赋值为所插入的数据x,接下来我们便要将该数据插进队列,即与其进行连接,在链接之前我们还要考虑一个问题,链接的话,很显然空是没办法成功链接上的,于是我们就需要分情况进行连接,当ptail为空的时候我们就需要直接将newNode给ptail了,如果不是空,我们就需要将ptial的next指向newNode了,当成功连接后,newNode将会成为新的尾部,所以我们不能忘记将ptial指向插进来的newNode了,成功操作后size也记得加一,便于记录数据个数。我们通过这些操作便可以写出如下的代码啦!
void QueuePush(Queue* pq, QDataType x)
{
QNode* newnode = (QNode*)malloc(sizeof(QNode));
newnode->next = NULL;
newnode->val = x;
if (pq->ptail == NULL)
{
pq->phead->next = pq->ptail = newnode;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
}
pq->size++;
}
接着我们来实现出队列,即在队头删除数据:
首先进行的依旧是进行判空,且数据个数不能为0,我们需要删除头部节点,就需要将其空间进行释放,如果我们直接进行释放的话我们就找不到新的头节点的位置将其变为新的头节点了,所哟我们需要创建一个next将头节点后的一个节点进行记录,当我们释放完了头节点就可以通过其将他变成新的头节点,但是我们还存在一种情况就是如果我们的头部节点数据为空,我们就需要删除其下一个节点(我们已经确保了数据个数不为0),即将其下一个节点ptail指向NULL,同时不要忘了size减一。
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->size != 0);
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
if (pq->phead == NULL)
{
pq->ptail = NULL;
}
pq->size--;
}
接着我们来实现获取头部元素与尾部元素:
在获取头部元素,我们只需要将phead节点的val返回即可,不过我们要进行pq与phead的判空:
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(pq->phead);
return pq->phead->val;
}
获取尾部元素也是与其一样,出来判空,返回尾部的数据即可:
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(pq->ptail);
return pq->ptail->val;
}
我们再来看队列个数的获取:同样也是只需要返回size即可啦:
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
我们再来实现一个函数来判断整个队列是否为空,我们使用bool类型的函数,我们直接将size与0相结合变可以看出来是否为空了,我们就可以直接写出下面的代码:
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
队列的各个实现操作我们都学会了,那么我么接着学习如何销毁我们所创建的队列,销毁自然是要free我们所创建的空间,那我们就直接free这个Queue不就好啦吗?如果我们这样做了,phead与ptail他们的节点空间并没有被释放,仍旧会占用空间,这显然不是我们想看见的,所以我们要一层层的释放,我们需要将节点来一个个的释放,于是有:
void QueueDestory(Queue* pq)
{
assert(pq);
QNode* cur = pq->phead;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->phead = pq->ptail = NULL;
pq->size = 0;
}
依旧使记得把size也赋值为0。
这就是我们所有的队列的实现操作。
下一篇文章我们来讲讲如何使用队列和栈进行互相实现!