栈
一、栈的概念及结构
栈(Stack)是一种特殊的线性表,其只允许在表的固定的一端进行插入和删除操作。
栈顶:进行插入数据和删除数据的一端。
栈底:相对于栈顶的另一端。
原则:栈的数据元素遵循后进先出LIFO( Last In First Out )的原则
通过图也能明显看出栈的特点只有当栈顶元素出栈后,才能出后面的元素,有点类似于手枪的弹夹,用的时候先一个一个把子弹压进去,最后压进去的子弹最先打出,所以有的进栈也称为压栈。
二、栈的实现
栈通常使用顺序存储结构来实现,因为像数组结构在末尾增加或者删除数据就比较方便。
需要实现的接口:
在使用栈时为了有足够的空间我们采用的动态开辟数组的方式来进行数据存储。
1. 栈的初始化与销毁
//初始化栈
void STInit(ST* ps)
{
assert(ps);
ps->a = (STDataType*)malloc(sizeof(STDataType) * DEFAULT_SIZE);
if (NULL == ps->a)
{
perror("STInit fail");
exit(1);
}
ps->capcity = DEFAULT_SIZE;
ps->top = 0;
}
//销毁栈
void STDestroy(ST* ps)
{
assert(ps);
free(ps->a);
}
初始化时我们会对栈分配初始化的空间,初始化大小可以根据需要自行更改。
注意:在初始化栈中特别要注意top指向的位置,例如我这里栈顶top赋值的是0,就默认top指向的是0表示栈顶元素的下一个位置。有的地方初始化的是-1,那就表示top指向的是栈顶元素的位置。
2. 入栈和出栈
//入栈
void STPush(ST* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capcity) //检查是否已满,已满则扩容
{
STDataType* empty = (STDataType*)realloc(ps->a, sizeof(STDataType) * (2 * ps->capcity));
if (empty == NULL)
{
perror("realloc fail");
return;
}
ps->a = empty;
ps->capcity = 2 * ps->capcity;
}
ps->a[ps->top] = x;
ps->top++;
}
//出栈
void STPop(ST* ps)
{
assert(ps);
assert(!STEmpty(ps));
ps->top--;
}
注意:
入栈的时候主要注意的就是判断空间是否存满,判断依据和top有关,如果初始化top=-1则这里的判断条件应该是ps->top+1 == ps->capacity
出栈时需要判断是否为空,栈内元素若已为空还继续出栈则会出栈失败。并且在实现出栈的方法里,并不需要销毁栈顶的元素,只用改变栈顶指针指向的位置,使得无法访问到栈顶元素,也就实现了“销毁”的效果。
3. 其余操作
//返回栈顶元素
STDataType STTop(ST* ps)
{
assert(ps);
assert(!STEmpty(ps));
return ps->a[ps->top - 1];
}
//返回元素个数
int STSize(ST* ps)
{
assert(ps);
return ps->top;
}
//判断是否已为空
bool STEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
4. 性能分析
顺序栈的入栈和出栈都比较简单,时间复杂度为均为O(1),但在空间上顺序存储的栈需要一大片连续的空间,对于内存浪费比较大,如果当栈满时还会进行扩容也会影响时间效率。如果换做链式栈则会有所改善,空间利用率会更高。总之不同的实现方式有他各自的优势。
队列
一、队列的概念及结构
队列(Queue)是只允许在一端进行插入操作,而在另一端进行删除操作的线性表。
队头:进行数据删除操作的一端。
队尾:进行数据插入操作的一端。
原则:队列具有先进先出FIFO( First In First Out )的原则。
队列在我们生活中也非常常见,像我们在各种场所需要进行排队,一般情况下都是先来先进入队列依次进行一系列活动。
二、队列的实现
队列的实现我们也可以采用顺序结构或者链式结构,但因为需要在队头进行操作,所以链式结构是比较好的,下面我将用单链表的形式实现队列。
需要实现的接口:
1. 队列的初始化和销毁
//初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = pq->tail = NULL;
pq->size = 0;
}
//销毁
void QueueDestroy(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = pq->tail = NULL;
pq->size = 0;
}
因为我们在实现队列时定义了两个结构体,所以在销毁时需要先把队列中每个结点依次释放掉,再将头和尾指针置为空。
2. 入队列及出队列
入队列代码:
//入队列
void QueuePush(Queue* pq, QDataType x)
{
assert(pq);
QNode* node = (QNode*)malloc(sizeof(QNode));//为新入队的元素申请一个结点空间
if (node == NULL)
{
perror("malloc fail");
return;
}
node->next = NULL;
node->data = x;
if (pq->head == NULL)//如果第一次入队则需将头尾指针指向该结点
{
assert(pq->tail == NULL);
pq->head = pq->tail = node;
}
else
{
pq->tail->next = node;
pq->tail = node;
}
pq->size++;
}
出队列代码;
//出队列
void QueuePop(Queue* pq)
{
assert(pq);
assert(pq->head && pq->tail);//判断有没有元素
if (pq->head->next == NULL)//判断是否为仅有一个元素
{
free(pq->head);
pq->head = pq->tail = NULL;
}
else
{
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
pq->size--;
}
3. 其余操作
//队列元素个数
int QueueSize(Queue* pq)
{
assert(pq);
return pq->size;
}
//队列是否为空
bool QueueEmpty(Queue* pq)
{
assert(pq);
return pq->size == 0;
}
//取队头元素
QDataType QueueFront(Queue* pq)
{
assert(pq);
return pq->head->data;
}
//取队尾元素
QDataType QueueBack(Queue* pq)
{
assert(pq);
return pq->tail->data;
}
4. 循环队列
在实际中还一种常用的队列叫做循环队列,可以用数组实现也能用链表实现,下面简单介绍一下用顺序存储结构实现的循环队列。
初始时:Q->front = Q->rear=0。
队首指针进1:Q->front = (Q->front + 1) % MAXSIZE。
队尾指针进1:Q->rear = (Q->rear + 1) % MAXSIZE。
队列长度:(Q->rear - Q->front + MAXSIZE) % MAXSIZE。
注意:在循环队列中,我们通常会舍去掉一个空间来表示空间已满,如果不这条rear就会和front指向同一个位置,这样不容易区分队满和队空。
队满条件: (Q->rear + 1)%Maxsize == Q->front
队空条件仍: Q->front == Q->rear
队列中元素的个数: (Q->rear - Q ->front + Maxsize)% Maxsize