一、栈到底是个啥?
说白了,栈就是一种操作受限的线性表。 普通的数组、链表,想在哪插在哪删都行,但栈不行:它只开放一端给你操作,这一端叫栈顶;另一端封死,叫栈底。所有的插入、删除都只能在栈顶做。
这种限制催生出了栈最核心的特性:后进先出(LIFO, Last In First Out)。 举个最生活化的例子:摞书。 你往桌上放书,一本本往上叠,最后放的那本在最上面;你要拿书,只能先拿最上面那本。最后放上去的,第一个被拿下来 —— 这就是标准的栈逻辑。
往栈里加数据叫入栈(压栈),从栈里删数据叫出栈(弹栈),俩操作都只碰栈顶,不碰栈底。
二、栈为什么偏爱数组实现?
理论上数组和链表都能实现栈,但实际写代码的时候,几乎所有人都会选数组。 原因非常实在:栈的所有操作都在尾部,而数组的尾插、尾删天然就是 O (1),完全对上了;再加上数组是连续内存,缓存命中率高,比链表省空间还跑得快,没理由不用。
栈的结构长什么样
一个动态数组实现的栈,结构体里就三样东西:
typedef int STDataType; typedef struct Stack { STDataType* a; // 存数据的动态数组 int top; // 栈顶标记,指向下一个可插入的位置 int capacity; // 数组总共能存多少数据 } ST;这里说下top的约定:一般我们让它指向 “栈顶元素的下一个空位”。比如空栈的时候top=0,入栈一个元素后top=1,这样top的值刚好等于栈里元素的个数,省得单独维护 size。
初始化和销毁
初始化就是把栈置成空状态,销毁就是把申请的数组释放掉,避免内存泄漏。
// 初始化栈 void STInit(ST* ps) { assert(ps); ps->a = NULL; ps->top = 0; ps->capacity = 0; } // 销毁栈 void STDestroy(ST* ps) { assert(ps); free(ps->a); ps->a = NULL; ps->top = 0; ps->capacity = 0; }入栈:先看容量够不够
入栈是最常写的操作,核心就两步:先检查容量,满了就扩容;再把数据放到栈顶,top往后挪一位。
扩容这里有个细节:不用直接改结构体里的capacity,先用局部变量newcap算好新容量,申请成功了再正式赋值。万一realloc失败了,原栈的数据和容量都不会乱,这是写动态结构的基本防御性写法。
// 入栈 void STPush(ST* ps, STDataType x) { assert(ps); // 容量满了,先扩容 if (ps->top == ps->capacity) { int newcap = ps->capacity == 0 ? 4 : 2 * ps->capacity; STDataType* tmp = (STDataType*)realloc(ps->a, newcap * sizeof(STDataType)); if (tmp == NULL) { perror("realloc 申请失败"); exit(1); } ps->a = tmp; ps->capacity = newcap; } // 栈顶放入数据,top后移 ps->a[ps->top++] = x; }出栈和取栈顶
出栈特别简单:只要栈不是空的,把top减 1 就完事了。 不用特意把原位置的数据清掉,因为下次入栈会直接覆盖。数据还在那里,但只要top不认可它,它就不算栈里的元素了。
// 出栈 void STPop(ST* ps) { assert(ps); assert(ps->top > 0); // 空栈不能弹 ps->top--; } // 取栈顶元素 STDataType STTop(ST* ps) { assert(ps); assert(ps->top > 0); 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; }三、栈的特点和适用场景
栈的几个关键特点
- 操作单一:只在栈顶增删,逻辑简单,不容易出 bug
- 效率极高:入栈、出栈、取栈顶全是 O (1),几乎没有额外开销
- 不支持随机访问:想拿栈底的元素,必须把上面的全弹出去
- 内存连续:数组实现的缓存友好,访问速度快