链表问题在面试过程当中也是很重要也很基础的一部分,链表自己很灵活,很考查编程功底,因此是很值得考的地方。我将复习过程当中以为比较好的链表问题整理了下。面试
下面是本文所要用到链表节点的定义:算法
struct Node{ int data; Node* next; };
题目描述:给定链表的头指针和一个节点指针,在O(1)时间删除该节点。[Google面试题]编程
分析:本题与《编程之美》上的「从无头单链表中删除节点」相似。主要思想都是「狸猫换太子」,即用下一个节点数据覆盖要删除的节点,而后删除下一个节点。可是若是节点是尾节点时,该方法就行不通了。数据结构
代码以下:(此代码不适合要删除的结点是尾结点)oop
Node *deleteNode(Node *head,Node *p) { assert(head!=NULL); assert(p->next!=NULL); Node* post=p->next; p->data=post->data; p->next=post->next; delete post; }
题目描述:输入一个单向链表,输出逆序反转后的链表post
分析:链表的转置是一个很常见、很基础的数据结构题了,非递归的算法很简单,用三个临时指针 pre、head、next 在链表上循环一遍便可。递归算法也是比较简单的,可是若是思路不清晰估计一时半会儿也写不出来吧。spa
下面是循环版本版本的链表转置代码:3d
void reverseList(Node *&head) { if(head==NULL) return; Node *q=head->next;
head->next=NULL; while(q) { Node *p=q->next; q->next=head; head=q; q=p; } return head; }
题目描述:输入一个单向链表,输出该链表中倒数第k个节点,链表的倒数第0个节点为链表的尾指针。指针
分析:设置两个指针 p一、p2,首先 p1 和 p2 都指向 head,而后 p2 向前走 k 步,这样 p1 和 p2 之间就间隔 k 个节点,最后 p1 和 p2 同时向前移动,直至 p2 走到链表末尾。code
代码以下:
Node* findK(Node* head,int k) { if(head==NULL||k<=0) return NULL; int count=0; Node *p=head; Node *q=head; while(q!=NULL&&count<k) { q=q->next; count++; } if(count<k) return NULL; while(q) { p=p->next; q=q->next; } return p; }
题目描述:求链表的中间节点,若是链表的长度为偶数,返回中间两个节点的任意一个,若为奇数,则返回中间节点。
分析:此题的解决思路和第3题「求链表的倒数第 k 个节点」很类似。能够先求链表的长度,而后计算出中间节点所在链表顺序的位置。可是若是要求只能扫描一遍链表,如何解决呢?最高效的解法和第3题同样,经过两个指针来完成。用两个指针从链表头节点开始,一个指针每次向后移动两步,一个每次移动一步,直到快指针移到到尾节点,那么慢指针便是所求。
代码以下:
//求链表的中间节点 Node* theMiddleNode(Node *head) { if(head == NULL) return NULL; Node *slow,*fast; slow = fast = head; //若是要求在链表长度为偶数的状况下,返回中间两个节点的第一个,能够用下面的循环条件 //while(fast && fast->next != NULL && fast->next->next != NULL) while(fast != NULL && fast->next != NULL) { fast = fast->next->next; slow = slow->next; } return slow; }
题目描述:输入一个单向链表,判断链表是否有环?
分析:经过两个指针,分别从链表的头节点出发,一个每次向后移动一步,另外一个移动两步,两个指针移动速度不同,若是存在环,那么两个指针必定会在环里相遇。
代码以下:
//判断单链表是否存在环,参数circleNode是环内节点,后面的题目会用到 bool hasCircle(Node *head,Node *&circleNode) { Node *slow,*fast; slow = fast = head; while(fast != NULL && fast->next != NULL) { fast = fast->next->next; slow = slow->next; if(fast == slow) { circleNode = fast; return true; } } return false; }
题目描述:输入一个单向链表,判断链表是否有环。若是链表存在环,如何找到环的入口点?
解题思路: 由上题可知,按照 p2 每次两步,p1 每次一步的方式走,发现 p2 和 p1 重合,肯定了单向链表有环路了。接下来,让p2回到链表的头部,从新走,每次步长不是走2了,而是走1,那么当 p1 和 p2 再次相遇的时候,就是环路的入口了。
为何?:假定起点到环入口点的距离为 a,p1 和 p2 的相交点M与环入口点的距离为b,环路的周长为L,当 p1 和 p2 第一次相遇的时候,假定 p1 走了 n 步。那么有:
p1走的路径: a+b = n
;
p2走的路径: a+b+k*L = 2*n
; p2 比 p1 多走了k圈环路,总路程是p1的2倍
根据上述公式能够获得 k*L=a+b=n
显然,若是从相遇点M开始,p1 再走 n 步的话,还能够再回到相遇点,同时p2从头开始走的话,通过n步,也会达到相遇点M。
显然在这个步骤当中 p1 和 p2 只有前 a 步走的路径不一样,因此当 p1 和 p2 再次重合的时候,必然是在链表的环路入口点上。
代码以下:
//找到环的入口点 Node* findLoopPort(Node *head) { Node *slow,*fast; slow = fast = head; //先判断是否存在环 while(fast != NULL && fast->next != NULL) { fast = fast->next->next; slow = slow->next; if(fast == slow) break; } if(fast != slow) return NULL; //不存在环 fast = head; //快指针从头开始走,步长变为1 while(fast != slow) //二者相遇即为入口点 { fast = fast->next; slow = slow->next; } return fast; }
题目描述:给出两个单向链表的头指针(以下图所示),
好比h一、h2,判断这两个链表是否相交。这里为了简化问题,咱们假设两个链表均不带环。
解题思路:
直接循环判断第一个链表的每一个节点是否在第二个链表中。但,这种方法的时间复杂度为O(Length(h1) * Length(h2))。显然,咱们得找到一种更为有效的方法,至少不能是O(N^2)的复杂度。
针对第一个链表直接构造hash表,而后查询hash表,判断第二个链表的每一个节点是否在hash表出现,若是全部的第二个链表的节点都能在hash表中找到,即说明第二个链表与第一个链表有相同的节点。时间复杂度为为线性:O(Length(h1) + Length(h2)),同时为了存储第一个链表的全部节点,空间复杂度为O(Length(h1))。是否还有更好的方法呢,既可以以线性时间复杂度解决问题,又能减小存储空间?
转换为环的问题。把第二个链表接在第一个链表后面,若是获得的链表有环,则说明两个链表相交。如何判断有环的问题上面已经讨论过了,但这里有更简单的方法。由于若是有环,则第二个链表的表头必定也在环上,即第二个链表会构成一个循环链表,咱们只须要遍历第二个链表,看是否会回到起始点就能够判断出来。这个方法的时间复杂度是线性的,空间是常熟。
进一步考虑“若是两个没有环的链表相交于某一节点,那么在这个节点以后的全部节点都是两个链表共有的”这个特色,咱们能够知道,若是它们相交,则最后一个节点必定是共有的。而咱们很容易能获得链表的最后一个节点,因此这成了咱们简化解法的一个主要突破口。那么,咱们只要判断两个链表的尾指针是否相等。相等,则链表相交;不然,链表不相交。
因此,先遍历第一个链表,记住最后一个节点。而后遍历第二个链表,到最后一个节点时和第一个链表的最后一个节点作比较,若是相同,则相交,不然,不相交。这样咱们就获得了一个时间复杂度,它为O((Length(h1) + Length(h2)),并且只用了一个额外的指针来存储最后一个节点。这个方法时间复杂度为线性O(N),空间复杂度为O(1),显然比解法三更胜一筹。
解法四的代码以下:
//判断两个链表是否相交 bool isIntersect(Node *h1,Node *h2) { if(h1 == NULL || h2 == NULL) return false; //异常判断 while(h1->next != NULL) { h1 = h1->next; } while(h2->next != NULL) { h2 = h2->next; } if(h1 == h2) return true; //尾节点是否相同 else return false; }
题目描述:上面的问题都是针对链表无环的,那么若是如今,链表是有环的呢?上面的方法还一样有效么?
分析:若是有环且两个链表相交,则两个链表都有共同一个环,即环上的任意一个节点都存在于两个链表上。所以,就能够判断一链表上俩指针相遇的那个节点,在不在另外一条链表上。
代码以下:
//判断两个带环链表是否相交 bool isIntersectWithLoop(Node *h1,Node *h2) { Node *circleNode1,*circleNode2; if(!hasCircle(h1,circleNode1)) //判断链表带不带环,并保存环内节点 return false; //不带环,异常退出 if(!hasCircle(h2,circleNode2)) return false; Node *temp = circleNode2->next; while(temp != circleNode2) { if(temp == circleNode1) return true; temp = temp->next; } return false; }
题目描述:若是两个无环单链表相交,怎么求出他们相交的第一个节点呢?
分析:采用对齐的思想。计算两个链表的长度 L1 , L2,分别用两个指针 p1 , p2 指向两个链表的头,而后将较长链表的 p1(假设为 p1)向后移动L2 - L1
个节点,而后再同时向后移动p1 , p2,直到 p1 = p2
。相遇的点就是相交的第一个节点。
代码以下:
//求两链表相交的第一个公共节点 Node* findIntersectNode(Node *h1,Node *h2) { int len1 = listLength(h1); //求链表长度 int len2 = listLength(h2); //对齐两个链表 if(len1 > len2) { for(int i=0;i<len1-len2;i++) h1=h1->next; } else { for(int i=0;i<len2-len1;i++) h2=h2->next; } while(h1 != NULL) { if(h1 == h2) return h1; h1 = h1->next; h2 = h2->next; } return NULL; }
能够发现,在链表的问题中,经过两个的指针来提升效率是很值得考虑的一个解决方案,因此必定要记住这种解题思路。记住几种典型的链表问题解决方案,不少相似的题目均可以转换到熟悉的问题再解决。