LeetCode《程序员面试金典》面试题 02.04. 分割链表

本文详细介绍了LeetCode面试题02.04.分割链表的解题方法,包括三种解法:哨兵节点法、直接指向原节点法以及错误思路分析。解法一和二均实现了O(1)的空间复杂度,解法三虽然直观但空间效率较低。文章强调了对链表操作的理解和优化,提醒读者避免将数组处理方式套用到链表问题上。
摘要由CSDN通过智能技术生成

LeetCode 面试题 02.04. 分割链表

题目

在这里插入图片描述

注意验证:链表为空的情况!!
你应当 保留 两个分区中每个节点的初始相对位置:算法要具备稳定性

解题

刚拿到这道题时,马上想到快速排序里面的 partition 算法,但是这题要求没有那么严格:大于等于 x 的元素只要在右半部分即可,没有要求 x 的左边全是小于等于 x 的数,右边全是大于等于 x 的数。

解法一

在这里插入图片描述

# python
class Solution:
    def partition(self, head: ListNode, x: int) -> ListNode:
        BeforeNode = BeforeListHead = ListNode(0) # 哨兵节点
        AfterNode = AfterListHead = ListNode(0) # 哨兵节点
        CurNode = head
        while CurNode:
            NewNode = ListNode(CurNode.val)
            if CurNode.val < x:
                BeforeNode.next = NewNode
                BeforeNode = BeforeNode.next
            else:
                AfterNode.next = NewNode
                AfterNode = AfterNode.next
            CurNode = CurNode.next
        BeforeNode.next = AfterListHead.next
        return BeforeListHead.next

上面的解答用了【哨兵节点】,第一次看到哨兵节点的介绍时还不以为然,现在就两个字“真香”。即便 AfterListHead 的链表没有插入元素,也至少有一个哨兵节点,它的 next 指向 None。

还有一点要注意的是:这里用了四个变量来跟踪两个链表,原因是 BeforeNode 和 AfterNode 会被挪动,需要另外两个变量来记录链表的头节点,否则两个链表没法相连,也找不到该返回的节点。

看了官方的解法后发现,因为这里没有去改变原链表,所以不用每次都 NewNode = ListNode(CurNode.val),可以直接指向原链表节点,但是有一个注意事项:最后要把 AfterNode.next 指向 None。

# python
class Solution:
    def partition(self, head: ListNode, x: int) -> ListNode:
        BeforeNode = BeforeListHead = ListNode(0)
        AfterNode = AfterListHead = ListNode(0)
        while head:
            if head.val >= x:
                AfterNode.next = head
                AfterNode = AfterNode.next
            else:
                BeforeNode.next = head
                BeforeNode = BeforeNode.next
            head = head.next
        AfterNode.next = None # ⚠️ attention!!!
        BeforeNode.next = AfterListHead.next
        return BeforeListHead.next

在这里插入图片描述
空间上,因为指向的是原链表节点,没有用额外的空间来存储链表。

// javascript
var partition = function(head, x) {
    let small = new ListNode(0), large = new ListNode(0);
    const smallHead = small, largeHead = large;
    while (head !== null) {
        if (head.val < x){
            small.next = head;
            small = small.next;
        }
        else{
            large.next = head;
            large = large.next;
        }
        head = head.next;
    }
    large.next = null;
    small.next = largeHead.next;
    return smallHead.next;
};

解法二

在这里插入图片描述
思路是先判断链表是否为空,如果是空的,则返回空链表;否则将两个代表链表头尾的变量指向原链表的第一个元素,跳过第一个节点去遍历后面的节点(head = head.next;)。如果 < x,因为不能改变原链表,所以 new 一个 node,值为 head.val,next 指向原头节点,并更新头节点;否则直接将尾节点指向原链表节点,并更新尾节点。

// javascript
var partition = function(head, x) {
    if (head === null) return head; // [], x=3
    let headNode = head, tailNode = head;
    head = head.next;
    while (head !== null) {
        if (head.val < x){
            let newNode = new ListNode(head.val);
            newNode.next = headNode;
            headNode = newNode;
        }
        else{
            tailNode.next = head;
            tailNode = tailNode.next;
        }
        head = head.next;
    }
    tailNode.next = null;
    return headNode;
};

官方比较狠的是,用一个变量去存储 head.next,这样就不用分配空间来存储新节点啦,原 head.next 也不会随着 head.next 的更新而丢失。

下面的代码里我注释掉了一段,起初我认为既然头尾变量已经指向第一个节点,那么该跳过第一个节点往后去遍历。但其实跟着代码跑一遍的话会发现,因为while循环并不像for循环一样在for语句里有指针的移动,实际上所有指针都定在原位,所以处理第一个节点后,头尾变量仍指着原链表第一个节点。

// javascript
var partition = function(head, x) {
    let headNode = head, tailNode = head;
    while (head !== null) {
        let next = head.next; // 保存下一个node
        /*if (headNode === head){
        	continue;
        }*/
        if (head.val < x){
            head.next = headNode; // 不怕丢,已经存起来啦
            headNode = head;
        }
        else{
            tailNode.next = head;
            tailNode = tailNode.next;
        }
        head = next; // 拿回下一个node
    }
    if (tailNode !== null){ // [], x=3
        tailNode.next = null;
    }   
    return headNode;
};

// or
var partition = function(head, x) {
    if (head === null) return head;
    let headNode = head, tailNode = head;
    head = head.next;
    while (head !== null) {
        let next = head.next;
        if (head.val < x) {
            head.next = headNode;
            headNode = head;
        }
        else {
            tailNode.next = head;
            tailNode = tailNode.next;
        }
        head = next;
    }
    tailNode.next = null;
    return headNode;
}; 

解法三

我第一次想的办法其实是,一旦看到 >= x 的节点,把该节点挪到链表尾端。但上面两种解法优化后 空间复杂度 都是 O ( 1 ) O(1) O(1),如果要去链表尾部插节点,再将原节点删除,势必要用额外的空间。从这个角度来想,解法三没能借助链表的优势,从一开始便输了。

# python
class Solution:
    def partition(self, head: ListNode, x: int) -> ListNode:
        SentinalNode = ListNode(0)
        SentinalNode.next = head
        i_Node = j_Node = SentinalNode
        while j_Node.next:
            j_Node = j_Node.next
        while i_Node.next != j_Node and i_Node.next: # [], x=3
            if i_Node.next.val >= x:
                NewNode = ListNode(i_Node.next.val)
                NewNode.next = j_Node.next
                j_Node.next = NewNode
                i_Node.next = i_Node.next.next
            else:
                i_Node = i_Node.next
        return SentinalNode.next

从这道题可以看出来,自己对链表的认识还是不太深刻,总喜欢把数组的处理方法带过来,还是要勤练、多思考。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值