LeetCode-链表

本文记录了LeetCode刷题中链表相关题目。介绍了链表解题的基本功,如避免空指针、处理头结点等。详细分析了移除重复节点、反转链表、相交链表等多道题目,给出不同复杂度的解题思路与代码,包括迭代和递归方法。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

摘要

​ 这篇文章主要是记录一下在LeetCode刷题过程中,与“链表“相关的题目。实际上刷的题数肯定不止,但要慢慢整理,写好思路跟代码。所以,慢慢来吧。

一. 前言

​ 链表是一种常用数据结构,其题型也是很有辨识度。对于链表的基础知识在这里也不必多说,直接整理一下LeetCode上比较经典的链表题目吧。很多链表的题目,看起来很简单,但自己动手就会发现很多问题。所以一定要亲自去实现一次,不能仅仅停留在读懂解题思路的阶段。

​ 链表问题通常都需要循环遍历,有时候需要判断是否为null。那么到底什么情况是判断head != null还是head.next != null呢?这种实际上也是编程的基本功。当你在调用一个对象的方法,你需要确保它不为空,否则就会抛出那每一个Java程序员都会碰到的NullPointerException。如果你的代码里用到了,比如current = current.next,你需要确保的是current不为空,这样就不会导致空指针错误。如果你的代码里用到了current = current.next.next,这时候首先你要确保current不为空,其次current.next也不为空。

​ 对于头结点的处理,通常都比较特殊,因为当我们改变了头结点时候,后续return就很麻烦。这时候可以新建一个我们自定义的“伪头结点"dummy,并且使得Dummy.next = head。这样不管后面head是否发生改变,只需要最后return的是dummy.next即可,即将头节点普通化,不再需要认为头节点是特殊的节点。但是dummy的值要巧妙设置,不能影响后续的处理。

​ 而且,切记不要直接操作head,虽然有一些题目无关紧要,但通常我们只想遍历,如果直接head = head.next,那么整个链表都会随着遍历而改变了。正确的做法是创建一个变量,比如current,指向head,然后改变这个current。

​ 对于链表的问题,如果不能直接想出来,那么就多画图吧,画图就很好理解了。

二. LeetCode 面试题02.01. 移除重复节点

题目描述: 编写代码,移除未排序链表中的重复节点。保留最开始出现的节点。题目链接

分析:如果使用O(n)的空间复杂度,那么很简单,只需要创建一个哈希表。如果已经存在该节点的值,便跳过,删除。如果不能使用额外的空间,也就是O(1)的空间复杂度,那么就需要用双指针进行两层遍历,此时时间复杂度为O(n ^ 2)。

代码①: (O(n)时间复杂度,O(n)空间复杂度)

class Solution {
    public ListNode removeDuplicateNodes(ListNode head) {
        Set<Integer> set = new HashSet<>();
        ListNode current = head, prev = head;
        while (current != null) {
            if (set.contains(current.val))
                prev.next = current.next;
            else {
                set.add(current.val);
                prev = current;         // 如果contains,就不需要更改prev
            }
            current = current.next;
        }
        return head;
    }
}

代码②: (O(n ^ 2)时间复杂度,O(1)空间复杂度)

class Solution {
    public ListNode removeDuplicateNodes(ListNode head) {
        ListNode dummy = new ListNode(-1);
        dummy.next = head;      // 创造一个dummy头结点
        ListNode current = head, prev = dummy, helper;
        while (current != null) {
            helper = head;
            boolean flag = false;
            while (helper != current) { // 检查current前面是否有重复
                if (helper.val == current.val) {
                    flag = true;
                    break;
                }
                helper = helper.next;
            }
            if (flag)
                prev.next = current.next;
            else 
                prev = current;         // 如果contains,就不需要更改prev
            current = current.next;
        }
        return dummy.next;
    }
}

三. LeetCode 206. 反转链表

描述:反转一个单链表。题目链接

分析:直接定义一个prev变量,用于记录当前值的前一个值。至于什么时候设置prev,当然是在上一轮循环的时候。在当前循环要进行的操作就是:current.next = prev①,这样就使得当前的current指向了上一个值。可是我们需要遍历,这时候我们遍历所用的current = current.next已经不可以了,因为current.next已经改变。所以我们还要增加一个next变量,在执行①之前先记录current.next的值,然后执行完①,就可以更改prev的值,并且将current指向原本的下一个值。(同样地,修改prev的操作需要在修改current前进行)。只要清晰整个了逻辑,代码就水到渠成。

迭代代码

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode current = head;
        ListNode prev = null;       // head的前一个是null
        while (current != null) {
            ListNode next = current.next;   // 先记录current.next
            current.next = prev;
            prev = current;         // prev改为指向当前的current
            current = next;         // current指向原本的current.next
        }
        return prev;    //最后current为null,prev就是最后的元素
    }
}

这是迭代的版本,如果使用递归,代码更简洁,但逻辑要更加清晰。递归的关键在于反向工作。假设列表的其余部分已经被反转,现在我该如何反转它前面的部分?

假设列表为:

n(1)→…→n(k−1)→(nk)→n(k+1)→…→n(m)→∅

若从节点 n(k+1) 到 n(m) 已经被反转,而我们正处于 n(k)。

n(1)→…→n(k−1)→(nk)→n(k+1)←…←n(m)

我们希望n(k+1)的下一个节点指向n(k),所以: n(k).next.next = nk

除此之外,n1的下一个必须指向null。否则当链表长度为2时,会形成循环,导致出错。

递归代码

class Solution {
    public ListNode reverseList(ListNode head) {
        if (head == null || head.next == null)
            return head;
        ListNode rest = reverseList(head.next);	// 把rest想成一个整体,rest实际上是原本的tail
        head.next.next = head;	// 注意此时head.next是rest的尾结点
        head.next = null;		// 避免链表成环
        return rest;
    }
}

四. LeetCode 160. 相交链表

题目描述:编写一个程序,找到两个单链表相交的起始节点。题目链接

分析: 寻找交点,如果我们要使用两个相同速度的指针,那么就是要确保在某一个时候,二者走的距离相同,并且会在交点相遇。假设二者存在交点,那么这两个链表的差别主要在于交点前的长度不同(相交之后是公共的链表部分)。那么很容易想到,只要让两个指针都同时遍历了一次这两段交点前部分即可。所以思路:让指针到达终点时,将指针指向另一段单链表的起点,然后继续前行。这样保证两个指针会在交点相遇。同时,这种“指向起点”的操作只需要进行1次,如果无法相遇,说明两个单链表没有交点。

代码

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode currentA = headA, currentB = headB;
        boolean flag1 = false, flag2 = false;   // 记录二者是否都到达了一次end
        while (currentA != null && currentB != null) {
            if (currentA == currentB)
                return currentA;    // 相交的结点
            currentA = currentA.next;
            currentB = currentB.next;
            if (currentA == null && !flag1) {
                currentA = headB;   // A跳转到headB的开头
                flag1 = true;
            }
            if (currentB == null && !flag2) {
                currentB = headA;
                flag2 = true;       // 确保只跳转一次
            }
        }
        return null;
    }
}

这段代码比较直接,完全就是按照解题思路写的。但其实还有更简便的方法,实际上只要两个指针指向同一个位置的时候,此处就是交点,当然到达终点依然会进行一次跳转操作。所以此时的循环结束条件是curentA == currentB。如果没有交点该如何?这时候两个指针必定会同时到达尾部,此时二者都为null,所以同样是返回currentA或者currentB即可。

简洁版代码

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if(headA == null || headB == null) return null;
        ListNode pA = headA, pB = headB;
        while(pA != pB) {
            pA = pA == null ? headB : pA.next;
            pB = pB == null ? headA : pB.next;
        }
        return pA;
    }
}

同时这道题有点像,如果一个链表有环,返回环的入口(双指针文章里有提到)。那么这里我们同样可以构造出一个环:遍历链表B,直到终点last,然后将last.next == headA。这时候对于单链表A来说,如果有交点,那么就存在环。如果无交点,就会构成一个环,而环入口就是二者的交点。如果fast最后会到达null,或者fast.next为null,说明不成环,无交点。如果存在环,二者一定在某一时刻相遇,此时只要将fast或者slow置于起始点(headA),然后以同样的速度继续前行,二者会在交点处/环入口相遇(证明过程见“双指针”一文)。同时,题目不允许对原链表进行修改,所以在返回之前,还得把last.next改回去,即last.next = null

构造环

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if (headA == null || headB == null)
            return null;
        ListNode last = headB;
        while (last.next != null)
            last = last.next;
        last.next = headB;      // 此时如果有交点,那么A存在环
        ListNode fast = headA, slow = headA;
        while (fast != null && fast.next != null) {
            fast = fast.next.next;
            slow = slow.next;
            if (slow == fast) {
                slow = headA;
                break;
            }
        }
        if (fast == null || fast.next == null) {
            last.next = null;       // reset
            return null;            // 无交点
        }
        while (slow != fast) {
            slow = slow.next;
            fast = fast.next;
        }
        last.next = null;   // reset
        return slow;        // or fast
    }
}

五. LeetCode 21. 合并两个有序链表

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

分析:直接比较两个链表,选择更小的值添加到新链表中。直到其中一条为null,直接把另一条直接添加即可。同时可以创建一个dummy头结点,便于操作。

代码

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(1);
        ListNode res = new ListNode(100);
        dummy.next = res;
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
                res.next = l1;
                l1 = l1.next;
            }
            else {
                res.next = l2;
                l2 = l2.next;
            }
            res = res.next;
        }
        if (l1 == null)
            res.next = l2;
        else
            res.next = l1;
        return dummy.next.next;
    }
}

六. LeetCode 83. 删除排序链表中的重复元素

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

分析:最简单的方法当然就是HashSet了,但那样就浪费了题中的条件“排序链表”。因为是排序,所以我们只要与前一个元素相比,只要相同,那么就跳过当前的元素。值得注意的是,使用pre来代表前一个元素,但pre并不是每一次都要改变。比如测试用例[1, 1, 1],当current与pre相同的时候,这时候不需要改变pre。本题的唯一难点。

代码

class Solution {
    public ListNode deleteDuplicates(ListNode head) {
        if (head == null)
            return head;
        ListNode prev = head;
        ListNode current = head.next;   // 从第二个开始
        while (current != null) {
            if (prev.val == current.val) 
                prev.next = current.next;
            else
                prev = current;         // 相同的情况下,无须改变prev
            current = current.next;
        }
        return head;
    }
}

七. LeetCode 19. 删除链表的倒数第N个结点

描述: RT(保证给定的n的有效的)题目链接

分析:如果是进行两趟扫描,那么思路很简单,第一遍先获取链表的长度,然后就可以知道倒数第n个是顺序的第几个。这道题的难点是如何顺序第一遍就知道正确的位置。可以使用两个指针,先让其中一个走n步,那么当这个先行的指针走到结尾,后行的指针就正处于倒数第n个的位置。

代码① (两趟扫描):

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        int length = 0;
        ListNode current = head;
        while (current != null) {
            length++;
            current = current.next;
        }
        current = dummy;    // 从dummy开始,不然删除head就比较麻烦
        int tmp = 0;
        while (tmp <= length - n - 1) {
            current = current.next;
            tmp++;
        }
        current.next = current.next.next;
        return dummy.next;
    }
}

代码② (一趟扫描)

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        ListNode fast = dummy;
        for (int i = 0; i <= n; i++)	// n从1开始,要多走一步
            fast = fast.next;
        ListNode slow = dummy;
        while (fast != null) {
            fast = fast.next;
            slow = slow.next;
        }
        slow.next = slow.next.next;
        return dummy.next;
    }
}

八. LeetCode 24. 两两较换链表中的结点

描述:给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。题目链接

你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。

分析:直接定义三个变量,pre,first,second。我们需要设置:second.next = firstfirst.next = second.next,可以提前保留second.next这个值。操作完成之后,更换pre,first,second的值。这里要注意是否会为null的情况。因为first = nextsecond = next.next,显然要判断next是否为null。如果为null,说明后面没有元素,无须继续操作,可以结束循环。直觉上很容易想,如果还剩下一个元素应该怎么办?此时next不为null,但next.null为null。这时候仍然可以结束循环,所以如果你愿意,你可以把这个判断条件更改为:

if (next != null || next.next != null)。但实际上,只剩下1个元素的情况下,实际上就是second为null,这时候再循环一次其实就在while循环里结束了,所以是一样的。(对于while跟if的结束条件,有很多种写法,理解就好)

代码

class Solution {
    public ListNode swapPairs(ListNode head) {
        if (head == null || head.next == null)
            return head;
        ListNode dummy = new ListNode(-1);
        dummy.next = head;
        ListNode pre = dummy;
        ListNode first = head;
        ListNode second = head.next;
        while (first != null && second != null) {
            ListNode next = second.next;
            pre.next = second;
            second.next = first;
            first.next = next;
            pre = first;
            first = next;
            if (next == null)
                break;
            second = next.next;
        }
        return dummy.next;
    }
}

以上是迭代算法,同样地,也写出递归算法,就跟206题一样。递归要考虑的是3个部分:

①终止条件 ②返回值 ③本级递归应该做什么 (此处参考“网恋教父”的的文章

主要是把整个链表看作3个部分,head,next,已经处理完的部分。然后经过操作变成:

next,head,已经处理完的部分

递归代码

class Solution {
    public ListNode swapPairs(ListNode head) {
        if (head == null || head.next == null)
            return head;
        ListNode next = head.next;
        head.next = swapPairs(next.next);   // 一个整体
        next.next = head;
        return next;
    }
}

九. LeetCode 445. 两数相加 Ⅱ

题目描述: 给定两个非空链表来代表两个非负整数。数字最高位位于链表开始位置。它们的每个节点只存储单个数字。将这两数相加会返回一个新的链表。题目链接

你可以假设除了数字 0 之外,这两个数字都不会以零开头。

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

分析:对链表进行倒序读取是很麻烦的操作,所以一下子就想到了用两个数组/栈来重新维护数据。实现的过程也遇到了一点小麻烦,效率也还可以,排前面的代码也是这种思路,所以看来这就是最终的方法了。这里显然是用栈来维护最好(LinkedList),因为我们只需要对首尾的元素进行操作。值得注意的是,当两个栈都为空,如果此时还有进位,那么还需要额外new一个值为1的头结点。

代码

class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        LinkedList<Integer> list1 = new LinkedList<>();
        LinkedList<Integer> list2 = new LinkedList<>();
        ListNode h1 = l1, h2 = l2;
        while (h1 != null) {
            list1.addLast(h1.val);
            h1 = h1.next;
        }
        while (h2 != null) {
            list2.addLast(h2.val);
            h2 = h2.next;			// 数据存放到list1和list2中
        }
        ListNode next = null;
        int increment = 0;
        while (true) {
            int val1 = list1.size() == 0 ? 0 : list1.removeLast();
            int val2 = list2.size() == 0 ? 0 : list2.removeLast();
            int sum = val1 + val2 + increment;
            if (sum >= 10) {
                sum %= 10;
                increment = 1;
            }
            else
                increment = 0;
            ListNode current = new ListNode(sum);
            current.next = next;
            next = current;
            if (list1.size() == 0 && list2.size() == 0) {
                if (increment == 0)
                    return current;
                ListNode head = new ListNode(1);	// 循环结束,但还有进位的情况
                head.next = current;
                return head;
            }
        }
    }
}

十. LeetCode 234. 回文链表

描述: 判断一个链表是否为回文链表,要求时间复杂度为O(n),空间复杂度为O(1)。题目链接

分析: 空间复杂度为O(1),所以不能使用List等额外的数据结构。对于回文链表,可以理解为中点左右的值都相等。所以我们可以先找到链表的中点,然后将后半段反转,这时候逐个比较后半段与链表开头的值。其实LeetCode里就有寻找中点的题,也是双指针。但该题题目要求写道,如果有两个中间结点,那么返回第二个结点。这里是一个细节。我们获得了中间结点之后,是不需要把中间结点与head比较的,所以我们会是从mid.next与head开始相比,这就导致了奇偶数情况不同:如果是奇数,可以直接mid.next即可。如果是偶数,因为那样的解法是获得第二个中点,显然我们应该获得“第一个“作为中点。这时候需要在寻找中点的做法里做一些改动。

代码

class Solution {
    public boolean isPalindrome(ListNode head) {
        if (head == null || head.next == null)
            return true;
        ListNode fast = head, slow = head;
        while (fast.next != null && fast.next.next != null) {   
        // 这会使得偶数链表返回第一个中间结点
        // 如果想返回第二个中间结点, 应该用 fast != null && fast.next != null
            fast = fast.next.next;
            slow = slow.next;
        }
        slow = reverse(slow.next);
        while (slow != null) {
            if (slow.val != head.val)
                return false;
            slow = slow.next;
            head = head.next;
        }
        return true;
    }
    public ListNode reverse(ListNode head) {
        if (head == null || head.next == null)
            return head;
        ListNode prev = null;
        ListNode current = head;
        while (current != null) {
            ListNode next = current.next;
            current.next = prev;
            prev = current;
            current = next;
        }
        return prev;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值