《数据结构:从0到1》-06-单链表&双链表

链表基础:单链表与双链表

作为一名程序员,你一定遇到过这样的困境:用数组存储数据时,插入删除操作让你头疼不已。今天就带你彻底掌握解决这个问题的利器------链表!

1. 链表到底是什么?我们先从生活场景理解

1.1 现实世界的链表例子

想象一下火车车厢:

  • 每节车厢都是独立的
  • 车厢之间通过挂钩连接
  • 可以轻松添加或移除车厢
  • 不需要连续的铁轨空间

这就是链表的精髓!链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

1.2 为什么需要链表?数组的痛点

先来看个真实案例:

java 复制代码
// 用数组实现排队系统 - 痛苦!
String[] queue = {"张三", "李四", "王五", null, null};
// 当李四离开时...
queue[1] = queue[2];  // 王五前移
queue[2] = queue[3];  // 后续元素都要前移
queue[3] = queue[4];  // 效率太低了!

数组的三大痛点:

  1. 插入删除需要移动大量元素
  2. 大小固定,扩容成本高
  3. 内存必须连续,可能分配失败

链表的优势:

  • ✅ 插入删除只需修改指针
  • ✅ 动态大小,按需分配
  • ✅ 内存不需要连续

2. 单链表:最简单的链式结构

2.1 单链表节点:链表的"原子"

java 复制代码
/**
 * 单链表节点类:就像火车的一节车厢
 * 
 * 结构分析:
 * ┌────────────┬────────────┐
 * │   数据域    │   指针域    │
 * │   data    │    next   │
 * └────────────┴────────────┘
 * 
 * 功能说明:
 * - data: 存储实际数据(乘客)
 * - next: 指向下一个节点(挂钩)
 */
class SingleListNode {
    int data;              // 节点存储的数据
    SingleListNode next;   // 指向下一个节点的指针
    
    // 构造函数
    SingleListNode(int data) {
        this.data = data;
        this.next = null;  // 初始时独立节点,不连接任何其他节点
    }
}

节点关系图示:

css 复制代码
节点A:┌─────┬─────┐   节点B:┌─────┬─────┐   节点C:┌─────┬─────┐
      │  10 │  ●──┼───→│  20 │  ●──┼───→│  30 │ null │
      └─────┴─────┘     └─────┴─────┘     └─────┴─────┘

2.2 单链表的完整实现与操作详解

java 复制代码
/**
 * 单链表实现类:管理整个链表结构
 * 
 * 核心组成:
 * head → 节点1 → 节点2 → ... → 节点n → null
 * 
 * 特点:
 * - 只能从头开始顺序访问
 * - 插入删除效率高
 * - 随机访问效率低
 */
class SingleLinkedList {
    private SingleListNode head;  // 头指针:链表的起点
    
    public SingleLinkedList() {
        head = null;  // 空链表:head指向null
    }
    
    /**
     * 在链表头部插入节点 - O(1)时间复杂度
     * 
     * 操作流程:
     * 1. 创建新节点
     * 2. 新节点指向原头节点
     * 3. 更新头指针指向新节点
     * 
     * 示例:
     * 插入前:head → 2 → 3 → null
     * 插入1:head → 1 → 2 → 3 → null
     */
    public void addAtHead(int data) {
        // 1. 创建新节点
        SingleListNode newNode = new SingleListNode(data);
        
        // 2. 新节点指向原头节点
        newNode.next = head;
        
        // 3. 更新头指针
        head = newNode;
        
        System.out.println("在头部插入节点: " + data);
    }
    
    /**
     * 在链表尾部插入节点 - O(n)时间复杂度
     * 
     * 操作流程:
     * 1. 创建新节点
     * 2. 找到最后一个节点(next为null)
     * 3. 最后一个节点指向新节点
     * 
     * 特殊情况:空链表时,新节点就是头节点
     */
    public void addAtTail(int data) {
        SingleListNode newNode = new SingleListNode(data);
        
        // 空链表特殊情况处理
        if (head == null) {
            head = newNode;
            return;
        }
        
        // 遍历找到最后一个节点
        SingleListNode current = head;
        while (current.next != null) {
            current = current.next;
        }
        
        // 连接新节点
        current.next = newNode;
        
        System.out.println("在尾部插入节点: " + data);
    }
    
    /**
     * 在指定位置插入节点 - O(n)时间复杂度
     * 
     * 操作流程:
     * 1. 处理特殊情况(头部插入、空链表)
     * 2. 找到插入位置的前一个节点
     * 3. 调整指针连接
     * 
     * 指针调整四步法:
     * 新节点.next = 前节点.next
     * 前节点.next = 新节点
     */
    public void addAtIndex(int index, int data) {
        if (index < 0) {
            System.out.println("索引不能为负数");
            return;
        }
        
        // 头部插入特殊情况
        if (index == 0) {
            addAtHead(data);
            return;
        }
        
        SingleListNode newNode = new SingleListNode(data);
        SingleListNode current = head;
        
        // 找到插入位置的前一个节点
        for (int i = 0; i < index - 1 && current != null; i++) {
            current = current.next;
        }
        
        // 检查索引是否有效
        if (current == null) {
            System.out.println("索引超出链表长度");
            return;
        }
        
        // 调整指针连接
        newNode.next = current.next;  // 新节点指向原位置节点
        current.next = newNode;       // 前节点指向新节点
        
        System.out.println("在位置 " + index + " 插入节点: " + data);
    }
    
    /**
     * 删除指定位置节点 - O(n)时间复杂度
     * 
     * 操作流程:
     * 1. 处理特殊情况(删除头节点)
     * 2. 找到要删除节点的前一个节点
     * 3. 调整指针跳过要删除的节点
     * 
     * 内存管理:被跳过的节点会被Java垃圾回收器自动回收
     */
    public void deleteAtIndex(int index) {
        if (head == null || index < 0) {
            System.out.println("链表为空或索引无效");
            return;
        }
        
        // 删除头节点特殊情况
        if (index == 0) {
            int deletedData = head.data;
            head = head.next;
            System.out.println("删除头节点: " + deletedData);
            return;
        }
        
        SingleListNode current = head;
        
        // 找到要删除节点的前一个节点
        for (int i = 0; i < index - 1 && current != null; i++) {
            current = current.next;
        }
        
        // 检查索引是否有效
        if (current == null || current.next == null) {
            System.out.println("索引超出链表长度");
            return;
        }
        
        int deletedData = current.next.data;
        current.next = current.next.next;  // 跳过要删除的节点
        
        System.out.println("删除位置 " + index + " 的节点: " + deletedData);
    }
    
    /**
     * 查找节点 - O(n)时间复杂度
     * 
     * 线性搜索:从头部开始逐个比较
     * 适用于无序链表
     */
    public boolean contains(int data) {
        SingleListNode current = head;
        int position = 0;
        
        while (current != null) {
            if (current.data == data) {
                System.out.println("找到节点 " + data + " 在位置 " + position);
                return true;
            }
            current = current.next;
            position++;
        }
        
        System.out.println("未找到节点: " + data);
        return false;
    }
    
    /**
     * 获取链表长度 - O(n)时间复杂度
     */
    public int getLength() {
        int length = 0;
        SingleListNode current = head;
        
        while (current != null) {
            length++;
            current = current.next;
        }
        
        return length;
    }
    
    /**
     * 打印链表:可视化链表结构
     * 
     * 输出格式:1 → 2 → 3 → null
     */
    public void printList() {
        SingleListNode current = head;
        
        if (current == null) {
            System.out.println("链表为空");
            return;
        }
        
        while (current != null) {
            System.out.print(current.data);
            if (current.next != null) {
                System.out.print(" → ");
            }
            current = current.next;
        }
        System.out.println(" → null");
    }
}

2.3 单链表操作流程详解

🔧 插入操作详细流程:

头部插入图示:

csharp 复制代码
初始状态:
head → 2 → 3 → null

步骤1:创建新节点(1)
[1] → null

步骤2:新节点指向原头节点
[1] → 2 → 3 → null

步骤3:更新head指针
head → [1] → 2 → 3 → null

中间插入图示(在位置1插入):

ini 复制代码
初始状态:
head → 1 → 3 → null

步骤1:找到位置0的节点(1)
current指向节点1

步骤2:创建新节点(2)
[2] → null

步骤3:调整指针
节点1.next = 节点2
节点2.next = 节点3

结果:
head → 1 → 2 → 3 → null
🗑️ 删除操作详细流程:

删除中间节点图示(删除位置1):

vbscript 复制代码
初始状态:
head → 1 → 2 → 3 → null

步骤1:找到位置0的节点(1)
current指向节点1

步骤2:跳过要删除的节点
节点1.next = 节点1.next.next
即:节点1直接指向节点3

结果:
head → 1 → 3 → null

节点2失去引用,被垃圾回收

2.4 单链表实战应用

应用场景1:浏览器历史记录

java 复制代码
// 使用单链表实现简单的浏览历史
SingleLinkedList history = new SingleLinkedList();

// 访问新页面
history.addAtHead("page1");
history.addAtHead("page2");
history.addAtHead("page3");

// 点击后退:删除头节点
history.deleteAtIndex(0); // 回到page2

// 打印历史:page2 → page1 → null
history.printList();

应用场景2:任务队列

java 复制代码
// 简单的任务调度系统
SingleLinkedList taskQueue = new SingleLinkedList();

// 添加任务
taskQueue.addAtTail(101); // 低优先级
taskQueue.addAtTail(102); // 低优先级
taskQueue.addAtHead(201); // 高优先级插队

// 执行任务:从头开始处理
// 队列:201 → 101 → 102 → null

3. 双链表:双向通行的增强链表

3.1 双链表节点:前后都能访问

如果说单链表是单行道,那双链表就是双行道!

java 复制代码
/**
 * 双链表节点类:像地铁车厢,前后都能通行
 * 
 * 结构分析:
 * ┌────────────┬────────────┬────────────┐
 * │   prev    │   数据域    │    next    │
 * │  指针域    │   data    │   指针域    │
 * └────────────┴────────────┴────────────┘
 * 
 * 优势:
 * - 可以向前遍历
 * - 可以向后遍历  
 * - 删除操作更高效
 * 
 * 代价:
 * - 每个节点多一个指针的内存开销
 */
class DoubleListNode {
    int data;               // 节点数据
    DoubleListNode prev;    // 指向前驱节点的指针
    DoubleListNode next;    // 指向后继节点的指针
    
    // 构造函数
    DoubleListNode(int data) {
        this.data = data;
        this.prev = null;
        this.next = null;
    }
}

双链表节点关系图示:

csharp 复制代码
null ⇄ 节点A ⇄ 节点B ⇄ 节点C ⇄ null
     ←        ←        ←
     →        →        →

3.2 双链表的完整实现

java 复制代码
/**
 * 双链表实现类:支持双向遍历的链表
 * 
 * 结构特点:
 * null ⇄ head ⇄ 节点1 ⇄ 节点2 ⇄ ... ⇄ tail ⇄ null
 * 
 * 优势操作:
 * - 头部插入/删除:O(1)
 * - 尾部插入/删除:O(1) 
 * - 双向遍历
 */
class DoubleLinkedList {
    private DoubleListNode head;  // 头指针
    private DoubleListNode tail;  // 尾指针
    private int size;             // 链表长度
    
    public DoubleLinkedList() {
        head = null;
        tail = null;
        size = 0;
    }
    
    /**
     * 头部插入 - O(1)时间复杂度
     * 
     * 操作流程:
     * 1. 创建新节点
     * 2. 处理空链表情况
     * 3. 调整头指针和相关指针
     * 
     * 指针调整规则:
     * - 新节点.next = 原head
     * - 原head.prev = 新节点
     * - head = 新节点
     */
    public void addAtHead(int data) {
        DoubleListNode newNode = new DoubleListNode(data);
        
        if (head == null) {
            // 空链表:新节点既是头也是尾
            head = newNode;
            tail = newNode;
        } else {
            // 非空链表:调整指针关系
            newNode.next = head;  // 新节点指向原头节点
            head.prev = newNode;  // 原头节点指回新节点
            head = newNode;       // 更新头指针
        }
        size++;
        
        System.out.println("在头部插入节点: " + data);
    }
    
    /**
     * 尾部插入 - O(1)时间复杂度
     * 
     * 双链表的巨大优势:不需要遍历整个链表!
     * 直接通过tail指针访问尾部
     */
    public void addAtTail(int data) {
        DoubleListNode newNode = new DoubleListNode(data);
        
        if (tail == null) {
            // 空链表
            head = newNode;
            tail = newNode;
        } else {
            // 非空链表
            tail.next = newNode;  // 原尾节点指向新节点
            newNode.prev = tail;  // 新节点指回原尾节点
            tail = newNode;       // 更新尾指针
        }
        size++;
        
        System.out.println("在尾部插入节点: " + data);
    }
    
    /**
     * 指定位置插入 - O(n)时间复杂度
     * 
     * 双链表中间插入的指针调整比较复杂:
     * 需要同时维护prev和next指针
     * 
     * 指针调整四步法:
     * 1. 新节点.prev = 前节点
     * 2. 新节点.next = 后节点  
     * 3. 前节点.next = 新节点
     * 4. 后节点.prev = 新节点
     */
    public void addAtIndex(int index, int data) {
        if (index < 0 || index > size) {
            System.out.println("索引无效: " + index);
            return;
        }
        
        // 头部插入特殊情况
        if (index == 0) {
            addAtHead(data);
            return;
        }
        
        // 尾部插入特殊情况
        if (index == size) {
            addAtTail(data);
            return;
        }
        
        // 找到要插入位置的节点
        DoubleListNode current = getNode(index);
        DoubleListNode newNode = new DoubleListNode(data);
        
        // 调整四个指针!
        newNode.prev = current.prev;  // 1. 新节点前指针
        newNode.next = current;       // 2. 新节点后指针
        current.prev.next = newNode;  // 3. 前节点的后指针
        current.prev = newNode;       // 4. 当前节点的前指针
        
        size++;
        System.out.println("在位置 " + index + " 插入节点: " + data);
    }
    
    /**
     * 删除指定位置节点 - O(1)或O(n)时间复杂度
     * 
     * 双链表删除的优势:
     * - 删除头尾节点:O(1)
     * - 删除中间节点:O(1)指针调整,但查找需要O(n)
     */
    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) {
            System.out.println("索引无效: " + index);
            return;
        }
        
        DoubleListNode toDelete = getNode(index);
        
        if (size == 1) {
            // 只有一个节点
            head = null;
            tail = null;
        } else if (toDelete == head) {
            // 删除头节点
            head = head.next;
            head.prev = null;
        } else if (toDelete == tail) {
            // 删除尾节点
            tail = tail.prev;
            tail.next = null;
        } else {
            // 删除中间节点:调整前后指针
            toDelete.prev.next = toDelete.next;
            toDelete.next.prev = toDelete.prev;
        }
        
        size--;
        System.out.println("删除位置 " + index + " 的节点: " + toDelete.data);
    }
    
    /**
     * 获取指定位置节点 - 优化版本
     * 
     * 根据位置选择从头或从尾遍历
     * 平均时间复杂度:O(n/2)
     */
    private DoubleListNode getNode(int index) {
        if (index < 0 || index >= size) return null;
        
        DoubleListNode current;
        
        // 优化:根据位置选择遍历方向
        if (index < size / 2) {
            // 从前向后遍历
            current = head;
            for (int i = 0; i < index; i++) {
                current = current.next;
            }
        } else {
            // 从后向前遍历
            current = tail;
            for (int i = size - 1; i > index; i--) {
                current = current.prev;
            }
        }
        
        return current;
    }
    
    /**
     * 前向遍历:从头到尾
     */
    public void printForward() {
        DoubleListNode current = head;
        System.out.print("前向遍历: null ⇄ ");
        
        while (current != null) {
            System.out.print(current.data);
            if (current.next != null) {
                System.out.print(" ⇄ ");
            }
            current = current.next;
        }
        System.out.println(" ⇄ null");
    }
    
    /**
     * 后向遍历:从尾到头 - 单链表做不到!
     */
    public void printBackward() {
        DoubleListNode current = tail;
        System.out.print("后向遍历: null ⇄ ");
        
        while (current != null) {
            System.out.print(current.data);
            if (current.prev != null) {
                System.out.print(" ⇄ ");
            }
            current = current.prev;
        }
        System.out.println(" ⇄ null");
    }
    
    /**
     * 获取链表长度
     */
    public int getSize() {
        return size;
    }
}

3.3 双链表操作流程详解

🔧 双链表插入操作流程:

头部插入详细步骤:

ini 复制代码
初始状态:
null ⇄ head[1] ⇄ [2] ⇄ [3] ⇄ null

步骤1:创建新节点[0]
[0] : prev=null, next=null

步骤2:新节点指向原head
[0].next = [1]
[1].prev = [0]

步骤3:更新head指针
head = [0]

结果:
null ⇄ head[0] ⇄ [1] ⇄ [2] ⇄ [3] ⇄ null

中间插入详细步骤(在位置1插入):

ini 复制代码
初始状态:
null ⇄ [1] ⇄ [3] ⇄ null

步骤1:找到位置1的节点[3]
current = [3]

步骤2:创建新节点[2]
[2] : prev=null, next=null

步骤3:调整四个指针:
[2].prev = [1]      // 新节点前指
[2].next = [3]      // 新节点后指  
[1].next = [2]      // 前节点后指
[3].prev = [2]      // 后节点前指

结果:
null ⇄ [1] ⇄ [2] ⇄ [3] ⇄ null

3.4 双链表实战应用

应用场景1:浏览器历史记录功能

java 复制代码
// 双链表实现支持前进后退的浏览器历史
DoubleLinkedList browserHistory = new DoubleLinkedList();
DoubleListNode currentPage = null;

// 访问页面
browserHistory.addAtTail("google.com");
browserHistory.addAtTail("github.com"); 
browserHistory.addAtTail("stackoverflow.com");

// 前进后退功能
public void goBack() {
    if (currentPage != null && currentPage.prev != null) {
        currentPage = currentPage.prev;
        System.out.println("后退到: " + currentPage.data);
    }
}

public void goForward() {
    if (currentPage != null && currentPage.next != null) {
        currentPage = currentPage.next;
        System.out.println("前进到: " + currentPage.data);
    }
}

应用场景2:音乐播放列表

java 复制代码
// 双链表实现音乐播放器
DoubleLinkedList playlist = new DoubleLinkedList();
DoubleListNode currentSong = null;

// 添加歌曲
playlist.addAtTail("歌曲1");
playlist.addAtTail("歌曲2");
playlist.addAtTail("歌曲3");

// 上一曲/下一曲功能
public void previousSong() {
    if (currentSong != null && currentSong.prev != null) {
        currentSong = currentSong.prev;
        System.out.println("播放上一曲: " + currentSong.data);
    }
}

public void nextSong() {
    if (currentSong != null && currentSong.next != null) {
        currentSong = currentSong.next;
        System.out.println("播放下一曲: " + currentSong.data);
    }
}

4. 链表 vs 数组:全方位对比

4.1 性能对比详细分析

操作 数组 单链表 双链表 详细说明
随机访问 🟢 O(1) 🔴 O(n) 🔴 O(n) 数组通过索引直接计算内存地址
头部插入 🔴 O(n) 🟢 O(1) 🟢 O(1) 链表只需调整1-2个指针
尾部插入 🟢 O(1) 🔴 O(n) 🟢 O(1) 双链表有tail指针优势
中间插入 🔴 O(n) 🟡 O(n) 🟡 O(n) 都需要查找位置,但链表插入本身O(1)
头部删除 🔴 O(n) 🟢 O(1) 🟢 O(1) 链表指针调整优势明显
尾部删除 🟢 O(1) 🔴 O(n) 🟢 O(1) 双链表直接通过tail操作
内存使用 固定 动态 动态 链表按需分配,更灵活
内存布局 连续 分散 分散 数组缓存友好,链表指针开销

4.2 内存布局对比图示

数组内存布局:

ini 复制代码
内存地址: 1000  1004  1008  1012  1016
数据:    [ 1 ] [ 2 ] [ 3 ] [ 4 ] [ 5 ]
特点:连续存储,缓存友好

链表内存布局:

ini 复制代码
内存地址: 2000      3048      1500      4096
数据:    [1|3048]→[2|1500]→[3|4096]→[4|null]
特点:分散存储,有指针开销

4.3 选择方案:什么时候用什么?

🟢 选择数组的情况:

适用场景:

  • 需要频繁随机访问元素
  • 数据量固定或可预测
  • 对性能要求极高,需要缓存友好性
  • 内存使用要求严格

具体例子:

java 复制代码
// 游戏中的地图网格
int[][] gameMap = new int[100][100];

// 学生成绩表(固定数量)
int[] scores = new int[50];

// 图像像素数据
int[] pixels = new int[1920 * 1080];
🟢 选择单链表的情况:

适用场景:

  • 频繁在头部插入删除
  • 数据量变化很大,无法预测
  • 内存比较紧张
  • 只需要单向遍历

具体例子:

java 复制代码
// 撤销操作栈
SingleLinkedList undoStack = new SingleLinkedList();

// 消息队列(先进先出)
SingleLinkedList messageQueue = new SingleLinkedList();

// 多项式表示(稀疏矩阵)
// 只存储非零项:系数|指数|next
🟢 选择双链表的情况:

适用场景:

  • 需要双向遍历
  • 频繁在头部和尾部操作
  • 需要实现撤销/重做功能
  • 内存充足,追求操作效率

具体例子:

java 复制代码
// 浏览器历史记录
DoubleLinkedList browserHistory = new DoubleLinkedList();

// 音乐播放列表
DoubleLinkedList musicPlaylist = new DoubleLinkedList();

// 文本编辑器中的行存储
// 支持向上向下遍历

5. 链表实战技巧与常见问题

5.1 链表操作的核心技巧

技巧1:虚拟头节点(Dummy Node)
java 复制代码
/**
 * 虚拟头节点技巧:简化边界条件处理
 * 
 * 优势:
 * - 避免空链表特殊情况
 * - 统一插入删除逻辑
 * - 简化代码复杂度
 */
public class LinkedListWithDummy {
    private SingleListNode dummyHead;  // 虚拟头节点
    
    public LinkedListWithDummy() {
        dummyHead = new SingleListNode(0);  // 数据域无意义
    }
    
    // 所有操作都通过dummyHead.next访问真实头节点
    public void addAtHead(int data) {
        SingleListNode newNode = new SingleListNode(data);
        newNode.next = dummyHead.next;  // 总是安全的!
        dummyHead.next = newNode;
    }
}
技巧2:快慢指针法
java 复制代码
/**
 * 快慢指针法:解决链表经典问题
 * 
 * 应用场景:
 * - 判断链表是否有环
 * - 找到链表中间节点
 * - 找到倒数第K个节点
 */
public SingleListNode findMiddleNode(SingleListNode head) {
    if (head == null) return null;
    
    SingleListNode slow = head;  // 慢指针:每次1步
    SingleListNode fast = head;  // 快指针:每次2步
    
    while (fast != null && fast.next != null) {
        slow = slow.next;        // 乌龟走1步
        fast = fast.next.next;   // 兔子走2步
    }
    
    return slow;  // 当兔子跑到终点,乌龟就在中间
}

5.2 常见问题与解决方案

问题1:空指针异常
java 复制代码
// ❌ 错误写法:可能抛出NullPointerException
public void badDelete(SingleListNode node) {
    node.next = node.next.next;  // 如果node.next为null就崩溃!
}

// ✅ 正确写法:先检查再操作
public void safeDelete(SingleListNode node) {
    if (node != null && node.next != null) {
        node.next = node.next.next;
    }
}
问题2:循环引用(内存泄漏)
java 复制代码
// ❌ 错误:创建循环引用
nodeA.next = nodeB;
nodeB.next = nodeA;  // 循环引用,无法被垃圾回收!

// ✅ 正确:确保链表的最终指向null
nodeA.next = nodeB;
nodeB.next = null;   // 正确终止

5.3 链表调试技巧

java 复制代码
/**
 * 增强的链表打印方法:包含更多调试信息
 */
public void debugPrintList() {
    SingleListNode current = head;
    int position = 0;
    
    System.out.println("=== 链表调试信息 ===");
    System.out.println("头节点: " + (head != null ? head.data : "null"));
    System.out.println("链表长度: " + getLength());
    System.out.println("详细内容:");
    
    while (current != null) {
        System.out.printf("位置%d: 数据=%d, next=%s\n", 
            position, 
            current.data,
            current.next != null ? String.valueOf(current.next.data) : "null"
        );
        current = current.next;
        position++;
    }
    System.out.println("===================");
}

6. 总结与进阶

📚 核心知识点回顾

单链表要点:
  1. 结构特点:每个节点包含数据和指向下一个节点的指针
  2. 操作复杂度
    • 插入删除:O(1)到O(n)
    • 查找访问:O(n)
  3. 适用场景:栈、队列、只需要单向遍历的场景
双链表要点:
  1. 结构特点:每个节点包含指向前后节点的指针
  2. 操作复杂度
    • 头尾操作:O(1)
    • 中间操作:O(n)查找 + O(1)调整
  3. 适用场景:需要双向遍历、频繁头尾操作的场景
选择策略总结:
  • 随机访问多 → 选择数组
  • 插入删除多 → 选择链表
  • 只需要单向遍历 → 单链表
  • 需要双向遍历 → 双链表
  • 内存紧张 → 单链表
  • 追求操作效率 → 双链表
推荐练习题目:
  • 简单:反转链表、合并两个有序链表
  • 中等:链表排序、重排链表、复制带随机指针的链表
  • 困难:LRU缓存机制、LFU缓存

结语

链表作为数据结构家族中的重要成员,是每个程序员必须掌握的硬核技能。记住:理解指针操作是掌握链表的关键,多动手实践,在纸上画出指针的变化过程,你会对链表有更深的理解。 有任何问题欢迎在评论区留言,我会认真解答每一个问题!🚀

相关推荐
南枝异客5 小时前
查找算法-顺序查找
python·算法
violet-lz5 小时前
数据结构八大排序:快速排序-挖坑法(递归与非递归)及其优化
数据结构
李牧九丶5 小时前
从零学算法59
算法
Mrliu__6 小时前
Python数据结构(七):Python 高级排序算法:希尔 快速 归并
数据结构·python·排序算法
一匹电信狗6 小时前
【C++】手搓AVL树
服务器·c++·算法·leetcode·小程序·stl·visual studio
月疯6 小时前
离散卷积,小demo(小波信号分析)
算法
敲代码的瓦龙7 小时前
西邮移动应用开发实验室2025年二面题解
开发语言·c++·算法
RTC老炮7 小时前
webrtc弱网-RembThrottler类源码分析及算法原理
网络·算法·webrtc
野蛮人6号7 小时前
力扣热题100道之73矩阵置零
算法·leetcode·矩阵