链表操作全攻略:从基础到进阶的算法技巧

202 阅读4分钟

链表操作全攻略:从基础到进阶的算法技巧 🌈

链表是算法学习中的核心数据结构,掌握链表操作是每个程序员的必修课。本文通过生动讲解和完整代码示例,带你深入理解链表的各种操作技巧!

🌈 链表结构:程序世界的珍珠项链

链表就像一串珍珠项链,每个珍珠(节点)都包含两部分:

  • 数据域:存储珍珠的价值(数据)
  • 指针域:连接下一颗珍珠的链条(指针)

image.png

// Java中的链表节点定义
class ListNode {
    int val;        // 节点存储的值
    ListNode next;  // 指向下一个节点的指针
    
    ListNode(int val) {
        this.val = val;
        this.next = null;
    }
}

🗑️ 删除链表元素:精准"拆链"技巧

头节点删除:处理"换头"情况

public ListNode removeHeadElements(ListNode head, int target) {
    // 删除所有值为target的头部节点
    while (head != null && head.val == target) {
        head = head.next;
    }
    return head;
}

普通节点删除:遍历寻找目标

public ListNode removeElements(ListNode head, int target) {
    if (head == null) return null;
    
    ListNode cur = head;
    while (cur != null && cur.next != null) {
        if (cur.next.val == target) {
            // 跳过目标节点,直接连接下一个节点
            cur.next = cur.next.next;
        } else {
            cur = cur.next;
        }
    }
    return head;
}

🌟 虚拟头节点技巧:统一处理逻辑(推荐)

public ListNode removeElementsWithDummy(ListNode head, int target) {
    // 创建虚拟头节点,指向真实头节点
    ListNode dummyHead = new ListNode(-1);
    dummyHead.next = head;
    
    ListNode cur = dummyHead;
    while (cur.next != null) {
        if (cur.next.val == target) {
            cur.next = cur.next.next;
        } else {
            cur = cur.next;
        }
    }
    // 返回虚拟头节点的下一个节点
    return dummyHead.next;
}

虚拟头节点的优势:统一处理头节点和普通节点,避免特殊逻辑判断!

🛠️ LeetCode 707:设计链表实战

image.png

class MyLinkedList {
    int size;          // 链表长度
    ListNode dummyHead; // 虚拟头节点
    
    public MyLinkedList() {
        size = 0;
        dummyHead = new ListNode(0);
    }
    
    // 获取第index个节点的值
    public int get(int index) {
        if (index < 0 || index >= size) return -1;
        
        ListNode cur = dummyHead.next;
        while (index-- > 0) {
            cur = cur.next;
        }
        return cur.val;
    }
    
    // 头部插入节点
    public void addAtHead(int val) {
        ListNode newNode = new ListNode(val);
        newNode.next = dummyHead.next;
        dummyHead.next = newNode;
        size++;
    }
    
    // 尾部插入节点
    public void addAtTail(int val) {
        ListNode newNode = new ListNode(val);
        ListNode cur = dummyHead;
        while (cur.next != null) {
            cur = cur.next;
        }
        cur.next = newNode;
        size++;
    }
    
    // 在第index个节点前插入
    public void addAtIndex(int index, int val) {
        if (index > size) return;
        if (index < 0) index = 0;
        
        ListNode cur = dummyHead;
        while (index-- > 0) {
            cur = cur.next;
        }
        ListNode newNode = new ListNode(val);
        newNode.next = cur.next;
        cur.next = newNode;
        size++;
    }
    
    // 删除第index个节点
    public void deleteAtIndex(int index) {
        if (index < 0 || index >= size) return;
        
        ListNode cur = dummyHead;
        while (index-- > 0) {
            cur = cur.next;
        }
        cur.next = cur.next.next;
        size--;
    }
}

🔄 翻转链表:经典双指针技巧

image.png

双指针法:直观易懂

public ListNode reverseList(ListNode head) {
    ListNode pre = null;
    ListNode cur = head;
    
    while (cur != null) {
        ListNode temp = cur.next; // 临时保存下一个节点
        cur.next = pre;          // 翻转指针方向
        pre = cur;                // 移动pre指针
        cur = temp;               // 移动cur指针
    }
    return pre; // 新头节点
}

递归法:简洁优雅

public ListNode reverseListRecursive(ListNode head) {
    return reverse(null, head);
}

private ListNode reverse(ListNode pre, ListNode cur) {
    if (cur == null) return pre;
    
    ListNode temp = cur.next;
    cur.next = pre;
    return reverse(cur, temp);
}

🔀 两两交换链表节点:指针操作艺术

public ListNode swapPairs(ListNode head) {
    ListNode dummyHead = new ListNode(-1);
    dummyHead.next = head;
    ListNode cur = dummyHead;
    
    while (cur.next != null && cur.next.next != null) {
        ListNode temp = cur.next;          // 保存节点1
        ListNode temp1 = cur.next.next.next; // 保存节点3
        
        // 执行交换
        cur.next = cur.next.next;  // 虚拟头指向节点2
        cur.next.next = temp;      // 节点2指向节点1
        temp.next = temp1;         // 节点1指向节点3
        
        cur = cur.next.next;       // 移动指针
    }
    return dummyHead.next;
}

🎯 删除倒数第N个节点:快慢指针妙用

public ListNode removeNthFromEnd(ListNode head, int n) {
    ListNode dummyHead = new ListNode(-1);
    dummyHead.next = head;
    
    ListNode fast = dummyHead;
    ListNode slow = dummyHead;
    
    // 快指针先走n+1步
    n++;
    while (n-- > 0 && fast != null) {
        fast = fast.next;
    }
    
    // 同时移动快慢指针
    while (fast != null) {
        fast = fast.next;
        slow = slow.next;
    }
    
    // 删除目标节点
    slow.next = slow.next.next;
    return dummyHead.next;
}

🔁 环形链表:龟兔赛跑算法

image.png

判断链表是否有环

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;
}

找到环形链表的入口

public ListNode detectCycle(ListNode head) {
    if (head == null) return null;
    
    ListNode slow = head;
    ListNode fast = head;
    boolean hasCycle = false;
    
    // 第一阶段:判断是否有环
    while (fast != null && fast.next != null) {
        slow = slow.next;
        fast = fast.next.next;
        
        if (slow == fast) {
            hasCycle = true;
            break;
        }
    }
    
    // 第二阶段:找到环入口
    if (hasCycle) {
        ListNode ptr = head;
        while (ptr != slow) {
            ptr = ptr.next;
            slow = slow.next;
        }
        return ptr;
    }
    return null;
}

数学原理:当快慢指针相遇时,从头节点和相遇点同时出发的两个指针,将在环入口相遇!

💎 总结:链表操作的核心技巧

  1. 虚拟头节点是处理链表问题的万能钥匙
  2. 双指针技巧(快慢指针)解决90%的链表问题
  3. 递归思想让复杂操作变得简洁优雅
  4. 画图分析是理解链表操作的最佳方式

掌握这些链表操作技巧,你就能轻松应对大多数链表相关的算法面试题!🌈

欢迎在评论区分享你的链表解题心得,一起交流进步!👇