目录
栈和队列是基于顺序表和链表基础上实现的一个数据结构,如果有了顺序表和链表的基础,栈和队列?那不是随便拿捏了~
栈
数据结构中的栈与平时说的系统栈不同,但共同的特点都是符合先进后出,后进先出的特点。类似于子弹压弹的样子,先压的子弹会最后被打出。
根据这个特点,学校期末考试通常会考哪个不是栈的出栈顺序这样的题目,这类题目只要简单的模拟分析一下就能得出结论。考虑到栈先进后出,后进先出所以使用顺序表,因为顺序表的尾插和尾删都更加方便,并且其实顺序表在硬件上也比链表具有一些先天性的优势。
对栈分析
用数组实现栈需要考虑下栈具体要实现些什么功能。
首先分析栈的结构。栈是一个数组,而静态的数组实际应用是不实用的,所以应该考虑下栈的扩容问题,考虑到扩容还要对栈进行一个空间释放。
其次是栈的特点,栈是后进先出的,所以栈不存在头删的概念。也就是只需要能完成尾插和尾删,那栈的任务就完成了。
通常我们还会取栈的栈顶元素,还会对栈进行判空,计算栈有多少个元素,这些都可以独立出另一个函数。
总结:需要的函数有:初始化和空间释放,尾删和尾插的功能、还要对栈判空、取栈顶元素和计算元素个数。
结构定义
因为要动态开辟空间,为了判断栈是否为满,要定义一个数组的空间大小,如果空间满了,要增容。
typedef int STDataType;
typedef struct Stack
{
STDataType* arr;//存放数据
int top;//栈顶下标
size_t capacity;//数组容量
}Stack;
初始化
栈有两种初始化方式,一种是最开始下标是-1,另一种方式在下标0位置。
方式一的特点是top顶最开始在-1的位置,当插入元素时应该让 top 先走然后在top下标位置上插入元素,这种方式的top始终都在栈顶的位置。
方式二的特点就是直接在0的位置初始化,top需要插入元素时,直接插入,然后让top向后移动一位。这种方式栈顶的元素在top-1的位置上。
两种方式没有任何差别,取哪一种方式都因人而异,看个人喜好,只要控制好下标位置就不容易出错。
所以我取的是方式二,至于初始化容量也是根据个人喜好:
void StackInit(Stack* ps)
{
assert(ps);
ps->arr = NULL;
ps->top = 0;
ps->capacity = 0;
}
入栈和出栈
入栈
插入元素很简单,重点还是在检查容量问题,与顺序表的检查方式一样。由于开辟空间没有其他函数跟它抢,所以没去独立封装一个函数。
插入元素时,方式一应该先让top先走一步后再插入元素,这个顺序很重要。
void StackPush(Stack* ps, STDataType x)
{
assert(ps);
//检查容量
if (ps->top == ps->capacity)
{
//capacity初始化为0,0乘任意数都等于0,capacity所以要先不等于0
size_t newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
//ps->capacity == 0吗?表达式为true capacity = 4 false capacity *2
ps->arr = (STDataType*)realloc(ps->arr, sizeof(STDataType) * newcapacity);
if (ps->arr == NULL)
{
printf("StackPush()::realloc fail\n");
exit(-1);
}
ps->capacity = newcapacity;
}
#if true
ps->arr[ps->top++] = x;
else false
ps->arr[ps->top] = x;
ps->top++;
#endif
}
出栈
出栈简直不要太简单,让top--一下就ok了。top代表有效数据个数,让top移动到下一个位置,则代表了一位数据被删除,虽然它还在,但不在有效数据个数范围内。唯一值得注意的是当top在下标0位置上,top-1就越界了,所以要判断一下。
void StackPop(Stack* ps)
{
assert(ps);
assert(ps->top > 0);
ps->top--;
}
判空、取栈顶和数组长度
判空
判空很简单,当top等于0时代表一个元素都没有,所以直接return ps->top == 0,如果表达式为真返回true,为假返回false
bool StackEmpty(Stack* ps)
{
assert(ps);
return ps->top == 0;
}
取栈顶
前面说过,初始化top是0的时候栈顶就在top-1的位置,但是当栈为空,top-1就会越界,所以top必须大于0。
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(ps->top > 0);
return ps->arr[ps->top-1];
}
栈长度
top就是栈的长度,直接返回 top下标,方式二 top要返回top+1,因为下标从0开始,所以比实际长度少n-1个。
int StackSize(Stack* ps)
{
assert(ps);
return ps->top;
}
释放空间
由于数组是从堆区向操作系统借来的,要把空间还给操作系统。
void StackDestory(Stack* ps)
{
assert(ps);
free(ps->arr);
ps->arr = NULL;
ps->capacity = ps->top = 0;
}
栈(代码传送门)
//Stack.h
#pragma once
#include<stdio.h>
#include<stdbool.h>
#include<assert.h>
#include<stdlib.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* arr;
int top;
size_t capacity;
}Stack;
void StackInit(Stack* ps);
void StackDestory(Stack* ps);
//入栈
void StackPush(Stack* ps, STDataType x);
//出栈
void StackPop(Stack* ps);
//判空
bool StackEmpty(Stack* ps);
//栈顶数据
STDataType StackTop(Stack* ps);
int StackSize(Stack* ps);
//Stack.c
#include"Stack.h"
void StackInit(Stack* ps)
{
assert(ps);
ps->arr = NULL;
ps->top = 0;
ps->capacity = 0;
}
void StackDestory(Stack* ps)
{
assert(ps);
free(ps->arr);
ps->arr = NULL;
ps->capacity = 0;
ps->top = 0;
}
//入栈
void StackPush(Stack* ps, STDataType x)
{
assert(ps);
//检查容量
if (ps->top == ps->capacity)
{
size_t newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
ps->arr = (STDataType*)realloc(ps->arr, sizeof(STDataType) * newcapacity);
if (ps->arr == NULL)
{
printf("StackPush()::realloc fail\n");
exit(-1);
}
ps->capacity = newcapacity;
}
#if false
ps->arr[ps->top++] = x;
#else true
ps->arr[ps->top] = x;
ps->top++;
#endif
}
//出栈
void StackPop(Stack* ps)
{
assert(ps);
assert(ps->top > 0);
ps->top--;
}
//判空
bool StackEmpty(Stack* ps)
{
assert(ps);
return ps->top == 0;
}
//栈顶数据
STDataType StackTop(Stack* ps)
{
assert(ps);
assert(ps->top > 0);
return ps->arr[ps->top-1];
}
//数组长度
int StackSize(Stack* ps)
{
assert(ps);
return ps->top;
}
队列
队列就顾名思义了,像排队一样,与栈不同的是,无论如何出队,它的顺序都是唯一的,先进先出,绝对的公平。
队列分析
先假设使用数组来实现队列,入队列是非常的轻松,但是当出队列时,数组的弊端就出现了。当元素出队列时,要对后面的每一个元素都需要移动一次,时间复杂度为O(N-1),效率明显的低了。使用直接不考虑数组,用头删更加链表。
但是链表找尾结点又是O(N),因为不存在尾删的问题,所以可以给链表加一个尾指针,让链表在尾插的间复杂度又是O(1)。
队列要实现的功能与栈基本相同,只不过栈只需要找栈顶,而队列还要找一个队头数据。
结构体
首先要定义一个链表的基本结构,为了记录头结点和尾结点,还要定义另一个结构体,成员是用来记录链表的头结点和尾结点位置的指针。
typedef int QNDataType;
typedef struct QueueNode
{
QNDataType data;
struct QueueNode* next;
}QNode;
typedef struct Queue
{
QNode* head;
QNode* tail;
}Queue;
入队和出队
入队
入队就是单链表尾插,因为有了尾结点tail的buff加持,我们可以直接就在tail结点的后面插入一个新结点。插入要考虑一下,当没有结点时应该把head和tail都等于新结点,之后head不动,让tail指针自己去走。
void QueuePush(Queue* pq, QNDataType x)
{
assert(pq);
QNode* newNode = (QNode*)malloc(sizeof(QNode));
assert(newNode);
newNode->data = x;
newNode->next = NULL;
if (pq->head == NULL)
{
//没有结点
pq->head = pq->tail = newNode;
}
else
{
pq->tail->next = newNode;
pq->tail = newNode;
}
}
出队
出队直接把头结点的删掉然后让头结点等于头结点的下一个位置即可。但是!如果当只有一个结点时,头结点的next就是空指针,对空指针进行解引用是不允许的,所以这种情况需要被单独处理才行。
void QueuePop(Queue* pq)
{
assert(pq);
if (pq->head == NULL)
{
free(pq->head);
pq->head = NULL;
pq->tail = NULL;
}
else
{
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
}
判空、长度、队头队数据
判空
判空和栈一样,如果head指针是空说明链表是个空链表,直接返回就行。
bool QueueEmpty(Queue* pq)
{
return pq->head == NULL;
}
长度
计算长度和释放队列可能是队列最慢的函数了,因为需要去遍历链表。当然也可以不遍历,只要在记录结点指针的结构体加一个计算链表长度的结构体成员就行,当插入一个元素时size++,删除元素时size--。
size_t QueueSize(Queue* pq)
{
assert(pq);
assert(pq->head);
size_t size = 0;
QNode* cur = pq->head;
while (cur)
{
size++;
cur = cur->next;
}
return size;
}
取队头、队尾结点数据
头指针不为空,返回头结点指向的数据就行了。
QNDataType QueueFront(Queue* pq)
{
assert(pq);
assert(pq->head);
return pq->head->data;
}
取尾结点与取头结点方式一样:
QNDataType QueueBack(Queue* pq)
{
assert(pq);
assert(pq->head);
return pq->tail->data;
}
释放空间
链表是一个一个malloc出来的,所以要一个一个释放。
void QueueDestory(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = NULL;
pq->tail = NULL;
}
队列(代码传送门)
//QUeue.h
#pragma once
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
#include<stdbool.h>
typedef int QNDataType;
typedef struct QueueNode
{
QNDataType data;
struct QueueNode* next;
}QNode;
typedef struct Queue
{
QNode* head;
QNode* tail;
}Queue;
//初始化
void QueueInit(Queue* pq);
//释放队列
void QueueDestory(Queue* pq);
//插入
void QueuePush(Queue* pq, QNDataType x);
//删除
void QueuePop(Queue* pq);
//判空
bool QueueEmpty(Queue* pq);
//长度
size_t QueueSize(Queue* pq);
//队头数据
QNDataType QueueFront(Queue* pq);
//队尾数据
QNDataType QueueBack(Queue* pq);
//Queue.c
#include"Queue.h"
//初始化
void QueueInit(Queue* pq)
{
assert(pq);
pq->head = NULL;
pq->tail = NULL;
}
//释放队列
void QueueDestory(Queue* pq)
{
assert(pq);
QNode* cur = pq->head;
while (cur)
{
QNode* next = cur->next;
free(cur);
cur = next;
}
pq->head = NULL;
pq->tail = NULL;
}
//插入
void QueuePush(Queue* pq, QNDataType x)
{
assert(pq);
QNode* newNode = (QNode*)malloc(sizeof(QNode));
assert(newNode);
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);
if (pq->head == NULL)
{
free(pq->head);
pq->head = NULL;
pq->tail = NULL;
}
else
{
QNode* next = pq->head->next;
free(pq->head);
pq->head = next;
}
}
//判空
bool QueueEmpty(Queue* pq)
{
return pq->head == NULL;
}
//长度
size_t QueueSize(Queue* pq)
{
assert(pq);
assert(pq->head);
size_t size = 0;
QNode* cur = pq->head;
while (cur)
{
size++;
cur = cur->next;
}
return size;
}
//队头数据
QNDataType QueueFront(Queue* pq)
{
assert(pq);
assert(pq->head);
return pq->head->data;
}
//队尾数据
QNDataType QueueBack(Queue* pq)
{
assert(pq);
assert(pq->head);
return pq->tail->data;
}
只要学好顺序表和链表,写一个栈和队列那就是轻松加愉快