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

一、概念

定义:链表由一系列节点(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

相关推荐
星马梦缘2 小时前
aaaaa
数据结构·c++·算法
OpenApi.cc3 小时前
神经网络结构驱动+数据结构分析
数据结构·人工智能·神经网络
菜菜的顾清寒3 小时前
力扣HOT100(42)链表-随机链表的复制
算法·leetcode·链表
星恒随风4 小时前
C语言数据结构排序算法详解(下):冒泡排序、快速排序、归并排序和计数排序
c语言·数据结构·笔记·学习·排序算法
初夏睡觉5 小时前
数据结构学习之~二叉堆 (P3378 【模版】堆)
数据结构·c++·学习
云泽8086 小时前
笔试算法 - 链表篇(一):移除、反转、合并、回文判断全解析
数据结构·c++·算法·链表
也曾看到过繁星6 小时前
数据结构-复杂度
数据结构
菜菜的顾清寒6 小时前
HOT力扣100(43)二叉树-翻转二叉树
数据结构·算法·leetcode
Jasmine_llq7 小时前
《B3939 [GESP样题 四级] 绝对素数》
数据结构·算法·素数判断算法·数字拆分与反转算法·区间遍历枚举·双条件判断逻辑
郝学胜-神的一滴8 小时前
干货版《算法导论》07:递归视角下的选择排序与归并排序
java·数据结构·c++·python·程序人生·算法·排序算法