栈的实现过程拆解:数组和链表如何一步步造一个栈?

栈的实现过程拆解:数组和链表如何一步步造一个栈?

这篇咱们不绕弯,直接从 “造栈” 的角度出发,分别用数组和链表实现一个简单的 “商品 ID 栈”(存最近浏览的商品 ID),每一步都讲清 “结构怎么定义、内存怎么变、操作怎么执行”,看完你就能自己写一个栈。

一、用数组实现栈:3 步造一个 “固定容量的栈”

数组实现栈的核心是 “用数组存数据 + 用一个指针记栈顶位置”,因为数组是连续内存,操作栈顶只需要动指针,不用挪数据。咱们分 “定义结构→初始化→核心操作”3 步来实现。

1. 第一步:定义数组栈的 “骨架”(数据结构)

要实现一个能存商品 ID 的栈,需要 3 个关键部分:

  • 数组 data:存商品 ID(比如 int 类型,每个 ID 占 4 字节);

  • 栈顶指针 top:记录当前栈顶的位置(用 int 表示,初始值很关键);

  • 容量 capacity:数组的最大大小(比如设为 5,最多存 5 个商品 ID)。

用代码表示(以 Java 为例,伪代码简化,好懂):

// 数组栈的结构定义

class ArrayStack {

   int\[] data;       // 存商品ID的数组

   int top;          // 栈顶指针(记录栈顶索引)

   int capacity;     // 栈的最大容量(数组大小)

   // 构造方法:初始化栈

   public ArrayStack(int capacity) {

       this.capacity = capacity;

       this.data = new int\[capacity];  // 申请一个capacity大小的数组

       this.top = -1;                  // 栈顶指针初始为-1:表示栈是空的(没有元素)

   }

}

初始化后的内存状态:

假设我们初始化一个容量为 5 的栈(new ArrayStack(5)),内存里会有:

  • 数组 data:占 20 字节(5 个 int,每个 4 字节),内存地址假设从0x400开始,此时数组里都是默认值 0(还没存数据);

  • 栈顶指针 top:存在栈对象里,值为 - 1(表示栈空,还没指向数组的任何索引);

  • 容量 capacity:值为 5,固定不变。

内存可视化(初始化后):

数组内存地址 数组索引 存储的商品 ID(初始值) 栈顶指针 top 状态说明
0x400 0 0 -1 栈空,指针没指向任何索引
0x404 1 0 -1 同上
0x408 2 0 -1 同上
0x40C 3 0 -1 同上
0x410 4 0 -1 同上

2. 第二步:实现核心操作 1—— 压栈(push):往栈顶加数据

压栈就是 “把商品 ID 存到栈顶”,核心是 “先动指针,再存数据”,因为 top 初始为 - 1,要先让 top 指向数组的第一个空位置(索引 0),再存数据。

压栈的步骤(以压入第一个商品 ID 1001 为例):

  1. 判满:先检查栈是不是满了(top == capacity-1)—— 如果 top 等于 4(capacity=5,5-1=4),说明数组索引 4 已经存了数据,栈满了,不能再压栈;

  2. 指针上移:top 从 - 1 变成 0(top++),现在 top 指向数组索引 0(第一个空位置);

  3. 存数据:把商品 ID 1001 存到数组索引 0 的位置(data [0] = 1001)。

压栈后的内存变化:

数组内存地址 数组索引 存储的商品 ID 栈顶指针 top 状态说明
0x400 0 1001 0 栈顶在索引 0,存了 1 个数据
0x404 1 0 0
0x408 2 0 0
0x40C 3 0 0
0x410 4 0 0

再压入 2 个商品 ID(1002、1003):

  • 压 1002:top 从 0→1,data [1] = 1002;

  • 压 1003:top 从 1→2,data [2] = 1003;

最终内存状态(存了 3 个数据):

数组内存地址 数组索引 存储的商品 ID 栈顶指针 top 状态说明
0x400 0 1001 2 栈顶在索引 2,存了 3 个数据
0x404 1 1002 2 已存数据
0x408 2 1003 2 已存数据(栈顶)
0x40C 3 0 2
0x410 4 0 2

压栈的代码实现:

// 压栈:把商品ID存到栈顶

public boolean push(int goodsId) {

   if (top == capacity - 1) {  // 判满:top到最大索引,栈满了

       System.out.println("栈满了,不能再存数据了");

       return false;

   }

   top++;                      // 指针上移,指向新的栈顶位置

   data\[top] = goodsId;        // 存数据到栈顶

   return true;

}

3. 第三步:实现核心操作 2—— 弹栈(pop):从栈顶删数据

弹栈就是 “把栈顶的商品 ID 取出来,并且从栈里删掉”,核心是 “先取数据,再动指针”—— 因为栈顶的数据在 top 指向的位置,取完后把 top 往下移,原来的栈顶数据就 “失效” 了(下次压栈会覆盖)。

弹栈的步骤(从存了 3 个数据的栈里弹栈):

  1. 判空:先检查栈是不是空的(top == -1)—— 如果 top 是 - 1,说明没数据,不能弹栈;

  2. 取数据:把 top 指向的索引 2 的数据(1003)取出来(这是要返回给用户的 “最新浏览商品 ID”);

  3. 指针下移:top 从 2 变成 1(top--),现在栈顶指向索引 1 的 1002,原来索引 2 的 1003 虽然还在数组里,但栈已经 “不认” 它了(下次压栈会把新数据存到索引 2,覆盖 1003)。

弹栈后的内存变化:

数组内存地址 数组索引 存储的商品 ID(未覆盖) 栈顶指针 top 状态说明
0x400 0 1001 1 已存数据
0x404 1 1002 1 已存数据(栈顶)
0x408 2 1003 1 数据还在,但栈不认可(下次压栈会覆盖)
0x40C 3 0 1
0x410 4 0 1

弹栈的代码实现:

// 弹栈:从栈顶取数据,并删除栈顶

public Integer pop() {

   if (top == -1) {  // 判空:栈里没数据

       System.out.println("栈空了,没数据可弹");

       return null;

   }

   int topGoodsId = data\[top];  // 取栈顶数据

   top--;                      // 指针下移,栈顶“删除”原数据

   return topGoodsId;

}

4. 数组栈的其他关键操作(判空、取栈顶)

  • 判空(isEmpty):看 top 是不是 - 1,是就是空栈;

  • 取栈顶(peek):和弹栈类似,但只取数据不删(不用 top--),比如 “看最新浏览的商品 ID,但不删除”。

代码实现:

// 判空:栈是不是空的

public boolean isEmpty() {

   return top == -1;

}

// 取栈顶:看栈顶数据,不删除

public Integer peek() {

   if (top == -1) {

       System.out.println("栈空了,没数据可看");

       return null;

   }

   return data\[top];  // 直接返回top位置的数据,不动指针

}

二、用链表实现栈:3 步造一个 “无容量限制的栈”

链表实现栈的核心是 “用单链表存数据 + 链表头作为栈顶”—— 因为单链表的头节点增删最快(不用找前驱节点),刚好符合栈 “只操作栈顶” 的需求。同样分 “定义结构→初始化→核心操作”3 步实现。

1. 第一步:定义链表栈的 “骨架”(节点 + 栈结构)

链表栈需要两个部分:

  • 链表节点(Node):存单个商品 ID 和下一个节点的地址(指针);

  • 链表栈(LinkedStack):用链表头(head)作为栈顶,记录栈顶位置(不用栈顶指针,头节点就是栈顶)。

用代码表示(Java 伪代码):

// 1. 链表节点的结构:存数据+指向下一个节点的指针

class Node {

   int goodsId;  // 商品ID(数据域)

   Node next;    // 指向下一个节点的指针(地址)

   public Node(int goodsId) {

       this.goodsId = goodsId;

       this.next = null;  // 新节点默认没有下一个节点

   }

}

// 2. 链表栈的结构:用头节点(head)作为栈顶

class LinkedStack {

   Node head;  // 栈顶 = 链表头节点(存最新的商品ID)

   // 初始化栈:头节点为null,栈空

   public LinkedStack() {

       this.head = null;

   }

}

初始化后的内存状态:

初始化一个链表栈(new LinkedStack()),内存里只有一个栈对象,head 指针为 null(表示栈空,没有任何节点)。

内存可视化(初始化后):

栈对象 head 指针(栈顶) 内存状态说明
LinkedStack null 栈空,没有任何节点

2. 第二步:实现核心操作 1—— 压栈(push):往栈顶(链表头)加节点

链表栈的压栈是 “在链表头插一个新节点”—— 因为头节点是栈顶,新节点插在头前面,新节点就变成新的栈顶,操作最快(只改指针,不用找其他节点)。

压栈的步骤(压入第一个商品 ID 1001 为例):

  1. 新建节点:创建一个新 Node,数据是 1001,next 指针为 null(刚创建时没指向任何节点);

  2. 新节点指向原头节点:原头节点是 null(栈空),所以新节点的 next = null;

  3. 头节点更新为新节点:栈的 head 指针指向新节点,新节点变成新的栈顶。

压栈后的内存变化(存 1001):

  • 新节点内存地址假设为0x200,数据 1001,next=null;

  • head 指针指向0x200(新节点是栈顶)。

内存可视化:

栈对象 head 指针(栈顶) 节点内存地址 节点数据(goodsId) 节点 next 指针 状态说明
LinkedStack 0x200 0x200 1001 null 栈顶是 1001,1 个节点

再压入 2 个商品 ID(1002、1003):

  • 压 1002:
  1. 新建节点(地址0x20C,数据 1002);

  2. 新节点的 next = 原 head(0x200,指向 1001 的节点);

  3. head 更新为 0x20C(新节点是栈顶);

  • 压 1003:
  1. 新建节点(地址0x218,数据 1003);

  2. 新节点的 next = 原 head(0x20C,指向 1002 的节点);

  3. head 更新为 0x218(新节点是栈顶)。

最终内存状态(存了 3 个数据):

栈对象 head 指针(栈顶) 节点内存地址 节点数据(goodsId) 节点 next 指针 状态说明
LinkedStack 0x218 0x218 1003 0x20C 栈顶(最新),指向 1002 的节点
0x20C 1002 0x200 中间节点,指向 1001 的节点
0x200 1001 null 最旧节点,没有下一个节点

压栈的代码实现:

// 压栈:在链表头插新节点(新节点成为栈顶)

public void push(int goodsId) {

   // 1. 新建节点

   Node newNode = new Node(goodsId);

   // 2. 新节点指向原头节点(原栈顶)

   newNode.next = head;

   // 3. 头节点更新为新节点(新节点成栈顶)

   head = newNode;

}

3. 第三步:实现核心操作 2—— 弹栈(pop):从栈顶(链表头)删节点

链表栈的弹栈是 “删除链表头节点”—— 因为头节点是栈顶,删头节点就是删栈顶,操作最快(只改 head 指针,不用找其他节点)。

弹栈的步骤(从存了 3 个数据的栈里弹栈):

  1. 判空:检查 head 是不是 null(栈空),是就不能弹栈;

  2. 取栈顶数据:头节点(0x218)的数据是 1003,先把这个数据取出来;

  3. 头节点更新为原头节点的 next:原头节点(0x218)的 next 指向 0x20C(1002 的节点),所以 head 更新为 0x20C(1002 的节点变成新栈顶);

  4. 原头节点失效:原来的头节点(0x218,数据 1003)没有指针指向它了,会被垃圾回收(相当于从栈里删掉)。

弹栈后的内存变化:

  • head 指针现在指向 0x20C(1002 的节点,新栈顶);

  • 原头节点 0x218(1003)变成垃圾内存,后续会被回收。

内存可视化:

栈对象 head 指针(栈顶) 节点内存地址 节点数据(goodsId) 节点 next 指针 状态说明
LinkedStack 0x20C 0x20C 1002 0x200 新栈顶,指向 1001 的节点
0x200 1001 null 最旧节点
0x218 1003 0x20C 垃圾内存(无指针指向)

弹栈的代码实现:

// 弹栈:删除链表头节点(栈顶),并返回数据

public Integer pop() {

   if (head == null) {  // 判空:栈里没节点

       System.out.println("栈空了,没数据可弹");

       return null;

   }

   int topGoodsId = head.goodsId;  // 取栈顶(头节点)数据

   head = head.next;               // 头节点更新为下一个节点,原头节点失效

   return topGoodsId;

}

4. 链表栈的其他关键操作(判空、取栈顶)

  • 判空(isEmpty):看 head 是不是 null,是就是空栈;

  • 取栈顶(peek):只取头节点的数据,不删节点(不用改 head)。

代码实现:

// 判空:栈是不是空的

public boolean isEmpty() {

   return head == null;

}

// 取栈顶:看栈顶数据,不删除

public Integer peek() {

   if (head == null) {

       System.out.println("栈空了,没数据可看");

       return null;

   }

   return head.goodsId;  // 直接返回头节点数据,不动指针

}

三、数组栈 vs 链表栈:核心区别和选型建议

现在你已经知道两种实现的完整过程了,最后总结一下它们的区别,帮你选对实现方式:

对比维度 数组栈 链表栈
容量限制 有(数组大小固定,满了不能压栈) 无(只要内存够,能一直加节点)
内存开销 低(只有数组,无额外指针开销) 高(每个节点多 8 字节 next 指针)
操作速度 快(只动指针,数组连续内存) 稍慢(要新建节点,指针跳转)
实现复杂度 简单(不用处理节点指针) 稍复杂(要定义节点,管理指针)
适用场景 数据量固定的场景(如固定 5 条浏览记录) 数据量不确定的场景(如不限条数的浏览记录)

选型建议:

  • 如果你知道栈最多存多少数据(比如 “最近浏览最多 10 条”),用数组栈 —— 速度快、省内存;

  • 如果你不知道栈的最大数据量(比如 “浏览记录不限条数”),用链表栈 —— 无容量限制,不会溢出。

最后:一句话搞懂栈的实现本质

  • 数组栈:用 “连续内存的数组” 存数据,用 “top 指针” 记栈顶,操作栈顶就是 “动指针 + 存 / 取数组数据”;

  • 链表栈:用 “离散的节点” 存数据,用 “链表头” 当栈顶,操作栈顶就是 “头插 / 头删节点 + 改 head 指针”。

不管是哪种实现,栈的核心规则 “先进后出” 都没变 —— 只是用不同的内存结构(连续 vs 离散)来实现这个规则,搞懂实现过程,你就真的懂栈了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容