LeetCode精选题之链表

LeetCode精选题之链表

参考资料:CyC2018的LeetCode题解

总结

  1. 链表中基本的穿针引线,画图即可明白指针变化的过程。
  2. 使用递归来操作链表,代码简洁,思路清晰,例如反转链表。
  3. 如果要修改或者删除头节点,可以在头节点前面加一个虚拟头节点dummyHead,这样原链表中每个节点都有前一个节点,就能进行一样的操作,不用对头节点区别对待了。
  4. 可以使用容器来存放节点,比如需要倒序访问链表中的节点(链表求和–LeetCode445)。
  5. 链表中的双指针,主要用法有:快慢指针求链表中点,固定长度的双指针一次遍历删除倒数第N个节点,有点类似于固定长度的滑动窗口。

1 相交链表–LeetCode160

编写一个程序,找到两个单链表相交的起始节点。
如下面的两个链表:
在这里插入图片描述

在节点 c1 开始相交。

注意:

  • 如果两个链表没有交点,返回 null.
  • 在返回结果后,两个链表仍须保持原有的结构。
  • 可假定整个链表结构中没有循环。
  • 程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。

思路:
设 A 的长度为 a + c,B 的长度为 b + c,其中 c 为尾部公共部分长度,可知 a + c + b = b + c + a。

当访问 A 链表的指针访问到链表尾部时,令它从链表 B 的头部开始访问链表 B;同样地,当访问 B 链表的指针访问到链表尾部时,令它从链表 A 的头部开始访问链表 A。这样就能控制访问 A 和 B 两个链表的指针能同时访问到交点。

如果不存在交点,那么 a + b = b + a,以下实现代码中 l1 和 l2 会同时为 null,从而退出循环。

代码如下:

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null) {
            return null;
        }
        ListNode curr1 = headA;
        ListNode curr2 = headB;
        while (curr1 != curr2) {
            curr1 = curr1.next;
            curr2 = curr2.next;

            if (curr1==null && curr2==null) {
                return null;
            }
            
            if (curr1 == null) {
                curr1 = headB;
            }
            if (curr2 == null) {
                curr2 = headA;
            }
        }
        return curr1;
    }
}

大佬的代码(真清爽):

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
    ListNode l1 = headA, l2 = headB;
    while (l1 != l2) {
        l1 = (l1 == null) ? headB : l1.next;
        l2 = (l2 == null) ? headA : l2.next;
    }
    return l1;
}

2 反转链表–LeetCode206

反转一个单链表。

示例:

输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL

进阶:你可以迭代或递归地反转链表。你能否用两种方法解决这道题?

迭代:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode prev = null;
        ListNode curr = head;
        ListNode next = null;
        while (curr != null) {
            next = curr.next;
            curr.next = prev;
            prev = curr;
            curr = next;
        }
        return prev;
    }
}

递归:

class Solution {
    public ListNode reverseList(ListNode head) {
        // 递归终止条件
        if (head == null || head.next == null) {
            return head;
        }

        ListNode newHead = reverseList(head.next);
        head.next.next = head;
        head.next = null;
        return newHead;
    }
}

3 合并两个有序链表–LeetCode21

将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

示例:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4

链表中的穿针引线:

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        // 边界条件
        if (l1 == null || l2 == null) {
            return l1==null ? l2 : l1;
        }
        // 首先确定新的头结点
        ListNode curr1 = l1;
        ListNode curr2 = l2;
        ListNode newHead = null;
        if (curr1.val < curr2.val) {
            newHead = curr1;
            curr1 = curr1.next;
        }else {
            newHead = curr2;
            curr2 = curr2.next;
        }
        ListNode prev = newHead;
        while (curr1 != null || curr2 != null) {
            if (curr1 == null) {
                prev.next = curr2;
                break;
            }
            if (curr2 == null) {
                prev.next = curr1;
                break;
            }
            if (curr1.val < curr2.val) {
                prev.next = curr1;
                curr1 = curr1.next;
            }else {
                prev.next = curr2;
                curr2 = curr2.next;
            }
            prev = prev.next;//这一步千万别忘了
        }
        return newHead;
    }
}

递归解法:

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null) return l2;
        if (l2 == null) return l1;
        if (l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        }else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }
}

4 删除排序链表中的重复元素–LeetCode83

给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。

示例 1:

输入: 1->1->2
输出: 1->2

示例 2:

输入: 1->1->2->3->3
输出: 1->2->3

代码如下:

class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode prev = head;
        while (prev.next != null) {
            if (prev.next.val == prev.val) {
                prev.next = prev.next.next;
            }else {
                prev = prev.next;
            }
        }
        return head;
    }
}

5 删除链表的倒数第N个节点–LeetCode19

给定一个链表,删除链表的倒数第 n 个节点,并且返回链表的头结点。

示例:

给定一个链表: 1->2->3->4->5, 和 n = 2.
当删除了倒数第二个节点后,链表变为 1->2->3->5.

说明:给定的 n 保证是有效的。
进阶:你能尝试使用一趟扫描实现吗?

思路:双指针,类似于固定长度的滑动窗口。

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        if (head == null) {
            return null;
        }
        ListNode dummyHead = new ListNode(-1);// 虚拟头节点
        dummyHead.next = head;
        ListNode slow = dummyHead;
        ListNode fast = dummyHead;
        for (int i = 1; i <= n; i++) {
            fast = fast.next;
            // 特殊情况,n大于链表长度
            // 题目中保证n有效,所以这里可以不用考虑
            if (fast == null) {
                return head;
            }
        }
        while (fast.next != null) {
            slow = slow.next;
            fast = fast.next;
        }
        // 此时slow指向待删除节点的前一个
        slow.next = slow.next.next;
        return dummyHead.next;
    }
}

6 两两交换链表中的节点–LeetCode24

给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

示例:

给定 1->2->3->4, 你应该返回 2->1->4->3.

代码如下:

class Solution {
    public ListNode swapPairs(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode dummyHead = new ListNode(-1);
        dummyHead.next = head;
        ListNode prev = dummyHead;
        ListNode curr = head;
        ListNode nextNode = null;
        while (curr != null && curr.next != null) {
            nextNode = curr.next;
            prev.next = nextNode;
            curr.next = nextNode.next;
            nextNode.next = curr;
            prev = curr;
            curr = curr.next;
        }
        return dummyHead.next;
    }
}

7 链表求和–LeetCode445

给你两个非空链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储一位数字。将这两数相加会返回一个新的链表。你可以假设除了数字 0 之外,这两个数字都不会以零开头。

进阶:如果输入链表不能修改该如何处理?换句话说,你不能对列表中的节点进行翻转。

示例:

输入:(7 -> 2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 8 -> 0 -> 7

使用了辅助集合来逆序存储元素。

 import java.util.Stack;
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        Stack<Integer> stack1 = buildStack(l1);
        Stack<Integer> stack2 = buildStack(l2);
        int carry = 0;
        ListNode dummyHead = new ListNode(-1);
        while (!stack1.isEmpty() || !stack2.isEmpty() || carry != 0) {
            int a = stack1.isEmpty() ? 0 : stack1.pop();
            int b = stack2.isEmpty() ? 0 : stack2.pop();
            int sum = a+b+carry;
            ListNode node = new ListNode(sum%10);
            node.next = dummyHead.next;
            dummyHead.next = node;
            carry = sum/10;
        }
        return dummyHead.next;
    }

    private Stack<Integer> buildStack(ListNode l) {
        Stack<Integer> stack = new Stack<>();
        ListNode curr = l;
        while (curr != null) {
            stack.push(curr.val);
            curr = curr.next;
        }
        return stack;
    }
}

8 回文链表–LeetCode234

请判断一个链表是否为回文链表。

示例 1:

输入: 1->2
输出: false

示例 2:

输入: 1->2->2->1
输出: true

进阶:你能否用 O(n) 时间复杂度和 O(1) 空间复杂度解决此题?

思路:首先利用快慢指针找到中点,将链表切成两半,把后半段反转,然后比较两半是否相等。

class Solution {
    public boolean isPalindrome(ListNode head) {
        if (head == null || head.next == null) {
            return true;
        }
        // 首先利用快慢指针找到中点,当长度为偶数时,中点指的是前一个
        ListNode slow = head, fast = head.next;
        while (fast != null && fast.next != null) {
            slow = slow.next;
            fast = fast.next.next;
        }
        // 将后一半进行反转
        ListNode newHead = reverseList(slow.next);
        ListNode curr1 = head;
        ListNode curr2 = newHead;
        while (curr2 != null) {//这里是以后一半链表的结束为循环结束条件,因为当原链表长度为奇数时,前一半链表会多一个节点
            if (curr1.val != curr2.val) {
                return false;
            }
            curr1 = curr1.next;
            curr2 = curr2.next;
        }
        return true;

    }

    private ListNode reverseList(ListNode head) {
        if (head == null || head.next == null) {
            return head;
        }
        ListNode newHead = reverseList(head.next);
        head.next.next = head;
        head.next = null;
        return newHead;
    }
}

9 分隔链表–LeetCode725

给定一个头结点为 root 的链表, 编写一个函数以将链表分隔为 k 个连续的部分。每部分的长度应该尽可能的相等: 任意两部分的长度差距不能超过 1,也就是说可能有些部分为 null。这k个部分应该按照在链表中出现的顺序进行输出,并且排在前面的部分的长度应该大于或等于后面的长度。返回一个符合上述规则的链表的列表。

举例: 1->2->3->4, k = 5 // 5 结果 [ [1], [2], [3], [4], null ]

示例 1:

输入: 
root = [1, 2, 3], k = 5
输出: [[1],[2],[3],[],[]]
解释:
输入输出各部分都应该是链表,而不是数组。
例如, 输入的结点 root 的 val= 1, root.next.val = 2, \root.next.next.val = 3, 且 root.next.next.next = null。
第一个输出 output[0] 是 output[0].val = 1, output[0].next = null。
最后一个元素 output[4] 为 null, 它代表了最后一个部分为空链表。

示例 2:

输入: 
root = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], k = 3
输出: [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]]
解释:
输入被分成了几个连续的部分,并且每部分的长度相差不超过1.前面部分的长度大于等于后面部分的长度。

提示:

  • root 的长度范围: [0, 1000].
  • 输入的每个节点的大小范围:[0, 999].
  • k 的取值范围: [1, 50].

自己写的代码,比较冗长。base表示每个每个小链表至少应该有base个节点,more表示多出来的节点,那么应该平均分给前more个小链表,每个链表分到一个。

class Solution {
    public ListNode[] splitListToParts(ListNode root, int k) {
        ListNode[] res = new ListNode[k];
        if (root == null) {
            return res;
        }
        // 首先计算出链表长度
        int n = 1;
        ListNode curr = root;
        while (curr.next != null) {
            curr = curr.next;
            n++;
        }
        int base = n / k;// 表示每个小链表至少有多少个节点
        int more = n % k;// 多出来的节点数量,应该前more个小链表每人多分一个
        curr = root;
        ListNode next = null;
        int i = 0;
        for (; i < more; i++) {
            for (int j = 1; j < (1+base); j++) {
                curr = curr.next;
            }
            next = curr.next;
            curr.next = null;
            res[i] = root;
            root = next;
            curr = root;
        }
        while (curr != null) {
            for (int j = 1; j < base; j++) {
                curr = curr.next;
            }
            next = curr.next;
            curr.next = null;
            res[i++] = root;
            root = next;
            curr = root;
        }
        return res;
    }
}

大佬的简洁版代码:

class Solution {
    public ListNode[] splitListToParts(ListNode root, int k) {
        // 首先计算出链表长度
        int n = 0;
        ListNode curr = root;
        while (curr != null) {
            n++;
            curr = curr.next;
        }

        ListNode[] res = new ListNode[k];
        int size = n / k;// 表示每个小链表至少有多少个节点
        int mod = n % k;// 多出来的节点数量,应该前mod个小链表每人多分一个
        curr = root;
        for (int i = 0; curr != null && i < k; i++) {
            res[i] = curr;
            int curSize = size + (mod-- > 0 ? 1 : 0);
            for (int j = 1; j < curSize; j++) {
                curr = curr.next;
            }
            ListNode next = curr.next;
            curr.next = null;
            curr = next;
        }
        return res;
    }
}

10 奇偶链表–LeetCode328

给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。

请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。

示例 1:

输入: 1->2->3->4->5->NULL
输出: 1->3->5->2->4->NULL

示例 2:

输入: 2->1->3->5->6->4->7->NULL 
输出: 2->3->6->7->1->5->4->NULL

说明:

  • 应当保持奇数节点和偶数节点的相对顺序。
  • 链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。

思路:将链表按照奇偶位拆分成两个链表,然后合起来。
代码如下:

class Solution {
    public ListNode oddEvenList(ListNode head) {
        if (head == null || head.next == null || head.next.next == null) {
            return head;
        }
        ListNode tempHead = head.next;
        ListNode curr1 = head;
        ListNode curr2 = tempHead;
        ListNode curr = tempHead.next;
        while (curr != null) {
            // 看curr的前一个节点是谁
            if (curr2.next == curr) {
                curr1.next = curr;
                curr1 = curr;
            }else {
                curr2.next = curr;
                curr2 = curr;
            }
            curr = curr.next;
        }
        curr1.next = tempHead;
        curr2.next = null;
        return head;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值