今天我们来学习两个数据结构的新结构:栈和队列。
1.栈
①栈的概念:
我们之前学过顺序表链表,知道数据在内存中可以以顺序方式存储也可以不顺序的方式存储,它们两个各有优劣,那今天要学的这两个也是线性表,那么先来看一下栈的定义吧。
栈:一种特殊的线性表,其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶,另一端称为栈底。栈中的数据元素遵守后进先出LIFO(Last In First Out)的原则。
我们可以将栈理解为一个枪的弹夹,压子弹的时候最先被压进去的子弹一定是最后被击发出来,最后压进去的子弹一定最先被击发,这种结构就好比栈结构,那么在栈中,“压子弹”和“击发子弹”又叫做:压栈、出栈。
压栈:栈的插入操作叫做进栈/压栈/入栈,入数据在栈顶。
出栈:栈的删除操作叫做出栈。出数据也在栈顶。
②栈的实现:
栈的实现一般可以使用数组或者链表实现,相对而言数组的结构实现更优一些。因为数组在尾上插入数据的代价比较小。因为顺序表插入数据除了尾插之外的插入都是需要挪动数据的,会有性能的消耗,而栈的话插入只是在尾部,出数据也是在尾部,而且使用链表的话指针的指向也要发生改变,因为在进行出栈操作后最后一个数据就无法被找到了,所以考虑种种原因,使用数组实现栈要比用链表实现栈更加方便。
使用数组实现栈的逻辑图:
使用链表实现栈的逻辑图:
③栈的接口实现:
栈的定义:
typedef int STDataType;
typedef struct Stack
{
STDataType* a;//动态开辟的数据
int top; //栈顶指针
int capacity; //栈的容量
}ST;
//top要比capacity少1,因为top指向栈顶,也就是数组的最后一个元素,所以top == capacity - 1
栈的初始化与销毁:
void StackInit(ST* ps)
{
assert(ps);
ps->a = NULL;
ps->top = 0;//ps->top = -1;
ps->capacity = 0;
}
void StackDestroy(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->top = 0;
}
压栈:
void StackPush(ST* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = realloc(ps->a, sizeof(STDataType) * newCapacity);
//在首次开辟内存时realloc和malloc的功能相同
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
ps->a = tmp;
ps->capacity = newCapacity;
}
ps->a[ps->top] = x;
ps->top++;
}
出栈:
void StackPop(ST* ps)
{
assert(ps);
assert(ps->top > 0);
ps->top--;
}
//由于栈是用数组实现,所以直接将数组的大小--就可以把尾上的数据出栈
获取栈顶的元素:
STDataType StackTop(ST* ps)
{
assert(ps);
assert(ps->top > 0);
return ps->a[ps->top - 1];
}
获取栈的大小:
int StackSize(ST* ps)
{
assert(ps);
return ps->top;
}
判断栈是否为空:
bool StackEmpty(ST* ps)
{
//比较暴力的方法:
assert(ps);
//比较温柔的方法:
//if (ps->top == 0)
//{
// return true;
//}
//else
//{
// return false;
//}
return ps->top == 0;
}
2.队列:
今天要学习的第二个结构就是队列,和栈一样同为线性表,但结结构却有明显的不同。
①队列的概念:
队列:只允许在一端进行插入数据操作,在另一端进行删除数据操作的特殊线性表,队列具有先进先出 FIFO(First In First Out) 。入队列:进行插入操作的一端称为队尾;出队列:进行删除操作的一端称为队头。
所以队列和栈的不同就是栈的出栈和压栈都是在栈顶,也就是顺序表的尾部进行,而队列进队列是在队尾,而出队列则是在对头。
②队列的实现:
队列也可以数组和链表的结构实现,使用链表的结构实现更优一些,因为如果使用数组的结构,出队列在数组头上出数据,效率会比较低。因为删除数组的第一个数据后后面的数据都要向前挪动。
③队列的接口实现:
typedef int QDataType;
typedef struct QueueNode
{
struct QueueNode* next;
QDataType data;
}QueueNode;//创建的一个队列中的节点
typedef struct Queue
{
QueueNode* head;
QueueNode* tail;
}Queue;//因为要经常用到head和tail指针,所以用其模拟出队列的结构
队列的初始化和销毁:
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = NULL;
pq->tail = NULL;
}
void QueueDestroy(Queue* pq)
{
assert(pq);
QueueNode* cur = pq->head;
while (cur != NULL)
{
QueueNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
}
队列中元素的打印:
void QueuePrint(Queue* pq)
{
assert(pq);
QueueNode* cur = pq->head;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
入队列:
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode));
newNode->data = x;
newNode->next = NULL;
if (pq->head == NULL)
{
pq->head = pq->tail = newNode;
}
else
{
pq->tail->next = newNode;
pq->tail = newNode;
}
}
出队列:
void QueuePop(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
QueueNode* next = pq->head->next;
free(pq->head);
pq->head = next;
pq->tail = NULL;
}
获取队列首元素:
QDataType QueueFront(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));//断言队列不为空
return pq->head->data;
}
获取队列尾元素:
QDataType QueueBack(Queue* pq)
{
assert(pq);
assert(!QueueEmpty(pq));
return pq->tail->data;
}
队列的大小:
int QueueSize(Queue* pq)
{
assert(pq);
size_t count = 0;
QueueNode* cur = pq->head;
while (cur != NULL)
{
count++;
cur = cur->next;
}
return count;
}
判断队列是否为空:
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->head == NULL;//因为队列出数据只会从队首出,所以只要判断队首是否有队列即可
}
④循环队列:
其实平时无论在考试中还是在练习中循环队列的考察也有不少,与我们讲的队列不同的是循环队列的tail->next就是head,如果head有prev指针的话那么head->prev也就是tail,其循环的结构使其形成一个圆一般。
那么,如何计算队列中的有效数据呢?
循环队列的接口就不再实现了,和上面的非循环的队列大同小异,只是下标略有不同,主要理解循环队列的一些特性即可。