链表的经典技巧及算法
来源:互联网 发布:驾驶远程教育及时软件 编辑:程序博客网 时间:2024/05/19 03:17
1 寻找链表的中间节点:最简单的方法是,先遍历一遍链表,计算出链表的长度,然后计算出中间节点的位置,然后再遍历一遍,边遍历边计算,直到找到中间节点,这个方法略显啰嗦,最坏的情况需要遍历2次链表,代码如下:
ListNode* findMiddle(ListNode* head){ if(!head) return NULL; int length = 0; while(head) { head = head->next; length ++ ; } int middle = (lenght+1)/2; --middle; while(middle--) head = head->next; return head;}
另一个更灵巧的方法是,用两个指针,慢指针每次走一步,快指针每次走两步,当快指针走到链表的末端(NULL)时,慢指针正好指向了中间节点,代码如下:
ListNode* findMiddle(ListNode* head){ if(!head) return NULL; ListNode* slow = head; ListNode* fast = head->next; while(fast && fast->next){ slow = slow->next; fast = fast->next->next; } return slow;}
2 检测链表是否有环:经典的做法也是用快慢指针,如果没有环,快指针一定先到达链表的末端(NULL),如果有环,快、慢指针一定会相遇在环中,代码如下:
bool hasCircle(ListNode* head, ListNode **node){ if(!head) {*node = NULL; return false; } ListNode* slow = head; ListNode* fast = head->next; while(fast && fast->next && fast!=slow){ slow = slow->next; fast = fast->next->next; } if(fast==slow) {*node = fast; return true; } else {*node = NULL; return false; }}
3 检测环的入口:经典的做法是先检测是否有环,如果有环,则计算出环的长度,然后使用前后指针(不是快慢指针),所谓的前后指针就是一个指针先出发,走了若干步以后,第二个指针也出发,然后两个指针一起走,当前后指针相遇时,它们正好指向了环的入口,代码如下:
ListNode* detectEntry(ListNode* head){ //判断是否有环,无环则返回NULL ListNode* node = NULL; if(!hasCircle(head, &node)) return NULL; //计算环的长度 int circleLen = 1; ListNode *temp = node->next; while(temp!=node) { temp = temp->next; circleLen ++; } //使用前后指针,此时fast每次也只移动一步 ListNode *first = head , *second = head; while(circleLen--) first = first->next; // first指针先出发 //然后两个指针同时向前走,每次走一步 while(first!=second){ first = first->next; second = second->next ; } return first;}
如果允许使用额外的内存,可以有更简单的做法,即一边遍历,一边将节点放在map中,当某个节点第二次出现在map中时,它就是入口节点,代码如下:
ListNode* detectEntry(ListNode* head){ if(!head) return NULL; map<ListNode*,bool> hm; while(!head){ if(hm.count(head)) return head; else hm[head] = true; head = head->next; } return NULL;}
4 链表翻转:假设原链表为1->2->3,翻转以后的链表应该是1<-2<-3,即节点3变成了头节点,代码如下:
ListNode* reverseList(ListNode *head){ if(!head || !head->next) return head; ListNode* p = head->next; head->next = NULL; while(p){ ListNode* t = p->next; p->next = head; head = p; p = t; } return head;}
5 删除链表中的节点,注意这里只给出要删除的那个节点,不给出链表头(假设被删除节点不是尾节点),代码如下:
void removeNode(ListNode *target){ if(!target) return ; ListNode *t = target->next; target->val = t->val; target->next = t->next; //用target的下一个节点覆盖了target,然后删除下一个节点即可 delete t;}
如果被删除节点是尾节点,上面的代码就无法将target上一个节点的next置为NULL,所以只有给了头节点后,才能遍历到target的上一个节点,并把其next置为NULL。
6 回文链表的检测:所谓回文链表,即链表元素关于中间对称,,如1->2->3->2->1,比较简单的方法是用一个栈,先顺序遍历链表,并把每个节点放入栈中,遍历完成后,栈的出栈顺序正好是原链表的逆,代码如下:
bool isPalindrome(ListNode* head){ if(!head) return true; //空链表姑且认为是回文的 stack<ListNode*> s; ListNode* p = head; while(p) { s.push(p); p = p->next ; } int length = s.size(); length /= 2; while(length--) { p = s.top(); s.pop(); if(p->val!=head->val) return false; head = head->next; } return true; }
上面代码的空间复杂度为O(N),其实还有空间复杂度为O(1)的算法,也很灵巧,运用了之前提到的一些技巧,代码如下:
bool isPalindrome(ListNode* head) { if (!head || !head->next) return true; //空链表或只有一个元素的链表都认为是回文的 //寻找链表的中间节点 ListNode* slow = head; ListNode* fast = head->next; while (fast && fast->next) { slow = slow->next; fast = fast->next->next; } //反转后半段链表 ListNode* prev = slow; //prev目前是前半段链表的尾节点,后半段链表的头结点 ListNode* p = slow->next; slow->next = nullptr; while (p) { ListNode* tmp = p->next; p->next = prev; prev = p; p = tmp; } // 反转完成后,prev是后半段链表的头节点,slow是前、后两段链表的公共尾节点 //判断是否为回文链表,从两边向中间靠拢 ListNode* l = head; ListNode* r = prev; while (l && r) { if (l->val != r->val) return false; l = l->next; r = r->next; } return true;}
这个方法的缺点是修改了原链表,但是综合运用了链表的很多技巧,值得收藏。:-D
7 合并有序链表:基本思路跟合并有序数组一样,但是不需要O(N)的空间复杂度了,只需要O(1)的空间复杂度,代码如下:
ListNode* mergeSortedList(ListNode* h1, ListNode* h2){ if(!h1) return h2; //如果链表1为空,直接返回链表2 if(!h2) return h1; ListNode *head, *p; if(h1->val<=h2->val) { head = h1; h1 = h1->next ; } else { head = h2; h2 = h2->next; } p = head; while(h1 && h2){ if(h1->val<=h2->val) { p->next = h1; h1 = h1->next; } else{ p->next = h2; h2 = h2->next; } p = p->next; } if(h1) p->next = h1; else p->next = h2; return head;}
其实如果不要求空间复杂度为O(1),可以用递归的思想,代码更简略,如下:
ListNode* mergeSortedList(ListNode* head1, ListNode* head2){ if(head1==NULL) return head2; if(head2==NULL) return head1; if(head1->val<=head2->val){ head1->next = merge(head1->next,head2); return head1; } else{ head2->next = merge(head1,head2->next); return head2; }}
8 链表排序:如果没有空间复杂度、时间复杂度的要求,那可选的方法太多了,像插入排序、选择排序、冒泡排序,但是如果要求时间复杂度为O(NlogN),而且空间复杂度为O(1)呢?归并排序!!!正好可以用上刚刚写的合并有序链表的代码,代码如下:
ListNode* sortList(ListNode* head){ if(head==NULL || head->next==NULL) return head; //寻找中间节点 ListNode* p = head; ListNode* q = p->next; while(q && q->next){ p = p->next; q = q->next->next; } ListNode* head2 = p->next; p->next = NULL; //排序子链表 head = sortList(head); head2 = sortList(head2); return mergeSortedList(head,head2);}
9 链表的循环右移:举例如下1->2->3->4->5->NULL,循环右移2位后,变成了4->5->1->2->3->NULL,可以这么考虑,如果链表的长度为N,循环右移K位,那么等效于循环右移 K%N位,K%N是一个小于N的数,然后我们只需要找到循环右移后的头节点即可,上面的例子就是4,然后直接把1->2->3链接到4->5->的后面,代码如下:
ListNode* rotateRight(ListNode* head, int k) { if(!head || k==0) return head; int len = 0; ListNode* p = head; while(p){ len ++; p = p->next; } //计算链表长度 k = k%len; if(k==0) return head; p = head; int m = len-k-1; while(m--) p = p->next; // 寻找循环右移K位后的头节点的前一个节点 ListNode* res = p->next; //res就是新的头节点 p->next = NULL; p = res; while(p->next!=NULL) p = p->next; //寻找原链表的尾节点 p->next = head; return res;}
10 以组为单位翻转链表,组的长度用K表示,比如原链表为1->2->3->4->5,当K=2时,翻转的结果为2->1->4->3->5,当K=3时,翻转的结果为3->2->1->4->5,即先翻转K个,再翻转K个,当剩下的节点数小于K时,就不用翻转了。用递归的方法很容易实现,代码如下:
ListNode* reverseKGroup(ListNode* head, int k) { if(!head || k<=1) return head; ListNode* p = head; int len = 0; while(p) { len++; p = p->next; } //计算链表长度 if(len<k) return head; p = head; ListNode* q = p->next; int i = k-1; while(i--) { ListNode* t = q->next; q->next = p; p = q; q = t; } //翻转前K个元素 head->next = reverseKGroup(q,k); return p;}
这个算法的空间复杂度为O(N/K),即正比于递归深度,如果要求空间复杂度为O(1)呢?其实也比较简单,只要循环处理每一段长度为K的链表,处理的时候注意保存上一段链表的尾节点,代码如下:
ListNode* reverseKGroup(ListNode* head, int k) { if(!head || k<=1) return head; ListNode *p = head; int len = 0; while(p) { len++; p = p->next; } int n = len/k; if(n==0) return head; ListNode* lastTail = new ListNode(-1); //lastTail是上一段链表的尾节点 lastTail->next = head; ListNode* newHead = NULL; //如果n=1,只执行一次翻转即可,故用newHead保存翻转后的头结点 ListNode *q = NULL; if(n--){ //执行第一次翻转 p = head; q = p->next; int i = k-1; while(i--) { ListNode *t = q->next; q->next = p; p = q; q = t; } lastTail->next = p; //p是翻转后的头结点,用lastTail->next指向它 newHead = p; //newHead是程序最终要返回的头结点 lastTail = head; //原来头节点变成了尾节点 lastTail->next = NULL; //把尾节点的next置为NULL,此步必须有,否则无法打印链表 head = q; //q是下一段链表的头节点 } while(n--){ //循环处理下一段链表 p = head; q = p->next; int i = k-1; while(i--) { ListNode *t = q->next; q->next = p; p = q; q = t; } lastTail->next = p; lastTail = head; lastTail->next = NULL; head = q; } if(q) lastTail->next = q; //把最终不需要翻转的那段链表(长度小于K)接上 return newHead;}
11 翻转链表的相邻节点,比如原链表为1->2->3->4,翻转后为2->1->4->3,这个其实就是上一道题的特例,即K=2,也要求空间复杂度为O(1),不过还是递归简洁啊,这里只给出递归的代码:
ListNode* swapPairs(ListNode* head) { if(head==NULL || head->next==NULL) return head; ListNode* p = head; ListNode* q = p->next; p->next = swapPairs(q->next); q->next = p; return q; }
12 删除链表的倒数第N个节点,要求只遍历一次,还记得检测环的入口吗?是的,用前后指针,前后指针需要相隔(N+1)步,这样当前指针为NULL的时候,后指针正好指向倒数第(N+1)个节点,然后直接删除倒数第N个节点即可,代码如下:
ListNode* removeNthFromEnd(ListNode* head, int n) { if(!head || n==0) return head; ListNode *p = head; ListNode *q = p; while(n>=0 && q) {q = q->next; n--; } while(q){ p = p->next; q = q->next; } if(n<0) { //要删除的不是头节点 ListNode *t = p->next; p->next = t->next; delete t; } else{ //要删除的是头结点 ListNode *t = p->next; delete p; head = t; } return head; }
13 删除有序链表中的重复元素,如原链表为1->2->3->3->4->4->5,删除后的链表为1->2->5,这道题的关键是如果某节点有重复,务必将其全部删掉,所以要对有重复的节点做个标记,代码如下:
ListNode* deleteDuplicates(ListNode* head) { if(!head || !head->next) return head; ListNode* dumy = new ListNode(0); //哑变量的引入使得头结点不再具有特殊性,从而简化处理流程 dumy->next = head; bool flag = true; //指示当前节点是否是第一次遇到 ListNode* p = dumy; // p 为当前节点的上一个节点 ListNode* q = head; // q 为当前节点 ListNode* t = NULL; // 临时变量 while(q) { if(q->next && q->next->val==q->val){ //如果当前节点与它的下一个节点相同,则删掉当前节点 t = q->next; delete q; p->next = t; q = t; flag = false; } else{ if(flag){ //如果当前节点在以前没有遇到过,且其不等于它的下一个节点。 p = q; q = q->next; flag = true; } else{ //如果当前节点在以前遇到过,则删除当前节点 t = q->next; delete q; p->next = t; q = t; flag = true; } } } return dumy->next;}
技巧:哑变量的引入使得头结点不再具有特殊性,从而简化处理流程。
14 像快排那样将链表分成前后两个部分,比如原链表为1->4->3->2->5->2,给出数字3,那么链表中比3小的放在前面,比3大(或等于3)的放在链表的后面,处理后的链表应该是这样的1->2->2->4->3->5,注意,4和5都大于3,那么处理后的链表中4仍然应该在5的前面,代码如下:
ListNode* partition(ListNode* head, int x) { if(!head || !head->next) return head; ListNode* dumyHead = new ListNode(0); dumyHead->next = head; ListNode* dumyTail = new ListNode(0); dumyTail->next = NULL; ListNode* p = dumyHead; //当前节点的上一个节点 ListNode* q = p->next; //当前节点 ListNode* t = NULL; //临时变量 ListNode* m = dumyTail; // dumyTail可以看做是后半部分链表的头(即dumyTail后面的节点都大于等于x) while(q){ if(q->val<x) { p = p->next; q = q->next; } else { t = q->next; m->next = q; //把大于等于x的节点接到dumyTail的后面 q->next = NULL; m = q; p->next = t; q = t; } } p->next = dumyTail->next; return dumyHead->next;}
15 合并K个有序链表,这个咋一听很简单,先合并第1个、第2个,然后将合并后的结果与第3个合并,然后将合并的结果与第4个合并……,假设每个链表的长度为N,那么时间复杂度为O(
ListNode* mergeKLists(vector<ListNode*>& lists) { if(lists.empty()) return NULL; if(lists.size() == 1) return lists[0]; vector<ListNode*> vec(lists); return helper(lists, vec); }ListNode* helper(vector<ListNode*>& lists, vector<ListNode*>& vec){ lists = vec; vec.clear(); int size = lists.size(); int i = 0; for(i=0; i<size-1; i+=2) vec.push_back(mergeTwoLists(lists[i],lists[i+1])); if(i==size-1) vec.push_back(lists[i]); if(vec.size() == 1) return vec[0]; else return helper(lists, vec);}
16 寻找两个链表的第一个公共节点,即两个链表从某个节点开始合并了,我们要找出这个节点,经典的方法是先计算两个链表的长度:L1,L2,假设L1>L2,那么公共节点一定不在链表1的前(L1-L2)个节点中,这样我们就可以让链表1的头节点指向第(L1-L2+1)个节点,然后同时推进两个链表的头节点,边推进边比较,直到遇到同一个节点,代码如下:
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { if(!headA || !headB) return NULL; ListNode *p = headA; int lenA = 0; while(p){ lenA++; p = p->next; } p =headB; int lenB = 0; while(p){ lenB++; p = p->next; } if(lenA>lenB){ int count = lenA-lenB; while(count--) headA = headA->next; } if(lenA<lenB){ int count = lenB-lenA; while(count--) headB = headB->next; } while(headA){ if(headA==headB) return headA; headA = headA->next; headB = headB->next; } return NULL; }
17 链表的插入排序,这个就很直白了,代码如下:
ListNode* insertionSortList(ListNode* head) { if(!head || !head->next) return head; //空链表或只有一个节点的链表不需要排序 ListNode* dumy = new ListNode(0); dumy->next = head; ListNode* p = head; //已经排序好的链表的尾节点 ListNode* q = p->next; //当前要处理的 while(q){ if(p->val<=q->val) { //如果当前节点大于已经排好序的链表的尾节点,那么不需要做插入 p = p->next; q = q->next; continue; } //插入,这时候dumy派上用场了 ListNode* m = dumy; ListNode* n = m->next; while(n->val<=q->val) { m = m->next; n = n->next; } m->next = q; p->next = q->next; q->next = n; q = p->next; } return dumy->next;}
18 把有序链表转换成一个尽量平衡的二叉树,其实所谓的尽量平衡,就是把链表的中间节点作为根节点,根节点左边的链表是树的左子树,根节点右边的链表是树的右子树,然后问题就转换成了原问题的子问题,即用前半段链表建立一个尽量平衡的左子树,用后半段链表建立一个尽量平衡的右子树,代码如下:
TreeNode* sortedListToBST(ListNode* head) { if(!head) return NULL; if(!head->next) return new TreeNode(head->val); ListNode* slow = head; ListNode* fast = head->next->next; //注意,这里fast比slow先走了两步,这是为了找到中间节点的上一个节点 while(fast && fast->next){ slow = slow->next; fast = fast->next->next; }// 这里稍微与之前的代码有些不同,我们最终会找到中间节点的上一个节点 TreeNode *root = new TreeNode(slow->next->val); ListNode *head2 = slow->next->next; slow->next = NULL; root->left = sortedListToBST(head); root->right = sortedListToBST(head2); return root;}
19 在链表上模拟加法运算,比如(2 -> 4 -> 3) + (5 -> 6 -> 5)=7 -> 0 -> 9,链表的头节点为个位,然后是十位……其实模拟的关键就是处理进位,代码如下(略长,但比较直白):
ListNode* addTwoNumbers(ListNode* a1, ListNode* a2) { if(!a1) return a2; if(!a2) return a1; int jinwei = 0; ListNode *head = new ListNode(a1->val + a2->val); if(head->val>=10){ jinwei = 1; head->val = head->val % 10; } a1 = a1->next; a2 = a2->next; ListNode *p = head; while(a1 && a2){ int sum = a1->val+a2->val+jinwei; if(sum>=10){ jinwei = 1; p->next = new ListNode(sum%10); p = p->next; } else{ jinwei = 0; p->next = new ListNode(sum); p = p->next; } a1 = a1->next; a2 = a2->next; } if(a1==NULL && a2==NULL){ // a1与a2等长,只处理最后的进位即可 if(jinwei==1) p->next = new ListNode(1); return head; } if(a1!=NULL){ //a1更长 while(a1!=NULL || jinwei==1){ if(a1==NULL){ p->next = new ListNode(1); return head; } int sum = a1->val + jinwei; if(sum>=10){ jinwei = 1; p->next = new ListNode(sum%10); p = p->next; } else{ jinwei = 0; p->next = new ListNode(sum); p = p->next; } a1 = a1->next; } } if(a2!=NULL){ // a2更长 while(a2!=NULL || jinwei==1){ if(a2==NULL){ p->next = new ListNode(1); return head; } int sum = a2->val + jinwei; if(sum>=10){ jinwei = 1; p->next = new ListNode(sum%10); p = p->next; } else{ jinwei = 0; p->next = new ListNode(sum); p = p->next; } a2 = a2->next; } } return head;}
- 链表的经典技巧及算法
- 二叉树的经典技巧及算法 I
- 经典的链表算法
- 经典的文件拷贝函数及算法
- 经典的双向链表排序算法。
- 有关链表的经典算法
- 解密CSS Sprites的使用技巧及经典实例
- 【经典】MySQL索引背后的数据结构及算法原理
- 经典聚类算法及在互联网的应用
- MySQL索引背后的数据结构及算法原理(经典)
- 经典算法归并排序的分析及PHP实现
- 5个经典的C语言基础算法及代码
- 经典排序算法的详细分析及C++实现代码
- 经典算法实现及思想
- 递归算法及经典案例
- 经典算法试题及答案
- 【经典算法】:链表翻转
- 批处理文件及LINUX经典技巧使用
- leetcode 191 Number of 1 Bits(难易度:Easy)
- 20150607的总结--纪念高考
- 剪花布条
- 1629 - Cake slicing
- 如何将编辑好的TXT转换为PDF文件
- 链表的经典技巧及算法
- centOS 源的更新
- C++多线程编程入门(转)
- Javascript:谈谈JS的全局变量跟局部变量
- SolrCloud功能和架构 SolrCloud Features and Architecture
- 接口和抽象类
- Undefined symbols for architecture x86_64/armv7 解决方法
- 放弃一切
- DOM