作为半路出家的程序员,只有通过不断地刻意训练,才能提升自己的编程水平。
目前的计划是完成LeetCode explore上的数据结构部分https://leetcode-cn.com/explore/>,持续更新。
本文中的示例代码主要参考自https://github.com/liuyubobobo/Play-Leetcode-Explore,作者为每个问题提供多种解决方法,非常赞?,请为作者点个Star。
单链表
简介
单链表中的每个结点不仅包含值,还包含链接到下一个结点的引用字段
。通过这种方式,单链表将所有结点按顺序组织起来。
下面是一个单链表的例子:
蓝色箭头显示单个链接列表中的结点是如何组合在一起的。
结点结构
以下是单链表中结点的典型定义:
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
之后添加新值,我们应该:
- 使用给定值初始化新结点
cur;
- 将
cur
的“next”字段链接到 prev 的下一个结点next
; - 将
prev
中的“next”字段链接到cur
。
与数组不同,我们不需要将所有元素移动到插入元素之后。因此,您可以在 O(1)
时间复杂度中将新结点插入到链表中,这非常高效。
示例
让我们在第二个结点 6 之后插入一个新的值 9。
我们将首先初始化一个值为 9 的新结点。然后将结点 9 链接到结点 15。最后,将结点 6 链接到结点 9。
插入之后,我们的链表将如下所示:
在开头添加结点
众所周知,我们使用头结点来代表整个列表。
因此,在列表开头添加新节点时更新头结点 head
至关重要。
- 初始化一个新结点
cur;
- 将新结点链接到我们的原始头结点
head
。 - 将
cur
指定为head
。
例如,让我们在列表的开头添加一个新结点 9。
- 我们初始化一个新结点 9 并将其链接到当前头结点 23。
- 指定结点 9 为新的头结点。
如何在列表的末尾添加新的结点?我们还能使用类似的策略吗?
删除操作
如果我们想从单链表中删除现有结点 cur
,可以分两步完成:
- 找到 cur 的上一个结点
prev
及其下一个结点next;
- 接下来链接
prev
到 cur 的下一个节点next。
在我们的第一步中,我们需要找出 prev
和 next
。使用 cur
的参考字段很容易找出 next
,但是,我们必须从头结点遍历链表,以找出 prev
,它的平均时间是 O(N)
,其中 N 是链表的长度。因此,删除结点的时间复杂度将是 O(N)
。
空间复杂度为 O(1)
,因为我们只需要常量空间来存储指针。
示例
让我们尝试把结点 6从上面的单链表中删除。
从头遍历链表,直到我们找到前一个结点
prev
,即结点 23将
prev
(结点 23)与next
(结点 15)链接
结点 6 现在不在我们的单链表中。
删除第一个结点
如果我们想删除第一个结点,策略会有所不同。
正如之前所提到的,我们使用头结点 head
来表示链表。我们的头是下面示例中的黑色结点 23。
如果想要删除第一个结点,我们可以简单地将下一个结点分配给 head
。也就是说,删除之后我们的头将会是结点 6。
链表从头结点开始,因此结点 23 不再在我们的链表中。
删除最后一个结点呢?我们还能使用类似的策略吗?
设计链表
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val
和 next
。val
是当前节点的值,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);
// }
双指针技巧
链表中的双指针
让我们从一个经典问题开始:
给定一个链表,判断链表中是否有环。
你可能已经使用哈希表
提出了解决方案。但是,使用双指针技巧
有一个更有效的解决方案。在阅读接下来的内容之前,试着自己仔细考虑一下。
想象一下,有两个速度不同的跑步者。如果他们在直路上行驶,快跑者将首先到达目的地。但是,如果它们在圆形跑道上跑步,那么快跑者如果继续跑步就会追上慢跑者。
这正是我们在链表中使用两个速度不同的指针时会遇到的情况:
如果没有环,快指针将停在链表的末尾。
如果有环,快指针最终将与慢指针相遇。
所以剩下的问题是:
这两个指针的适当速度应该是多少?
一个安全的选择是每次移动慢指针一步
,而移动快指针两步
。每一次迭代,快速指针将额外移动一步。如果环的长度为 M,经过 M 次迭代后,快指针肯定会多绕环一周,并赶上慢指针。
那其他选择呢?它们有用吗?它们会更高效吗?
环形链表
给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos
来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos
是 -1
,则在该链表中没有环。
示例 1:
输入:head = [3,2,0,-4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
进阶:
你能用 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
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1,2], pos = 0
输出:tail connects to node index 0
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:no cycle
解释:链表中没有环。
进阶:
你是否可以不用额外空间解决此题?
#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就是环的入口,即第一个节点。
相交链表
编写一个程序,找到两个单链表相交的起始节点。
如下面的两个链表:
在节点 c1 开始相交。
示例 1:
输入: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:
输入: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:
输入: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
之前,需要检查 fast
和 fast.next
不为空。
2. 仔细定义循环的结束条件。
运行几个示例,以确保你的结束条件不会导致无限循环。在定义结束条件时,你必须考虑我们的第一点提示。
复杂度分析
空间复杂度分析容易。如果只使用指针,而不使用任何其他额外的空间,那么空间复杂度将是 O(1)
。但是,时间复杂度的分析比较困难。为了得到答案,我们需要分析运行循环的次数
。
在前面的查找循环示例中,假设我们每次移动较快的指针 2 步,每次移动较慢的指针 1 步。
- 如果没有循环,快指针需要
N/2 次
才能到达链表的末尾,其中 N 是链表的长度。 - 如果存在循环,则快指针需要
M 次
才能赶上慢指针,其中 M 是列表中循环的长度。
显然,M <= N 。所以我们将循环运行 N
次。对于每次循环,我们只需要常量级的时间。因此,该算法的时间复杂度总共为 O(N)
。
自己分析其他问题以提高分析能力。别忘了考虑不同的条件。如果很难对所有情况进行分析,请考虑最糟糕的情况。
经典问题
反转链表
让我们从一个经典问题开始:
反转一个单链表。
一种解决方案是按原始顺序迭代结点
,并将它们逐个移动到列表的头部
。似乎很难理解。我们先用一个例子来说明我们的算法。
算法概述
让我们看一个例子:
请记住,黑色结点 23 是原始的头结点。
- 首先,我们将黑色结点的下一个结点(即结点 6)移动到列表的头部:
- 然后,我们将黑色结点的下一个结点(即结点 15)移动到列表的头部:
- 黑色结点的下一个结点现在是空。因此,我们停止这一过程并返回新的头结点 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”
字段。有了这个额外的字段,您就能够知道当前结点的前一个结点。
让我们看一个例子:
绿色箭头表示我们的“prev”字段是如何工作的。
结点结构
// Definition for doubly-linked list.
struct DoublyListNode {
int val;
DoublyListNode *next, *prev;
DoublyListNode(int x) : val(x), next(NULL), prev(NULL) {}
};
与单链接列表类似,我们将使用头结点
来表示整个列表。
操作
与单链表类似,我们将介绍在双链表中如何访问数据、插入新结点或删除现有结点。
我们可以与单链表相同的方式访问数据:
- 我们不能在常量级的时间内
访问随机位置
。 - 我们必须从头部遍历才能得到我们想要的第一个结点。
- 在最坏的情况下,时间复杂度将是
O(N)
,其中N
是链表的长度。
对于添加和删除,会稍微复杂一些,因为我们还需要处理“prev”字段。在接下来的两篇文章中,我们将介绍这两个操作
之后,我们提供练习,让你使用双链表重新设计链表。
添加操作
如果我们想在现有的结点 prev
之后插入一个新的结点 cur
,我们可以将此过程分为两个步骤:
- 链接
cur
与prev
和next
,其中next
是prev
原始的下一个节点; - 用
cur
重新链接prev
和next
。
与单链表类似,添加操作的时间和空间复杂度都是 O(1)
。
示例
让我们在现有结点 6 之后添加一个新结点 9:
- 链接
cur
(结点 9)与prev
(结点 6)和next
(结点 15) - 用
cur
(结点 9)重新链接prev
(结点 6)和next
(结点 15)
如果我们想在
开头
或结尾
插入一个新结点怎么办?
删除操作
如果我们想从双链表中删除一个现有的结点 cur
,我们可以简单地将它的前一个结点 prev
与下一个结点 next
链接起来。
与单链表不同,使用“prev”字段可以很容易地在常量时间内获得前一个结点。
因为我们不再需要遍历链表来获取前一个结点,所以时间和空间复杂度都是O(1)
。
示例
我们的目标是从双链表中删除结点 6。
因此,我们将它的前一个结点 23 和下一个结点 15 链接起来:
结点 6 现在不在我们的双链表中。
如果我们要删除
第一个结点
或最后一个结点
怎么办?
设计链表
设计链表的实现。您可以选择使用单链表或双链表。单链表中的节点应该具有两个属性:val
和 next
。val
是当前节点的值,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);
}
小结
小结
复习
让我们简要回顾一下单链表和双链表的表现。
它们在许多操作中是相似的。
- 它们都无法在常量时间内
随机访问数据
。 - 它们都能够
在 O(1) 时间内在给定结点之后或列表开头添加一个新结点
。 - 它们都能够
在 O(1) 时间内删除第一个结点
。
但是删除给定结点(包括最后一个结点)时略有不同。
- 在单链表中,它无法获取给定结点的前一个结点,因此在删除给定结点之前我们必须花费
O(N)
时间来找出前一结点。 - 在双链表中,这会更容易,因为我们可以使用“prev”引用字段获取前一个结点。因此我们可以在
O(1)
时间内删除给定结点。
对照
这里我们提供链表和其他数据结构(包括数组,队列和栈)之间时间复杂度
的比较:
经过这次比较,我们不难得出结论:
如果你需要经常添加或删除结点,链表可能是一个不错的选择。
如果你需要经常按索引访问元素,数组可能是比链表更好的选择。
合并两个有序链表
将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入: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
以上示例的说明:
给出以下多级双向链表:
我们应该返回如下所示的扁平双向链表:
#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;
}
};
复制带随机指针的链表
给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。
要求返回这个链表的深拷贝。
示例:
输入:
{"$id":"1","next":{"$id":"2","next":null,"random":{"$ref":"2"},"val":2},"random":{"$ref":"2"},"val":1}
解释:
节点 1 的值是 1,它的下一个指针和随机指针都指向节点 2 。
节点 2 的值是 2,它的下一个指针指向 null,随机指针指向它自己。
提示:
- 你必须返回给定头的拷贝作为对克隆列表的引用。
#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;
}