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

一、概念

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

相关推荐
j_xxx404_6 小时前
力扣题型--链表(两数相加|两两交换链表中的节点|重排链表)
数据结构·c++·算法·leetcode·蓝桥杯·排序算法
_日拱一卒7 小时前
LeetCode:240搜索二维矩阵Ⅱ
数据结构·线性代数·leetcode·矩阵
计算机安禾8 小时前
【数据结构与算法】第44篇:堆(Heap)的实现
c语言·开发语言·数据结构·c++·算法·排序算法·图论
汀、人工智能8 小时前
[特殊字符] 第91课:课程表
数据结构·算法·数据库架构·图论·bfs·课程表
测绘第一深情9 小时前
MapQR:自动驾驶在线矢量化高精地图构建的端到端 SOTA 方法
数据结构·人工智能·python·神经网络·算法·机器学习·自动驾驶
想带你从多云到转晴9 小时前
04、数据结构与算法---双向链表
java·数据结构·算法·链表
橘颂TA10 小时前
【笔试】算法的暴力美学——牛客 BC140:杨辉三角
数据结构·牛客
Lsk_Smion10 小时前
Hot100(开刷) 之 长度最小的数组--删除倒数第N个链表--层序遍历
java·数据结构·算法·kotlin
郝学胜-神的一滴11 小时前
从链表到二叉树:树形结构的入门与核心性质解析
数据结构·c++·python·算法·链表
北顾笙98011 小时前
day23-数据结构力扣
数据结构·算法·leetcode