数据结构丨链表

作为半路出家的程序员,只有通过不断地刻意训练,才能提升自己的编程水平。

目前的计划是完成LeetCode explore上的数据结构部分https://leetcode-cn.com/explore/>,持续更新。

本文中的示例代码主要参考自https://github.com/liuyubobobo/Play-Leetcode-Explore,作者为每个问题提供多种解决方法,非常赞?,请为作者点个Star。

单链表

简介

单链表中的每个结点不仅包含值,还包含链接到下一个结点的引用字段。通过这种方式,单链表将所有结点按顺序组织起来。

下面是一个单链表的例子:

img

蓝色箭头显示单个链接列表中的结点是如何组合在一起的。

结点结构

以下是单链表中结点的典型定义:

struct SinglyListNode {
    int val;
    SinglyListNode *next;
    SinglyListNode(int x) : val(x), next(NULL) {}
};

操作

与数组不同,我们无法在常量时间内访问单链表中的随机元素。 如果我们想要获得第 i 个元素,我们必须从头结点逐个遍历。 我们按索引访问元素平均要花费 O(N)时间,其中 N 是链表的长度。

例如,在上面的示例中,头结点是 23。访问第 3 个结点的唯一方法是使用头结点中的“next”字段到达第 2 个结点(结点 6); 然后使用结点 6 的“next”字段,我们能够访问第 3 个结点。

你可能想知道为什么链表很有用,尽管它在通过索引访问数据时(与数组相比)具有如此糟糕的性能。 在接下来的两篇文章中,我们将介绍插入和删除操作,你将了解到链表的好处。

之后,我们将为你提供练习设计自己的单链表。

添加操作

如果我们想在给定的结点 prev 之后添加新值,我们应该:

  1. 使用给定值初始化新结点 cur;img
  2. cur的“next”字段链接到 prev 的下一个结点 nextimg
  3. prev 中的“next”字段链接到 curimg

与数组不同,我们不需要将所有元素移动到插入元素之后。因此,您可以在 O(1) 时间复杂度中将新结点插入到链表中,这非常高效。

示例

img

让我们在第二个结点 6 之后插入一个新的值 9。

我们将首先初始化一个值为 9 的新结点。然后将结点 9 链接到结点 15。最后,将结点 6 链接到结点 9。

插入之后,我们的链表将如下所示:

img

在开头添加结点

众所周知,我们使用头结点来代表整个列表。

因此,在列表开头添加新节点时更新头结点 head 至关重要。

  1. 初始化一个新结点 cur;
  2. 将新结点链接到我们的原始头结点 head
  3. cur 指定为 head

例如,让我们在列表的开头添加一个新结点 9。

  1. 我们初始化一个新结点 9 并将其链接到当前头结点 23。img
  2. 指定结点 9 为新的头结点。 img

如何在列表的末尾添加新的结点?我们还能使用类似的策略吗?

删除操作

如果我们想从单链表中删除现有结点 cur,可以分两步完成:

  1. 找到 cur 的上一个结点 prev 及其下一个结点 next;img
  2. 接下来链接 prev 到 cur 的下一个节点 next。img

在我们的第一步中,我们需要找出 prevnext。使用 cur 的参考字段很容易找出 next,但是,我们必须从头结点遍历链表,以找出 prev,它的平均时间是 O(N),其中 N 是链表的长度。因此,删除结点的时间复杂度将是 O(N)

空间复杂度为 O(1),因为我们只需要常量空间来存储指针。

示例

img

让我们尝试把结点 6从上面的单链表中删除。

  1. 从头遍历链表,直到我们找到前一个结点 prev,即结点 23

  2. prev(结点 23)与 next(结点 15)链接

img

结点 6 现在不在我们的单链表中。

删除第一个结点

如果我们想删除第一个结点,策略会有所不同。

正如之前所提到的,我们使用头结点 head 来表示链表。我们的头是下面示例中的黑色结点 23。

img

如果想要删除第一个结点,我们可以简单地将下一个结点分配给 head。也就是说,删除之后我们的头将会是结点 6。

img

链表从头结点开始,因此结点 23 不再在我们的链表中。

删除最后一个结点呢?我们还能使用类似的策略吗?

设计链表

设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:valnextval 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。

在链表类中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

示例:

MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2);   //链表变为1-> 2-> 3
linkedList.get(1);            //返回2
linkedList.deleteAtIndex(1);  //现在链表是1-> 3
linkedList.get(1);            //返回3

提示:

  • 所有值都在 [1, 1000] 之内。
  • 操作次数将在 [1, 1000] 之内。
  • 请不要使用内置的 LinkedList 库。
#include <iostream>

using namespace std;

# Singly Linked List
# Without dummyhead;
# Time Complexity: init: O(1)
#                  get: O(n)
#                  addAtHead: O(1)
#                  addAtTail: O(n)
#                  addAtIndex: O(n)
#                  deleteAtIndex: O(n)
# Space Complexity: O(n)

class MyLinkedList
{
private:
    class Node
    {
    public:
        int val;
        Node *next;

        Node(int val, Node *next) : val(val), next(next) {}
        Node(int val) : Node(val, NULL) {}
    };

public:
    Node *head;
    MyLinkedList()
    {
        head = NULL;
    }

    int get(int index)
    {
        if (index < 0)
            return -1;
        Node *cur = head;
        for (int i = 0; i < index && cur; i++)
            cur = cur->next;
        if (!cur)
            return -1;
        return cur->val;
    }

    void addAtHead(int val)
    {
        head = new Node(val, head);
    }

    void addAtTail(int val)
    {
        if (head == NULL)
            head = new Node(val);
        else
        {
            Node *cur = head;
            while (cur->next)
                cur = cur->next;
            cur->next = new Node(val);
        }
    }

    void addAtIndex(int index, int val)
    {
        if (index <= 0)
            addAtHead(val);
        else
        {
            Node *cur = head;
            for (int i = 1; i < index && cur; i++)
                cur = cur->next;
            if (cur)
                cur->next = new Node(val, cur->next);
        }
    }

    void deleteAtIndex(int index)
    {
        if (index == 0)
        {
            if (head)
            {
                Node *delNode = head;
                head = head->next;
                delete delNode;
            }
        }
        else
        {
            Node *cur = head;
            for (int i = 1; i < index && cur; i++)
                cur = cur->next;
            if (cur && cur->next && index > 0)
            {
                Node *delNode = cur->next;
                cur->next = delNode->next;
                delete delNode;
            }
        }
    }
};

// int main()
// {
//     MyLinkedList list = MyLinkedList();
//     list.addAtHead(1);
//     list.addAtTail(3);
//     list.addAtIndex(1, 2);
//     list.get(-1);
//     list.deleteAtIndex(1);
//     list.get(-3);
// }

双指针技巧

链表中的双指针

让我们从一个经典问题开始:

给定一个链表,判断链表中是否有环。

你可能已经使用哈希表提出了解决方案。但是,使用双指针技巧有一个更有效的解决方案。在阅读接下来的内容之前,试着自己仔细考虑一下。

想象一下,有两个速度不同的跑步者。如果他们在直路上行驶,快跑者将首先到达目的地。但是,如果它们在圆形跑道上跑步,那么快跑者如果继续跑步就会追上慢跑者。

这正是我们在链表中使用两个速度不同的指针时会遇到的情况:

  1. 如果没有环,快指针将停在链表的末尾。
  2. 如果有环,快指针最终将与慢指针相遇。

所以剩下的问题是:

这两个指针的适当速度应该是多少?

一个安全的选择是每次移动慢指针一步,而移动快指针两步。每一次迭代,快速指针将额外移动一步。如果环的长度为 M,经过 M 次迭代后,快指针肯定会多绕环一周,并赶上慢指针。

那其他选择呢?它们有用吗?它们会更高效吗?

环形链表

给定一个链表,判断链表中是否有环。

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos-1,则在该链表中没有环。

示例 1:

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

img

示例 2:

输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。

img

示例 3:

输入:head = [1], pos = -1
输出:false
解释:链表中没有环。

img

进阶:

你能用 O(1)(即,常量)内存解决此问题吗?


#include <iostream>
#include <unordered_set>

using namespace std;

struct ListNode
{
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

///哈希表法:创建一个空哈希表,遍历链表中每个节点。每遇见一个节点,在哈希表中查找是否存在该节点。若存在,则说明该节点出现过后又回到它自身,因此存在环;若不存在,则将该节点插入哈希表中。当遍历到链表尾部尚未遇到出现过的节点,则无环。
/// Using Hash Table
/// Time Complexity: O(n)
/// Space Complexity: O(n)
class SolutionA
{
public:
    bool hasCycle(ListNode *head)
    {
        if (head == NULL)
            return false;

        unordered_set<long long> node_address;

        ListNode *p = head;
        while (p != NULL)
        {
            if (node_address.find((long long)(p)) == node_address.end())
                node_address.insert((long long)(p));//在哈希表中没找到该节点,则插入。
            else
                return true;

            p = p->next;
        }
        return false;
    }
};

/// Two Pointers - fast and slow
/// faster pointer moves two steps a time, and slow pointer moves one step a time.
///
/// faster pointer will meet slow pointer eventually.
/// Simple prove: suppose faster pointer is x steps behind slow pointer
/// if x == 1, then in the next time, fast pointer will meet slow pointer
/// if x > 1, then in the next tme, fast pointer will get one step closer to slow pointer
///           the process continues until faster pointer is just one step behind slow pointer
///           thenm in the next time, they meet together:)
///
/// Time Complexity: O(n)
/// Space Complexity: O(1)
class SolutionB
{
public:
    bool hasCycle(ListNode *head)
    {
        if (head == NULL)
            return false;

        if (head->next == NULL)
            return false;

        ListNode *slow = head;
        ListNode *fast = head->next;
        while (fast != slow)
        {
            if (fast->next == NULL)
                return false;
            if (fast->next->next == NULL)
                return false;

            fast = fast->next->next;
            slow = slow->next;
        }
        return true;
    }
};

环形链表II

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null

为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos-1,则在该链表中没有环。

说明:不允许修改给定的链表。

示例 1:

输入:head = [3,2,0,-4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。

img

示例 2:

输入:head = [1,2], pos = 0
输出:tail connects to node index 0
解释:链表中有一个环,其尾部连接到第一个节点。

img

示例 3:

输入:head = [1], pos = -1
输出:no cycle
解释:链表中没有环。

img

进阶:
你是否可以不用额外空间解决此题?

#include <iostream>
#include <cassert>
#include <set>

using namespace std;

struct ListNode
{
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

/// Using Hash
///
/// Time Complexity: O(N)
/// Space Complexity: O(N)
class SolutionA
{
public:
    ListNode *detectCycle(ListNode *head)
    {
        if (head == NULL || head->next == NULL)
            return NULL;

        set<ListNode *> records;
        ListNode *curNode = head;
        while (curNode != NULL)
        {
            if (records.find(curNode) != records.end())
                return curNode;

            records.insert(curNode);
            curNode = curNode->next;
        }
        return NULL;
    }
};


/// Two Pointers - Floyd's Tortoise and Hare
///
/// A great overview for the algorithm is here:
/// -- https://stackoverflow.com/questions/3952805/proof-of-detecting-the-start-of-cycle-in-linked-list
///
/// UPDATE: 09 Dec 2017
/// The official solution in leetcode is extremely great for this problem:
/// https://leetcode.com/problems/linked-list-cycle-ii/solution/#approach-2-floyds-tortoise-and-hare-accepted
///
/// Time Complexity: O(N)
/// Space Complexity: O(1)
class SolutionB
{
public:
    ListNode *detectCycle(ListNode *head)
    {
        if (head == NULL || head->next == NULL)
            return NULL;

        ListNode *dummyHead = new ListNode(-1);
        dummyHead->next = head;

        ListNode *slow = dummyHead;
        ListNode *fast = dummyHead;
        do
        {
            if (fast->next == NULL || fast->next->next == NULL)
                return NULL;
            fast = fast->next->next;
            slow = slow->next;
        } while (slow != fast);

        ListNode *entrance = slow;
        assert(entrance == fast);

        ListNode *p = dummyHead;
        while (p != entrance)//根据相遇位置确定入环第一个节点位置,见下面解析。
        {
            p = p->next;
            entrance = entrance->next;
        }

        delete dummyHead;
    
        return entrance;
    }
};

ListNode *
createLinkedList(int arr[], int n)
{
    if (n == 0)
        return NULL;
    ListNode *head = new ListNode(arr[0]);
    ListNode *curNode = head;
    for (int i = 1; i < n; i++)
    {
        curNode->next = new ListNode(arr[i]);
        curNode = curNode->next;
    }
    return head;
}

ListNode *tail(ListNode *head)
{
    if (head == NULL)
        return NULL;

    ListNode *curNode = head;
    while (curNode->next != NULL)
        curNode = curNode->next;
    return curNode;
}

void deleteLinkedList(ListNode *head)
{
    ListNode *curNode = head;
    while (curNode != NULL)
    {
        ListNode *delNode = curNode;
        curNode = curNode->next;
        delete delNode;
    }
    return;
}

int main()
{
    int n1 = 2;
    int nums1[2] = {1, 2};
    ListNode *head1 = createLinkedList(nums1, n1);

    ListNode *entrance1 = SolutionB().detectCycle(head1);
    if (entrance1 != NULL)
        cout << "Loop entrance at " << entrance1->val << endl;
    else
        cout << "No Loop" << endl;

    deleteLinkedList(head1);

    int n2 = 6;
    int nums2[6] = {3, 2, 0, -4, 9, 8};
    ListNode *head2 = createLinkedList(nums2, n2);
    ListNode *tail2 = tail(head2);
    tail2->next = head2->next;

    ListNode *entrance2 = SolutionB().detectCycle(head2);
    if (entrance2 != NULL)
        cout << "Loop entrance at " << entrance2->val << endl;
    else
        cout << "No Loop" << endl;

    tail2->next = NULL;
    deleteLinkedList(head2);

    return 0;
}

SolutionB可参考:https://www.jianshu.com/p/c36e69e27d73

设:链表头是X,环的第一个节点是Y,slow和fast第一次的交点是Z。各段的长度分别是a,b,c,如图所示 第一次相遇时slow走过的距离:a+b,fast走过的距离:a+b+c+b 因为fast的速度是slow的两倍,所以fast走的距离是slow的两倍,有 2(a+b) = a+b+c+b,可以得到a=c(这个结论很重要!) 这时候,slow从X出发,fast从Z出发,以相同速度走,同时到达Y,Y就是环的入口,即第一个节点。

img

相交链表

编写一个程序,找到两个单链表相交的起始节点。

如下面的两个链表

img

在节点 c1 开始相交。

示例 1:

img

输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。

示例 2:

img

输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1
输出:Reference of the node with value = 2
输入解释:相交节点的值为 2 (注意,如果两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。

示例 3:

img

输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。

注意:

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

struct ListNode
{
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

/// Brute Force
/// Time Complexity: O(m*n)
/// Space Complexity: O(1)
class SolutionA
{
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
    {
        for (ListNode *pA = headA; pA; pA = pA->next)
            for (ListNode *pB = headB; pB; pB = pB->next)
                if (pA == pB)
                    return pA;
        return NULL;
    }
};

/// Using HashSet
/// Time Complexity: O(m + n)
/// Space Complexity: O(m)
class SolutionB
{
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
    {
        unordered_set<ListNode *> set;
        for (ListNode *pA = headA; pA; pA = pA->next)
            set.insert(pA);
        for (ListNode *pB = headB; pB; pB = pB->next)
            if (set.count(pB))
                return pB;
        return NULL;
    }
};

/// Two Pointers    解析如下
/// Time Complexity: O(m + n)
/// Space Complexity: O(1)
class SolutionC
{
public:
    ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
    {
        ListNode *pA = headA;
        ListNode *pB = headB;
        while (pA || pB)
        {
            if (pA == pB)
                return pA;

            if (pA)
                pA = pA->next;
            else
                pA = headB;

            if (pB)
                pB = pB->next;
            else
                pB = headA;
        }
        return NULL;
    }
}

双指针法

若链表A和链表B相交,说明链表A、链表B尾部是重合的。但由于链表A和链表B长度不一致,一般没办法从表头同时抵达相交点,所以想办法消除长度差。

于是我们第一轮让两个指针pA、pB分别遍历链表A、B,一定有一个先到达表尾。假设pA先到达A表尾,说明A表长更短,此时pB离B表尾还有距离x。抵达表尾后,让指针从表头开始遍历另一个表。这样第二轮当指针pB从A表头出发,指针pA就会在离B表头距离x的地方出发,从而消除长度差,若存在相交点,它们将同时抵达。

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

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

示例:

给定一个链表: 1->2->3->4->5, 和 n = 2.

当删除了倒数第二个节点后,链表变为 1->2->3->5.

说明:

给定的 n 保证是有效的。

进阶:

你能尝试使用一趟扫描实现吗?

#include <iostream>
#include <cassert>

using namespace std;

struct ListNode
{
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

ListNode *createLinkedList(int arr[], int n)
{
    if (n == 0)
        return NULL;
    ListNode *head = new ListNode(arr[0]);
    ListNode *curNode = head;
    for (int i = 1; i < n; i++)
    {
        curNode->next = new ListNode(arr[i]);
        curNode = curNode->next;
    }
    return head;
}
void printLinkedList(ListNode *head)
{
    if (head == NULL)
    {
        cout << "NULL" << endl;
        return;
    }
    ListNode *curNode = head;
    while (curNode != NULL)
    {
        cout << curNode->val;
        if (curNode->next != NULL)
            cout << "->";
        curNode = curNode->next;
    }
    cout << endl;
    return;
}
void deleteLinkedList(ListNode *head)
{
    ListNode *curNode = head;
    while (curNode != NULL)
    {
        ListNode *delNode = curNode;
        curNode = curNode->next;
        delete delNode;
    }
    return;
}

/// Get the total length and remove the nth node
/// Two Pass Algorithm
///
/// Time Complexity: O(n)
/// Space Complexity: O(1)
class SolutionA
{
public:
    ListNode *removeNthFromEnd(ListNode *head, int n)
    {
        ListNode *dummyHead = new ListNode(0);
        dummyHead->next = head; //dummyHead并不是链表的头结点

        int length = 0;
        for (ListNode *cur = dummyHead->next; cur != NULL; cur = cur->next)
            length++;

        int k = length - n;
        assert(k >= 0);
        ListNode *cur = dummyHead;
        for (int i = 0; i < k; i++)
            cur = cur->next;

        //cur->next = cur->next->next
        ListNode *delNode = cur->next;
        cur->next = delNode->next;
        delete delNode;

        ListNode *retNode = dummyHead->next;
        delete dummyHead;
        return retNode;
    }
};

/// 倒序第x个 即 正序第(表长-x)个。
/// Two Pointers - One Pass Algorithm
/// Time Complexity: O(n)
/// Space Complexity: O(1)
class SolutionB
{
public:
    ListNode *removeNthFromEnd(ListNode *head, int n)
    {
        ListNode *dummyHead = new ListNode(0);
        dummyHead->next = head;

        ListNode *p = dummyHead;
        ListNode *q = dummyHead;
        for (int i = 0; i < n + 1; i++)
        {
            assert(q);
            q = q->next;
        }

        while (q)
        {
            p = p->next;
            q = q->next;
        }

        ListNode *delNode = p->next;
        p->next = delNode->next;
        delete delNode;

        ListNode *retNode = dummyHead->next;
        delete dummyHead;

        return retNode;
    }
};

int main()
{
    int arr[] = {1, 2, 3, 4, 5};
    int n = sizeof(arr) / sizeof(int);

    ListNode *head = createLinkedList(arr, n);
    printLinkedList(head);

    head = SolutionA().removeNthFromEnd(head, 2);
    printLinkedList(head);

    deleteLinkedList(head);

    return 0;
}

小结

在这里,我们为你提供了一个模板,用于解决链表中的双指针问题。

// Initialize slow & fast pointers
ListNode* slow = head;
ListNode* fast = head;
/**
 * Change this condition to fit specific problem.
 * Attention: remember to avoid null-pointer error
 **/
while (slow && fast && fast->next) {
    slow = slow->next;          // move slow pointer one step each time
    fast = fast->next->next;    // move fast pointer two steps each time
    if (slow == fast) {         // change this condition to fit specific problem
        return true;
    }
}
return false;   // change return value to fit specific problem

提示

它与我们在数组中学到的内容类似。但它可能更棘手而且更容易出错。你应该注意以下几点:

1. 在调用 next 字段之前,始终检查节点是否为空。

获取空节点的下一个节点将导致空指针错误。例如,在我们运行 fast = fast.next.next 之前,需要检查 fastfast.next 不为空。

2. 仔细定义循环的结束条件。

运行几个示例,以确保你的结束条件不会导致无限循环。在定义结束条件时,你必须考虑我们的第一点提示。

复杂度分析

空间复杂度分析容易。如果只使用指针,而不使用任何其他额外的空间,那么空间复杂度将是 O(1)。但是,时间复杂度的分析比较困难。为了得到答案,我们需要分析运行循环的次数

在前面的查找循环示例中,假设我们每次移动较快的指针 2 步,每次移动较慢的指针 1 步。

  1. 如果没有循环,快指针需要 N/2 次才能到达链表的末尾,其中 N 是链表的长度。
  2. 如果存在循环,则快指针需要 M 次才能赶上慢指针,其中 M 是列表中循环的长度。

显然,M <= N 。所以我们将循环运行 N 次。对于每次循环,我们只需要常量级的时间。因此,该算法的时间复杂度总共为 O(N)

自己分析其他问题以提高分析能力。别忘了考虑不同的条件。如果很难对所有情况进行分析,请考虑最糟糕的情况。


经典问题

反转链表

让我们从一个经典问题开始:

反转一个单链表。

一种解决方案是按原始顺序迭代结点,并将它们逐个移动到列表的头部。似乎很难理解。我们先用一个例子来说明我们的算法。

算法概述

让我们看一个例子:

img

请记住,黑色结点 23 是原始的头结点。

  1. 首先,我们将黑色结点的下一个结点(即结点 6)移动到列表的头部:

img

  1. 然后,我们将黑色结点的下一个结点(即结点 15)移动到列表的头部:

img

  1. 黑色结点的下一个结点现在是空。因此,我们停止这一过程并返回新的头结点 15。

更多

在该算法中,每个结点只移动一次

因此,时间复杂度为 O(N),其中 N 是链表的长度。我们只使用常量级的额外空间,所以空间复杂度为 O(1)。

这个问题是你在面试中可能遇到的许多链表问题的基础。如果你仍然遇到困难,我们的下一篇文章将更多地讨论实现细节。

还有许多其他的解决方案。您应该熟悉至少一个解决方案并能够实现它。

反转链表

反转一个单链表。

示例:

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

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

#include <iostream>

using namespace std;

struct ListNode
{
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

class SolutionA
{
public:
    ListNode *reverseList(ListNode *head)
    {
        ListNode *pre = NULL;
        ListNode *cur = head;
        while (cur != NULL)
        {
            ListNode *next = cur->next;
            cur->next = pre;
            pre = cur;
            cur = next;
        }
        return pre;
    }
};

// Recursive
// Time Complexity: O(n)
// Space Complexity: O(n)
class SolutionB
{
public:
    ListNode *reverseList(ListNode *head)
    {
        if (head == NULL || head->next == NULL)
            return head;
        ListNode *rhead = reverseList(head->next);

        head->next->next = head;
        head->next = NULL;

        return rhead;
    }
};

移除链表元素

删除链表中等于给定值 val 的所有节点。

示例:

输入: 1->2->6->3->4->5->6, val = 6
输出: 1->2->3->4->5
#include <iostream>

using namespace std;

struct ListNode
{
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

ListNode *createLinkedList(int arr[], int n)
{
    if (n == 0)
        return NULL;

    ListNode *head = new ListNode(arr[0]);
    ListNode *curNode = head;
    for (int i = 1; i < n; i++)
    {
        curNode->next = new ListNode(arr[i]);
        curNode = curNode->next;
    }
    return head;
};

void printLinkedList(ListNode *head)
{
    if (head == NULL)
    {
        cout << "NULL" << endl;
        return;
    }

    ListNode *curNode = head;
    while (curNode != NULL)
    {
        cout << curNode->val;
        if (curNode->next != NULL)
            cout << "->";
        curNode = curNode->next;
    }
    cout << endl;
    return;
}

void deleteLinkedList(ListNode *head)
{
    ListNode *curNode = head;
    while (curNode != NULL)
    {
        ListNode *delNode = curNode;
        curNode = curNode->next;
        delete delNode;
    }
    return;
}

/// Linear Scan with dummy head
/// Time Complexity: O(n)
/// Space Complexity: O(1)
class SolutionA
{
public:
    ListNode *removeElements(ListNode *head, int val)
    {
        ListNode *dummyhead = new ListNode(0);
        dummyhead->next = head;

        ListNode *cur = dummyhead;
        while (cur->next != NULL)
        {
            if (cur->next->val == val)
            {
                ListNode *delNode = cur->next;
                cur->next = delNode->next;
                delete delNode;
            }
            else
                cur = cur->next;
        }
        ListNode *retNode = dummyhead->next;
        delete dummyhead;

        return retNode;
    }
};

/// Recursive
/// Time Complexity: O(n)
/// Space Complexity: O(n)
class SolutionB
{
public:
    ListNode* removeElements(ListNode* head, int val){
        if(!head)
            return head;
        if(head->val == val){
            ListNode* node = head->next;
            delete head;
            return removeElements(node, val);
        }
        head->next = removeElements(head->next, val);
        return head;
    }
};

奇偶链表

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

请尝试使用原地算法完成。你的算法的空间复杂度应为 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

说明:

  • 应当保持奇数节点和偶数节点的相对顺序。
  • 链表的第一个节点视为奇数节点,第二个节点视为偶数节点,以此类推。
#include <iostream>
using namespace std;

struct ListNode
{
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

/// Split the Linked List into two and then merge
/// Time Complexity: O(n)
/// Space Complexity: O(1)
class SolutionA
{
public:
    ListNode *oddEvenList(ListNode *head)
    {
        if (head == NULL || head->next == NULL || head->next->next == NULL)
            return head;

        ListNode *dummyHead1 = new ListNode(-1);
        ListNode *dummyHead2 = new ListNode(-1);
        ListNode *p1 = dummyHead1;
        ListNode *p2 = dummyHead2;
        ListNode *p = head;
        for (int i = 0; p; i++)
            if (i % 2 == 0)
            {
                p1->next = p;
                p = p->next;
                p1 = p1->next;
                p1->next = NULL;
            }
            else
            {
                p2->next = p;
                p = p->next;
                p2 = p2->next;
                p2->next = NULL;
            }
        p1->next = dummyHead2->next;
        ListNode *ret = dummyHead1->next;

        delete dummyHead1;
        delete dummyHead2;
        return ret;
    }
};

/// Split the Linked List into two and then merge
/// Keep one in original Linked List
///
/// Time Complexity: O(n)
/// Space Complexity: O(1)
class SolutionB
{
public:
    ListNode *oddEvenList(ListNode *head)
    {
        if (head == NULL || head->next == NULL || head->next->next == NULL)
            return head;
        ListNode *dummyHead2 = new ListNode(-1);
        ListNode *p2 = dummyHead2;
        ListNode *p = head;
        while (p->next) //p2存放偶数位置并让p只存放奇数位置。
        {
            p2->next = p->next;
            if(p->next->next == NULL){
                p->next = NULL;
                break;
            }
            // p->next = p->next->next;
            // p = p->next;
            p = p->next->next;
            p2 = p2->next;
            p2->next = NULL;
        }
        p->next = dummyHead2->next;
        delete dummyHead2;
        return head;
    }
};

回文链表

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

示例 1:

输入: 1->2
输出: false

示例 2:

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

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

#include <iostream>
using namespace std;

struct ListNode
{
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

/// Two Pointers to Reverse and Traverse
/// Time Complexity: O(n)
/// Space Complexity: O(1)
class Solution
{
public:
    bool isPalindrome(ListNode *head)
    {
        if (head == NULL || head->next == NULL)
            return true;
        ListNode *slow = head;
        ListNode *fast = head;
        while (fast->next != NULL && fast->next->next != NULL)
        {
            slow = slow->next;
            fast = fast->next->next;
        }
        slow->next = reverse(slow->next);

        slow = slow->next;
        ListNode *cur = head;
        while (slow != NULL)
        {
            if (cur->val != slow->val)
                return false;
            else
            {
                slow = slow->next;
                cur = cur->next;
            }
        }
        return true;
    }
//反转链表
private:
    ListNode *reverse(ListNode *head)
    {
        if (head == NULL || head->next == NULL)
            return head;
        ListNode *pre = head;
        ListNode *cur = head->next;
        ListNode *next = cur->next;
        head->next = NULL;

        while (true)
        {
            cur->next = pre;
            pre = cur;
            cur = next;
            if (cur == NULL)
                break;
            next = cur->next;
        }
        return pre;
    }
};

将链表后半段反转,然后从链表头部和中间开始逐个判断。

小结

我们为你提供了几个练习。你可能已经注意到它们之间的相似之处了。这里我们提供一些提示:

1. 通过一些测试用例可以节省您的时间。

使用链表时不易调试。因此,在编写代码之前,自己尝试几个不同的示例来验证您的算法总是很有用的。

2. 你可以同时使用多个指针。

有时,当你为链表问题设计算法时,可能需要同时跟踪多个结点。您应该记住需要跟踪哪些结点,并且可以自由地使用几个不同的结点指针来同时跟踪这些结点。

如果你使用多个指针,最好为它们指定适当的名称,以防将来必须调试或检查代码。

3. 在许多情况下,你需要跟踪当前结点的前一个结点。

你无法追溯单链表中的前一个结点。因此,您不仅要存储当前结点,还要存储前一个结点。这在双链表中是不同的,我们将在后面的章节中介绍。


双链表

简介

我们在前面的章节中介绍了单链表。

单链接列表中的结点具有 Value 字段,以及用于顺序链接结点的“Next”引用字段。

在本文中,我们将介绍另一种类型的链表:双链表

定义

双链表以类似的方式工作,但还有一个引用字段,称为“prev”字段。有了这个额外的字段,您就能够知道当前结点的前一个结点。

让我们看一个例子:

img

绿色箭头表示我们的“prev”字段是如何工作的。

结点结构

// Definition for doubly-linked list.
struct DoublyListNode {
    int val;
    DoublyListNode *next, *prev;
    DoublyListNode(int x) : val(x), next(NULL), prev(NULL) {}
};

与单链接列表类似,我们将使用头结点来表示整个列表。

操作

与单链表类似,我们将介绍在双链表中如何访问数据、插入新结点或删除现有结点。

我们可以与单链表相同的方式访问数据:

  1. 我们不能在常量级的时间内访问随机位置
  2. 我们必须从头部遍历才能得到我们想要的第一个结点。
  3. 在最坏的情况下,时间复杂度将是 O(N),其中 N 是链表的长度。

对于添加和删除,会稍微复杂一些,因为我们还需要处理“prev”字段。在接下来的两篇文章中,我们将介绍这两个操作

之后,我们提供练习,让你使用双链表重新设计链表。

添加操作

如果我们想在现有的结点 prev 之后插入一个新的结点 cur,我们可以将此过程分为两个步骤:

  1. 链接 curprevnext,其中 nextprev 原始的下一个节点;img
  2. cur 重新链接 prevnextimg

与单链表类似,添加操作的时间和空间复杂度都是 O(1)

示例

img

让我们在现有结点 6 之后添加一个新结点 9:

  1. 链接 cur(结点 9)与 prev(结点 6)和 next(结点 15)img
  2. cur(结点 9)重新链接 prev(结点 6)和 next(结点 15)img

如果我们想在开头结尾插入一个新结点怎么办?

删除操作

如果我们想从双链表中删除一个现有的结点 cur,我们可以简单地将它的前一个结点 prev 与下一个结点 next 链接起来。

与单链表不同,使用“prev”字段可以很容易地在常量时间内获得前一个结点。

因为我们不再需要遍历链表来获取前一个结点,所以时间和空间复杂度都是O(1)

示例

img

我们的目标是从双链表中删除结点 6。

因此,我们将它的前一个结点 23 和下一个结点 15 链接起来:

img

结点 6 现在不在我们的双链表中。

如果我们要删除第一个结点最后一个结点怎么办?

设计链表

设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:valnextval 是当前节点的值,next 是指向下一个节点的指针/引用。如果要使用双向链表,则还需要一个属性 prev 以指示链表中的上一个节点。假设链表中的所有节点都是 0-index 的。

在链表类中实现这些功能:

  • get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1
  • addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
  • addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
  • addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。
  • deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。

示例:

MyLinkedList linkedList = new MyLinkedList();
linkedList.addAtHead(1);
linkedList.addAtTail(3);
linkedList.addAtIndex(1,2);   //链表变为1-> 2-> 3
linkedList.get(1);            //返回2
linkedList.deleteAtIndex(1);  //现在链表是1-> 3
linkedList.get(1);            //返回3

提示:

  • 所有值都在 [1, 1000] 之内。
  • 操作次数将在 [1, 1000] 之内。
  • 请不要使用内置的 LinkedList 库。
#include <iostream>
using namespace std;

/// Doubly Linked List
/// Using dummyhead;
/// Time Complexity: init: O(1)
///                  get: O(n)
///                  addAtHead: O(1)
///                  addAtTail: O(n)
///                  addAtIndex: O(n)
///                  deleteAtIndex: O(n)
/// Space Complexity: O(n)
class MyLinkedList
{
private:
    class Node
    {
    public:
        int val;
        Node *next;
        Node *prev;

        Node(int val, Node *prev, Node *next) : val(val), prev(prev), next(next) {}
        Node(int val) : Node(val, NULL, NULL) {}
    };
    Node *dummyHead;

public:
    MyLinkedList()
    {
        dummyHead = new Node(-1);
    }

    int get(int index)
    {
        if (index < 0)
            return -1;
        Node *cur = dummyHead->next;
        for (int i = 0; i < index && cur; i++)
            cur = cur->next;
        if (!cur)
            return -1;
        return cur->val;
    }

    void addAtHead(int val)
    {
        dummyHead->next = new Node(val, dummyHead, dummyHead->next);
    }

    void addAtTail(int val)
    {
        Node *pre = dummyHead;
        while (pre->next)
            pre = pre->next;
        pre->next = new Node(val, pre, NULL);
    }

    void addAtIndex(int index, int val)
    {
        if (index < 0)  //LeetCode会存在index<0的情况,此时需要在表头插入。
            addAtHead(val);
        Node *pre = dummyHead;
        for (int i = 0; i < index && pre; i++)
            pre = pre->next;
        if (pre)
            pre->next = new Node(val, pre, pre->next);
    }

    void deleteAtIndex(int index)
    {
        if (index < 0)
            return;

        Node *pre = dummyHead;
        for (int i = 0; i < index && pre; i++)
            pre = pre->next;
        if (pre && pre->next)
        {
            Node *delNode = pre->next;
            pre->next = delNode->next;
            if (pre->next)
                pre->next->prev = pre;
            delete delNode;
        }
    }
};

int main()
{
    MyLinkedList List = MyLinkedList();
    List.addAtHead(1);
    List.addAtTail(3);
    List.addAtIndex(1, 2);
    List.get(-1);
    List.deleteAtIndex(1);
    List.get(-3);
}

小结

小结

复习

让我们简要回顾一下单链表和双链表的表现。

它们在许多操作中是相似的。

  1. 它们都无法在常量时间内随机访问数据
  2. 它们都能够在 O(1) 时间内在给定结点之后或列表开头添加一个新结点
  3. 它们都能够在 O(1) 时间内删除第一个结点

但是删除给定结点(包括最后一个结点)时略有不同。

  • 在单链表中,它无法获取给定结点的前一个结点,因此在删除给定结点之前我们必须花费 O(N) 时间来找出前一结点。
  • 在双链表中,这会更容易,因为我们可以使用“prev”引用字段获取前一个结点。因此我们可以在 O(1) 时间内删除给定结点。

对照

这里我们提供链表和其他数据结构(包括数组,队列和栈)之间时间复杂度的比较:

img

经过这次比较,我们不难得出结论:

如果你需要经常添加或删除结点,链表可能是一个不错的选择。

如果你需要经常按索引访问元素,数组可能是比链表更好的选择。

合并两个有序链表

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

示例:

输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
/// Iterative
/// Time Complexity: O(len(l1) + len(l2))
/// Space Complexity: O(1)
class SolutionA
{
public:
    ListNode *mergeTwoLists(ListNode *l1, ListNode *l2)
    {
        ListNode *dummyHead = new ListNode(-1);
        ListNode *p = dummyHead;
        ListNode *l1p = l1;
        ListNode *l2p = l2;
        while (l1p != NULL & l2p != NULL)
        {
            if (l1p->val < l2p->val)
            {
                p->next = l1p;
                l1p = l1p->next;
            }
            else
            {
                p->next = l2p;
                l2p = l2p->next;
            }
            p = p->next;
        }
        if (l1p != NULL) //将多的半段接上
            p->next = l1p;
        else
            p->next = l2p;

        ListNode *ret = dummyHead->next;
        dummyHead->next = NULL;
        delete dummyHead;

        return ret;
    }
};

/// Recursive
/// Time Complexity: O(len(l1) + len(l2))
/// Space Complexity: O(len(l1) + len(l2))
class SolutionB
{
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);
            // cout << "l1:";
            // printLinkedList(l1);
            return l1;
        }
        l2->next = mergeTwoLists(l1, l2->next);
        // cout << "l2:";
        // printLinkedList(l2);
        return l2;
    }
};

两数相加

给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。

如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。

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

示例:

输入:(2 -> 4 -> 3) + (5 -> 6 -> 4)
输出:7 -> 0 -> 8
原因:342 + 465 = 807
#include <iostream>
using namespace std;

struct ListNode
{
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL){};
};

/// Create new LinkedList for result
/// Time Complexity: O(n)
/// Space Complexity: O(n)
class SolutionA
{
public:
    ListNode *addTwoNumbers(ListNode *l1, ListNode *l2)
    {
        ListNode *p1 = l1, *p2 = l2;
        ListNode *dummyHead = new ListNode(-1);
        ListNode *cur = dummyHead;

        int carried = 0;
        while (p1 || p2)
        {
            int a = p1 ? p1->val : 0;
            int b = p2 ? p2->val : 0;
            cur->next = new ListNode((a + b + carried) % 10);
            carried = (a + b + carried) / 10;

            cur = cur->next;
            p1 = p1 ? p1->next : NULL;
            p2 = p2 ? p2->next : NULL;
        }
        //若最高位存在进位,则需要新建一个结点。
        cur->next = carried ? new ListNode(1) : NULL;
        ListNode *ret = dummyHead->next;
        delete dummyHead;
        return ret;
    }
};

/// Using l1 as the result list
/// Time Complexity: O(n)
/// Space Complexity: O(n)
class SolutionB
{
public:
    ListNode *addTwoNumbers(ListNode *l1, ListNode *l2)
    {
        ListNode *p1 = l1, *p2 = l2;
        ListNode *pre = NULL;
        int carried = 0;

        while (p1 || p2)
        {
            int a = p1 ? p1->val : 0;
            int b = p2 ? p2->val : 0;
            if (p1)
                p1->val = (a + b + carried) % 10;
            else
            {
                pre->next = new ListNode((a + b + carried) % 10);
                p1 = pre->next;
            }
            carried = (a + b + carried) / 10; //用来记录进位。

            pre = p1;
            p1 = p1->next;
            if (p2)
                p2 = p2->next;
        }

        pre->next = carried ? new ListNode(1) : NULL;   //判断最高位是否需要进位。
        return l1;
    }
};

/// Using the longest list in l1 and l2 as the result list
/// Time Complexity: O(n)
/// Space Complexity: O(1)
class SolutionC
{
public:
    ListNode *addTwoNumbers(ListNode *l1, ListNode *l2)
    {
        int len1 = getLen(l1), len2 = getLen(l2);
        ListNode *p1 = len1 > len2 ? l1 : l2; //p1一定是长的
        ListNode *p2 = len1 > len2 ? l2 : l1; //p2一定是短的

        ListNode *pre = NULL;
        int carried = 0;
        while (p1)
        {
            //从两条链分别取下一个值
            int a = p1->val;
            int b = p2 ? p2->val : 0;
            //更新长链上的值
            p1->val = (a + b + carried) % 10;
            carried = (a + b + carried) / 10;

            pre = p1;   //pre每次都在更新,跳出while循环后,pre用来判断最高位是否需要进位。
            p1 = p1->next;
            p2 = p2 ? p2->next : NULL;
        }
        pre->next = carried ? new ListNode(1) : NULL;
        return len1 > len2 ? l1 : l2;   //返回长链
    }

private:
    int getLen(ListNode *l)
    {
        int res = 0;
        while (l)
            res++;
        l = l->next;
        return res;
    }
};

扁平化多级双向链表

您将获得一个双向链表,除了下一个和前一个指针之外,它还有一个子指针,可能指向单独的双向链表。这些子列表可能有一个或多个自己的子项,依此类推,生成多级数据结构,如下面的示例所示。

扁平化列表,使所有结点出现在单级双链表中。您将获得列表第一级的头部。

示例:

输入:
 1---2---3---4---5---6--NULL
         |
         7---8---9---10--NULL
             |
             11--12--NULL

输出:
1-2-3-7-8-11-12-9-10-4-5-6-NULL

以上示例的说明:

给出以下多级双向链表:

img

我们应该返回如下所示的扁平双向链表:

img

#include <iostream>
using namespace std;

/// DFS
/// Time Complexity: O(n)
/// Space Complexity: O(n)

/// Definition for a Node.
class Node
{
public:
    int val = NULL;
    Node *prev = NULL;
    Node *next = NULL;
    Node *child = NULL;

    Node(){};

    Node(int _val, Node *_prev, Node *_next, Node *_child)
    {
        val = _val;
        prev = _prev;
        next = _next;
        child = _child;
    }
};

class Solution
{
public:
    Node *flatten(Node *head)
    {
        if(!head)
            return NULL;
        Node* tail;
        return flatten(head, tail);
    }
private:
    Node* flatten(Node* head, Node*& tail){
        Node* cur = head;
        while(cur){
            if(cur->child){
                Node* subtail;
                Node* subhead = flatten(cur->child, subtail);
                Node* next = cur->next;

                cur->next = subhead;
                subhead->prev = cur;
                subtail->next = next;

                if(next)
                    next->prev = subtail;
                else
                    tail = subtail;

                cur->child = NULL;
                cur = next;
            }
            else
            {
                if(!cur->next)
                    tail = cur;
                cur = cur->next;
            }   
        }
        return head;
    }
};

复制带随机指针的链表

给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。

要求返回这个链表的深拷贝

示例:

img

输入:
{"$id":"1","next":{"$id":"2","next":null,"random":{"$ref":"2"},"val":2},"random":{"$ref":"2"},"val":1}

解释:
节点 1 的值是 1,它的下一个指针和随机指针都指向节点 2 。
节点 2 的值是 2,它的下一个指针指向 null,随机指针指向它自己。

提示:

  1. 你必须返回给定头的拷贝作为对克隆列表的引用。
#include <iostream>
#include <unordered_map>

using namespace std;

/// Using HashMap
/// Time Complexity: O(n)
/// Space Complexity: O(n)

/// Definition for singly-linked list with a random pointer.
struct RandomListNode
{
    int label;
    RandomListNode *next, *random;
    RandomListNode(int x) : label(x), next(NULL), random(NULL) {}
};
class SolutionA
{
public:
    RandomListNode *copyRandomList(RandomListNode *head)
    {
        // oldNode -> newNode
        unordered_map<RandomListNode *, RandomListNode *> nodeMap;

        RandomListNode *dummyHead = new RandomListNode(-1);
        RandomListNode *pre = dummyHead;
        RandomListNode *node = head;
        while (node)
        {
            if (!nodeMap.count(node))
                nodeMap[node] = new RandomListNode(node->label);
            pre->next = nodeMap[node];
            pre = pre->next;

            if (node->random)
            {
                if (!nodeMap.count(node->random))
                    nodeMap[node->random] = new RandomListNode(node->random->label);
                pre->random = nodeMap[node->random];
            }
            node = node->next;
        }
    }
};

/// Link all the new nodes after every old node
/// Very fancy solution:)
///
/// Time Complexity: O(n)
/// Space Complexity: O(1)

/// Definition for singly-linked list with a random pointer.
class SolutionB
{
public:
    RandomListNode *copyRandomList(RandomListNode *head)
    {
        if(!head)
            return NULL;
        RandomListNode* cur = head;
        while(cur){
            RandomListNode* next = cur->next;   //复制完成后为了找回
            cur->next = new RandomListNode(cur->label); //节点后紧接着复制一个
            cur->next->next = next;
            cur = next;
        }

        cur = head;
        while(cur){
            if(cur->random)
                cur->next->random = cur->random->next;  //random->next才是我们复制后的random
            cur = cur->next->next;
        }

        cur = head; //旧链头结点
        RandomListNode* ret = cur->next;    //新链头结点
        while(cur->next->next){
            RandomListNode* next = cur->next->next; //旧链的下一个结点
            cur->next->next = next->next;   //连接新链
            cur->next = next;   //恢复旧链
        }
        cur->next = NULL;

        return ret;
    }
};
*解法B参考*:<https://www.cnblogs.com/grandyang/p/4261431.html>

当然,如果使用 HashMap 占用额外的空间,如果这道题限制了空间的话,就要考虑别的方法。下面这个方法很巧妙,可以分为以下三个步骤:

1. 在原链表的每个节点后面拷贝出一个新的节点。

2. 依次给新的节点的随机指针赋值,而且这个赋值非常容易 cur->next->random = cur->random->next。

3. 断开链表可得到深度拷贝后的新链表。

举个例子来说吧,比如原链表是 1(2) -> 2(3) -> 3(1),括号中是其 random 指针指向的结点,那么这个解法是首先比遍历一遍原链表,在每个结点后拷贝一个同样的结点,但是拷贝结点的 random 指针仍为空,则原链表变为 1(2) -> 1(null) -> 2(3) -> 2(null) -> 3(1) -> 3(null)。然后第二次遍历,是将拷贝结点的 random 指针赋上正确的值,则原链表变为 1(2) -> 1(2) -> 2(3) -> 2(3) -> 3(1) -> 3(1),注意赋值语句为:

cur->next->random = cur->random->next;

这里的 cur 是原链表中结点,cur->next 则为拷贝链表的结点,cur->next->random 则为拷贝链表的 random 指针。cur->random 为原链表结点的 random 指针指向的结点,因为其指向的还是原链表的结点,所以我们要再加个 next,才能指向拷贝链表的结点。最后再遍历一次,就是要把原链表和拷贝链表断开即可。

旋转链表

给定一个链表,旋转链表,将链表每个节点向右移动 k 个位置,其中 k 是非负数。

示例 1:

输入: 1->2->3->4->5->NULL, k = 2
输出: 4->5->1->2->3->NULL
解释:
向右旋转 1 步: 5->1->2->3->4->NULL
向右旋转 2 步: 4->5->1->2->3->NULL

示例 2:

输入: 0->1->2->NULL, k = 4
输出: 2->0->1->NULL
解释:
向右旋转 1 步: 2->0->1->NULL
向右旋转 2 步: 1->2->0->NULL
向右旋转 3 步: 0->1->2->NULL
向右旋转 4 步: 2->0->1->NULL
#include <iostream>
using namespace std;

// Definition for singly-linked list.
struct ListNode {
    int val;
    ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};

ListNode *createLinkedList(int arr[], int n)
{
    if (n == 0)
        return NULL;
    ListNode *head = new ListNode(arr[0]);
    ListNode *curNode = head;
    for (int i = 1; i < n; i++)
    {
        curNode->next = new ListNode(arr[i]);
        curNode = curNode->next;
    }
    return head;
}
void printLinkedList(ListNode *head)
{
    if (head == NULL)
    {
        cout << "NULL" << endl;
        return;
    }
    ListNode *curNode = head;
    while (curNode != NULL)
    {
        cout << curNode->val;
        if (curNode->next != NULL)
            cout << "->";
        curNode = curNode->next;
    }
    cout << endl;
    return;
}

/// Brute Force
/// Two Pointers
///
/// Time Complexity: O(N)
/// Space Complexity: O(1)
class Solution
{
public:
    ListNode* rotateRight(ListNode* head, int k){
        if(head == NULL)
        return NULL;

        int len = get_len(head);
        k = k % len;

        ListNode* end = head;
        for(int i=0; i<k; i++)
            end = end->next;
        
        //循环右移x次,相当于head从原链的第(len-x)个结点开始遍历。
        ListNode* start = head;
        while(end->next != NULL){
            start = start->next;
            end = end->next;
        }

        end->next = head;
        head = start->next;
        start->next = NULL;

        return head;   
    }
private:
    int get_len(ListNode* head){
        int res = 0;
        while(head){
            res ++;
            head = head->next;
        }
        return res;
    }
};

int main() {
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
    ListNode* l1 = createLinkedList(arr, 9);
    printLinkedList(l1);
    Solution a = Solution();
    ListNode* l2 = a.rotateRight(l1, 3);
    printLinkedList(l2);

    return 0;
}

转载于:https://www.cnblogs.com/vincent1997/p/10897325.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值