帮你从算法的角度来认识链表------(一)

一、概念

定义:链表由一系列节点(Node)组成,每个节点包含两部分:数据域 :存储数据;指针域:指向下一个节点的地址。链表整体像一条链子,节点之间通过指针 "相连"。

基本结构:单链表、双链表、循环链表等(在算法题中最常见的是单链表)

特点:链表的内存空间是非连续存储;动态分配内存,与数组的连续空间不同,并且不能够像数组一样通过下标来访问元素;

与数组的主要区别:

对比项 数组 链表
内存空间 连续 不连续、分散
访问元素 随机访问,O (1) 只能从头遍历,O (n)
插入 / 删除 移动元素,O (n) 改指针即可,O (1)
大小 固定,不易扩容 动态扩容,灵活

二、单链表

1.结构

单链表每个节点有:data:存储数据、next:指针,指向下一个节点,只能向后遍历

2.核心操作

(1)遍历链表

从 head 开始,依次通过 next 访问,直到 null。

复制代码
void print() {
    ListNode cur = head;
    while (cur != null) {
        System.out.print(cur.val + " -> ");
        cur = cur.next;
    }
    System.out.println("null");
}

(2)查找节点

只从头遍历,找到对应值或位置。时间复杂度:O(n)

(3)插入节点(重点)

头部插入:① 创建一个新节点next=head ② head=新节点 。时间复杂度:O(1)

中间插入:① 找到前驱节点 pre ② 新节点.next = pre.next ③ pre.next = 新节点。时间复杂度O(1)

尾部插入:① 遍历到尾节点 ② 尾节点.next = 新节点。时间复杂度:O(n)

在插入节点时,要注意先连后断,防止链表断裂

(4)删除节点

① 找到要删除节点的前驱节点 predelNode = pre.next ③ pre.next = delNode.next ④ 释放 delNode

时间复杂度:O (n)(找节点),删除操作 O (1)

(5)链表长度

从头遍历计数

三、双链表

1.结构

双链表每个节点有:prev:指针,指向上一个节点、data:存储数据、next:指针,指向下一个节点,可以正向反向遍历

2.核心操作

(1)遍历链表

从前向后遍历、从后向前遍历

复制代码
// 正向遍历
public void traverseForward() {
    if (head == null) {
        System.out.println("链表为空");
        return;
    }

    DoubleNode current = head;
    while (current != null) {
        System.out.print(current.data + " <-> ");
        current = current.next;
    }
    System.out.println("null");
}
// 反向遍历
public void traverseBackward() {
    if (tail == null) {
        System.out.println("链表为空");
        return;
    }

    DoubleNode current = tail;
    while (current != null) {
        System.out.print(current.data + " <-> ");
        current = current.prev;
    }
    System.out.println("null");
}

(2)查找节点

和单链表基本一样都是从开头(或结尾)遍历一遍,时间复杂度:O(n)

复制代码
// 查找值为 key 的节点,找到返回节点,否则返回 null
public DoubleNode search(int key) {
    DoubleNode current = head;

    while (current != null) {
        if (current.data == key) {
            return current; // 找到
        }
        current = current.next;
    }
    return null; // 没找到
}

(3)插入节点(重点)

头部插入:创建一个新节点;如果链表为空:直接让头指针和尾指针都指向这个新节点;如果链表不为空:新节点的 next 指向原来的头节点、原来头节点的 prev 指向新节点、将头指针更新为新节点;插入完成,新节点成为新的头。

复制代码
public void addAtHead(int data) {
    DoubleNode newNode = new DoubleNode(data);

    if (head == null) {
        // 链表为空
        head = newNode;
        tail = newNode;
    } else {
        newNode.next = head;
        head.prev = newNode;
        head = newNode;
    }
}

尾部插入:创建一个新节点。如果链表为空:直接让头指针和尾指针都指向这个新节点。如果链表不为空:原来尾节点的 next 指向新节点。新节点的 prev 指向原来的尾节点。将尾指针更新为新节点。插入完成,新节点成为新的尾。

复制代码
public void addAtTail(int data) {
    DoubleNode newNode = new DoubleNode(data);

    if (tail == null) {
        head = newNode;
        tail = newNode;
    } else {
        tail.next = newNode;
        newNode.prev = tail;
        tail = newNode;
    }
}

中间插入:创建新节点。让新节点的 next 指向 prevNode 的下一个节点。让新节点的 prev 指向 prevNode。如果 prevNode 后面还有节点(不是尾节点):让 prevNode 的下一个节点的 prev 指向新节点。如果 prevNode 是尾节点:直接更新尾指针为新节点。最后让 prevNode 的 next 指向新节点。插入完成

复制代码
// 在 prevNode 后面插入新节点
public void insertAfter(DoubleNode prevNode, int data) {
    if (prevNode == null) {
        System.out.println("前驱节点不能为空");
        return;
    }

    DoubleNode newNode = new DoubleNode(data);

    newNode.next = prevNode.next;
    newNode.prev = prevNode;

    // 如果不是最后一个节点
    if (prevNode.next != null) {
        prevNode.next.prev = newNode;
    } else {
        // 是最后一个节点,更新 tail
        tail = newNode;
    }

    prevNode.next = newNode;
}

(4)删除节点

删除头节点:如果链表为空,直接不做操作。如果链表只有一个节点:将头指针和尾指针都置空。如果链表有多个节点:先将头指针移动到原头节点的下一个节点,把新头节点的前驱指针置空。原头节点脱离链表,删除完成。

复制代码
public void deleteHead() {
    if (head == null) return;

    // 只有一个节点
    if (head == tail) {
        head = null;
        tail = null;
    } else {
        head = head.next;
        head.prev = null;
    }
}

删除尾节点:如果链表为空,直接不做操作。如果链表只有一个节点:将头指针和尾指针都置空。如果链表有多个节点:先将尾指针移动到原尾节点的上一个节点。把新尾节点的后继指针置空。原尾节点脱离链表,删除完成。

复制代码
public void deleteTail() {
    if (tail == null) return;

    if (head == tail) {
        head = null;
        tail = null;
    } else {
        tail = tail.prev;
        tail.next = null;
    }
}

删除中间节点:先找到要删除的目标节点。让目标节点的前驱节点的后继指针 ,直接指向目标节点的后继节点。让目标节点的后继节点的前驱指针,直接指向目标节点的前驱节点。目标节点被前后节点 "跳过",完全脱离链表,删除完成。

复制代码
// 删除中间节点 delNode
public void deleteMiddleNode(DoubleNode delNode) {
    if (delNode == null) return;

    // 拿到前驱和后继
    DoubleNode prevNode = delNode.prev;
    DoubleNode nextNode = delNode.next;

    // 前后互相指向,跳过要删除的节点
    prevNode.next = nextNode;
    nextNode.prev = prevNode;
}

(5)链表长度

依旧是依次遍历计数

四、时间复杂度对比

操作 单链表 双向链表
查找 O(n) O(n)
头部插入 / 删除 O(1) O(1)
尾部插入 O(n) O (1)(有尾指针)
任意位置插入 O(n) O(n)
任意位置删除 O(n) O(n)

五、总结

优点

  1. 内存不连续,无空间浪费
  2. 插入删除效率高(只需改指针)
  3. 大小动态变化,无需预先分配
  4. 适合频繁增删、长度不确定的场景

缺点

  1. 不能随机访问,只能遍历
  2. 每个节点多存指针,额外内存开销
  3. 查找慢,不适合大量查询操作

一句话就是:读少写多(数组是读多写少)

六、力扣基础练习

203、206

相关推荐
jiushiapwojdap8 小时前
LU分解法求解线性方程组Matlab实现
数据结构·其他·算法·matlab
纽扣6679 小时前
【算法进阶之路】链表进阶:删除、合并、回文与排序全解析
数据结构·算法·链表
xvhao201311 小时前
单源、多源最短路
数据结构·c++·算法·深度优先·动态规划·图论·图搜索算法
m0_6294947313 小时前
LeetCode 热题 100-----17.缺失的第一个正数
数据结构·算法·leetcode
hnjzsyjyj14 小时前
洛谷 P1443:马的遍历 ← BFS
数据结构·bfs
做时间的朋友。14 小时前
精准核酸检测
java·数据结构·算法
如君愿15 小时前
考研复习 Day28 | 习题--计算机网络第四章(网络层 中)、数据结构(树与二叉树 下)
数据结构·计算机网络·考研·课后习题·记录考研
江南十四行15 小时前
排序算法进阶:直接插入排序(简单排序)与希尔排序
数据结构·算法·排序算法
洛水水15 小时前
【Redis入门】一篇详解Redis五大数据结构
数据结构·数据库·redis
CoderCodingNo15 小时前
【CSP】CSP-J 2021真题 | 插入排序 luogu-P7910 (适合GESP四-六级及以上考生练习)
数据结构·算法·排序算法