算法与数据结构(三):栈

栈与队列一样也是一种线性的数据结构,与队列不同的是栈是一种先进后出的结构,有点类似于现实中的弹夹,最后压进去的子弹总是最先被打出来,在计算机中栈用到的地方就是用作函数传参与函数中局部变量的保存,也就是我们经常说的函数栈。栈同样有基于数组和基于链表的实现

基于链表的实现

基于链表实现的栈只需要一个头指针即可,插入删除都在头部进行。基于链表的栈没有栈满这一说,栈空的条件是头指针为NULL。

元素入栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
bool Push(int nValue)
{
LPSTACK_NODE p = (LPSTACK_NODE)malloc(sizeof(STACK_NODE));
if (NULL == p)
{
return false;
}
memset(p, 0x00, sizeof(STACK_NODE));
p->nVal = nValue;

p->pNext = g_Top;
g_Top = p;
return true;
}

参数入栈是不需要判断栈是否为空的,不管是否为空,我们都采用同样的算法,即先使新节点的next指针域指向头,然后再重新给头指针赋值,由于不涉及到头指针所指向地址的访问,所以不需要额外判断头指针是否为空

元素出栈

元素出栈首先需要判断栈是否为空,如果不为空,则需要一个临时指针保存出栈元素,然后将头指针进行偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool Pop(int* pValue)
{
if (NULL == pValue)
{
return false;
}

if (IsStackEmpty())
{
return false;
}

*pValue = g_Top->nVal;
LPSTACK_NODE p = g_Top;
g_Top = g_Top->pNext;
free(p);
return true;
}

基于数组实现的栈

基于数组实现的栈也需要一个指针(或者在这里称之为下标),指向栈顶,在函数栈中这个充当栈顶指针的是一个寄存器ESP,而在函数栈中还有一个栈的基地址是ebp,基于数组的栈中,这个基地址就是数组的首地址。

在基于数组实现的栈中,根据栈顶指针是否为0来判断。另外这种栈需要判断栈是否已满,一般采用的判断方式是判断栈顶指针是否为数组的最大长度。

栈空、栈满判断

1
2
3
4
5
6
7
8
9
bool IsStackEmpty()
{
return g_Top == 0;
}

bool IsStackFull()
{
return g_Top == MAX_SIZE;
}

根据不同的实现方式会有不同的判断方式,这里是让栈顶指针指向下一个即将入栈的元素所在的位置,如果栈顶指针指向的是当前栈顶元素的位置,那么这里可能需要改为 等于 -1来判断栈空,等于MAX_SIZE - 1来判断栈满

元素入栈

1
2
3
4
5
6
7
8
9
10
bool Push(int nValue)
{
if (IsStackFull())
{
return false;
}

g_STACK[g_Top++] = nValue;
return true;
}

元素出栈

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool Pop(int* pValue)
{
if (NULL == pValue)
{
return false;
}

if (IsStackEmpty())
{
return false;
}

*pValue = g_STACK[--g_Top];
return true;
}

由于栈都是在一端进行操作,所以不存在向队列那样有空间浪费,也不需要实现什么循环数组。从实现上来看栈比队列要简单的多。