很多面试相关的书籍都有链表和字符串操作相关的题,这类问题更考验编程基本功和对基础知识的掌握情况。由于之前很多题目做过就忘,再做时总是磕磕绊绊,说明没有真正消化。于是想到把做过的题整理出来,写上自己的一些做题感悟,多多总结常用的方法和技巧。如果能把问题向别人讲明白,那才说明真的理解了。本文中的部分代码参考了网上一些源码的写法,且在 leetcode 上测试通过。
链表常用方法和技巧
Dummy Node
哑节点的使用针对单链表没有前向指针的问题。保证head
节点不会在删除操作中丢失。当head
有变化(比如被删除或被修改),或者涉及到边界判断时,使用Dummy Node
可以很好的简化代码。Fast and slow pointer
快慢指针的快和慢指的是指针向后移动的步长。每次移动步长大的叫快指针,步长小的即为慢指针。
快慢指针有如下应用:- 寻找未知长度的特殊位置的某个节点,比如中间节点、倒数第
k
个节点。其中,寻找中间节点可以设置快指针*fast
移动速度是慢指针*slow
的两倍,两个指针都从单链表的头节点出发,当*fast
指向尾节点时,*slow
正好走到中间了。寻找倒数第k
个节点时,可以让*fast
指针先出发,移动k-1
步,然后同时移动*fast
和*slow
指针,当*fast
指针移动到尾节点时,*slow
指针正好移动到倒数第k
个节点。 - 判断单链表是否存在环。同样设置两个指针
*fast
、*slow
都指向单链表的头节点,其中*fast
的移动速度是*slow
的两倍。如果*fast = NULL
,说明该单链表以NULL
结尾,不存在环;如果*fast = *slow
,则快指针追上慢指针,说明该链表存在环。
- 寻找未知长度的特殊位置的某个节点,比如中间节点、倒数第
以下各问题会用到下面的单链表定义:
// Definition for singly-Linked List
struct ListNode {
int val;
ListNode* next;
ListNode(int x) : val(x), next(NULL) {}
};
在O(1)时间删除链表节点
题目描述
237. Delete Node in a Linked ListWrite a function to delete a node (except the tail) in a singly linked list, given only access to that node.
Supposed the linked list is 1 -> 2 -> 3 -> 4 and you are given the third node with value 3, the linked list should become 1 -> 2 -> 4 after calling your function.思路
用该节点的后继节点覆盖当前节点,然后删除下一个节点即可。该节点不能是尾节点。C++
void deleteNode (ListNode *cur)
{
assert(cur != NULL);
assert(cur->next != NULL); //不能是尾节点
ListNode* p = cur->next;
cur->val = p->val;
cur->next = p->next;
delete p;
}
反转链表
-
Reverse a singly linked list.
思路
迭代。迭代的本质是遍历,迭代的外部操作是循环。对于每一次的循环,先把当前节点
head
指向直接后继的指针改为指向其直接前驱pre
,为此需先记录其直接后继head->next
的值。然后把pre
的值修改为head
的值,把head
的值修改为记录过的后继值,一步一步迭代下去。递归。类似于栈,遇到终止条件则返回到上一层递归的调用处,然后继续向下执行,执行完成后返回该层的再上一层,就这样一层一层地返回。递归的执行顺序类似于深度优先遍历。一边递归一边修改指针指向,递归完成时,翻转也完成。
这里运用到链表操作中一个重要的技巧:
Dummy Node
。 设置一个哑节点dummy
,让dummy
的next
指针指向链表的第一个节点。每次循环时把head
后面的head->next
节点插入到哑节点后面。
以第一次循环为例,把节点2
插入到dummy
之后,1
之前。
第二次循环时把3
插入到dummy
之后,2
之前。依此类推。
由于每次循环后链表的第一个节点都会变化,所以要先记录它的值,以便后续操作中在它的前面插入。
C++ - iteration
ListNode* reverseList(ListNode* head)
{
if (head == NULL || head->next == NULL) {
return head;
}
ListNode *pre = NULL, *next;
while (head != NULL) {
next = head->next;
head->next = pre;
pre = head;
head = next;
}
return pre;
}
边界条件
当最后head
为NULL
时,整个翻转才完成。C++ - recursion
ListNode* reverseList(ListNode* head)
{
if (head == NULL || head->next == NULL) {
return head;
}
ListNode *newHead = reverseList(head->next);
head->next->next = head;
head->next = NULL;
return newHead;
}
边界条件
head == NULL
:判断异常。
head->next == NULL
:递归终止。C++ - Dummy Node
ListNode* reverseList(ListNode* head)
{
ListNode dummy(0);
dummy.next = head;
ListNode* p = &dummy;
while (head && head->next) {
ListNode *temp = dummy.next; //记录链表的第一个节点
p->next = head->next;
head->next = p->next->next;
p->next->next = temp;
}
return dummy.next;
}
- 边界条件
当head
为空时,不执行循环返回为空值。当head->next
为空时,说明到了最后一个节点,退出循环。
判断单链表是否存在环
-
Given a linked list, determine if it has a cycle in it.
Follow up:
Can you solve it without using extra space? 思路
用
set
设访问标记。把单链表的节点一个一个放入set
,判断节点是否已经存在于set
中。如果set
中已存在该节点,说明单链表有环。使用快慢指针
fast
和slow
。两个指针都从表头开始往后走。设置slow
每次走一步,fast
每次走两步。如果fast
遇到null
,说明没有环,返回false
。如果slow=fast
,说明有环。时间复杂度为 O(n) 。
C++ -set
bool hasCycle(ListNode* head)
{
set<ListNode*> visited;
while (head) {
set<ListNode*>::iterator iter = visited.find(head);
if (iter == iter.end()) {
visited.insert(head);
} else {
return true;
}
head = head->next;
}
return false;
}
- C++ -Fast and slow pointer
bool hasCycle(ListNode* head)
{
ListNode *fast, *slow;
fast = slow = head;
while (fast && fast->next) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) {
return true;
}
}
return false;
}
扩展问题:单链表环的入口及长度
- 题目描述
142. Linked List Cycle II
Given a linked list, return the node where the cycle begins. If there is no cycle, return null.
Note: Do not modify the linked list.
Follow up:
Can you solve it without using extra space? 思路
如上图:
slow
走到相遇点时走过的距离为 distslow=x+y
fast
走到相遇点时走过的距离为 distfast=(x+y+z)+y
由于fast
的速度是slow
的2倍,所以 distfast=2distslow ,即 2(x+y)=(x+y+z)+y ,可知 x=z 。
当快慢指针到达相遇点时:
①. 只需让fast
指针继续从相遇点出发,slow
指针从表头出发,并保持相同的速度,当它们再次相遇时,即是环的入口。
②. 让fast
指针继续以两倍于slow
指针的速度前进,记录各自走过的距离。当它们再次相遇时,fast
刚好比slow
多跑一圈,此时用fast
走过的距离减去slow
走过的距离即可得到环的长度。C++ -Fast and slow pointer
ListNode *detectCycle(ListNode *head)
{
bool hasCycle = false;
ListNode *fast, *slow;
fast = slow = head;
while (fast->next && fast->next->next) {
fast = fast->next->next;
slow = slow->next;
if (fast == slow) {
hasCycle = true;
break;
}
}
if (hasCycle == false) {
return NULL;
} else {
slow = head;
while (slow != fast) {
slow = slow->next;
fast = fast->next;
}
}
return slow;
}
链表倒数第k个节点
设置前后两个指针
pre
和cur
指向同一个起点,让前一个指针先走k-1
步,然后两个指针再同时一步一步移动,当cur
走到最后一个节点时,此时pre
正好指向倒数第k
个节点。
题目描述
19. Remove Nth Node From End of ListGiven a linked list, remove the nth node from the end of list and return its head.
For example,Given linked list: 1->2->3->4->5, and n = 2. After removing the second node from the end, the linked list becomes 1->2->3->5.
思路
运用上面的方法,使用前后两个指针。由于要删除第k
个节点,当后面一个指针移动到尾节点时,前一个指针应移动到倒数第k
个节点的前一个节点。然后pre->next = pre->next->next
。C++ -Fast and slow pointer
ListNode* removeNthFromEnd(ListNode* head, int n)
{
ListNode dummy(0);
dummy.next = head;
ListNode *pre = &dummy;
while (--n) {
head = head->next;
}
while (head->next) { /*移动head指针到尾节点*/
head = head->next;
pre = pre->next;
}
pre->next = pre->next->next;
return dummy.next;
}
- 边界条件
这个题要格外注意边界条件,应注意链表中只有一个节点的情况。不然很容易出错,或者把代码写复杂。
为了使代码更简洁,减少不必要的边界判断,可以在表头前添加一个哑节点。让前一个指针pre
指向哑节点,让后一个指针移动k-1
步后再同时移动两个指针,当后一个指针移动到尾节点时,pre
指针刚好移动到倒数第k
个节点的前一个节点。而且这种做法保证了当链表中只有一个节点时,不必担心pre->next->next
的越界问题。
扩展问题:求链表的中间节点
- 题目描述
148. Sort List
Sort a linked list in O(nlogn) time using constant space complexity.
思路
排序时间复杂度为 O(nlogn) 的算法能想到快速排序和归并排序。排序时只需改变指针,所以不必像数组那样需要额外的存储空间,空间复杂度为 O(1) 。找中间节点只需 O(n2) 的线性时间,所以总时间复杂度为 O(nlogn) ,满足题目要求。C++ -Fast and slow pointer
ListNode* sortList(ListNode* head)
{
if (head == NULL || head->next == NULL) {
return head;
}
ListNode *slow, *fast;
slow = fast = head;
while (fast->next && fast->next->next) {
fast = fast->next->next;
slow = slow->next;
}
ListNode* mid = slow->next;
slow->next = NULL; //把一段链表分为两段子链表
ListNode* list1 = sortList(head);
ListNode* list2 = sortList(mid);
return mergeList(list1, list2);
}
ListNode* mergeList(ListNode* list1, ListNode* list2)
{
ListNode dummy(0);
dummy.next = list1;
ListNode *p = &dummy;
while (NULL != list1 && NULL != list2) {
if (list1->val <= list2->val) {
p->next = list1;
list1 = list1->next;
} else {
p->next = list2;
list2 = list2->next;
}
p = p->next;
}
if (NULL != list1) {
p->next = list1;
} else {
p->next = list2;
}
return dummy.next;
}
- 边界条件
求中间节点时,一时脑抽把while
中的循环条件写成了fast && fast->next
,结果一直RE
。可见边界很重要啊,对循环中涉及到fast->next->next
的操作一定要格外小心,不然很容易就越界啊各种错误。
判断两个链表是否相交
题目描述
160. Intersection of Two Linked ListsWrite a program to find the node at which the intersection of two singly linked lists begins.
For example, the following two linked lists:
begin to intersect at node c1.思路
如果两个链表长度相同,只要一个一个地对比下去就能找到。当链表长度不同时,只需要从较长的链表头结点往下遍历,直到与短链表长度相同时再一个一个地比对即可。C++
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB)
{
if (headA == NULL || headB == NULL) {
return NULL;
}
int lenA = 1, lenB = 1;
ListNode *pA = headA, *pB = headB;
while (pA->next != NULL) {
lenA++;
pA = pA->next;
}
while (pB->next != NULL) {
lenB++;
pB = pB->next;
}
if (pA != pB) {
return NULL;
}
if (lenA > lenB) {
for (int i = 0; i < lenA - lenB; i++) {
headA = headA->next;
}
} else {
for (int i = 0; i < lenB - lenA; i++) {
headB = headB->next;
}
}
while (headA != headB) {
headA = headA->next;
headB = headB->next;
}
return headA;
}
Reference
面试精选:链表问题集锦
链表的反转问题:递归和非递归方式
求有环单链表中的环长、环起点、链表长
Proof of detecting the start of cycle in linked list