针对leetcode上面的20多个链表的算法题,总结了一下链表操作中的几个技巧。
1. 快慢指针
快慢指针是在遍历链表的时候使用两个指针,快指针每次比慢指针多跑一步或多步,或者快指针先跑n步。这在查找倒数第n个结点、找中间结点时只需要遍历一次,在判断链表是否有环时不需要额外的空间。例如,查找一个链表中间结点,
ListNode * findMid(ListNode * head) {
assert(head && head->next);
ListNode *slow = head;
ListNode *fast = head->next;
while(fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
我们只需要将slow指针每次前进一位,fast指针每次前进两位,当fast指针指向末尾时,slow刚好是链表中点。再如,当删除倒数第n个结点时,我们只需将back指针先前进n位,然后两指针同速前进,直到链表结尾时,front->next指向倒数第n个结点。
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode * front = head;
ListNode * back = head;
for(int i = 0; i < n; i++)
back = back->next;
if(back == NULL) {
ListNode * tmp = head;
head = head->next;
delete tmp;
return head;
}
while(back->next != NULL) {
front = front->next;
back = back->next;
}
ListNode * tmp = front->next;
front->next = tmp->next;
delete tmp;
return head;
}
而在判断一个链表是否有环时,slow和fast各自前进一步和两步,若有环,则终会相聚一起。
bool hasCycle(ListNode *head) {
if(head == NULL || head->next == NULL) return false;
ListNode * fast = head->next;
ListNode * slow = head;
while(fast != NULL && fast->next != NULL && slow != fast) {
fast = fast->next->next;
slow = slow->next;
}
return (fast != NULL && fast->next != NULL);
}
2. 虚结点
当对一个链表的操作会改变头指针时,可以增加一个虚拟结点放在头结点前,当链表操作结束时再删除,这样可以减少一些不必要的判断,使代码更简洁更有可读性。例如,在删除一个给定值的结点时,这个结点可能会在链表中间,也可能是头结点,当是头结点时就需要修改链表表头指针,而添加一个虚拟结点后就不需要关系删除的是头结点还是中间结点了。
ListNode* removeElements(ListNode* head, int val) {
ListNode * dummy = new ListNode(-1);
dummy->next = head;
ListNode * prev = dummy;
ListNode * p = head;
while(p != NULL) {
if(p->val == val) {
prev->next = p->next;
delete p;
}
else {
prev = p;
}
p = prev->next;
}
head = dummy->next;
delete dummy;
return head;
}
又如,当要在一个链表随意位置插入一个结点时,我们必须找到插入位置的前一个结点,而如果是头结点前,这就需要更多的判断,并且每一步都不知道插入的位置是否是头结点前,因此,增加的虚拟结点使得不用关心这个问题而让代码更简洁。如leetcode86 partition list
ListNode* partition(ListNode* head, int x) {
ListNode * dummy = new ListNode(-1);
dummy->next = head;
ListNode * prev = dummy;
ListNode * first = head;
while(first != NULL && first->val < x) {
first = first->next;
prev = prev->next;
}
if(first == NULL || first->next == NULL) {
delete dummy;
return head;
}
ListNode * p = first->next;
ListNode * q = first;
while(p != NULL) {
if(p->val < x) {
q->next = p->next;
p->next = prev->next;
prev->next = p;
p = q->next;
prev = prev->next;
} else {
p = p->next;
q = q->next;
}
}
head = dummy->next;
delete dummy;
return head;
}
3. 假删除操作
当删除一个结点时我们必须知道它的前一个结点指针,而如编程之美上面删除一个单链表中间结点这个问题,只给定了指向删除结点的指针,这时我们不可能再得到前一个结点指针了。这时可采用假删除操作,将下一个结点的值赋给这个结点,再删除下一个结点。这样的技巧也可用于其它一些地方(目前我还没见到,反正能用又简洁高效的算法就是好算法)。
4. 问题转换
判断一个单链表是否有环问题,如果还需要找到这个交叉结点呢?我们可以将问题转换一下,前面在用快慢指针判断一个链表是否有环时已经找到了环上的一个结点指针,那么我们从这个结点处断开这个链表就变为了另外一个问题,即判断两个无环单链表是否交叉问题。这个问题在编程之美里面就有,如下图。
A: a1 → a2 ↘ c1 → c2 → c3 ↗ B: b1 → b2 → b3
这个问题又怎么解决呢?我们想想,如果两个链表有共同结点,那么从最后一个结点到交叉点都是他们的共同结点。如果两个链表一样长的话,问题就好办了,只要两个链表同时遍历,总会同时指向这个交叉结点。现在就知道了,利用前面的快慢指针的思想,首先求得两个链表的长度,然后让长的这个表的遍历指针先跑一段距离,让他们从离链表尾相同的距离开始,这样就能找到交叉结点了。现在总结单链表有环问题II步骤是:
1. 解单链表有环问题I,得到环中一指针。
2. 以得到的指针断开这个环。
3. 解两单链表交叉结点问题,得到最终结果。
ListNode *detectCycle(ListNode *head) {
if(head == NULL || head->next == NULL) return NULL;
ListNode * slow = head;
ListNode * fast = head->next;
while(fast != NULL && fast->next != NULL && slow != fast) {
slow = slow->next;
fast = fast->next->next;
}
if(fast == NULL || fast->next == NULL) {
return NULL;
}
ListNode * tail1 = slow;
ListNode * head2 = slow->next;
tail1->next = NULL;
int length1 = 0;
ListNode * p = head;
while(p != NULL) {
length1++;
p = p->next;
}
int length2 = 0;
p = head2;
while(p != NULL) {
length2++;
p = p->next;
}
ListNode * newhead1 = head;
ListNode * newhead2 = head2;
if(length1 > length2) {
int k = length1 - length2;
while(k-- > 0) newhead1 = newhead1->next;
} else if ( length2 < length1) {
int k = length2 - length1;
while(k-- > 0) newhead2 = newhead2->next;
}
while(newhead1 != newhead2) {
newhead1 = newhead1->next;
newhead2 = newhead2->next;
}
tail1->next = head2;
return newhead1;
}
又如,在反转链表问题时,问题II反转[m,n]这个中间的部分,那么可以先处理一下,标记好左边尾结点和右边头结点,反转中间部分后再链接起来。当然也可不必,因为直接反转会更好一点。