2024-08-13

ListNode是一个在进行链表操作时常用的数据结构,它通常用于表示链表中的一个节点。在Python中,我们可以通过定义一个类来实现ListNode。

以下是一个简单的实现:




class ListNode:
    def __init__(self, val=0, next=None):
        self.val = val
        self.next = next

在这个定义中,ListNode有一个值域(val)和一个指向下一个节点的指针(next)。

以下是一些使用ListNode的常见操作:

  1. 创建链表



node1 = ListNode(1)
node2 = ListNode(2)
node3 = ListNode(3)
 
node1.next = node2
node2.next = node3
  1. 遍历链表



current = node1
while current is not None:
    print(current.val)
    current = current.next
  1. 添加节点



new_node = ListNode(4)
node1.next = new_node
new_node.next = node2
  1. 删除节点



node1.next = node2.next
  1. 查找节点



current = node1
while current is not None and current.val != value:
    current = current.next
return current
  1. 插入节点



current = node1
while current.next is not None and current.next.val < new_node.val:
    current = current.next
new_node.next = current.next
current.next = new_node
  1. 删除节点



current = node1
while current.next is not None and current.next.val != value:
    current = current.next
current.next = current.next.next

以上就是ListNode的一些基本操作,在实际应用中,你可以根据需要进行相应的扩展和修改。

2024-08-11

以下是一个简单的Java代码示例,演示了如何初始化一个线性表的链表结构,并添加一个元素。




class Node<T> {
    T data;
    Node<T> next;
 
    public Node(T data) {
        this.data = data;
        this.next = null;
    }
}
 
public class LinkedList<T> {
    private Node<T> head;
 
    public LinkedList() {
        head = null;
    }
 
    public void add(T data) {
        Node<T> newNode = new Node<>(data);
        if (head == null) {
            head = newNode;
        } else {
            Node<T> current = head;
            while (current.next != null) {
                current = current.next;
            }
            current.next = newNode;
        }
    }
 
    // 其他方法,例如打印链表、插入节点、删除节点等
}
 
public class Main {
    public static void main(String[] args) {
        LinkedList<Integer> linkedList = new LinkedList<>();
        linkedList.add(1); // 添加元素1到链表
        // 此处可以添加更多的方法调用来演示其他功能
    }
}

这个示例定义了一个泛型的Node类来表示链表节点,以及一个LinkedList类,它有一个head节点作为链表的开始,并且有一个add方法来向链表添加新的节点。在main方法中,我们创建了一个LinkedList实例,并向其添加了一个整数1。这个简单的示例展示了如何初始化一个链表并添加元素。

2024-08-09

'# Golang | Leetcode Golang题解之第61题旋转链表

一、背景与问题

LeetCode 第 61 题「旋转链表」要求我们对单向链表进行旋转操作。题目描述如下:

给你一个链表,旋转一定的次数后,返回旋转后的链表。

例如,输入链表 1->2->3->4->5,旋转 2 次后,结果应为 4->5->1->2->3。

该问题的核心在于理解链表旋转的逻辑,以及如何高效地实现这一逻辑。我们需要考虑以下关键点:

  • 如何高效计算旋转后的头节点
  • 如何处理边界情况(如空链表、n=0、n大于链表长度等)
  • 如何避免重复遍历链表,提高性能

二、基本原理

1. 旋转链表的逻辑

旋转链表的本质是将链表的末尾部分移动到头部。例如,旋转一次相当于将链表的最后一位节点移动到头部,旋转两次则将倒数第二位节点移动到头部。

假设链表长度为 length,旋转次数为 n。我们可以将旋转次数简化为 n % length(当 n >= length 时),这样可以避免不必要的重复旋转。

2. 找到分割点

假设链表长度为 length,旋转次数为 k,则分割点位置为 length - k。例如,对于链表 1->2->3->4->5,当 k=2 时,分割点为位置 5-2=3,即节点 3。将链表拆分为两部分:1->2->3 和 4->5,然后将第二部分连接到第一部分的末尾。

3. 处理特殊情况

  • 空链表:直接返回 nil
  • n=0:无需旋转,直接返回原链表
  • n > length:通过取模操作简化旋转次数

三、环境准备

确保你的开发环境支持 Go 1.18+,并安装必要的工具:

# 安装 Go
https://golang.org/dl/

# 验证安装
go version

四、核心实现

1. 常规实现(两次遍历)

package main

import "fmt"

type ListNode struct {
    Val  int
    Next *ListNode
}

func rotateRight(head *ListNode, k int) *ListNode {
    if head == nil || head.Next == nil || k == 0 {
        return head
    }

    // 第一次遍历:计算链表长度
    length := 0
    current := head
    for current != nil {
        current = current.Next
        length++
    }

    // 计算有效旋转次数
    k = k % length
    if k == 0 {
        return head
    }

    // 第二次遍历:找到分割点
    current = head
    for i := 1; i < length - k; i++ {
        current = current.Next
    }

    // 分割链表
    newHead := current.Next
    current.Next = nil

    // 合并两部分
    tail := newHead
    for tail.Next != nil {
        tail = tail.Next
    }
    tail.Next = head

    return newHead
}

关键代码解释:

  • 第一次遍历:计算链表长度,确保代码处理空链表的情况。
  • k % length:处理旋转次数超过链表长度的情况,避免重复遍历。
  • 第二次遍历:找到分割点,将链表拆分为两部分。
  • 合并两部分:找到第二部分的尾节点,将其连接到原链表的头部。

2. 快慢指针法(一次遍历)

func rotateRightFast(head *ListNode, k int) *ListNode {
    if head == nil || head.Next == nil || k == 0 {
        return head
    }

    // 计算链表长度
    length := 0
    current := head
    for current != nil {
        current = current.Next
        length++
    }

    // 计算有效旋转次数
    k = k % length
    if k == 0 {
        return head
    }

    // 快慢指针法
    slow, fast := head, head
    for i := 0; i < k; i++ {
        fast = fast.Next
    }

    // 找到分割点
    for fast.Next != nil {
        slow = slow.Next
        fast = fast.Next
    }

    // 分割链表
    newHead := slow.Next
    slow.Next = nil

    // 合并两部分
    tail := newHead
    for tail.Next != nil {
        tail = tail.Next
    }
    tail.Next = head

    return newHead
}

关键代码解释:

  • 快慢指针:通过快指针移动 k 次,找到分割点。快指针始终比慢指针快 k 步,最终慢指针指向分割点。
  • 一次遍历:减少遍历次数,提高性能。

3. 优化版实现(处理空链表和边界情况)

func rotateRightOptimized(head *ListNode, k int) *ListNode {
    if head == nil || head.Next == nil || k == 0 {
        return head
    }

    // 计算链表长度
    length := 0
    current := head
    for current != nil {
        current = current.Next
        length++
    }

    // 计算有效旋转次数
    k = k % length
    if k == 0 {
        return head
    }

    // 快慢指针法
    slow, fast := head, head
    for i := 0; i < k; i++ {
        fast = fast.Next
    }

    // 找到分割点
    for fast.Next != nil {
        slow = slow.Next
        fast = fast.Next
    }

    // 分割链表
    newHead := slow.Next
    slow.Next = nil

    // 合并两部分
    tail := newHead
    for tail.Next != nil {
        tail = tail.Next
    }
    tail.Next = head

    return newHead
}

关键代码解释:

  • 优化处理:对空链表和 k=0 的情况直接返回原链表,避免冗余计算。
  • 性能优化:使用快慢指针法,仅遍历一次链表。

五、完整案例

案例描述

输入链表:1->2->3->4->5,旋转 2 次,输出应为 4->5->1->2->3。

实现代码

package main

import "fmt"

type ListNode struct {
    Val  int
    Next *ListNode
}

func rotateRight(head *ListNode, k int) *ListNode {
    if head == nil || head.Next == nil || k == 0 {
        return head
    }

    // 计算链表长度
    length := 0
    current := head
    for current != nil {
        current = current.Next
        length++
    }

    // 计算有效旋转次数
    k = k % length
    if k == 0 {
        return head
    }

    // 快慢指针法
    slow, fast := head, head
    for i := 0; i < k; i++ {
        fast = fast.Next
    }

    // 找到分割点
    for fast.Next != nil {
        slow = slow.Next
        fast = fast.Next
    }

    // 分割链表
    newHead := slow.Next
    slow.Next = nil

    // 合并两部分
    tail := newHead
    for tail.Next != nil {
        tail = tail.Next
    }
    tail.Next = head

    return newHead
}

func main() {
    // 构造链表 1->2->3->4->5
    head := &ListNode{Val: 1}
    head.Next = &ListNode{Val: 2}
    head.Next.Next = &ListNode{Val: 3}
    head.Next.Next.Next = &ListNode{Val: 4}
    head.Next.Next.Next.Next = &ListNode{Val: 5}

    // 旋转链表
    rotatedHead := rotateRight(head, 2)

    // 打印结果
    for rotatedHead != nil {
        fmt.Printf("%d -> ", rotatedHead.Val)
        rotatedHead = rotatedHead.Next
    }
    fmt.Println("nil")
}

运行结果:

4 -> 5 -> 1 -> 2 -> 3 -> nil

代码解释

  • 构造了一个长度为5的链表,旋转2次后,输出结果符合预期。
  • 使用快慢指针法,在一次遍历中完成分割和合并操作。

六、源码解析

1. 快慢指针法原理

  • 快指针:从头节点开始,移动 k 次,最终指向分割点的前一个节点。
  • 慢指针:始终比快指针慢 k 步,最终指向分割点。
  • 分割点:快指针到达链表末尾时,慢指针指向分割点。

2. 分割链表逻辑

  • newHead := slow.Next:获取第二部分的头节点。
  • slow.Next = nil:将原链表分割为两部分。
  • tail.Next = head:将第二部分的尾节点连接到原链表的头部。

3. 边界条件处理

  • 空链表:直接返回 nil。
  • n=0:无需旋转,直接返回原链表。
  • n > length:通过 k = k % length 简化旋转次数。

七、进阶使用

1. 多次旋转优化

在实际项目中,如果需要对链表进行多次旋转操作,可以考虑预处理旋转次数,避免重复计算。

func rotateMultipleTimes(head *ListNode, k int) *ListNode {
    if head == nil || head.Next == nil || k == 0 {
        return head
    }

    // 预处理旋转次数
    length := 0
    current := head
    for current != nil {
        current = current.Next
        length++
    }

    k = k % length
    if k == 0 {
        return head
    }

    // 实现同上...
}

2. 链表旋转与循环链表的结合

在某些场景下,链表可能需要循环处理。例如,在缓存系统中,使用链表实现LRU缓存时,可能需要对链表进行旋转操作。

3. 性能优化

对于大规模链表数据,可以考虑使用数组存储节点,减少指针操作的开销。

八、性能与工程实践

1. 时间复杂度

  • 常规实现:O(n) 时间复杂度,两次遍历。
  • 快慢指针法:O(n) 时间复杂度,一次遍历。
  • 优化版:O(n) 时间复杂度,一次遍历。

2. 空间复杂度

  • 所有实现的空间复杂度均为 O(1),仅使用少量指针。

3. 异常处理

  • 空链表:直接返回 nil。
  • n=0:无需旋转,直接返回原链表。
  • n > length:通过取模操作简化旋转次数。

4. 安全风险

  • 空指针访问:在断开链表时,需要确保指针非 nil。
  • 循环引用:在合并链表时,要避免形成循环链表。

5. 代码可维护性

  • 使用快慢指针法使代码更简洁,易于维护。
  • 对边界情况的处理提高代码健壮性。

九、常见问题与踩坑

1. 忘记处理 k=0 的情况

错误代码:

func rotateRightWrong(head *ListNode, k int) *ListNode {
    if head == nil || head.Next == nil {
        return head
    }
    // 其他逻辑
}

问题:未处理 k=0 的情况,导致不必要的旋转。

解决方法:在代码开头增加 if k == 0 的判断。

2. 未处理 k > length 的情况

错误代码:

func rotateRightWrong2(head *ListNode, k int) *ListNode {
    // 其他逻辑
    k = k % length
    // 其他逻辑
}

问题:未处理 k > length 的情况,导致多次遍历。

解决方法:在计算 k 时,使用 k = k % length。

3. 快慢指针未正确移动

错误代码:

for i := 0; i < k; i++ {
    fast = fast.Next
}

问题:未考虑快指针初始位置,导致分割点计算错误。

解决方法:确保快指针从头节点开始移动 k 次。

4. 合并链表时未找到尾节点

错误代码:

tail := newHead
tail.Next = head

问题:未找到尾节点,导致链表循环。

解决方法:遍历找到尾节点,再连接到原链表头部。

十、最佳实践

1. 使用快慢指针法

  • 快慢指针法仅遍历一次链表,提高性能。
  • 适用于大多数场景,尤其是需要频繁旋转链表的场景。

2. 处理边界情况

  • 在代码开头处理 head == nil、k == 0 等边界条件。
  • 避免因边界条件导致的错误或死循环。

3. 预处理旋转次数

  • 在多次旋转操作中,预处理 k 的值,避免重复计算。
  • 提高代码效率,减少不必要的遍历。

4. 确保链表无循环

  • 在合并链表时,确保尾节点的 Next 指向原链表头部,而非自身。
  • 避免形成循环链表,导致无法遍历。

十一、总结

LeetCode 第 61 题「旋转链表」是链表操作中的经典问题,其核心在于理解链表旋转的逻辑和高效实现。通过快慢指针法,我们可以在一次遍历中完成链表的分割和合并,从而提高代码性能。

在实际开发中,旋转链表的场景可能包括缓存系统、数据队列处理等。在这些场景中,合理使用链表旋转可以提高数据处理效率。

需要注意的是,旋转链表的实现必须考虑边界条件,如空链表、旋转次数为零等情况。此外,避免形成循环链表是确保代码健壮性的关键。

总之,通过深入理解旋转链表的原理和实现细节,我们可以编写出高效、可靠的链表操作代码,为实际项目提供支持。

2024-08-08

'# Java LeetCode篇-深入了解关于单链表的经典解法

一、背景与问题

在LeetCode算法题中,单链表是出现频率最高的数据结构之一。据LeetCode官方统计,涉及链表的题目占比超过15%,其中包含链表反转、合并、环检测、排序等经典问题。这些题目不仅考察数据结构的基础理解,更需要对指针操作和边界条件的深刻把握。

单链表的典型应用场景包括:

  • 链表反转(如206题)
  • 链表合并(如21题)
  • 环检测(如141/142题)
  • 链表排序(如86题)
  • 链表中点查找(如876题)

在实际开发中,链表常用于实现缓存系统(如LRU缓存)、消息队列等场景。理解链表的底层原理,有助于在复杂业务场景中设计高效的算法。

二、基本原理

单链表由节点组成,每个节点包含:

  1. 数据域(存储具体值)
  2. 指针域(指向下一个节点)

在Java中,可以通过类定义节点结构:

class ListNode {
    int val;
    ListNode next;
    ListNode(int val) {
        this.val = val;
        this.next = null;
    }
}

关键操作包括:

  • 插入节点(头插法/尾插法)
  • 删除节点(按值/按位置)
  • 遍历链表
  • 反转链表
  • 查找中间节点
  • 环检测

三、环境准备

确保开发环境包含:

  • JDK 1.8+
  • IntelliJ IDEA 或 VSCode
  • Maven/Gradle 构建工具

建议创建标准Maven项目结构:

src
├── main
│   └── java
│       └── com
│           └── example
│               └── linkedlist
│                   ├── ListNode.java
│                   ├── Solution.java
│                   └── TestLinkedList.java

四、核心实现

1. 链表反转(LeetCode 206)

这是最基础且重要的链表操作,通过指针的三次跳跃实现反转。

public ListNode reverseList(ListNode head) {
    ListNode prev = null;
    ListNode curr = head;
    while (curr != null) {
        ListNode next = curr.next; // 保存当前节点的下一个节点
        curr.next = prev;          // 当前节点指向prev
        prev = curr;               // prev向后移动
        curr = next;               // curr向后移动
    }
    return prev;
}

关键点解析:

  • 指针三步走:next -> curr -> prev
  • 通过循环迭代逐个反转节点指向
  • 时间复杂度O(n),空间复杂度O(1)

2. 合并两个有序链表(LeetCode 21)

这道题考察链表的合并能力,需要保持有序性。

public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
    ListNode dummy = new ListNode(0); // 虚拟头节点
    ListNode curr = dummy;
    
    while (list1 != null && list2 != null) {
        if (list1.val < list2.val) {
            curr.next = list1;
            list1 = list1.next;
        } else {
            curr.next = list2;
            list2 = list2.next;
        }
        curr = curr.next;
    }
    
    // 处理剩余节点
    curr.next = list1 != null ? list1 : list2;
    return dummy.next;
}

关键点解析:

  • 使用虚拟头节点简化边界处理
  • 通过循环逐个比较节点值
  • 复杂度O(n),且保持有序性

3. 环检测(LeetCode 141/142)

环检测需要特别注意指针移动策略。

public boolean hasCycle(ListNode head) {
    if (head == 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;
}

关键点解析:

  • 快慢指针法的数学原理
  • 需要处理空指针异常
  • 时间复杂度O(n),空间复杂度O(1)

五、完整案例

实现一个LRU缓存系统(LeetCode 468)

class LRUCache {
    private int capacity;
    private Map<Integer, ListNode> cache;
    private ListNode head; // 头节点
    private ListNode tail; // 尾节点
    
    public LRUCache(int capacity) {
        this.capacity = capacity;
        this.cache = new HashMap<>();
        this.head = new ListNode(0);
        this.tail = new ListNode(0);
        head.next = tail;
        tail.prev = head;
    }
    
    public int get(int key) {
        if (!cache.containsKey(key)) return -1;
        
        ListNode node = cache.get(key);
        removeNode(node);
        addNodeToHead(node);
        return node.val;
    }
    
    public void put(int key, int value) {
        ListNode node = new ListNode(value);
        if (cache.containsKey(key)) {
            removeNode(cache.get(key));
        }
        addNodeToHead(node);
        cache.put(key, node);
        
        if (cache.size() > capacity) {
            ListNode lruNode = tail.prev;
            removeNode(lruNode);
            cache.remove(lruNode.key);
        }
    }
    
    private void removeNode(ListNode node) {
        node.prev.next = node.next;
        node.next.prev = node.prev;
    }
    
    private void addNodeToHead(ListNode node) {
        node.next = head.next;
        head.next.prev = node;
        node.prev = head;
        head.next = node;
    }
}

关键点解析:

  • 使用双向链表实现快速插入删除
  • 通过头节点维护最新访问节点
  • 尾节点维护最久未使用节点
  • 时间复杂度O(1)的get/put操作

六、源码解析

以链表反转为例,逐行分析:

public ListNode reverseList(ListNode head) {
    ListNode prev = null; // 前驱节点
    ListNode curr = head; // 当前节点
    while (curr != null) {
        ListNode next = curr.next; // 保存当前节点的下一个节点
        curr.next = prev;          // 当前节点指向prev
        prev = curr;               // prev向后移动
        curr = next;               // curr向后移动
    }
    return prev;
}

关键点分析:

  1. prev初始化为null,表示当前没有前驱节点
  2. curr从头节点开始遍历
  3. next变量保存当前节点的下一个节点,防止在修改curr.next时丢失后续节点
  4. 每次循环将当前节点指向prev,实现反转
  5. 最终prev指向原链表的尾节点,即反转后的头节点

七、进阶使用

在实际项目中,链表可以用于:

  1. 缓存系统(如上述LRU缓存)
  2. 消息队列:实现先进先出的队列结构
  3. 文件系统:实现目录结构的遍历
  4. 图遍历:邻接表存储图结构

在Spring框架中,某些组件可能使用链表结构处理事件监听器,但需要谨慎使用。

八、性能与工程实践

1. 性能分析

操作时间复杂度空间复杂度
链表反转O(n)O(1)
合并两个链表O(n)O(1)
环检测O(n)O(1)
链表插入O(1)O(1)
链表删除O(1)O(1)

优化建议:

  • 对频繁随机访问的场景,使用双向链表或平衡树结构
  • 对大规模数据处理,可考虑使用数组或更高效的数据结构
  • 对于频繁插入删除的操作,使用双向链表

2. 安全风险

  • 指针操作不当可能导致空指针异常
  • 循环引用可能导致内存泄漏(需配合GC)
  • 环检测失效可能导致死循环

解决方案:

  • 所有指针操作前都进行null检查
  • 使用WeakHashMap处理可能存在的循环引用
  • 在算法实现中加入边界条件检测

九、常见问题与踩坑

1. 常见错误

错误示例:

public void reverseList(ListNode head) {
    ListNode curr = head;
    while (curr != null) {
        ListNode next = curr.next;
        curr.next = next.next;
        curr = next;
    }
}

问题分析:

  • 直接修改curr.next会破坏链表结构
  • 忽略了指针的移动顺序
  • 导致链表断裂或丢失节点

改进方案:

public void reverseList(ListNode head) {
    ListNode prev = null;
    ListNode curr = head;
    while (curr != null) {
        ListNode next = curr.next;
        curr.next = prev;
        prev = curr;
        curr = next;
    }
    head = prev;
}

2. 常见坑点

  • 边界条件处理:空链表、单节点链表的处理
  • 指针移动顺序:先保存next再修改指针
  • 循环检测:快慢指针法的初始条件设置
  • 内存泄漏:未正确释放节点对象

十、最佳实践

1. 使用建议

  • 适合场景:

    • 需要频繁插入删除操作
    • 保持元素有序性
    • 实现缓存系统
    • 需要快速访问链表头部或尾部
  • 推荐实现:

    • 使用双向链表提高操作效率
    • 维护头尾指针简化操作
    • 使用虚拟头节点处理边界条件

2. 避免使用场景

  • 不适用场景:

    • 需要随机访问的场景(使用数组)
    • 数据量极大时(考虑使用更高效的结构)
    • 需要频繁中间位置插入的场景(使用平衡树)

十一、总结

单链表作为基础数据结构,其核心价值在于指针操作的灵活性。通过深入理解指针移动原理、边界条件处理、以及不同算法的实现方式,可以解决LeetCode中的多种经典问题。在实际开发中,需要根据具体业务场景选择合适的链表实现方式,同时注意性能优化和安全风险。对于复杂的链表操作,建议采用双向链表和虚拟头节点等优化手段,确保代码的健壮性和可维护性。通过不断实践和总结,可以将链表操作提升到更高的层次,为解决更复杂的算法问题打下坚实基础。

2024-08-07

在Java中,LinkedList是一个实现了List接口的链表数据结构,它允许在近乎于零的时间内对列表的首部或尾部进行插入和删除操作。LinkedList还可以用作队列或栈。

以下是一些常用的LinkedList方法:

  • add(E e): 在列表的尾部添加元素。
  • add(int index, E element): 在指定位置插入元素。
  • remove(int index): 移除列表中指定位置的元素。
  • remove(Object o): 移除列表中第一次出现的指定元素。
  • get(int index): 返回列表中指定位置的元素。
  • set(int index, E element): 用指定元素替换列表中指定位置的元素。
  • addFirst(E e): 将元素添加到列表的开头。
  • addLast(E e): 将元素添加到列表的末尾。
  • getFirst(): 返回列表的第一个元素。
  • getLast(): 返回列表的最后一个元素。
  • removeFirst(): 移除并返回列表的第一个元素。
  • removeLast(): 移除并返回列表的最后一个元素。
  • peek(): 查看队列的第一个元素,但不移除。
  • poll(): 移除并返回队列的第一个元素。
  • push(E e): 将元素推入栈顶。
  • pop(): 移除栈顶元素。

示例代码:




import java.util.LinkedList;
 
public class LinkedListExample {
    public static void main(String[] args) {
        LinkedList<String> linkedList = new LinkedList<>();
 
        // 添加元素
        linkedList.add("A");
        linkedList.add("B");
        linkedList.add("C");
 
        // 在首部添加元素
        linkedList.addFirst("0");
 
        // 在尾部添加元素
        linkedList.addLast("D");
 
        // 查看元素
        System.out.println(linkedList); // 输出: [0, A, B, C, D]
 
        // 获取首元素
        System.out.println(linkedList.getFirst()); // 输出: 0
 
        // 获取尾元素
        System.out.println(linkedList.getLast()); // 输出: D
 
        // 移除首元素
        linkedList.removeFirst();
 
        // 移除尾元素
        linkedList.removeLast();
 
        // 查看元素
        System.out.println(linkedList); // 输出: [A, B, C]
 
        // 使用栈的方式使用LinkedList
        LinkedList<String> stack = new LinkedList<>();
        stack.push("A");
        stack.push("B");
        System.out.println(stack); // 输出: [B, A]
        System.out.println(stack.pop()); // 输出: B
        System.out.println(stack.pop()); // 输出: A
 
        // 使用队列的方式使用LinkedList
        LinkedList<String> queue = new LinkedList<>();
        queue.offer("A");
        queue.offer("B");
        System.out.println(queue); // 输出: [A, B]
        System.out.println(queue.poll()); // 输出: A
        System.out.println(queue.poll()); // 输出: B
    }
}

以上代码演示了\`

2024-08-06

[Go] LeetCode 24.两两交换链表中的节点 19.删除链表的倒数第N个节点 面试题02.07.链表相交 142.环形链表 II

一、背景与问题

链表作为基础数据结构,在软件开发中广泛应用。这四个LeetCode题目分别涉及链表的常见操作:节点交换、倒数节点删除、链表相交查找、环形链表入口点定位。这些问题在实际开发中常出现在以下场景:

  1. 数据结构设计:如构建链表缓存、链表队列等
  2. 算法实现:如图的邻接表表示、树的序列化等
  3. 系统底层开发:如内存管理、资源回收机制
  4. 并发控制:如链表节点的原子操作

这些题目共同的特点是:需要对链表的指针操作有深刻理解,同时需要考虑边界条件、空指针、异常处理等场景。本文将深入分析这四个题目的核心原理,探讨其在实际开发中的应用价值和注意事项。

二、基本原理

1. 链表节点交换(LeetCode 24)

核心原理:通过指针的链式操作,逐个交换相邻节点。关键点在于保持链表的连续性,处理头节点的特殊情况。

2. 删除倒数第N个节点(LeetCode 19)

核心原理:利用快慢指针法,先让快指针移动N步,再同时移动快慢指针,最终慢指针指向要删除的节点。需要特别注意空链表和头节点删除的边界情况。

3. 链表相交(面试题02.07)

核心原理:通过哈希表存储节点,或者利用双指针法(先移动长链表指针到等长位置,再同时移动指针)来判断相交点。

4. 环形链表 II(LeetCode 142)

核心原理:使用快慢指针法,当快指针追上慢指针时,说明存在环。进一步通过数学推导找到环的入口点。

三、环境准备

package main

import (
    "fmt"
    "os"
)

// 定义链表节点
type ListNode struct {
    Val  int
    Next *ListNode
}

// 创建链表
func createList(nums []int) *ListNode {
    if len(nums) == 0 {
        return nil
    }
    head := &ListNode{Val: nums[0]}
    current := head
    for i := 1; i < len(nums); i++ {
        current.Next = &ListNode{Val: nums[i]}
        current = current.Next
    }
    return head
}

// 打印链表
func printList(head *ListNode) {
    for head != nil {
        fmt.Print(head.Val, " -> ")
        head = head.Next
    }
    fmt.Println("nil")
}

四、核心实现

1. 两两交换链表中的节点(LeetCode 24)

// 两两交换链表中的节点
func swapPairs(head *ListNode) *ListNode {
    dummyHead := &ListNode{Val: 0, Next: head}
    current := dummyHead
    
    for current.Next != nil && current.Next.Next != nil {
        // 保存当前节点的下一个节点
        first := current.Next
        second := current.Next.Next
        
        // 交换指针
        current.Next = second
        first.Next = second.Next
        second.Next = first
        
        // 移动指针
        current = current.Next.Next
    }
    
    return dummyHead.Next
}

关键代码解释:

  • 创建虚拟头节点dummyHead处理头节点特殊情况
  • 使用双指针first和second保存待交换的两个节点
  • 通过指针重定向完成交换操作
  • 通过current指针移动完成遍历

2. 删除链表的倒数第N个节点(LeetCode 19)

// 删除链表的倒数第N个节点
func removeNthFromEnd(head *ListNode, n int) *ListNode {
    dummyHead := &ListNode{Val: 0, Next: head}
    fast, slow := dummyHead, dummyHead
    
    // 快指针先走n步
    for i := 0; i < n; i++ {
        fast = fast.Next
    }
    
    // 快慢指针同时移动
    for fast != nil {
        fast = fast.Next
        slow = slow.Next
    }
    
    // 删除节点
    slow.Next = slow.Next.Next
    return dummyHead.Next
}

关键代码解释:

  • 使用虚拟头节点处理头节点删除的特殊情况
  • 快指针先走n步,确保慢指针最终指向要删除的节点
  • 删除节点时需要调整指针链接

3. 链表相交(面试题02.07)

// 链表相交
func getIntersectionNode(headA, headB *ListNode) *ListNode {
    if headA == nil || headB == nil {
        return nil
    }
    
    // 计算链表长度
    lenA, lenB := 0, 0
    for current := headA; current != nil; current = current.Next {
        lenA++
    }
    for current := headB; current != nil; current = current.Next {
        lenB++
    }
    
    // 调整指针位置
    for lenA > lenB {
        headA = headA.Next
        lenA--
    }
    for lenB > lenA {
        headB = headB.Next
        lenB--
    }
    
    // 同步移动指针
    for headA != headB {
        headA = headA.Next
        headB = headB.Next
    }
    return headA
}

关键代码解释:

  • 通过遍历计算链表长度
  • 通过调整指针位置使两个链表长度相等
  • 同步移动指针直到找到相交点

五、完整案例

func main() {
    // 创建测试链表
    list1 := createList([]int{1, 2, 3, 4, 5})
    list2 := createList([]int{6, 1, 2, 3})
    
    // 链表相交测试
    list2.Next = list1.Next.Next // 让两个链表在节点3处相交
    
    fmt.Println("原始链表:")
    printList(list1)
    printList(list2)
    
    // 查找相交点
    intersectNode := getIntersectionNode(list1, list2)
    fmt.Printf("相交点:%v\n", intersectNode.Val)
    
    // 删除倒数第N个节点
    fmt.Println("删除倒数第2个节点后:")
    list1 = removeNthFromEnd(list1, 2)
    printList(list1)
    
    // 两两交换节点
    fmt.Println("交换后:")
    list1 = swapPairs(list1)
    printList(list1)
    
    // 环形链表测试
    list3 := createList([]int{1, 2, 3, 4, 5})
    list3.Next.Next.Next.Next.Next = list3.Next // 创建环
    
    fmt.Println("环形链表:")
    printList(list3)
    
    // 找到环入口点
    entryNode := detectCycle(list3)
    fmt.Printf("环入口点:%v\n", entryNode.Val)
}

六、源码解析

1. 环形链表II(LeetCode 142)

// 环形链表 II
func detectCycle(head *ListNode) *ListNode {
    if head == nil {
        return nil
    }
    
    // 快慢指针
    slow, fast := head, head
    
    // 找到快慢指针相遇点
    for fast != nil && fast.Next != nil {
        slow = slow.Next
        fast = fast.Next.Next
        if slow == fast {
            break
        }
    }
    
    // 如果没有环
    if slow != fast {
        return nil
    }
    
    // 计算环的长度
    length := 0
    for slow.Next != fast {
        slow = slow.Next
        length++
    }
    
    // 移动指针找到入口点
    slow = slow.Next
    for i := 0; i < length; i++ {
        slow = slow.Next
    }
    
    return slow
}

关键代码解释:

  • 快慢指针法寻找相遇点
  • 计算环的长度时需要从相遇点开始遍历
  • 通过数学推导找到入口点:slow = slow.Next后,让快指针走环的长度

七、进阶使用

在实际开发中,这些链表操作可以用于:

  1. 缓存系统:使用链表实现LRU缓存,通过删除倒数节点实现最近最少使用策略
  2. 资源管理:通过链表相交检测实现内存泄漏检测
  3. 并发控制:在锁机制中使用环形链表实现等待队列

优化方案

  1. 空间优化:所有解法都使用O(1)空间复杂度
  2. 时间优化:所有解法都使用O(n)时间复杂度
  3. 多线程安全:在链表操作时需要加锁,避免竞态条件

八、性能与工程实践

1. 性能分析

题目时间复杂度空间复杂度优化点
24O(n)O(1)无需额外空间
19O(n)O(1)快慢指针法
02.07O(n)O(1)双指针法
142O(n)O(1)数学推导

2. 异常处理

  • 链表为空时的处理
  • 删除头节点时的处理
  • 环形链表的边界条件处理

3. 安全风险

  • 指针越界访问(如current.Next时未检查current是否为nil)
  • 空指针解引用(如head.Next时未检查head是否为nil)
  • 无限循环(如环形链表未正确处理)

九、常见问题与踩坑

1. 常见错误

错误示例:

// 错误的链表相交实现
func getIntersectionNodeWrong(headA, headB *ListNode) *ListNode {
    if headA == nil || headB == nil {
        return nil
    }
    
    for headA != nil && headB != nil {
        if headA == headB {
            return headA
        }
        headA = headA.Next
        headB = headB.Next
    }
    return nil
}

错误原因:

  • 没有处理链表长度不一致的情况
  • 快慢指针法未正确实现

解决办法:

  • 使用双指针法调整链表长度
  • 在循环中处理指针移动

2. 常见坑点

坑点解决方案
删除头节点时未处理虚拟头节点使用虚拟头节点统一处理
环形链表未正确找到入口点使用数学推导计算环长
指针操作时未检查空指针增加nil判断
快慢指针未正确处理相遇条件确保指针移动逻辑正确

十、最佳实践

  1. 使用虚拟头节点:统一处理头节点删除的特殊情况
  2. 使用快慢指针法:高效处理链表长度相关问题
  3. 边界条件检查:在操作指针前检查空指针
  4. 数学推导:在环形链表问题中使用数学公式计算入口点
  5. 代码注释:对关键指针操作添加详细注释
  6. 单元测试:为每个函数编写测试用例覆盖边界情况

十一、总结

这四个链表问题展示了Go语言在处理指针操作时的灵活性和效率。通过深入理解指针的链式操作原理,我们能够设计出高效的链表处理算法。在实际开发中,这些技术可以用于缓存系统、资源管理等场景,但需要注意处理边界条件、指针越界等问题。

需要特别注意的是:链表操作虽然在某些场景下效率较高,但在需要频繁随机访问或数据量较大的情况下,应考虑使用数组或更高效的数据结构。同时,多线程环境下需要特别注意指针的同步和安全问题。

这些算法的掌握不仅能帮助解决LeetCode题目,更能提升我们对底层数据结构的理解,为开发高性能系统打下坚实基础。