链表题目总结(第一篇)
首先感谢下面几个博客的技术支持:
http://www.cnblogs.com/newth/archive/2012/05/03/2479903.html
另外,这里再贴上一些我的好朋友的关于链表的总结贴子
http://www.cnblogs.com/carlsama/p/4123709.html
http://www.cnblogs.com/carlsama/p/4126470.html
http://www.cnblogs.com/carlsama/p/4126503.html
http://www.cnblogs.com/carlsama/p/4127201.html
这里分析的链表默认情况下都是单链表,本篇主要分析一条单链表上的各种问题与解答
(一)单链表数据结构
1 struct ListNode{ 2 int val; 3 ListNode *next; 4 ListNode(int val = 0) : val(val), next(NULL){} 5 };
(二)单链表相关题目
(1)倒序打印
以倒序的方式访问节点,比如打印或者其他操作.
1 // 方法 i: 递归的打印, 代码非常的简洁 2 void ReversePrint(ListNode *pHead){ 3 if(!pHead) return; 4 ReversePrint(pHead->next); 5 std::cout<<pHead->val<<std::endl; 6 }
1 // 方法 ii: 使用栈,仿照递归的遍历 2 void ReversePrint(ListNode *pHead){ 3 if(!pHead) return; 4 stack<ListNode*> data; 5 ListNode *pos = pHead; 6 while(pos){ 7 data.push(pos); 8 pos = pos->next; 9 } 10 ListNode *cur; 11 while(!data.empty()){ 12 cur = data.top(); 13 std::cout<<cur->val<<std::endl; 14 data.pop(); 15 } 16 }
(2)获取链表倒数第K个节点
1 // 设置两个指针,两个指针之间距离保持为k,前面的指针从头节点开始, 然后一起往后面走 2 ListNode * RGetKthNode(ListNode *pHead, unsigned int k){ 3 if(k < 1) return NULL; 4 ListNode *pre = pHead; 5 ListNode *last = pHead; 6 while(k-- > 0 && last){ 7 last = last->next; 8 } 9 if(!last) return NULL; 10 while(last){ 11 last = last->next; 12 pre = pre->next; 13 } 14 return pre; 15 }
(3)查找链表的中间节点
1 // 慢指针以每步一个节点的速度前进,快指针以每步两个节点的速度前进,这样快指针到达末端时候,慢指针就指向中间节点 2 ListNode* getMidNode(ListNode *pHead){ 3 if(!pHead || !pHead->next) return pHead; 4 ListNode *slow = pHead, *fast = pHead; 5 while(fast && fast->next){ 6 slow = slow->next; 7 fast = fast->next; 8 fast = fast->next; 9 } 10 return slow; 11 }
(4)链表反转
//方法i new一个头节点,然后遍历链表,不断的插入到头节点的next位置 ListNode *ReverseList(ListNode *pHead){ if(!pHead || !pHead->next) return pHead; ListNode * newhead = new ListNode(); ListNode *pos = pHead, *tmp; newhead->next = pHead; while(pos->next){ tmp = pos->next; pos->next = tmp->next; tmp->next = newhead->next; newhead->next = tmp; } return newhead->next; }
1 //方法ii 就地倒转 + 迭代, 反转之前pre->cur, 反转之后 cur->pre 2 ListNode *ReverseList(ListNode *pHead){ 3 if(!pHead || !pHead->next) return pHead; 4 ListNode *pre = pHead, *cur = pHead->next, *next = NULL; 5 pHead->next = NULL; 6 while(cur){ 7 next = cur->next; 8 cur->next = pre; 9 pre = cur; 10 cur = next; 11 } 12 return pre; 13 }
1 //方法iii 就地倒转 + 递归, 非常具有技巧性 2 ListNode *ReverseList(ListNode *pHead){ 3 if(!pHead || !pHead->next) return pHead; 4 ListNode *left = ReverseList(pHead->next); // 返回left为left段倒转后的首节点 5 pHead->next->next = pHead; // pHead->next 一开始是left段的首节点,倒转后就是末节点 6 pHead->next = NULL; 7 return left; 8 }
(5)链表节点的删除
给定链表的头节点指针和要删除的节点指针,一般来说通常想到的方法就是从头节点开始遍历,找到删除节点的pre节点,借助pre节点删除该节点,这个时间复杂度是O(n),这种方法实在不允许直接数值交换的情况下才只能这样,如果允许节点之间的数值交换或者拷贝,那么有平均时间为O(1)的方法:
1 // delete Node, with O(1), 平均时间复杂度 2 void deleteNode(ListNode *pHead, ListNode *pDelete){ 3 if(!pHead || !pDelete) return; 4 if(pDelete->next){ 5 // 不是最后一个节点,只需要复制下一个节点的值过来然后删除下一个节点, O(1) 6 ListNode *tmp = pDelete->next; 7 pDelete->val = pDelete->next->val; 8 pDelete->next = pDelete->next->next; 9 delete tmp; // 很容易忘记 10 }else{ 11 // 是最后一个节点,只能从前遍历到倒数第二个节点,O(n) 12 if(pHead == pDelete){ // 只有一个节点是个特殊情况 13 delete pHead; 14 pHead = NULL; 15 } 16 ListNode *pos = pHead; 17 while(pos->next != pDelete){ 18 pos = pos->next; 19 } 20 pos->next = NULL; 21 delete pDelete; // 这一句很容易忘记 22 } 23 }
(6)在链表指定节点前插入某个节点
对于单链表,我们知道插入指定节点后面是很容易的,但是要插入到指定节点前面似乎需要从头遍历链表,如果允许节点之间值拷贝的话那么就可以先插入到后面
然后两个节点交换一下值便变相实现插入到前面
1 // 题目要求在指定节点pPos前插入,我们可以先插入到后面,然后交换他们的值即可 2 void insertNode(ListNode *pPos, ListNode *pInsert){ 3 if(!pPos || !pInsert) return; 4 pInsert->next = pPos->next; 5 pPos->next = pInsert; 6 int tmp = pPos->val; 7 pPos->val = pInsert->val; 8 pInsert->val = tmp; 9 }
(7)链表是否有环
// 快慢指针, 慢指针每步一个节点,快指针每步两个节点,如果有环肯定会相遇 bool isCircleList(ListNode *pHead){ if(!pHead || !pHead->next) return false; ListNode *slow = pHead, *fast = pHead; while(fast && fast->next){ slow = slow->next; fast = fast->next->next; if(slow == fast) return true; } return false; }
这里解释一下: 假设链表里面有环,那么快指针会先进入环内,然后在里面不断的循环,当慢指针进入环内那一刻,可以根据相对运动的观念,可以看成慢指针静止,快指针以每步一节点的速度走,那么很显然必然会相遇
(8)判断有环链表的环入口点
先使用一快一慢指针,快指针一步两个节点,慢指针一步一个节点,一是用来探测链表是否有环,二是如果有环那么就已经把快指针送到了环内部。
这样之后,慢指针重置为指向链表头节点,快指针指向不变,但是移动速度变为每步一个节点,即和慢指针速度一样,然后快慢指针同步移动,那么相遇的时候
就是环的入口点。
// ListNode *GetFirstCircleNode(ListNode* pHead){ if(!pHead || !pHead->next) return pHead; ListNode *slow = pHead, *fast = pHead; // fast指针先进入环内 第一部分代码 while(fast && fast->next){ slow = slow->next; fast = fast->next->next; if(slow == fast) break; } if(!fast || !fast->next) return NULL; // slow指针再次从头开始, fast指针减速 slow = pHead; // 第二部分代码 while(slow != fast){ slow = slow->next; fast = fast->next; } return slow; // 相遇点就是环的入口 }
这里面代码似乎很简单,但是要证明其正确性需要费一点周折:
如上图所示: 假设链表的头节点在s处, O为链表入口点, m为第一部分代码中快慢指针的相遇点,
以O点为坐标原点,以向右为正方向,以相隔链表节点数目为坐标的值,那么m点坐标为m(0, p), p >= 0
p 为m点到o点中间的节点个数,同理s点坐标为(0, -d), d >= 0; 另外设环的大小为r, 即有r个节点
那么在第一部分代码中, 两个节点在m点相遇时候,慢指针移动距离 d + p, 快指针为2d + 2p;
那么 2d + 2p - d - p = d + p ,因为快指针先进入圈内,然后再和慢指针相遇时候必然比慢指针多走n圈,
n >= 1, 所以
d + p = n * r (* 结论1)
然后在第二部分代码中,快慢指针一起同步的走, 我们可以看到当慢指针走动距离为d的时候,快指针这时候
和慢指针同速,所以走动距离也是d:
那么, 此时慢指针的坐标是 -d + d = 0, 即O(0,0)点,而快指针的坐标是 d + p, 即(0,d+p),而根据结论1
我们可以看到 d+ p = n*r , n >= 1, 所以点(0,d+p)就是O(0,0),此时两个指针相遇,而之前慢指针一直都未进入到环,快
指针则一直在环内,所以这一次相遇是它们的第一次相遇点,同样也是环的入口点
(9)链表的排序
这里链表的排序主要都是基于归并排序,不过可以分为迭代的归并排序和递归的归并排序
1 // 递归的方式实现归并排序 2 ListNode *ListSort(ListNode *pHead){ 3 if(!pHead || !pHead->next) return pHead; 4 ListNode *mid = getMidNode(pHead); 5 ListNode *right = pHead, *left = mid->next; 6 mid->next = NULL; 7 right = ListSort(right); 8 left = ListSort(left); 9 pHead = MergeSortedList(right, left); 10 return pHead; 11 }
1 //仿照SGI STL里面的List容器的sort函数,实现迭代版的归并排序 2 ListNode *ListSort(ListNode *pHead){ 3 if(!pHead || !pHead->next) return pHead; 4 vector<ListNode*> counter(64, NULL); 5 ListNode *carry; 6 ListNode *pos = pHead; 7 int fill = 0; 8 while(pos){ 9 carry = new ListNode(pos->val); 10 pos = pos->next; 11 int i = 0; 12 for(i = 0; i < fill && counter[i]; i++){ 13 carry = MergeSortedList(carry, counter[i]); // 合并两个已排序的链表,参见链表总结第二篇 14 counter[i] = NULL; 15 } 16 counter[i] = carry; 17 if(i == fill) fill++; 18 } 19 for(int i = 1; i < fill; i++){ 20 counter[i] = MergeSortedList(counter[i-1], counter[i]); 21 } 22 return counter[fill-1]; 23 }
下面以链表数据4,2,1,5,6,9,7,8,10为例分析这个迭代版的代码过程
在while循环里面每次都是从原链表里取出一个节点,然后往counter数组里面归并,fill值表明目前counter数组中
存有数据的最大的那个数组标号+1,比如说,我们首先取出4,此时fill = 0, 直接就把4放在counter[0]链表上,fill变为1,
然后取出2,就拿2与counter[0]进行merge,得到结果放到count[1],fill变为2,此时counter数组的情况是counter[0]为空,
counter[1]存放2,4,再加入1时候直接放到counter[0], 再加入5时候,1与5merge,得到1,5再继续向上和couter[1]中
的2,4,merge得到1,2,4,5,如果此时counter[2]不为空,那么继续merge,这里为空则1,2,4,5存到counter[2],
于是就这样一步步的向上merge,每次merge链表长度都是加倍
最终取完所有数据之后,counter数组里面的数据需要最终整合一下,代码中最后一个for循环就是不断的把couter中的内容
向上merge,最终形成最后的结果
这里其实也就是形成了一颗归并树,时间复杂度依然是O(nlgn)