目录
1. 栈的介绍
1.1 栈的概念
栈是插入和删除只能在一个位置上进行的线性表,该位置是线性表的末端,被称为栈顶。
1.2 栈的优点
(1)栈具有先进后出的特性,并且只能操作栈顶的元素,这个特性使得栈能够实现快顺序存储。
(2)对于局部变量和函数参数而言,它们被声明时会被存储在栈上,函数结束之后便被释放,这样极大的节省了空间。
1.3 栈的实现方式
栈的实现方式一共有两种,一种是用链表实现,另外一种是用数组实现。我个人建议使用链表来实现栈,因为用数组实现栈时需要事先确定存储空间的大小,如果存储空间过小,可能会写入数据失败。
2. 栈的链表实现
2.1 初始化栈
初始化栈就是创建一个空栈,用链表实现时,先创建一个头节点,然后头节点的指针域指向NULL即可。如果对链表不够熟悉,或者不理解为什么申请内存需要用二级指针,可以先看下面这两篇文档。
//节点声明
struct node
{
int element; //数据域
struct node* next; //指针域
}
//栈的初始化
void init_stack(struct node **pNode)
{
*pNode = (struct node*)malloc(sizeof(struct node));
if(NULL == *pNode)
{
printf("栈初始化申请内存失败");
return NULL;
}
*pNode ->next = NULL;
}
//测试用例
struct node *pStack = NULL;
init_list(&pStack);
2.2 栈的判空
对于用链表实现的栈来说,判空这个操作非常简单,只需对头节点的下一个节点判空即可。
//栈判空 1-空 0-非空
bool is_empty(struct node *pHead)
{
assert(NULL != pHead));
return (NULL == pHead)->next);
}
//测试用例
struct node *pStack;
is_empty(pStack);
2.3 入栈(PUSH)
用链表实现入栈,实际上就是用链表插入新元素,而链表插入一个元素又分为头插法和尾插法。
头插法是将链表头节点的后一个节点作为栈顶,这样做的好处的插入和删除的速度比较快。尾插法将最后一个元素作为栈顶,这样做的好处是便于理解,缺点是每次插入和删除都需要从头节点开始遍历整个链表再进行插入删除,速度很明显不如头插法,因此我个人建议使用头插法来入栈。
//入栈函数
void push(struct node *pHead, int element)
{
//校验入参
assert(NULL == pHead);
//入参指针不要直接使用
struct node *pNode = pHead;
//创建新节点
struct node *pNew = NULL;
pNew = (struct node*)malloc(sizeof(struct node));
if(NULL == pNew)
{
printf("新节点申请内存失败");
return;
}
pNew->element = element;
//插入操作
if(is_empty(pNode)) //插入前是空栈
{
pNode->next = pNew;
pNew->next = NULL;
}
else //插入前不是空栈
{
pNode->next = pNew;
pNew->next = pNode->next;
}
}
//测试用例
struct node *pStack;
push(pStack, 10);
2.4 出栈(POP)
上面使用了头插法来插入,即默认头节点的下一个元素为栈顶,那么我们出栈操作也要删除头节点的后一个元素。
//出栈函数
void pop(struct node *pHead)
{
//校验入参
assert(NULL == pHead);
//入参指针不要直接使用
struct node *pNode = pHead;
//插入操作
if(is_empty(pNode)) //插入前是空栈
{
printf("空栈,无法删除");
return;
}
else //插入前不是空栈
{
pNode->next = pNode->next->next;
free(pNode->next);
}
}
//测试用例
struct node *pStack;
pop(pStack);
2.5 获取栈顶元素
获取栈顶元素非常简单,只需要读取头节点的下一个节点值即可。
//获取栈顶元素
int get_topElement(struct node *pHead)
{
//校验入参
assert(NULL == pHead);
//不要直接使用指针
struct node *pNode = pHead;
//判断是否空栈
if(is_empty(pNode))
{
printf("空栈,无栈顶元素");
return 0;
}
else
{
return pNode->next->element;
}
}
//测试用例
struct node *pStack;
get_topElement(pStack);
3. 栈的数组实现
栈的数组实现避免了指针,是一种简单的方法。但是存在的问题如前文所说,需要提前声明一个数组的大小,如果没有充足开销的话不建议使用这种方法。
3.1 初始化栈
和链表实现栈的方法不同,用数组实现的栈不需要指针域,但是需要添加额外的栈顶下标TopIdx来跟踪栈顶索引,同时要定好栈的最大容量Maxsize。
//栈结构体
struct Stack
{
int Data[Maxsize]; //栈数据
int TopIdx; //栈顶下标
}
//栈初始化
void init_stack(struct node *stack)
{
stack.TopIdx = -1;
}
//测试用例
int MaxElement = 100;
struct Stack stack;
init_stack(&stack);
3.2 栈的判空
用数组实现的栈的判空实际上就是判断栈顶下标是否合法,如果栈顶下标小于0,则说明这是一个空栈。
//栈的判空函数 1-空 0-非空
void is_stackEmpty(struct node *stack)
{
return (-1 == (*stack).TopIdx);
}
3.3 入栈(PUSH)
入栈前需要校验栈是否是已满,如果栈未满的话则正常写入数据,并将栈顶元素++。
//入栈函数
void Push(struct node *stack, int element)
{
if((*stack)[TopIdx] >= (MaxSize - 1))
{
printf("栈已满,无法插入数据");
return;
}
(*stack).Data[TopIdx++] = element;
}
//测试用例
Push(&stack, 20);
3.4 出栈(POP)
出栈前需要校验栈是否是空栈,如果不是的话将栈顶数据请零,并将栈顶下标--。
//出栈函数
void Pop(struct node *stack)
{
if(is_stackEmpty(stack))
{
printf("栈空,无法出栈");
}
stack.Data[TopIdx--] = 0;
}
//测试用例
Pop(&stack);
3.5 获取栈顶元素
获取栈顶元素需要判断当前栈是否为空,不为空直接读取即可。
//获取栈顶元素
int get_topElement(struct node *stack)
{
if(is_stackEmpty(stack))
{
printf("栈空,无栈顶元素");
}
return (*stack).Data[TopIdx];
}
//测试用例
int value = get_topElement(&stack);