赞
踩
前言:刷「链表」高频面试题。
详细介绍参考 04 讲: 链表简析(点击链接直达)
结构:
1
1
1->
2
2
2->
3
3
3->NULL,链表是 指向型
结构 。
查找:随机访问的时间复杂度是
O
(
n
)
O(n)
O(n)。
增删:删除和插入元素的时间复杂度都是
O
(
1
)
O(1)
O(1) 。
对于链表,给我们一个链表时,我们拿到的是头节点(head
) 。如果没有头结点证明整个链表为空 NULL
,如果已有头结点则证明链表不为空。
针对链表头结点 (head
)为空和不为空需要执行不同的操作。每次对应头结点都需要单独处理,所以使用头结点的技巧,可以解决这个问题。
例如,删除链表中的某个节点必须要找到前一个节点才能操作。这就造成头结点的尴尬,对于头结点来说没有前一个节点。
如果链表为空(head = null
),那么 访问 null.val 与 null.next 会出错。为了避免这种情况,增加一个虚拟头结点(dummy
)可以统一操作,不用关心头结点是否为空。这样 dummy.next = null,避免直接访问空指针。
其中 dummy
的值 (val
)常用 -1
表示,next
指向 头结点(head
)。
// 增加虚拟头节点的链表遍历
dummy; dumm->next = head; p = dummy;
while (p)
{
}
// 没有虚拟头结点的链表遍历
head;
while (head)
{
head = head->next;
}
原题链接:反转链表(点击链接直达)
n
条边;next
指针改为指向前一个节点(last
);pre
指针访问前一个节点。针对单链表,没有 pre
指针无法访问前一个节点(last
),需要新开一个变量维护前一个节点(last
);head
)没有前一个节点,创建 last
并赋为 NULL
;class Solution {
public:
ListNode* reverseList(ListNode* head) {
if (!head || !head->next) return head;
ListNode* last = nullptr; //(1)
ListNode* cur = head; //(2)
while (cur) //(3)
{
ListNode* next = cur->next; //(4)
cur->next = last; //(5)
last = cur; //(6)
cur = next; //(7)
}
return last; //(8)
}
};
last
和 cur
, last
指向上一个节点,cur
指向当前节点;cur
)的下一个节点(next
),防止丢失;last
和 cur
分别向后移动一位;cur
停下时指向原链表的 NULL
,此时 last
指向反转后链表的头结点;O ( n ) O(n) O(n)
原题链接:反转链表(点击链接直达)
tmp
节点移动到 left-1
的位置处;[left, right]
部分的节点。从 left
位置开始反转,反转 right-left
次;class Solution {
public:
ListNode* reverseBetween(ListNode* head, int left, int right) {
if (left == right) return head; //(1)
ListNode* dummy = new ListNode(-1); //(2)
dummy->next = head;
ListNode* tmp = dummy;
for (int i = 0; i < left - 1; i ++) tmp = tmp->next; //(3)
//(4)
ListNode* pre = tmp->next;
ListNode* cur = pre->next;
for (int i = 0; i < right - left; i ++)
{
ListNode* next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
//(5)
tmp->next->next = cur;
tmp->next = pre;
return dummy->next; //(6)
}
};
left=right
证明只有一个头结点;dummy
为哨兵节点。因为 left
可能在 head
位置,故添加哨兵节点;tmp
节点移动到 left-1
的位置;(4)- (5)
之间的代码为反转 [left, right]
部分的节点,逻辑同上题;(5)-(6)
之间的代码为调整其它节点的指向。如示例1,2
的 next
指向 5
,1
的 next
指向 4
;O ( n ) O(n) O(n)
原题链接:移除链表元素(点击链接直达)
dummy
哨兵节点的目的是统一操作,少写特判断头结点(head
)是否为空。// 不增加哨兵节点dummy
if (!head) {
return head;
} else {
}
// 增加哨兵节点dummy
class Solution {
public:
ListNode* removeElements(ListNode* head, int val) {
ListNode* dummy = new ListNode(-1);
dummy->next = head;
ListNode* p = dummy;
while (p->next)
{
if (p->next->val == val) p->next = p->next->next;
else p = p->next;
}
return dummy->next;
}
};
if (!head) return head;
head->next = removeElements(head->next, val);
return head->val == val? head->next : head;
O ( n ) O(n) O(n)
原题链接: 删除链表的第N个节点(点击链接直达)
纸上画图实际模拟一遍即可。
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* dummy = new ListNode(-1); //(1)
dummy->next = head;
ListNode* p = dummy, *q = dummy;
for (int i = 0; i < n; i ++) p = p->next; //(2)
while (p->next != nullptr) //(3)
{
p = p->next;
q = q->next;
}
q->next = q->next->next; //(4)
return dummy->next;
}
};
dummy
,不用考虑头结点的特殊情况;p
指针先走 n 步;p
指针和 q
指针同时走,直到 p
指针走到最后一个节点,两指针都停下;q
指向的就是要删除节点的前一个节点(n-1
处),删除第 n
个节点;双指针遍历时间复杂度为 O ( n ) O(n) O(n)
原题链接: 链表的中间节点(点击链接直达)
q
走到中点时,p->next
为NULL
。偶数个节点,q
走到中点时,fast
为空 NULL
。class Solution {
public:
ListNode* middleNode(ListNode* head) {
auto p = head, q = head;
while (p && p->next) { // 只要p和p->next都不为空时,两指针就一种往后走
p = p->next->next;
q = q->next;
}
return q;
}
};
双指针遍历时间复杂度为 O ( n ) O(n) O(n)
原题链接: 相交链表(点击链接直达)
a
: 1=>2=>3=>4,链表 b
:5=>3=>4,相同节点为 3
。对于3
前面的链表部分,两个链表长度不同;a
,遍历完后再遍历链表 b
。同理,先先遍历链表 b
,遍历完后再遍历链表 a
。这样,相同节点前面的长度就保持一致了,可以通过遍历相同的次数走到相同的节点;a
逻辑上变为:1=>2=>3=>4=>5=>3=>4,链表 b
逻辑上变为:5=>3=>4=>1=>2=>3=>4;class Solution {
public:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
auto p = headA, q = headB;
while (p != q) {
// p没走到A链表终点就一直往后走,走到终点就开始走B链表
p = p != NULL? p->next : headB;
// q没走到B链表终点就一直往后走,走到终点就开始走A链表
q = q != NULL? q->next : headA;
}
return p;
}
};
O ( n ) O(n) O(n)
原题链接: 环型链表(点击链接直达)
fast
是跑得快的指针,slow
是跑的慢的指针。快指针每次走两步,慢指针每次走一步;fast
和 slow
两指针在环形操场跑,如果 fast
和 slow
相遇,那一定是 fast
超过了 slow
一圈;fast
和 slow
两指针在直道操场跑,因为快指针跑的快会先达到终点,则两指针一定不会遇到;class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode* slow = head, *fast = head;
while (fast && fast->next) //(1)
{
fast = fast->next->next; //(2)
slow = slow->next; //(3)
if (fast == slow) return true; //(4)
}
return false; //(5)
}
};
O ( n ) O(n) O(n)
原题链接: 环形链表 II(点击链接直达)
fast
,一个是 slow
, fast
一次走两步,slow
一次走一步;fast
指针走过的路程:
a
+
b
+
n
×
圈
a + b + n×圈
a+b+n×圈,
圈
=
b
+
c
圈 = b + c
圈=b+c,得出
a
+
b
+
n
(
b
+
c
)
a + b + n(b + c)
a+b+n(b+c);slow
指针走过的路程
a
+
b
a + b
a+b;head
出发,另一个指针从相遇点出发,两指针以相同速度走,直到相遇为止,相遇的点就是链表环的入口节点;|
表示入环节点的位置,a
表示从起点出发到入环节点位置的路程,b
表示从入环节点的位置到相遇节点位置的路程,c
表示从相遇节点位置到入环节点位置的路程,*
表示两指针相遇节点的位置class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if (!head || !head->next) return NULL;
ListNode* slow = head, *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
ListNode* cur = head;
while (cur != slow)
{
cur = cur->next;
slow = slow->next;
}
return cur;
}
}
return NULL;
}
};
O ( n ) O(n) O(n)
原题链接: 回文链表(点击链接直达)
fast
指针没有指向 null
,说明链表长度为奇数,slow
还要再向前一步;class Solution {
public:
bool isPalindrome(ListNode* head) {
ListNode* slow = head, *fast = head;
ListNode* pre = nullptr;
while (fast != nullptr && fast->next != nullptr)
{
fast = fast->next->next;
ListNode* next = slow->next;
slow->next = pre;;
pre = slow;
slow = next;
}
if (fast != nullptr) slow = slow->next; // 如果fast没有指向null,说明链表长度为奇数,slow还要再往前走一步
while (pre && slow)
{
if (pre->val != slow->val) return false;
pre = pre->next;
slow = slow->next;
}
return true;
}
};
O ( n ) O(n) O(n)
原题链接: 合并两个有序链表(点击链接直达)
二路归并
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
auto p = list1, q = list2;
auto dummy = new ListNode(-1);
auto cur = dummy;
while (p && q)
{
if (p->val <= q->val)
{
cur->next = p;
p = p->next;
cur = cur->next;
}
else
{
cur->next = q;
q = q->next;
cur = cur->next;
}
}
if (p) cur->next = p;
if (q) cur->next = q;
return dummy->next;
}
};
O ( n ) O(n) O(n)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。