03 单向链表

May 31, 2025 / 老大 / 1阅读 / 0评论/ 分类: 数据结构和算法

单向链表

单向链表是一种线性数据结构,其每个节点包含数据和指向下一节点的指针。适用于频繁插入和删除操作的场景。


简介

链表(linked list)的组成单位是节点(node)对象。每个节点都包含两项数据:节点的“值”和指向下一节点的“引用”。

  • 链表的首个节点被称为“头节点”,最后一个节点被称为“尾节点”

  • 单向链表的最后一个节点指向“空”,表示链表的结束;在 Java、C++ 和 Python 中分别记为 nullnullptrNone

  • 在 C、C++、Go、Rust 等支持指针的语言中,“引用”应替换为“指针”

  • 引用记录了下一节点的内存地址,通过它可以从当前节点访问到下一节点

核心特性

  1. 动态大小:链表可以动态增长和缩小,无需预先定义大小

  2. 非连续性存储:节点在内存中分散存储,通过指针相连

  3. 单向性:每个节点只知道下一节点在哪里,不知道上一节点

  4. 顺序访问:必须从头节点开始,沿着指针逐个访问节点

  5. 无索引访问:不能像数组那样通过索引直接访问元素

基本操作

时间复杂度分析

操作

时间复杂度

说明

访问元素

O(n)

必须从头节点开始,沿链表遍历,直到找到目标节点

在头部插入

O(1)

创建新节点,并指向当前头节点,然后更新头节点

在尾部插入

O(n)/O(1)

O(n):如果只有头指针,需要遍历到尾部;

O(1)如果维护了尾指针,可以直接在尾部操作

在中间插入

O(n)

先找到目标位置的前一节点,再执行插入操作

删除节点

O(n)/O(1)

O(1):删除头节点;

O(n):删除其他节点,需要找到待删除节点的前一节点

查找元素

O(n)

从头节点开始遍历链表,查找目标元素

基础实现

// 定义节点类
class Node {
    int data;       // 数据域
    Node next;      // 指针域,指向下一个节点
    
    // 构造函数
    public Node(int data) {
        this.data = data;
        this.next = null;
    }
}

// 单向链表类
class SinglyLinkedList {
    private Node head;  // 头节点
    
    // 构造函数,创建一个空链表
    public SinglyLinkedList() {
        this.head = null;
    }
    
    // 判断链表是否为空
    public boolean isEmpty() {
        return head == null;
    }
    
    // 在链表头部插入节点
    public void insertAtHead(int data) {
        Node newNode = new Node(data);
        if (isEmpty()) {
            head = newNode;
        } else {
            newNode.next = head;
            head = newNode;
        }
    }
    
    // 在链表尾部插入节点
    public void insertAtTail(int data) {
        Node newNode = new Node(data);
        if (isEmpty()) {
            head = newNode;
            return;
        }
        
        Node current = head;
        // 找到最后一个节点
        while (current.next != null) {
            current = current.next;
        }
        current.next = newNode;
    }
    
    // 在指定位置插入节点
    public void insertAtPosition(int data, int position) {
        // 位置无效
        if (position < 0) {
            System.out.println("位置无效");
            return;
        }
        
        // 插入位置为0,相当于在头部插入
        if (position == 0) {
            insertAtHead(data);
            return;
        }
        
        Node newNode = new Node(data);
        Node current = head;
        Node previous = null;
        int count = 0;
        
        // 找到要插入位置的前一个节点
        while (current != null && count < position) {
            previous = current;
            current = current.next;
            count++;
        }
        
        // 如果position超出链表长度,则插入到尾部
        previous.next = newNode;
        newNode.next = current;
    }
    
    // 删除第一个值为data的节点
    public void delete(int data) {
        // 链表为空
        if (isEmpty()) {
            System.out.println("链表为空,无法删除");
            return;
        }
        
        // 删除的是头节点
        if (head.data == data) {
            head = head.next;
            return;
        }
        
        // 删除的不是头节点
        Node current = head;
        Node previous = null;
        
        // 查找要删除的节点
        while (current != null && current.data != data) {
            previous = current;
            current = current.next;
        }
        
        // 如果找到了要删除的节点
        if (current != null) {
            previous.next = current.next;
        } else {
            System.out.println("未找到要删除的元素");
        }
    }
    
    // 查找节点
    public boolean search(int data) {
        if (isEmpty()) {
            return false;
        }
        
        Node current = head;
        while (current != null) {
            if (current.data == data) {
                return true;
            }
            current = current.next;
        }
        return false;
    }
    
    // 打印链表
    public void display() {
        if (isEmpty()) {
            System.out.println("链表为空");
            return;
        }
        
        Node current = head;
        while (current != null) {
            System.out.print(current.data + " -> ");
            current = current.next;
        }
        System.out.println("null");
    }
    
    // 获取链表长度
    public int size() {
        int count = 0;
        Node current = head;
        while (current != null) {
            count++;
            current = current.next;
        }
        return count;
    }
}

// 使用示例
public class LinkedListDemo {
    public static void main(String[] args) {
        SinglyLinkedList list = new SinglyLinkedList();
        
        list.insertAtTail(10);
        list.insertAtTail(20);
        list.insertAtTail(30);
        list.insertAtHead(5);
        list.display();  // 输出: 5 -> 10 -> 20 -> 30 -> null
        
        list.insertAtPosition(15, 2);
        list.display();  // 输出: 5 -> 10 -> 15 -> 20 -> 30 -> null
        
        list.delete(15);
        list.display();  // 输出: 5 -> 10 -> 20 -> 30 -> null
        
        System.out.println("查找结果: " + list.search(20));  // 输出: 查找结果: true
        System.out.println("链表长度: " + list.size());      // 输出: 链表长度: 4
    }
}

优缺点

优点

  • 动态大小:链表可以根据需要动态分配内存,不需要预先定义大小

  • 插入和删除高效:在已知位置插入或删除节点的时间复杂度为 O(1)

  • 内存利用灵活:不需要连续的内存空间,可以充分利用可用内存

  • 实现简单:基本操作实现相对简单直观

缺点

  • 随机访问低效:无法通过数组那样通过索引直接访问元素,必须从头开始遍历

  • 额外内存开销:每个节点除了存储数据外,还需要存储指针,增加了内存消耗

  • 缓存不友好:由于内存不连续,不能有效利用 CPU 缓存,可能导致性能下降

  • 反向遍历困难:单向链表不支持反向遍历

应用场景

  • 作为其他数据结构的基础:

  • 栈与队列

  • 哈希表

  • 历史记录

  • 撤销功能:编辑器撤销操作

扩展

双向链表

循环链表(环形链表)

热门题目

206. 反转链表

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

示例:

输入:head = [1,2,3,4,5]

输出:[5,4,3,2,1]

题解:

双指针(迭代)

遍历链表时,修改各节点的引用指向

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;

        while (cur != null) {
            ListNode nextTemp = cur.next;  // 暂存下一节点
            cur.next = pre;                // 反转当前节点指针
            pre = cur;                     // 移动 pre 指针
            cur = nextTemp;                // 移动当前节点 cur 指针
        }

        return pre;                        // 最终 pre 指向新链表的头节点
    }
}

###

#数据结构(3)

评论