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

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

很多程序员用了多年 ArrayListLinkedList,却说不出链表插入一个节点需要几步操作。我用 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 == nullcurr 回退到 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)**的数据结构,需要维护两个指针 headertail

  • 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? 因为函数调用栈是有限的,每次递归都会压栈,不退出就会溢出。

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


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

相关推荐
MegaDataFlowers2 小时前
141.环形链表
数据结构·链表
JAVA面经实录91711 小时前
计算机基础(完整版·超详细可背诵)
java·linux·数据结构·算法
浅念-12 小时前
「一文吃透 BFS:从层序遍历到锯齿形、最大宽度、每层最大值」
数据结构·算法
AI人工智能+电脑小能手14 小时前
【大白话说Java面试题】【Java基础篇】第30题:JDK动态代理和CGLIB动态代理有什么区别
java·开发语言·后端·面试·代理模式
苍煜14 小时前
二叉树、红黑树、B树、B+树通俗教学:各自适配场景+MySQL索引终极选型原因
数据结构·b树·mysql
头发够用的程序员15 小时前
C++和Python面试经典算法汇总(一)
开发语言·c++·python·算法·容器·面试
炸膛坦客15 小时前
嵌入式 - 数据结构与算法:(1-1)数据结构 - 顺序表(Sequential List)
数据结构·算法·嵌入式
水龙吟啸15 小时前
数据结构与算法随机复习–Day1
数据结构·c++·算法
云泽80817 小时前
二叉树高阶笔试算法题精讲(二):非递归遍历与序列构造全解析
c++·算法·面试