链表操作

17 阅读3分钟

简单

剑指 Offer 24. 反转链表

反转链表操作作为链表操作的最基本操作,一般可以通过迭代或者递归的方式解决,这里以反转链表为例,讨论反转链表相关问题。

反转全部链表

反转全部链表,即1->2->3->4->5->NULL转变成NULL<-1<-2<-3<-4<-5,反转全部链表是比较简单的操作,先来看一下递归的解决代码:

ListNode* reverseList(ListNode* head) {
    if (head == NULL) return NULL; // 传入的链表可能为空的情况
    if (head->next == NULL) return head;
    ListNode* last = reverseList(head->next);
    head->next->next = head;
    head->next = NULL;

    return last;
}

对于递归解决的理解,首先不要跳进递归的调用中,这样很容易把自己绕晕了,考虑最原始的思路,递归函数是干嘛的?这里reverseList函数,接受一个头指针head指向头节点,然后反转headhead->next两个节点的顺序,然后递归处理head->next后面的节点,注意由于最后需要返回的是反转后链表的头节点,因此这里需要返回的是last。同时还要注意,递归函数是需要递归出口的,当只有一个节点时,直接返回即可,不用再反转了。

反转链表的前N个节点

与反转整个链表类似,反转前N个节点,需要在反转节点时,计算反转次数,反转N次则代表已经结束。

ListNode* after = NULL; // 后继节点
ListNode* reverseListN(ListNode* head, int n) {
    if (n == 1) {
        after = head->next; // 记录断开的节点
        return head;
    }
    ListNode* last = reverseListN(head->next, n - 1);
    head->next->next = head;
    head->next = after; // 连接
    return last;
}

这里默认n不会超过链表长度,因此递归出口只需判断n是否为1,为1时表示处理结束。另外,我们发现用到了后继节点,因为在反转前N个节点时,被反转的部分的尾结点是指向空的,因此需要将其指向断开的节点。

中等

92. 反转链表 II

反转链表中的[left, right]中的节点

left,right分别表示第left,right个节点,可以发现,这个问题是在前面的基础上再次加深了,但不要被其所迷惑,我们已经解决了反转链表的前n个节点,如果我们找到left节点,这时,将left节点作为头节点,此时就是反转前n个节点了。

ListNode* reverseListBetween(ListNode* head, int left, int right) {
    if (left == 1) {
        return reverseListN(head, right);
    }
    head->next = reverseListBetween(head->next, left - 1, right - 1);
    return head;
}

这里需要注意的是,最终递归结束后,reverseListN返回的是right所在的节点,因此在递归时,需要将前后连接,即head->next = reverseListBetween(head->next, left - 1, right - 1)

需要注意的是,以上三种解决办法都是通过递归实现的,但由于递归需要调用栈,因此它的空间复杂度是O(n),而迭代实现的空间复杂度是O(1)

困难

K 个一组翻转链表

对于这道题,我们先考虑如何反转整个链表,这个操作应该不难

ListNode* reverse(ListNode* a, ListNode* b) {
    ListNode* pre, *cur, *nxt;
    pre = nullptr, cur = a, nxt = a;
    while(cur != nullptr) {
        nxt = cur->next;
        cur->next = pre;
        pre = cur;
        cur = nxt;
    }
    return pre;
}

那么,在此基础上,如何反转节点[a,b)之间的链表呢?其实只需要在上面代码中修改cur != b即可。 如果我们将k个一组反转,拆解成若干个子问题,会发现这些子问题都是类似的,即以某个节点为头节点,链表长度为k,反转整个链表。

ListNode* reverseKGroup(ListNode* head, int k) {
    ListNode* b = head;
    for (int i = 0; i < k; i++) {
        if (b == nullptr) return head; // 若不足k个,保持原有顺序
        b = b->next;
    }
    ListNode* pre = reverse(head, b); // 记录反转后的头节点
    head->next = reverseKGroup(b, k); // 将前面子问题反转后的链表与后面反转后的链表连接

    return pre;
}