链表典型题目分析
链表的题目刷完一个月了,才把这篇博客写出来,算是非常拖延了。希望大家能做一道题记录一道,否则像我写这篇的时候好几道题都要重新去梳理思路,应该趁热打铁,把最好的思路记录下来。
题目从[200道高频leetcode题](https://github.com/CyC2018/CS-Notes/blob/master/notes/Leetcode 题解 - 目录.md/)中选取的链表部分10道题,链表的基础都涉及到了,例如反转,删除节点,双指针等一些常用到的思路和操作。10道题的难度多数是easy,两三道是medium。我在面试中第二题和第三题都被问过,对于像我这样几乎是新手的来说,这些题目非常典型,值得一做。我给出的题解思路都是我认为比较基础易懂的,不涉及递归等复杂的思路,目的是希望大家一遍就能看懂,每道题都能掌握一种最基础的解法,所有代码都用C++编写,下面就一道一道来分析吧。
-
思路:找到两个链表的交点,需要两个链表各给一个指针,并让他们指向同一个节点。两个链表区别在于前半部分长度(可能)不同,因此只要让两个指针分别把两个链表前半部分都遍历一次,最后两个指针必然会在第一个共同节点交汇。
ListNode * getInterSectionNode(ListNode * headA,ListNode * headB){ ListNode * p1 = headA; ListNode * p2 = headB; while(p1!=p2){ p1=(p1->next==nullptr)?headB:p1->next; p2=(p2->next==nullptr)?headA:p2->next; } return p1; }
-
方法1:头插法
思路:头插法是反转链表最常用,也是比较易于理解的一种方法。该方法从头到尾遍历,让每个节点的next指针指向前一个节点,来完成链表的反转,具体实现思路如下。
我们需要三个ListNode指针作为辅助,分别是pre(前驱指针),tmp(临时点指针),cur(当前点指针)。循环遍历链表,完成如下操作:
(1)让临时节点指向当前节点的下一个节点
(2)当前点的next指向前驱节点
(3)前驱指针指向当前节点
(4)当前节点指向临时节点ListNode * reverseList(ListNode * head){ ListNode * pre=nullptr; ListNode * tmp=nullptr; ListNode * cur = head; while(cur!=nullptr){ tmp=cur->next; cur->next=pre; pre=cur; cur=tmp; } return pre; }
我当时做过一次结果面试被问到了,愣是没写出来,因为当时理解的不够深刻。后来这道题反复做反复理解反复动手画图,总结出来三个理解记忆的tips给大家:
(1)先把三个指针的定义和初始化做好。
(2)头插法最重要的是先复制当前点的下一个节点,不要急于改变next指针的指向。
(3)while循环里4行代码是首尾相接的(上一行等号右侧变量名与下一行等号左侧变量名对应),如果你暂时对算法理解不够透彻,记住这点也能帮助你写出代码。 -
思路:这个题思路比较简单,两个链表给两个指针从头开始遍历,比较两个指针当前指向的节点的值,将值小的添加进新的链表,并将值小的节点的指针向后移动,直到某一个链表先被遍历完(即两个指针中有一个为空),再把另一个不为空的指针指向的节点以及后面连接的所有节点添加到新链表的末尾。
技巧:(1)这是我第一次遇到的返回整个链表的题,因此必须给新链表首先确定一个头指针head并初始化,再创建一个新的指针ret指向头指针。这样即使头指针不断的添加新节点,ret始终不会移动,ret->next就指向新链表的第一个节点,就能返回我们新生成的链表
(2)如果只剩下一个链表还没遍历完,直接让head->next=p,p指向这个链表的剩余部分,不需要再去逐个节点的遍历。
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
ListNode * head=new ListNode(1);
ListNode *ret=head;
while(l1&&l2){
if(l1->val<l2->val){
head->next=l1;
l1=l1->next;
}
else{
head->next=l2;
l2=l2->next;
}
head=head->next;
}
head->next=(l1==nullptr)?l2:l1;
return ret->next;
}
-
思路:因为是有序链表,只要比较当前点和当前点的next就能保证查到所有的重复元素。
如果当前点和下一个点值相同,删掉下一个点,并让当前点next指针再向后移动一位。
否则就将当前点指针向后移动一位。
ListNode* deleteDuplicates(ListNode* head) { ListNode *cur=head; while(cur&&cur->next){ if(cur->val==cur->next->val){ ListNode * temp=cur->next; cur->next=temp->next; temp=nullptr; } else{ cur=cur->next; } } return head; }
-
思路:如何找到倒数第n个节点?
暴力思路:上来先遍历一趟,求出链表长度,再遍历第二趟的时候找到倒数第n个节点删除掉。
显然这个思路不符合遍历一遍的要求。这时候就要搬出屡试不爽的双指针大法了。
双指针思路:两个指针都指向头节点,让右指针先走n步,然后两个指针同步向后遍历,当右指C++针的next为空时,左指针的next指向的节点就是要删除的元素。
ListNode* removeNthFromEnd(ListNode* head, int n) { ListNode * pr=head; ListNode * pl=head; for (int i=0;i<n;i++) { pr=pr->next; } if(!pr){ ListNode* temp=new ListNode(1); temp->next=pl->next; pl->next=nullptr; return temp->next; } while(pr->next){ pl=pl->next; pr=pr->next; } ListNode*temp=pl->next; pl->next=temp->next; temp->next=nullptr; return head; }
-
思路:在头节点之前定义一个节点p,连接到链表的头部。当p后面还有两个以上节点的时候,就可以进行两两交换。调整指针指向的思路如下图所示,非常的精髓。建议多画图多理解。
ListNode* swapPairs(ListNode* head) {
ListNode * p=new ListNode(1);
p->next=head;
ListNode * rtp=p;
ListNode * temp;
while(p->next&&p->next->next){
temp = p->next;
p->next=temp->next;
temp->next=temp->next->next;
p->next->next=temp;
p=temp;
}
return rtp->next;
}
-
思路1:利用两个栈分别保存两个链表的值。同时从头开始pop两个栈,把这两个pop出来的数值进行相加,再加上上一次相加留下的十位数(第一次pop相加时十位数为0),作为这次的相加结果,个位数放入新链表,十位数保存,在下一次计算时被调用。当所有栈都被pop空了,并且十位数为0,就结束迭代的循环。
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) { stack<int> s1,s2; while(l1){ s1.push(l1->val); l1=l1->next; } while(l2){ s2.push(l2->val); l2=l2->next; } int carry=0; ListNode * ans=nullptr; while(!s1.empty()||!s2.empty()||carry!=0){ int a= s1.empty()?0:s1.top(); int b= s2.empty()?0:s2.top(); if(!s1.empty()) s1.pop(); if(!s2.empty()) s2.pop(); int cur =a+b+carry; carry=cur/10; cur=cur%10; ListNode * temp=new ListNode (cur); temp->next=ans; ans=temp; } return ans; }
-
思路:回文链表特性是,将链表后半部分倒转,与前半部分完全相同。因此首先要将链表分成两半,然后将后半部分倒转,最后再比较这两部分每个节点值是否对应相等。
很明确的分三步实现算法
利用快慢指针,快指针每次向前走两步,慢指针每次向前走一步,当快指针指向空时,慢指针恰好将链表分成两部分。设链表节点个数为n,如果链表节点个数为偶数,慢指针指向第n/2+1个节点;如果n为奇数,则n恰好指向中间节点。
倒转链表的后半部分,实现部分参考本篇博客第一篇文章。
遍历两部分链表,如果完全相同返回true,如果有不同就返回false。
bool isPalindrome(ListNode* head) { if(!head||!head->next) return true; ListNode * slow=head,*fast=head; while(fast&&fast->next){ slow=slow->next; fast=fast->next->next; } ListNode * cur=slow; ListNode * nextNode=slow->next; while(nextNode){ ListNode * temp=nextNode->next; nextNode->next=cur; cur=nextNode; nextNode=temp; } slow->next=nullptr; while(head&&cur){ if(head->val!=cur->val){ return false; } head=head->next; cur=cur->next; } return true; }
-
思路:
分两种情况,一种是节点数小于k,这种情况每个节点单独作为一个链表,剩下的补上空链表。一种是节点数大于k,这种情况要保证前面的链表节点数比后面多,节点多的链表比少的只能多一个节点。
bool isPalindrome(ListNode* head) { if(!head||!head->next) return true; ListNode * slow=head,*fast=head; while(fast&&fast->next){ slow=slow->next; fast=fast->next->next; } ListNode * cur=slow; ListNode * nextNode=slow->next; while(nextNode){ ListNode * temp=nextNode->next; nextNode->next=cur; cur=nextNode; nextNode=temp; } slow->next=nullptr; while(head&&cur){ if(head->val!=cur->val){ return false; } head=head->next; cur=cur->next; } return true; }
-
题意:将链表拆分成两部分,在奇数位置的串成一个链表,偶数位置的串成一个链表,然后再将这两个链表首尾相接,奇链表在前,偶链表在后。
思路:先判断链表长度,如果链表<=1,则不需要聚集,直接返回头节点。否则设置两个指针,奇指针与偶指针,让奇指针指向头节点,偶指针指向链表的第二个节点(偶数位置的第一个节点)。让奇指针的next指向两个位置后的节点,并把奇指针更新到当前位置,偶指针同理,当偶指针为空或偶指针的next为空时,结束迭代。此时已经分别串联好了两个链表,再将奇指针的next指向偶链表的头部,就实现了奇偶聚集的功能。返回值仍然是链表的头节点head。
ListNode* oddEvenList(ListNode* head) { if(!head||!head->next||!head->next->next) return head; ListNode *single=head; ListNode * doub=head->next; ListNode * ndh=doub; while(doub&&doub->next){ single->next=single->next->next; single=single->next; doub->next=doub->next->next; doub=doub->next; } single->next=ndh; return head; }
10道题的博客断断续续也写了好几天,基本都是用一些碎片的时间来写的,写的不好的请多见谅,欢迎您提出宝贵的建议。后续可能会针对这些题目出一些递归方法的解题思路
下几篇打算写一下二叉树题目,题目数量应该不会一次有10道这么多,分开几次把二叉树做完。因为在leetcode上也做了几十道题目了,同时并行的打算把之前做过的题目也分类写出来,便于后面复习。希望我的博客也能帮助到更多的人。
题解的来源有的是学习别人的,有的是自己理解的,做完题过去太久记不清楚了所以没有标明。后面的题解思路和代码会标明来源。