栈是一种特殊的线性表,它增加了一个限制,只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出的原则。
举例,假设我们依次将1,2,3,4压入栈中,压栈就是栈的插入操作,出栈就是删除操作
当我们再全部出栈是,就从原来的1,2,3,4变成了4,3,2,1.
这就是栈的先进后出的原则。
有个问题进栈的是1,2,3,4后进先出一定是4,3,2,1吗?
其实不一定也可以1,2,3,4,比如我进一个出一个,还有其他很多种可能,自己代进去试一下就知道。但3,1,2,4肯定不行,试一下就知道。
既然栈是一种特殊的线性表,那意味着用单链表和顺序表都可以实现,但那种更好呢?
单链表虽然不适合用尾插和尾删,但是我们可以把单链表的头当作栈顶,因为单链表适合头插和头删。
虽然数组有下标随机访问的优势,但是根据栈的后进先出的规则,这里只需要访问栈顶元素。
所以其实两种都还是适合的,下面我就只用顺序表这种方法来实现。
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
//栈顶
int top;
int capacity;
}Stack;
跟顺序表一样
void STInit(Stack* pst)
{
assert(pst);
pst->a = NULL;
//top应该是多少?
//如果是0的话,top指向栈顶元素的下一个
//如果是-1的话,top指向栈顶元素
pst->top = 0;
pst->capacity = 0;
}
1 .top的话两种情况的都可以,只是略有差异,对后面写代码会有一定的影响,这里就用0。
2.一开始为什么容量是0?
后面入栈的时候直接扩容,当realloc的那块空间为0的话,它就会发挥malloc的作用。
void STPush(Stack* pst, STDataType x)
{
assert(pst);
//判断增容
//这里也就能看出top初始化不同的值,所带来的差异
if (pst->top == pst->capacity)
{
int newcapacity = pst->capacity == 0?4 :pst->capacity * 2;
STDataType* tmp = (STDataType*)realloc(pst->a, sizeof(STDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
pst->a = tmp;
pst->capacity = newcapacity;
}
pst->a[pst->top] = x;
pst->top++;
}
每次将数据放在栈顶的位置,再将栈顶往后推。
void STPop(Stack* pst)
{
assert(pst);
assert(pst->top);
pst->top--;
}
出栈只需要把栈顶往回退一格。
注意要对top进行断言,当top为0的时候就不能删了。
STDataType STTop(Stack* pst)
{
assert(pst);
assert(pst->top!=0);
return pst->a[pst->top-1];
}
只需要返回栈顶的前一个位置
int STSize(Stack* pst)
{
assert(pst);
return pst->top;
}
void STDestroy(Stack* pst)
{
assert(pst);
//error错误
//free(pst);
free(pst->a);
pst->top = 0;
pst->capacity = 0;
}
注意,pst不是malloc或者realloc出来的,所以不能用free。
写成这样的话,还需要增加对顺序表的理解,顺序表是用结构体里的一个成员指针去维护的,pst只是这个结构体的地址。
void test1()
{
Stack st;
//记得要初始化
STInit(&st);
STPush(&st, 4);
STPush(&st, 3);
STPush(&st, 2);
STPush(&st, 1);
//获取栈顶元素只能一个一个获取
while (st.top != 0)
{
STPop(&st);
printf("%d ", STTop(&st));
}
//记得销毁栈,避免内存泄漏
STDestroy(&st);
}
队列同样是线性表的一种特殊结构,它与栈不同的是,队列要满足先进先出的特性。
进行插入操作的一端称为队尾 ,出队列进行删除操作的一端称为队头。
这就跟我们排队的顺序是一样的,先排队的先出去。
请问进队列是1,2,3,4,出队列一定是1,2,3,4吗?
答案是一定的。 这个大家可以试一下。
请问队列用单链表实现还是用数组实现好?
1. 这里其实用数组已经很不方便了,数组要在队头删除数据需要挪动数组。
2. 单链表虽然不适合尾插,但是记录尾结点的时候,每次入数据就不用在重新找尾了。
下面就用单链表来实现队列。
首先单链表需要定义一个结点
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QNode;
因为我们要记录尾方便尾插,在加上我们本来就要有单链表头节点的指针来维护,为了方便 ,我们就再定义
一个结构体。
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;
}
void QueueDestroy(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;
}
还是要注意,只有动态开辟的空间才能用free;
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* newnode = (QNode*)malloc(sizeof(QNode));
if (newnode == NULL)
{
perror("malloc fail\n");
return;
}
newnode->data = x;
newnode->next = NULL;
if (pq->ptail == NULL)
{
//也可以不断,以防万一
assert(pq->phead == NULL);
pq->phead = pq->ptail = newnode;
pq->tail->next=NULL;
}
else
{
pq->ptail->next = newnode;
pq->ptail = newnode;
pq->tail->next=NULL;
}
pq->size++;
}
1. 尾插的时候要分类
链表为空链表的时候,这个时候要把新开辟的结点的地址赋值给tail;
链表为非空链表的时候,这个时候需要把新开辟 的结点再尾部链接。
当然如果你创建一个哨兵卫就只需要考虑链接 ,不需要 分类了。
2. 之前写单链表的时候还是要用二级指针,这次为什么不用?
其实仔细分析还是一样,改变的只是结构体的成员而非指向结构体的指针。
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
// 1、一个节点
// 2、多个节点
if (pq->phead->next == NULL)
{
free(pq->phead);
pq->phead = pq->ptail = NULL;
}
else
{
// 头删
QNode* next = pq->phead->next;
free(pq->phead);
pq->phead = next;
}
pq->size--;
}
之前写单链表的时候还是挺简洁不用分类的,为什么这个时候要分类呢?
如果链表只有一个结点的时候,那tail也就等于phead,这时tail不置空就会变成一个野指针!!!
QDataType QueueFront(Queue* pq)
{
assert(pq);
//写成这样增加可读性。
assert(!QueueEmpty(pq));
return pq->phead->data;
}
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->ptail->data;
}
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
总的来说栈和对列的实现只要链表和顺序表这块学的好,写起来还是相对轻松的。顺便还可以复习一下链表和顺序表的知识。