算法理解(三)链表

一、是什么

链表是一种线性数据结构,由一系列节点(Node)组成,每个节点包含两个部分:数据部分(存储数据的值)和指针部分(存储下一个节点的地址)。链表的第一个节点称为头节点(Head),最后一个节点的指针指向null,表示链表的结束。

链表的基本类型

  1. 单链表(Singly Linked List):每个节点只有一个指针,指向下一个节点。
  2. 双链表(Doubly Linked List):每个节点有两个指针,一个指向下一个节点,一个指向前一个节点。
  3. 循环链表(Circular Linked List):链表的最后一个节点指向链表的头节点,形成一个环。

链表的优势

相比于其他数据结构(如数组),链表有以下几个优势:

  1. 动态大小:链表的大小可以动态调整,不需要像数组那样在创建时指定大小。因此,当数据量增加或减少时,链表不会像数组那样导致内存浪费或需要重新分配内存。

  2. 插入和删除操作快:链表的插入和删除操作只需要修改指针,时间复杂度为O(1)。相较于数组,插入或删除元素时需要移动大量元素,链表在这方面更高效。

  3. 节省内存:链表不需要预留额外的内存空间。对于大小不确定的场景,链表可以节省空间。

链表的劣势

  1. 随机访问速度慢:链表不支持下标访问,需要从头节点开始遍历,时间复杂度为O(n),因此查找速度较慢。

  2. 内存开销大:由于每个节点都需要存储指针信息,链表的内存开销比数组大。

  3. 不支持缓存:由于链表节点不连续存储,链表不太适合缓存友好的应用(比如数组那样的连续内存块)。

链表通常用于需要频繁插入和删除操作的数据结构中,如实现队列(Queue)、栈(Stack)等。它特别适合用来管理动态数据。

注:数组是一块连续的内存块(为了直接访问的高效性和内存管理的便利性。在内存中,数组的元素是紧挨着存储的,中间不允许有空白或跳跃。因为数组的这一特性,我们可以通过索引直接访问数组的任意元素,时间复杂度为 O(1)

如果需要一个能在中间插入或删除元素的数据结构,可以使用链表动态数组。

动态数组(如 ArrayList
在内部仍然使用数组,但会在内存不足时自动扩展,且扩展时新数组还是连续的内存。它的随机访问性能与数组相同O(1),但在插入和删除操作时可能会有较大的开销。

二、为什么

链表是一种重要的数据结构,它在很多场景下比其他数据结构(如数组)更合适。以下是链表的几个主要用途和需要链表的原因:

1. 动态数据管理

链表的大小是动态的,可以根据需要增加或减少节点。这对于那些数据量不确定或频繁变化的场景非常有用。例如,内存管理中的空闲块链表、浏览器历史记录、音乐播放列表等需要频繁插入或删除元素的场景,链表能够灵活处理数据。

2. 高效的插入和删除操作

在链表中,插入和删除操作非常高效,时间复杂度为O(1)。对于数组,如果在中间插入或删除元素,可能需要移动大量数据,时间复杂度为O(n)。因此,当需要频繁插入或删除元素时,链表是一个更好的选择。例如:

  • 实现栈和队列:链表可以用来高效地实现栈和队列的数据结构,支持在头部或尾部的快速插入和删除。
  • 实时数据流处理:在处理数据流时,链表可以高效地添加和移除数据。

3. 节省内存和避免内存浪费

链表不会像数组那样需要在创建时就分配固定的内存空间,因此它避免了因数据量不确定导致的内存浪费。例如:

  • 处理大文件或大数据:链表可以逐步读取数据并添加到结构中,而不需要一次性分配大量内存。
  • 实现动态内存分配器:链表用于操作系统中的内存管理器(如堆)来动态分配和释放内存。

4. 灵活的数据存储

链表可以轻松实现各种高级数据结构(如双向链表、循环链表、跳跃表等),这些结构可以进一步优化特定类型的操作。例如:

  • 图(Graph)和树(Tree)结构:链表可以用来表示图中的邻接表和树结构中的子节点。

5. 方便的扩展和连接操作

链表可以轻松合并两个链表而不需要移动数据。链表的拆分和合并操作可以在O(1)时间内完成,这在许多算法中(如快速排序中的链表版本)是很有用的。

6. 适用于链式存储的算法和应用

某些算法的设计思路和应用场景更适合链式存储。例如:

  • LRU缓存机制:链表可以用来实现缓存淘汰策略(如LRU缓存),因为它允许快速地移动、删除和插入元素。

总结来说,链表的优势在于其动态性和插入/删除效率,因此在数据频繁变化、大小不确定以及需要灵活的内存管理和数据结构设计时,链表是一种非常有用的选择。

三、怎么办

链表的实现通常是通过节点类(Node)和链表类(LinkedList)来完成的。以下是链表的基本实现和一些常见的算法题考查方式。

链表的基本实现

单链表(Singly Linked List)为例,说明链表的实现方法:

1. 单链表的基本实现

定义节点类(ListNode

每个节点包含数据部分和指向下一个节点的指针部分。

class ListNode {
    int value;          // 数据部分
    ListNode next;      // 指向下一个节点的指针

    // 构造函数:初始化对象,特点:名与类同,无返回类型,new的时候自动调用
    ListNode(int value) {
        
        this.value = value;

//Java中没有像C/C++那样的指针操作,而是使用引用来指向对象的内存地址。引用与指针相似,但更加安全
//引用是一个对象的内存地址的“句柄”。你不能直接操作这个地址,引用只是一个用来访问对象的安全指针

//this是 Java 中的一个关键字,表示对当前对象的引用。它在类的方法和构造函数中非常有用,帮助区分成员变量和局部变量,或者在需要引用当前对象的情况下使用。
//在 ListNode 类中,next 的数据类型是 ListNode,属于 引用类型
        this.next = null;
    }
}
定义链表类(LinkedList

链表类支持常见的链表操作,如插入、删除、查找等。

class LinkedList {
    ListNode head;  // 链表的头节点

    // 构造函数初始化链表为空
    public LinkedList() {
        this.head = null;
    }

    // 在链表末尾添加一个节点
    public void append(int value) {
        ListNode newNode = new ListNode(value);
        if (head == null) {  // 如果链表为空,新的节点就是头节点
            head = newNode;
        } else {  // 遍历链表找到最后一个节点
            ListNode current = head;
            while (current.next != null) {
                current = current.next;
            }
            current.next = newNode;
        }
    }

    // 打印链表的所有元素
    public void printList() {
        ListNode current = head;
        while (current != null) {
            System.out.print(current.value + " -> ");
            current = current.next;
        }
        System.out.println("null");
    }
}

使用上述代码,可以创建一个链表,并向其中添加节点和打印节点内容:

public class Main {
    public static void main(String[] args) {
        LinkedList ll = new LinkedList();
        ll.append(1);
        ll.append(2);
        ll.append(3);
        
        ll.printList(); // 输出: 1 -> 2 -> 3 -> null
    }
}

2. 链表的常见算法题

(1) 反转链表

反转链表是最常见的链表问题。它要求将链表的节点顺序反转。

public ListNode reverseList(ListNode head) {
    ListNode prev = null;   // 前一个节点
    ListNode current = head; // 当前节点

    while (current != null) {
        ListNode nextNode = current.next; // 保存下一个节点
        current.next = prev;              // 反转当前节点的指向
        prev = current;                   // 更新 prev
        current = nextNode;               // 更新 current
    }

    return prev; // 新的头节点
}
(2) 寻找链表的中间节点

使用快慢指针法找到链表的中间节点。

public ListNode middleNode(ListNode head) {
    ListNode slow = head; // 慢指针
    ListNode fast = head; // 快指针

    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
    }

    return slow; // 慢指针到达中间节点
}
(3) 合并两个有序链表

将两个升序链表合并为一个新的升序链表。

public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    ListNode dummy = new ListNode(0); // 创建一个虚拟头节点
    ListNode current = dummy;

//链表值决定current的next是什么,节点后移
    while (l1 != null && l2 != null) {
        if (l1.value < l2.value) {
            current.next = l1;
            l1 = l1.next;
        } else {
            current.next = l2;
            l2 = l2.next;
        }
        current = current.next;
    }

    // 连接剩余的节点
    current.next = (l1 != null) ? l1 : l2;

    return dummy.next;
}
(4) 删除链表的倒数第N个节点

删除链表中的倒数第N个节点。

public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummy = new ListNode(0);
    dummy.next = head;
    ListNode first = dummy;
    ListNode second = dummy;

    // 先让第一个指针走 n+1 步
    for (int i = 0; i <= n; i++) {
        first = first.next;
    }

    //f始终比s多走n+1,f到结尾(结尾本身就是null的,所以不存在指到虚空去了),则s隔f有n+1这么远,再往前就隔n个那么远
    //即f指向的下一个为倒数第n个节点
    // 同时移动两个指针,直到第一个指针到达末尾
    while (first != null) {
        first = first.next;
        second = second.next;
    }

    // 删除倒数第 N 个节点
    second.next = second.next.next;

    return dummy.next;
}
(5) 判断链表是否有环

使用快慢指针判断链表中是否存在环。

public boolean hasCycle(ListNode head) {
    if (head == null || head.next == null) return false;
    
    ListNode slow = head;  // 慢指针
    ListNode fast = head;  // 快指针

    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        if (slow == fast) { // 快慢指针相遇,说明有环
            return true;
        }
    }

    return false; // 没有环
}

总结

在Java中,链表的实现和算法题考察与其他语言类似,都是通过节点类和链表类的组合来实现各种操作。在编程面试中,链表题目主要考察指针操作、链表结构的理解和掌握。熟悉基本的链表操作和算法,可以帮助应对各种链表相关的算法题。

  • 10
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值