算法小技巧 -- 链表

一、快慢指针

1、核心思想

【核心思想:】
    采用双指针完成,一个指针永远比另一个指针稍快一点。

【常见案例:】
    找到单链表的中间节点
    环形链表 【单链表结构:】
class ListNode { int val; ListNode next; ListNode() {} ListNode(int val) { this.val = val; } ListNode(int val, ListNode next) { this.val = val; this.next = next; } }

2、案例实现(找到单链表的中间节点)

【LeetCode 题目:】
标题:
    876. 链表的中间结点
    
题目描述:
    给定一个头结点为 head 的非空单链表,返回链表的中间结点。
    如果有两个中间结点,则返回第二个中间结点。

限制:
    给定链表的结点数介于 1 和 100 之间。

测试用例:
    输入:[1,2,3,4,5]
    输出:3 

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

【案例分析:】
    假设两个指针 slow、fast。
    每次指向下一个节点时,fast 都比 slow 快一个节点,
    即 fast 每次跳过两个节点,slow 每次跳过一个节点。
    当 fast 遍历到链表末尾时,slow 恰好处于链表中间节点处。
即
    slow = slow.next;
    fast = fast.next.next;
注:
    链表节点为偶数时,存在两个中间节点,此处以第二个节点作为中间节点。
    
【伪代码实现:】
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、案例实现(环形链表)

(1)LeetCode 题目

【LeetCode 题目:】
标题:
    141. 环形链表
    
题目描述:
    给定一个链表,判断链表中是否有环。
    如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 
    如果链表中存在环,则返回 true。 否则,返回 false 。
注:
    为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。
    如果 pos 是 -1,则在该链表中没有环。
    pos 不作为参数进行传递,仅仅是为了标识链表的实际情况(用于表示哪个位置开始会出现环)。

限制:
    链表中节点的数目范围是 [0, 104]
    -105 <= Node.val <= 105
    pos 为 -1 或者链表中的一个 有效索引 。

测试用例:
    输入:head = [3,2,0,-4], pos = 1
    输出:true
    解释:链表中有一个环,其尾部连接到第二个节点。
    
    输入:head = [1,2], pos = 0
    输出:true
    解释:链表中有一个环,其尾部连接到第一个节点。
    
    输入:head = [1], pos = -1
    输出:false
    解释:链表中没有环。

(2)案例分析

【案例分析:】
    链表存在环,则进行链表遍历时将会进入 死循环,需要设置结束遍历的条件。
    若不存在环,即使链表足够长,总能遍历结束,无需设置结束遍历的条件。
联想到一个场景:
    晚上寂寞无聊,约哥们去体育馆操场跑圈。假设两个人为 A、B。
    A、B 同时出发,由于身体素质问题,A 跑的比 B 快点。
    经过一段时间后,A 甩了 B 半圈,再经过一段时间后,A 追上了 B。
    只要操场跑道是个圈,A 总能追上 B,只是时间长短的问题。
即:
    A、B 的跑步频率只要设置的不太离谱,总有重合的一天。
    此处设置 A 为快指针,B 为慢指针。
    A 跑两米,B 跑一米,当 A 追上了 B,则表示是个环。    

(3)伪代码

【伪代码:】
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;
}

二、单链表的遍历

1、核心思想

【核心思想:】
    head 与 head.next 一定要区分开。
    每次经过一个节点后,需要跳到下一个节点,
即 
    head = head.next。

【常见实现:】
    使用 while 进行遍历。
    使用 递归 进行遍历。(类似于树的 前序、后序 遍历的实现)
    
【常见案例:】
    正序输出链表(可使用 while、递归 实现)
    反序输出链表(可使用 递归 实现)

2、案例实现(正序输出链表 -- while 实现)

【案例分析:】
    temp 与 temp.next 一定要区分开。
    temp 表示当前所在链表节点位置,temp.next 表示当前链表节点的下一个位置。
注:
    判断条件可以为 temp != null 或者 temp.next != null,灵活使用。

【伪代码:】
public void printList(ListNode head) {
    ListNode temp = head;
    if (temp == null) {
        System.out.println("Empty");
        return;
    }
    while(temp != null) {
        System.out.println(temp.val);
        temp = temp.next;
    }
}

3、案例实现(遍历输出链表 -- 递归实现)

【案例分析:】
    遍历输出链表,正序、反序 的代码非常相似,可等同于 树结构的 前序、后序遍历进行理解。
    递归 类似于 栈结构,先进后出。入栈的顺序就是 正序的,出栈的顺序就是 反序的。

【伪代码:】
public static void printList(ListNode head) {
    if (head == null) {
        System.out.println("Empty");
        return;
    }
    nextNode(head);
}

public static void nextNode(ListNode head) {
    if (head == null) {
        return;
    }
    // System.out.println(head.val); // 正序遍历输出
    nextNode(head.next);
    System.out.println(head.val); // 反序遍历输出
}

三、单链表反转

1、核心思想

【核心思想:】
    head 与 head.next 一定要区分开。
    每经过一个节点,确保当前节点的下一个节点指向自己。
即:
    head.next.next = head
    若从头开始反转,可以使用 多指针 实现。
    若从尾开始反转,可以使用 递归 实现。

【常见实现:】
    使用 栈 作为中转站,遍历链表,将值入栈,出栈时构建为一个新的链表。
    使用多个指针,从头开始,逐个节点进行反转。
    使用 递归 进行反转,从尾开始,逐个节点进行反转。
    
【常见案例:】
    反转链表
    反转部分链表(指定范围进行反转)
    K 个一组翻转链表
    回文链表

2、案例实现(反转链表 -- 栈实现)

【LeetCode 题目:】
标题:
    206. 反转链表

题目描述:
    给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。

限制:
    链表中节点的数目范围是 [0, 5000]
    -5000 <= Node.val <= 5000

测试用例:
    输入:head = [1,2,3,4,5]
    输出:[5,4,3,2,1]
    
    输入:head = [1,2]
    输出:[2,1]
    
    输入:head = []
    输出:[]

【案例分析:】
    使用 栈 作为中转站,遍历链表,将值入栈,出栈时构建为一个新的链表。

【伪代码:】
public ListNode reverseList(ListNode head) {
    if (head == null) {
        return head;
    }
    ListNode temp = head;
    Stack<Integer> stack = new Stack<>();
    while(temp != null) {
        stack.push(temp.val);
        temp = temp.next;
    }
    temp = head;
    while(!stack.isEmpty()) {
        temp.next = new ListNode(stack.pop());
        temp = temp.next;
    }
    return head.next;
}

3、案例实现(反转链表 -- 多指针实现)

【LeetCode 题目:】
标题:
    206. 反转链表

【案例分析:】
    此方式从头向尾执行。
    使用多个指针,假设当前指针为 current,其上一个指针为 pre,下一个指针为 next。
其中:
    pre 用于存储 当前指针 需要指向的 上一个节点。
    next 用于存储 当前指针 需要跳到的 下一个节点。
    每次经过一个节点时,保证当前节点指向上一个节点,然后跳转到下一个节点继续操作。
即:
    next = current.next;
    current.next = pre;
    pre = current;
    current = next;
    
【伪代码:】
public ListNode reverseList(ListNode head) {
    ListNode pre = null;
    ListNode next = null;
    ListNode current = head;
    while(current != null) {
        next = current.next;
        current.next = pre;
        pre = current;
        current = next;
    }
    return pre;
}

4、案例实现(反转链表 -- 递归实现)

【LeetCode 题目:】
标题:
    206. 反转链表

【案例分析:】
    此方式从尾向头进行。
    每次经过一个节点时,保证当前节点的下一个节点指向当前节点,移除当前节点指向的下一个节点。
    然后跳转到上一个节点继续操作(此过程由递归的逻辑实现,返回上一层)。
即
    head.next.next = head;
    head.next = null;

【伪代码:】
public static ListNode reverseList(ListNode head) {
    if (head == null) {
        return head;
    }
    return nextNode(head);
}

public static ListNode nextNode(ListNode head) {
    if (head.next == null) {
        return head;
    }
    // lastNode 表示最后一个节点位置,没有被修改,不断的向递归的上层传递
    ListNode lastNode = nextNode(head.next);
    head.next.next = head;
    head.next = null;
    return lastNode;
}

5、案例实现(反转部分链表 -- 多指针)

【LeetCode 题目:】
标题:
    92. 反转链表 II

题目描述:
    给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。
    请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。

限制:
    链表中节点数目为 n
    1 <= n <= 500
    -500 <= Node.val <= 500
    1 <= left <= right <= n

测试用例:
    输入:head = [1,2,3,4,5], left = 2, right = 4
    输出:[1,4,3,2,5]
    
    输入:head = [5], left = 1, right = 1
    输出:[5]

【案例分析:】
    反转指定范围的链表。关键就在于 反转范围边界 的四个节点是如何连接的。
    假设反转范围边界节点分别为 a、b、c、d,需要反转的是 b -> ... -> c 这部分节点。
    开始顺序为 head -> ... a -> b -> ... -> c -> d -> ...
    最终需要得到的顺序为 head -> ... a -> c -> ... -> b -> d -> ...
    这么看上去就很直观了,先令 b、c 之间的链表节点反转,然后让 a 指向 c,b 指向 d 即可完成。
注:
    范围为 0 时,此时只反转一个节点,即 b、c 为同一个节点,无需反转。    
    若从头开始反转,即没有 a 节点,此时 c 直接作为新的头结点即可。

【伪代码:】
public ListNode reverseBetween(ListNode head, int left, int right) {
    ListNode first, temp;
    first = temp = head;
    int count = right - left;
    if (count != 0) {
        if (left == 1) {
            // 从头开始反转,没有 a 节点,c 节点即为新的头结点
            return reverseList(head, count);
        }
        while (temp != null && left != 1) {
            left--;
            // 找到开始反转的节点的上一个节点,即 a 节点
            first = temp;
            // 找到开始反转的节点,即 b 节点
            temp = temp.next;
        }
        // 从中间位置开始反转,需要连接上反转范围的链表的新头节点,即 a -> c
        first.next = reverseList(temp, count);
    }
    return head;
}

public ListNode reverseList(ListNode head, int count) {
    ListNode first, pre, next, current;
    pre = next = null;
    first = current = head;
    // 反转指定范围的链表
    while(current != null && count != 0) {
        next = current.next;
        current.next = pre;
        pre = current;
        current = next;
        count--;
    }
    // 指向反转范围的新尾节点的下一个节点,即 b -> d
    first.next = current.next;
    // 返回反转范围链表的新头结点,即 c 节点
    current.next = pre;
    return current;
}

6、案例实现(K 个一组翻转链表)

【LeetCode 题目:】
标题:
    25. K 个一组翻转链表

题目描述:
    给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
    k 是一个正整数,它的值小于或等于链表的长度。
    如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
    你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。

限制:
    列表中节点的数量在范围 sz 内
    1 <= sz <= 5000
    0 <= Node.val <= 1000
    1 <= k <= sz

测试用例:
    输入:head = [1,2,3,4,5], k = 2
    输出:[2,1,4,3,5]
    
    输入:head = [1,2,3,4,5], k = 3
    输出:[3,2,1,4,5]
    
    输入:head = [1,2,3,4,5], k = 1
    输出:[1,2,3,4,5]
    
    输入:head = [1], k = 1
    输出:[1]
    
【案例分析:】
    k 个节点一组去反转,原理与 部分节点反转 类似,只是多了重复调用的过程。
    关注点同样是 a、b、c、d 四个节点指向的问题。
注:
    此处从头开始反转,即第一次没有 a 节点,直接使用 c 节点作为新的头节点。

【伪代码:】
public ListNode reverseKGroup(ListNode head, int k) {
    if (head == null) {
        return head;
    }
    ListNode temp = head;
    // 找到需要反转的节点范围
    for (int i = 0; i < k; i++) {
        // 若反转节点范围不足,则不进行反转
        if (temp == null) {
            return head;
        }
        temp = temp.next;
    }
    // 获取反转节点后的新头结点
    ListNode newHead = reverseList(head, k - 1);
    // 进行下一次反转,用于获取下一部分的新头节点,并令当前尾节点指向下一次获取的头结点
    head.next = reverseKGroup(temp, k);
    return newHead;
}

public ListNode reverseList(ListNode head, int count) {
    //ListNode first, pre, next, current;
    ListNode pre, next, current;
    pre = next = null;
    //first = current = head;
    current = head;
    while (current != null && count != 0) {
        next = current.next;
        current.next = pre;
        pre = current;
        current = next;
        count--;
    }
    //first.next = current.next;
    current.next = pre;
    return current;
}

7、案例实现(回文链表)

【LeetCode 题目:】
标题:
    剑指 Offer II 027. 回文链表
    
题目描述:
    给定一个链表的 头节点 head ,请判断其是否为回文链表。
    如果一个链表是回文,那么链表节点序列从前往后看和从后往前看是相同的。
    能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?
    
限制:
    链表 L 的长度范围为 [1, 105]
    0 <= node.val <= 9

测试用例:
    输入: head = [1,2,3,3,2,1]
    输出: true
    
    输入: head = [1,2]
    输出: fasle
    
【案例分析:】
    判断回文,常用方法就是:从两侧向中间逼近 或者 从中间向两侧展开 或者 暴力反转构建新的链表,然后逐个比较。
从两侧向中间逼近(效率低):
    由于链表的结构特殊,无法直接从 尾节点向头结点遍历,
    但可以使用 递归的方式,模拟 栈 的使用,先递归到最后一个节点,然后逐级回退到上层节点。
从中间向两侧逼近(效率稍高):
    先找到链表的中点,可以使用快慢指针。
    然后对 前半段链表进行 反转,这样就可以从 中间节点 向两侧展开并比较。
    当然,也可以反转后半段链表,然后从头结点开始比较。
暴力反转构建新链表(需要额外空间):
    直接对链表进行反转,构建一个新链表。
    然后 新链表、旧链表 逐个进行值比较。

【伪代码:(从两侧向中间逼近)】
public static ListNode first = null;
public static boolean isPalindrome(ListNode head) {
    first = head;
    return nextNode(head);
}

public static boolean nextNode(ListNode head) {
    if (head == null) {
        return true;
    }
    boolean flag = nextNode(head.next);
    if (flag && head.val == first.val) {
        first = first.next;
        return flag;
    }
    return false;
} 

【伪代码:(从中间向两侧展开,反转后半段链表)】
public static boolean isPalindrome(ListNode head) {
    // 找中间节点
    ListNode middleNode = middleNode(head);
    // 反转后半段链表
    ListNode newNode = reverseList(middleNode);
    // 从头结点 开始比较 新的后半段链表
    while(newNode != null && head != null) {
        if (newNode.val != head.val) {
            return false;
        }
        newNode = newNode.next;
        head = head.next;
    }
    return true;
}

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

public static ListNode reverseList(ListNode head) {
    ListNode pre, next, current;
    pre = next = null;
    current = head;
    while (current != null) {
        next = current.next;
        current.next = pre;
        pre = current;
        current = next;
    }
    return pre;
}

别把自己太当回事,也别太把自己不当回事!Life is Fantastic!!!
原文地址:https://www.cnblogs.com/l-y-h/p/15250530.html