02-手写链表、栈、队列——不依赖任何集合框架

老程序员回炉补基础(二):手写链表、栈、队列------不依赖任何集合框架

很多程序员用了多年 ArrayList、LinkedList,却说不出链表插入一个节点需要几步操作。我用 Java 从零实现了三种基础数据结构,没有用 java.util 里的任何一个集合类。


一、双向链表(Doubly Linked List)

学习时间:2023年8月10日~12日

这是我补基础时写的第一个数据结构。

节点设计

java 复制代码
public class mNode<T> {
    private T data;
    private String name;
    private mNode<T> pre;   // 前驱指针
    private mNode<T> next;  // 后继指针

    public mNode(T data) {
        this.data = data;
    }
    public mNode(String name, T data) {
        this.name = name;
        this.data = data;
    }
    // getter/setter 省略...
}

我选择了双向链表 而不是单链表,因为双向链表的插入和删除操作更完整,也更贴近实际应用。JDK 的 LinkedList 底层也是双向链表。

链表实现

java 复制代码
public class mLink<T> {
    private mNode<T> header = null; // 头节点
    private mNode<T> curr = null;   // 当前节点(游标)

    public mLink(T data) {
        header = new mNode<T>(data);
        curr = header;
    }

    // 追加节点
    public void append(mNode<T> n) {
        curr.setNext(n);
        n.setPre(curr);
        curr = n; // 游标移动到新节点
    }

    // 在当前节点前插入
    public void insert(mNode<T> n) {
        mNode<T> pre = curr.getPre();
        if (pre == null) {
            header = n;
            n.setNext(curr);
            curr.setPre(n);
        } else {
            pre.setNext(n);
            n.setNext(curr);
            n.setPre(pre);
            curr.setPre(n);
        }
    }

    // 删除当前节点
    public void remove() {
        mNode<T> pre = curr.getPre();
        mNode<T> next = curr.getNext();

        if (next != null) {
            next.setPre(pre);
            curr = next;
        } else {
            curr = pre;
        }
        if (pre != null) {
            pre.setNext(next);
        } else {
            header = curr;
        }
    }
}

设计上的"架构师习惯"

我引入了一个 curr 游标,记录当前操作位置。这样链表就有了"当前位置"的概念:

  • append() 在当前节点后面追加
  • insert() 在当前节点前面插入
  • remove() 删除当前节点

这不是教科书上的标准实现,但更贴近实际使用场景。很多时候我们需要一个"光标"在链表中移动,而不是每次都从头开始遍历。

核心操作图解

删除节点(最复杂的操作):

复制代码
删除前:  [A] <-> [B] <-> [C]
                 ↑ curr

删除后:  [A] <-> [C]
                 ↑ curr(移向 next)

需要处理三种边界情况:

  1. 删除的是中间节点(最普通)
  2. 删除的是头节点(pre == null,需要更新 header)
  3. 删除的是尾节点(next == null,curr 回退到 pre)

二、栈(Stack)

学习时间:2023年10月11日

节点设计

java 复制代码
public class sNote<T> {
    private T data;
    private sNote<T> next;

    public sNote(T data) {
        this.data = data;
    }
    // getter/setter 省略...
}

栈实现

java 复制代码
public class mStack<T> {
    private sNote<T> top = null;

    // 入栈
    public void push(sNote<T> n) {
        if (top == null) {
            top = n;
        } else {
            n.setNext(top);
            top = n;
        }
    }

    // 出栈
    public sNote<T> pop() {
        sNote<T> temp = top;
        if (top != null) top = top.getNext();
        temp.setNext(null);
        return temp;
    }

    // 判空
    public boolean isNull() {
        return top == null;
    }
}

我的理解

栈是**后进先出(LIFO)**的数据结构,用单链表就能实现,而且只需要维护一个 top 指针:

  • push:新节点指向当前 top,top 更新为新节点
  • pop:取出 top,top 后移一位

实现非常简洁,但栈的威力在于它的应用场景:

  • 函数调用栈
  • 表达式求值
  • 浏览器的前进/后退
  • 在后续的树遍历中,我用栈来模拟递归(非递归中序遍历)

三、队列(Queue)

学习时间:2023年8月11日

节点设计

java 复制代码
public class qNode<T> {
    private T data;
    private qNode<T> next;

    public qNode(T data) {
        this.data = data;
    }
    // getter/setter 省略...
}

队列实现

java 复制代码
public class mQueue<T> {
    private qNode<T> header = null; // 队头(出队端)
    private qNode<T> tail = null;   // 队尾(入队端)
    private Long maxSize = Long.MAX_VALUE;
    private Long currSize = 0L;

    // 入队
    public void inQueue(qNode<T> n) throws Exception {
        if (currSize == maxSize) {
            throw new Exception("队列已满!");
        }
        if (header == null) {
            header = n;
            tail = n;
        } else {
            tail.setNext(n);
            tail = n;
        }
        currSize++;
    }

    // 出队
    public qNode<T> outQueue() throws Exception {
        if (header == null) {
            throw new Exception("空队列!");
        }
        qNode<T> temp = header;
        header = header.getNext();
        currSize--;
        temp.setNext(null);
        return temp;
    }

    public Long getCurrSize() {
        return currSize;
    }
}

我的理解

队列是**先进先出(FIFO)**的数据结构,需要维护两个指针 header 和 tail:

  • inQueue:在 tail 端追加,tail 后移
  • outQueue:从 header 端取出,header 后移

我加了一个 maxSize 限制,可以初始化为有界队列。这是一个"架构师职业病"------在生产环境中,无界队列是 OOM 的常见元凶,所以习惯性地做了容量限制。

队列在后续代码中的复用

这个手写的队列在后面的代码中被大量复用:

  1. 图的 BFS(广度优先搜索):用队列逐层遍历顶点
  2. 树的层序遍历:用队列按层输出节点
  3. 拓扑排序:入度为0的顶点入队,逐个出队处理

这验证了一个道理:基础数据结构是上层算法的基石。


三种数据结构对比

结构 操作特点 核心指针 典型应用
双向链表 双向遍历、插入、删除 header + curr LRU缓存、浏览器历史
栈 后进先出(LIFO) top 函数调用栈、递归模拟
队列 先进先出(FIFO) header + tail BFS、任务调度

从这些基础结构看架构选型

作为架构师,理解这些底层结构直接影响技术选型:

  • 为什么 ArrayList 的随机访问是 O(1),而 LinkedList 是 O(n)? 因为数组有连续内存+索引计算,链表需要从头遍历。
  • 为什么 LinkedList 的头插入是 O(1),而 ArrayList 是 O(n)? 因为链表只需要改指针,数组需要移动后面所有元素。
  • 为什么消息队列用"队列"而不是"栈"? 因为消息需要按顺序消费,后进先出会导致老消息饿死。
  • 为什么递归会 StackOverflow? 因为函数调用栈是有限的,每次递归都会压栈,不退出就会溢出。

这些问题的答案,不在任何框架的文档里,而在数据结构的基础知识里。


下一篇预告 :《老程序员回炉补基础(三):二叉树------从递归遍历到非递归实现》

相关推荐
Yuzhiyuxia6 小时前
【AI Agent面试系列】AI Agent记忆机制知识点1
人工智能·面试·职场和发展
UIU1146 小时前
P1233 [ICPC 2001 Taejon R] 木棍加工
数据结构·c++·算法·动态规划
考研保研资料分享9 小时前
2025公卫保研清华专硕:三校考核、英文读译与择校复盘
面试
niyongsheng9 小时前
3D 装箱算法工程化:数据结构、放置启发式与支撑规则
数据结构·算法·rust
叉烧滑蛋11 小时前
B站台湾大学郭彦甫|MATLAB 学习笔记|04 变量与档案存取
数据结构
j7~12 小时前
【穷举、深搜与回溯算法】一棵决策树讲透全排列与子集
数据结构·c++·算法·leetcode·回溯·递归
Lichee2013 小时前
招行Java 面试知识点梳理:Spring 事务、锁、线程池、集合
java·面试
aiautotools14 小时前
Claude Code MCP 怎么配:命令、scope 与避坑
面试
soft_hub114 小时前
海外会议怎么用录音APP翻译会议内容
面试·职场和发展
考研保研资料分享14 小时前
2025生物信息跨保浙大:四校考核、文献读译与材料准备
面试