本文详细介绍了栈这一数据结构的概念、特点及其应用。包括栈的基本操作、顺序存储与链式存储结构实现,以及栈在递归和四则运算表达式求值中的应用。

一、栈的定义

1.栈的定义

栈(stack)是限定仅在表尾进行插入和删除操作的线性表。

我们把允许插入和删除的一端称为栈顶(top)另一端称为栈底(bottom),不含任何数据元素的称为空栈。栈又称为后进先出(Last In First Out)的线性表,简称LIFO结构

首先它是一个线性表,也就是说,栈元素具有线性关系,即前驱后继关系。只不过它是一种特殊的线性表而已。定义中说在线性表的表尾进行插入和删除操作,这里的表尾是指栈顶,而不是栈底。

它的特殊之处就在于限定了这个线性表的插入和删除位置,它始终只在栈顶进行。也就是说:栈底是固定的,最先进栈的只能在栈底。

栈的插入操作,叫做进栈,也称压栈、入栈。

栈的删除操作,叫做出栈,也有的叫作弹栈。

2.进栈出栈变化形式

最先进栈的元素,是不是就只能最后出栈呢?

答案是不一定。栈对线性表的插入和删除的位置进行了限制,并没有对元素进出的时间进行限制,也就是说,在不是所有元素都进栈的情况下,事先进去的元素也可以出栈,只要保证是栈顶元素就可以。

举例来说,如果我们现在是有3个整型数字元素1、2、3一次进栈,会有哪些出栈次序呢?
①第一种:1、2、3进,再3、2、1出。出栈次序321

②第二种:1进,1出,2进,2出,3进,3出。出栈顺序123

③第三种:1进,2进,2出,1出,3进,3出。出栈顺序213

④第四种:1进,1出,2进,3进,3出,2出。出栈顺序132

⑤第五种:1进,2进,2出,3进,3出,1出。出栈顺序231

有没有可能是312这种出栈顺序呢?答案是肯定不会,因为3先出栈,就意味着3曾经进栈,既然3都进栈了,1和2也已经进栈了,此时,2一定是在1的上面,就是跟接近栈顶,那么出栈只可能是321。

二、栈的抽象数据类型

对于栈来讲,理论上线性表的操作它都具备,可是由于它的特殊性,所以针对它在操作上回有些变化,特别是插入和删除操作,我们改为push和pop。我们一般叫进栈和出栈。

ADT 栈 (stack)
Data
    同线性表。元素具有相同的类型,相邻元素具有前驱和后继关系。
Operation
    InitStack(*S):初始化操作,建立一个空栈。
    DestroyStack(*s):若栈存在,则销毁它。
    ClearStack(*s):将栈清空。
    StackEmpty(s):若栈为空,返回true,否则返回false。
    GetTop(S,*e):若栈存在且非空,用e返回S的栈顶元素。
    Push(*S,e):若栈S存在,插入新元素e到栈S中并称为栈顶元素
    Pop(*S,*e):删除栈S中栈顶元素,并用e返回其值。
    StackLength(S):返回栈S的元素个数。

由于栈本身就是一个线性表,那么上一章我们讨论了线性表的顺序存储和链式存储,对栈来说,也是同样适用的。

三、栈的顺序存储结构及实现

1.栈的顺序存储结构

既然栈是线性表的特例,那么栈的顺序存储其实也是线性表顺序存储的简化。我们简称为顺序栈。顺序存储的线性表使用数组实现的。对于栈这种只能一头插入删除的线性表来说,用下标为0的一端作为栈底比较好,因为首元素都存在栈底,变化最小,所以让它作为栈底。

定义一个top变量来指示栈顶元素在数组中的位置,若存储栈的长度为StackSize,则栈顶位置top必须小于StackSize。栈顶存在一个元素时,top等于0,因此通常把空栈的判定条件设为top等于-1。

来看栈的结构定义

typedef int SElemType;//类型根据实际情况而定,这里假设为int
typedef struct
{
    SElemType data[MAXSIZE];
    int top;//用于栈顶
}SqStack;

假设有一个栈,StackSize是5,则栈普通情况、空栈和栈满的情况。
①栈有两个元素 top = 1
②空栈 top = -1
③栈满 top = 4

2.栈的顺序存储结构——进栈操作

对于栈的插入,即进栈操作。

因此对于进栈操作push,其代码如下:
一些宏定义

#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int Status;
//插入元素e为新的栈顶元素
Status Push(SqStack *S,SElemType e)
{
    if(S->top == MAXSIZE - 1)//栈满
    {
        return ERROR;
    }
    S->top++;//栈顶指针增加1
    S->data[S->top] = e;//将新插入元素赋给栈顶空间
    return OK;
}

3.栈的顺序存储结构——出栈操作

出栈操作pop

//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK,否则返回ERROR
Status Pop(SqStack *s , SElemType *e)
{
    if(S->top < 0)//空栈
        return ERROR;
    *e = S->data[S->top];//将要删除的栈顶元素赋给e
    S->top--;//栈顶指针减1
    return OK;
}

两者没有涉及任何循环语句,因此时间复杂度均是O(1)。

四、两栈共享空间

顺序存储还是很方便的,因为它只准栈顶进出元素,所以不存在线性表的插入和删除时需要移动元素的问题。
不过它由一个很大的缺陷,就是必须事先确定数组存储空间大小,万一不够用了,就需要编程手段来扩展数组的容量,非常麻烦。对于一个栈,我们也只能尽量考虑周全,设计适合大小的数组来处理,但对于同类型的栈,我们却可以做到最大限度地利用其事先开辟的存储空间来进行操作

如果我们有两个相同类型的栈,我们为它们各自开辟了数组空间,极有可能是第一个栈已经满了,再进栈就溢出了,而另一个栈还有很多空闲。我们完全可以用一个数组来存储两个栈,只不过需要点小技巧。

数组有两个端点,两个栈有两个栈底,让一个栈的栈底为数组的始端,即下标为0出,另一个栈为数组的末端,即下标为数组长度n - 1出。这样两个栈如果增加元素,就是两端点向中间延伸。

其实关键思路是:它们是在数组的两端,向中间靠拢。top1和top2是栈1和栈2的栈顶指针。可以想象,只要它们两个不见面,两个栈就可以一直使用。

从这里可以分析出来,当栈1为空时候,就是top1等于-1时;而当top2等于n时,即是栈2为空时,那么什么时候栈满呢?

考虑几个极端情况,若栈2是空栈(等于n),栈1的top1等于n-1时,就是栈1满了。反之,当栈1为空栈时(等于-1),栈2等于0时,为栈2满。考虑更多的情况,也就是两个见面之时,也就是两个指针之间相差1时,即top1 + 1 = top2为栈满。

两栈共享空间的结构的代码如下:

//两栈共享空间结构
typedef struct 
{
    SElemType data[MAXSIZE];
    int top1;//栈顶1栈顶指针
    int top2;//栈顶2栈顶指针
}SqDoubleStack;

对于两栈共享空间的push方法,我们除了要插入元素值参数外,还需要有一个判断是栈1还是栈2参数的stackNumber。插入元素代码如下:

//插入元素e为新的栈顶元素
Status Push(SqDoubleStack *S,SElemType e,int stackNumber)
{
    if(S->top1 + 1 == S->top2)//栈满,不能在push新元素了
        return ERROR;
    if(stackNumber == 1)//栈1元素进栈
        S->data[++S->top1] = e; //若栈1则先top1+1后赋值
    else if(stackNumber == 2)//栈2元素进栈
        S->data[--S->top2] = e;//若栈2则先top2-1后赋值
    else//出现其他情况,说明出现错误
        return ERROR;
    return OK;
}

因为在开始已经判断了是否有栈满的情况,所以后面的top1 + 1或者 top2 - 1是不担心溢出问题的。

对于两栈共享空间的pop方法,参数就只是判断栈1栈2的参数stackNumber,代码如下:

//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK;否则返回false
Status Pop(SqDoubleStack *S,SElemType *e,int stackNumber)
{
    if(stackNumber == 1)
    {
        if(S->top1 == -1)//说明栈1已经是空栈,溢出
            return ERROR;
        *e = S->data[S->top1--];//将栈1的栈顶元素出栈
    }
    else if(stackNumber == 2)
    {
        if(S->top == MAXSIZE)
            return ERROR;//说明栈2是空栈,溢出
        *e = S->data[S->top2++];//将栈2的栈顶元素出栈
    }
    else
    {
        return ERROR;
    }
    return OK;
}

事实上,使用这样的数据结构,通常是当两个栈的空间需求有相反关系时,也就是一个栈增长时另一个栈在缩短的情况。就像买卖股票一样,你买入时,一定是一个不知道人在卖出。有人赚钱,就一定有人赔钱。这样使用两栈共享空间存储方法才比较有较大意义。否则两个栈都在不停增长,那很快就会因栈满而溢出了。

当然,这只是针对两个具有相同数据类型的栈的一个设计上的技巧,如果不相同数据类型的栈,这种办法不但不能更好地处理问题,反而会使问题更复杂

五、栈的链式存储结构及实现

1.栈的链式存储结构

现在我们来看栈的链式存储结构,简称链栈
栈只是栈顶来做插入和删除操作,栈顶放在链表头还是尾部呢?由于单链表有头指针,而栈顶指针也是必须的,所以把栈顶放在单链表的头部,由于栈顶在头部了,单链表比较常用的头结点也就失去了意义,通常对于链栈来说,是不需要头结点的

对于链栈来说,基本不存在栈满的情况,除非内存已经没有可以使用的空间,如果真的发生,那么此时的计算机已经面临死机崩溃的情况,而不是这个链栈是否溢出的问题。

但对于空栈来说,链表原定义是头指针指向空,那么链栈的空其实就是top = NULL;的时候

链栈的结构代码如下:

typedef struct StackNode
{
    SElemType data;
    struct StackNode *next;
}StackNode , *LinkStackPtr;

typedef struct LinkStack
{
    LinkStackPtr top;
    int count;
}LinkStack;

链栈的操作绝大部分都和单链表类似,只是在插入和删除上,特殊一些。

2.栈的链式存储结构——进栈操作

对于链栈的进栈push操作,假设元素值为e的新结点是s,top为栈顶指针。

//插入元素e为新的栈顶元素
Status Push(LinkStack *S,SElemType e)
{
    LinkStackPtr s=(LinkStackPtr)malloc(sizeof(StackNode));
    s->data = e;
    s->next = S->top;//把当前栈顶元素赋值给新结点的直接后继
    S->top = s;
    S->count++;

    return OK;
}

3.栈的链式存储结构——出栈操作

链栈的出栈操作pop操作,也是很简单的三句操作。假设变量p用来存储要删除的栈顶结点,将栈顶指针下移一位,最后释放p即可。

//若栈不空,则删除S的栈顶元素,用e返回其值,并返回OK,否则返回ERROR
Status Pop(LinkStack *S,SElemType *e)
{
    LinkStatckPtr p;
    if(StackEmpty(*S))
        return ERROR;
    *e = S->top->data;
    p = S->top;//将栈顶结点赋给p
    S->top = S->top->next;//使得栈顶指针下移一位,指向后一结点
    free(p);//释放结点p
    S->count--;
    return OK;
}

对于顺序栈与链栈,它们在时间复杂度上是一样的,均为O(1),对于空间性能,顺序栈需要事先确定一个固定的长度,可能会存在内存空间浪费的问题,但它的优势是存取定位很方便而链栈则要求每个元素都有指针域,这同时也增加了一些内存开销,但对于栈的长度无限制

所以它们的区别和线性表中讨论的一样,如果使用过程中元素变化不可预料,有时很小,有时非常大,那么最好是用链栈,反之,如果他的变化在可控范围内,建议使用顺序栈会更好些

六、栈的作用

栈的引入简化了程序设计的问题,划分了不同关注层级,使得思考范围缩小,更加聚焦于我们要解决问题核心。反之,像数组等,因为要分散精力去考虑数组下表等增减等细节问题,反而掩盖了问题的本质。

七、栈的应用——递归

1.斐波那契数列实现

斐波那契数列:1 , 1 , 2 , 3 , 5, 8 , 13….
这个数列有一个十分明显的特点,那是:前面相邻之和,构成了后一项。

我们实现这样的数列常用常规的迭代的办法如何实现?比如打印出前40项。
代码如下:

int main()
{
    int i;
    int a[40];
    a[0] = 0;
    a[1] = 1;
    printf("%d",a[0]);
    printf("%d",a[1]);
    for(i = 2;i < 40;i++)
    {
        a[i] = a[i - 1] + a[i - 2];
        println("%d ",a[i]);
    }
    return 0;
}

如果我们用递归来实现,代码可以更简单:

//斐波那契递归函数
int Fbi(int i)
{
    if(i < 2)
        return i == 0 ? 0 : 1;
    return Fbi(i - 1) + Fbi(i - 2);//这个Fbi就是函数自己
}

int main()
{
    int i;
    for(i = 0;i < 40;i++)
    {
        printf("%d ",Fbi(i));
    }
    return 0;
}

相比迭代的代码,递归更简洁。

2.递归定义

在高级语言中,调用自己和其他函数病没有本质不同。
我们把一个直接调用自己或通过一系列的调用语句间接调用自己的函数,称为递归函数

当然,写递归最怕的就是陷入死循环,所以至少有一个条件,满足时递归不再进行,即不再引用自身而返回值退出。比如刚才的例子,总有一次一次递归会使得i < 2的,这样就可以执行return i的语句结束递归。

对比两种实现斐波那契的代码。迭代和递归的区别是:迭代使用的是循环结构,递归使用的是选择结构。递归能使程序结构更清晰、更简洁、更容易让人理解。但是大量的递归调用会建立函数的副本,会耗费大量的时间和内存。迭代则不需要反复调用函数和占用额外的内存。

那递归和栈有什么关系呢?

递归过程退回的顺序是它前行的逆序。在退回过程中,可能要执行某些动作,包括恢复在前行中存储起来的某些数据。

这种存储某些数据,并在后面又以存储的逆序恢复这些数据,以提高以后使用的需求,显然很符合栈这样的数据结构,因此编译器使用栈实现递归。

简单来说,就是在前行阶段,对于每一层递归,函数的局部变量、参数值以及返回地址都被压入栈中。在退回阶段,位于栈顶的局部变量、参数值和返回地址被弹出,用于返回调用层次中执行代码的其余部分,也就是恢复了调用的状态

当然在现在的高级语言中,这样的递归问题不需要用户来管理这个栈的,一切由系统代劳了。

八、栈的应用——四则运算表达式求值

1.后缀(逆波兰)表示法定义

假设我们要设计一个简单的计算器,如果只是两个数的加减,当然是很简单。
但是如果我们要计算的是一个相对复杂的四则运算,其中还有括号的运算呢?比如 : 9 + (3 - 1) X 3 + 10 ÷ 2。

这里的困难主要在于乘除在加减的后面,却要先运算,而加入了括号,有得先算括号内的。

仔细观察发现,括号都是成对出现的,有左括号则一定有右括号,对于多重括号,最终也是完全嵌套匹配的。这种用栈结构正好合适,只要碰到左括号,就将左括号进栈,不管表达式有多少重括号,反正遇到左括号就进栈,而后面出现右括号时,就让栈顶的左括号出栈,期间让数字运算。当然,括号只是四则运算的一部分,先乘除后加减的问题依然复杂。如何有效地处理呢?

波兰逻辑学家想到了一种不需要括号的后缀表达式法,我们也把它成为逆波兰(Reverse Polish Notation,RPN)表示

2.中缀表达式转后缀表达式

我们把平时所用的标准四则运算表达式,即”9 + (3 - 1) X 3 + 10 ÷ 2”叫做中缀表达式。因为所有的运算符在两数字的中间,现在我们的问题就是中缀表达式到后缀的转化。

规则:从左到右遍历中缀表达式的每个数字和符号,若是数字便输出,即成为后缀表达式的一部分;若是符号,则判断其与栈顶符号的优先级,若是右括号或者优先级不高于栈顶符号则栈顶元素依次出栈并输出,并将当前符号进栈。知道最后输出表达式为止。

比如上面所说的 9 + (3 - 1) X 3 + 10 ÷ 2
步骤:
①初始化一空栈,用来对符号进出栈使用。

②第一个字符是数字9,输出9 , “+”号是符号,进栈。

位置栈内元素
3
2
1
0+

输出:9

③第三个字符时“(”,依然是符号,因其是左括号,进栈。

位置栈内元素
3
2
1(
0+

输出:9
④第四个字符时数字3,输出,总表达式是9 3 ,接着是”-“符号,进栈。

位置栈内元素
3
2-
1(
0+

输出:9 3

⑤接着是数字1,后面是符号”)”。此时我们需要匹配此前的”(“,所以栈顶依次出栈,知道”(“出栈位置,此时括号上方只有”-“,所以输出”-“。总表达式为9 3 1 -。

位置栈内元素
3
2
1
0+

输出:9 3 1 -

⑥接着是符号”X”,此时栈顶符号为”+”,优先级低于“X”,因此不输出,”*”进栈,接着是数字3,输出。总表达式是9 3 1 - 3。

位置栈内元素
3
2
1X
0+

输出:9 3 1 - 3

⑦之后是“+”号,此时栈顶元素是 ” * “,比”+”优先级高,因此栈中元素输出(因为没有比”+”号更低优先级,所以全部出栈)。
此时表达式为:9 3 1 - 3 * +。然后9后面的“+”进栈。

位置栈内元素
3
2
1
0+

输出:9 3 1 - 3 * +

⑧紧接着是数字10,输出,总表达式为:9 3 1 - 3 * + 10。因为是”÷”,然后”/”进栈。

位置栈内元素
3
2
1/
0+

输出:9 3 1 - 3 * + 10

⑨最后一个数字2,总的表达式为 9 3 1 - 3 * + 10 2。由于到最后,所以将栈中所有符号出栈。

位置栈内元素
3
2
1
0

输出:9
最终表达式为:9 3 1 - 3 * 2 + 10 2 / +

3.后缀表达式的计算方法

后缀表达式:9 3 1 - 3 * 2 + 10 2 / +

计算规则:从左到右遍历表达式的每个数字和符号,遇到数字就进栈,遇到是符号,就将处于栈顶的两个数字出栈,进行运算,运算结果出栈。

① 9 、3、 1入栈

位置栈内元素
3
21
13
09

②遇到“-”,将栈顶2个元素出栈,1作为减数,3出栈作为被减数。得到2。

位置栈内元素
3
2
12
09

③遇到3进栈

位置栈内元素
3
23
12
09

④遇到 * 。2 * 3 = 6。6进栈

位置栈内元素
3
2
16
09

⑤遇到+。9 + 6 = 15

位置栈内元素
3
2
1
015

⑥遇到10、2

位置栈内元素
3
22
110
015

⑦遇到“/”。10 ÷ 2 = 5

位置栈内元素
3
2
15
015

⑧遇到“+”。15 + 5 = 20。20进栈

位置栈内元素
3
2
1
020

⑨最后20出栈。栈为空。

位置栈内元素
3
2
1
0
<think>我们正在讨论数据结构中关于的操作,特别是清空的方法和实现。根据之前的引用内容,清空通常是将顶指针重置到初始状态(如-1),而不是释放的存储空间。销毁则是释放所占用的内存资源。 根据引用[1]中的清空操作: ```c /* 清空 */ bool ClearStack(SqStack &S) { if (S.top == -1) return false; S.top = -1; // 将顶指针指向-1,遍历的时候到top就结束 return true; } ``` 这里,清空的操作非常简单,直接将顶指针top置为-1(初始状态)即可。这样中原来的数据虽然没有被覆盖,但因为顶指针已经指向了初始位置,后续的操作会覆盖原来的数据,因此逻辑上已经被清空。 引用[3]中也提到了初始化时是将顶指针置为-1,并且将数组中元素置0(可选操作)。清空的操作与初始化类似,但通常不需要将数组中的每个元素都重置(除非有特殊的安全要求),因为只需要重置顶指针,在逻辑上就为空了。 另外,注意清空和销毁的区别: - 清空(ClearStack):重置的状态,使其成为空,但的存储空间仍然保留,可以继续使用。 - 销毁(DestroyStack):释放所占用的内存资源,不再存在。引用[1]中销毁的操作为: ```c /* 销毁 */ bool DestroyStack(SqStack &S) { if (S.top == -1) return false; free(S.data); // 释放的存储空间 return true; } ``` 注意:销毁的操作中释放了的存储数组(S.data),并将顶指针置为-1(在引用[1]的销毁操作中没有重置top,但通常销毁后结构本身可能也不再使用,所以重置top不是必须的,但释放内存是必须的)。另外,如果结构本身是动态分配的(如引用[1]中的是静态数组,但如果是动态分配的整个结构体,则销毁时需要释放整个结构体),则销毁方式会有所不同。 下面我将详细介绍清空的方法和实现,包括顺序(静态分配和动态分配)和链式。 ## 一、顺序的清空操作 ### 1. 静态分配的顺序 静态分配的顺序使用固定大小的数组存储元素,清空操作只需重置顶指针。 ```c // 结构定义 #define MAXSIZE 100 // 的最大容量 typedef struct { int data[MAXSIZE]; // 静态数组 int top; // 顶指针,初始为-1 } SqStack; // 清空 void ClearStack(SqStack *S) { S->top = -1; // 重置顶指针 } ``` ### 2. 动态分配的顺序 动态分配的顺序在初始化时动态分配了存储数组,清空操作同样只需重置顶指针,不需要释放数组内存(因为清空后还要继续使用)。 ```c // 动态分配的顺序结构 typedef struct { int *data; // 动态数组 int top; // 顶指针 int capacity; // 的容量 } DynStack; // 清空 void ClearStack(DynStack *S) { S->top = -1; // 重置顶指针 // 注意:动态数组的内存不释放,因为还要继续使用 } ``` ## 二、链式的清空操作 链式使用链表存储元素,清空操作需要释放所有节点的内存,并将顶指针置为NULL。 ```c // 链式节点 typedef struct StackNode { int data; struct StackNode *next; } StackNode; // 链式结构 typedef struct { StackNode *top; // 顶指针 } LinkStack; // 清空(需要释放所有节点) void ClearStack(LinkStack *S) { StackNode *p = S->top; while (p) { StackNode *temp = p; p = p->next; free(temp); // 释放节点 } S->top = NULL; // 顶指针置空 } ``` ## 三、注意事项 1. **清空与销毁的区别**: - 清空:重置为空状态,保留存储空间。 - 销毁:释放所有资源,不再存在。 2. **时间复杂度**: - 顺序清空:$O(1)$(只需重置指针) - 链式清空:$O(n)$(需要遍历释放所有节点) 3. **安全考虑**: - 在清空链式时,要防止内存泄漏,必须释放所有节点。 - 顺序清空后,原来的数据可能残留在数组中,如果涉及敏感数据,应该手动覆盖(但通常清空操作只重置指针)。 ## 四、清空操作示例 以下是一个完整的顺序(静态分配)清空操作的示例代码: ```c #include <stdio.h> #include <stdbool.h> #define MAXSIZE 10 typedef struct { int data[MAXSIZE]; int top; } SqStack; // 初始化 void InitStack(SqStack *S) { S->top = -1; } // 清空 void ClearStack(SqStack *S) { S->top = -1; } // 判断空 bool StackEmpty(SqStack S) { return S.top == -1; } int main() { SqStack S; InitStack(&S); // ... 入操作 ... // 清空 ClearStack(&S); if (StackEmpty(S)) { printf("已清空\n"); // 输出:已清空 } return 0; } ``` ## 五、引用说明 - 清空顺序只需重置顶指针(静态和动态分配)[^1][^3]。 - 链式的清空需要逐个释放节点并将顶指针置空。 §§相关问题§§ 1. 的清空操作和销毁操作在哪些场景下需要分别使用? 2. 链式的清空操作中如何避免内存泄漏? 3. 如何实现一个支持撤销操作(使用多个)的编辑器? 4. 在递归函数调用中如何保存现场? 5. 顺序和链式在清空操作上的性能差异对实际应用有什么影响? </think>## 数据结构中清空的方法与实现详解 ### 一、清空的核心概念 清空(Clear Stack)是指**重置的状态使其成为逻辑空**,但不释放的存储空间。这与销毁(Destroy Stack)有本质区别: - 清空:保留数据结构,仅移除所有元素(时间复杂度$O(1)$) - 销毁:释放占用的内存资源(时间复杂度$O(1)$或$O(n)$)[^1][^3] ### 二、顺序的清空实现 #### 1. 静态分配的顺序 ```c /* 清空静态顺序 */ bool ClearStack(SqStack *S) { if (S->top == -1) return false; // 已是空 S->top = -1; // 核心操作:重置顶指针 return true; } ``` - **操作原理**:顶指针`top`置为初始值-1,标记为空状态 - **时间复杂度**:$O(1)$ - **空间复杂度**:$O(1)$ - **特点**:原数据未被覆盖但逻辑失效[^1] #### 2. 动态分配的顺序 ```c /* 清空动态顺序 */ bool ClearStack_Dynamic(DynStack *S) { if (S->top == -1) return false; S->top = -1; // 重置顶指针 // S->data数组保留不释放 return true; } ``` - **关键区别**:保留动态分配的存储数组,仅重置指针[^3] ### 三、链式的清空实现 ```c /* 清空链式 */ void ClearStack_Linked(LinkStack *S) { while (S->top != NULL) { // 遍历释放所有节点 StackNode *temp = S->top; S->top = S->top->next; free(temp); // 释放节点内存 } S->count = 0; // 重置节点计数器 } ``` - **时间复杂度**:$O(n)$(需遍历所有节点) - **空间复杂度**:$O(1)$ - **特点**:必须显式释放每个节点内存[^3] ### 四、清空操作的技术要点 1. **状态重置**: - 顺序:`top = -1` - 链式:`top = NULL` + `count = 0` 2. **内存处理差异**: ```mermaid graph LR A[清空操作] --> B[顺序] A --> C[链式] B --> D[仅重置指针] C --> E[释放所有节点] ``` 3. **错误处理**: - 检查是否已空(避免冗余操作) - 检查指针有效性(防野指针) ### 五、清空 vs 销毁 vs 初始化 | 操作 | 目标 | 内存变化 | 状态 | |--------|-----------------------|--------------------|--------------| | 清空 | 移除所有元素 | 存储空间保留 | 逻辑空 | | 销毁 | 释放所有资源 | 内存完全释放 | 不存在 | | 初始化 | 创建空结构 | 分配/重置内存 | 初始空 | > **应用场景**:清空操作常用于需要复用结构的场景,如编译器语法分析中的符号重用[^1] ### 六、完整示例(顺序清空) ```c #include <stdio.h> #define MAXSIZE 100 typedef struct { int data[MAXSIZE]; int top; } SqStack; // 初始化 void InitStack(SqStack *S) { S->top = -1; // 初始空标记[^3] } // 清空 void ClearStack(SqStack *S) { S->top = -1; // 核心清空操作 } // 检测空 int IsEmpty(SqStack S) { return (S.top == -1); } int main() { SqStack S; InitStack(&S); // ... 入操作 ... ClearStack(&S); printf("是否空: %d\n", IsEmpty(S)); // 输出1(真) return 0; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值