链表真的那么难吗?一文带你从0到1轻松掌握链表!

126 阅读4分钟

还在被数据结构"链表"折磨得头疼不已?还在为LeetCode上的链表题抓耳挠腮?别担心!今天我就带你用最通俗易懂的方式,从零开始彻底理解链表,让你不再害怕这个看似复杂的数据结构!

一、什么是链表?为什么需要它?

假设你和朋友们排队买票。如果是"普通队列",有人离开就得所有人往前挪。但如果每个人只记住前面那个人是谁,有人离开只需要改变记忆就行了,不用挪动。这就是链表的核心思想。

数组 vs 链表

数组就像一排整齐的储物柜:

  • 存储连续,可以直接通过下标访问
  • 查找速度快(O(1)时间复杂度)
  • 但插入、删除元素需要移动大量数据

而链表则像一条"信息传递链":

  • 存储离散,通过"指针"连接各个节点
  • 插入、删除操作只需修改指针,效率高(O(1)时间复杂度)
  • 但查找必须从头遍历,效率较低(O(n)时间复杂度)

二、链表的基本结构

链表由一个个"节点"组成,每个节点包含两部分:

  1. 数据域:存储实际的数据
  2. 指针域:指向下一个节点的地址

用JavaScript代码表示一个简单的链表节点:

function ListNode(val, next) {
    this.val = val               // 数据域
    this.next = next ? next : null  // 指针域
}

一个完整的链表结构:

const list = {  // 链表的头节点
    val: 'a',
    next: {
        val: 'b',
        next: {
            val: 'c',
            next: null  // 链表的尾节点指向null
        }
    }
}

三、链表的基本操作

1. 创建链表

创建一个简单的链表:

const head = new ListNode('a');
const nodeB = new ListNode('b');
const nodeC = new ListNode('c');

head.next = nodeB;
nodeB.next = nodeC;

2. 遍历链表

let current = head;
while (current !== null) {
    console.log(current.val);  // 输出当前节点的值
    current = current.next;    // 移动到下一个节点
}

3. 插入节点

在位置B和C之间插入新节点X:

const nodeX = new ListNode('x');
nodeX.next = nodeB.next;  // 先连后面
nodeB.next = nodeX;       // 再连前面

关键点:先连后面,再连前面,避免丢失后续链表。

4. 删除节点

删除节点C:

nodeB.next = nodeC.next;  // 直接"跳过"nodeC

四、LeetCode实战:解决经典链表问题

问题1:合并两个有序链表 (LeetCode 21)

给你两个升序链表,请合并成一个新的升序链表。

var mergeTwoLists = function(list1, list2) {
    let dummy = new ListNode();  // 创建一个哑节点作为新链表的起点
    let current = dummy;         // 用current遍历并构建新链表
    
    // 当两个链表都不为空时循环比较
    while (list1 !== null && list2 !== null) {
        // 取较小值的节点加入新链表
        if (list1.val < list2.val) {
            current.next = list1;
            list1 = list1.next;
        } else {
            current.next = list2;
            list2 = list2.next;
        }
        current = current.next;
    }
    
    // 处理剩余部分
    if (list1 !== null) {
        current.next = list1;
    } else {
        current.next = list2;
    }
    
    return dummy.next;  // 返回哑节点的下一个节点
};

这题的关键是理解"哑节点"技巧——用一个临时节点作为新链表的起点,避免处理头节点的特殊情况。

问题2:删除排序链表中的重复元素 (LeetCode 83/136)

删除链表中的重复元素,使每个元素只出现一次。

var deleteDuplicates = function(head) {
    let current = head;
    
    while (current && current.next) {
        if (current.val === current.next.val) {
            // 遇到重复值,直接跳过下一节点
            current.next = current.next.next;
        } else {
            // 没有重复,继续前进
            current = current.next;
        }
    }
    
    return head;
};

问题3:删除排序链表中的所有重复元素 (LeetCode 82)

删除链表中所有出现重复的元素,只保留原始链表中没有重复出现的数字。

var deleteDuplicates = function(head) {
    if (!head) return head;
    
    // 创建哑节点,处理头节点可能被删除的情况
    let dummy = new ListNode(0, head);
    let prev = dummy;
    
    while (prev.next && prev.next.next) {
        if (prev.next.val === prev.next.next.val) {
            // 遇到重复值
            let val = prev.next.val;
            // 跳过所有重复值
            while (prev.next && prev.next.val === val) {
                prev.next = prev.next.next;
            }
        } else {
            // 没有重复,前进一步
            prev = prev.next;
        }
    }
    
    return dummy.next;
};

五、链表的进阶操作

除了基本的单向链表,还有:

  1. 双向链表:每个节点有两个指针,分别指向前后节点
  2. 循环链表:尾节点指向头节点,形成一个环
  3. 跳表:在链表基础上添加多级索引,提高查找效率

六、链表的实际应用

链表在实际开发中广泛应用:

  1. 浏览器的前进/后退功能:双向链表的典型应用
  2. 操作系统中的任务调度:循环链表管理进程
  3. 大数据存储:当数据量不确定时,链表比数组更灵活
  4. LRU缓存算法:使用链表实现缓存淘汰策略

七、链表学习技巧总结

  1. 画图理解:链表操作最怕"断链",画图可以帮助你理清指针变化
  2. 掌握哑节点技巧:创建一个dummy节点简化头节点的处理
  3. 双指针法:解决"倒数第K个节点"、"中间节点"等问题的利器
  4. 递归思维:链表天然适合递归,很多复杂操作用递归更简洁

本文从初学者视角出发,深入浅出地讲解了链表的基本概念、结构特点与数组的对比,详细介绍了链表的创建、遍历、插入和删除操作,并通过LeetCode经典题目实战演练,帮助读者快速掌握链表的核心技巧,轻松应对链表相关问题。