上一篇博文描述了一小部分单链表的算法,这篇将继续深入,看看还有哪些面试中常见的链表题目。
题目6: 合并两个有序链表,元素升序。
ListNode *mergeTwoLists(ListNode *l1, ListNode *l2)
{
if(l1==NULL) return l2;
if(l2==NULL) return l1;
ListNode *head = (l1 -> val) > (l2->val) ? l2:l1;
ListNode *remain = (l1 -> val) > (l2->val) ? l1:l2;
ListNode *tmp;
ListNode *tail = head;
while(tail != NULL&&tail -> next != NULL && remain != NULL)
{
if( tail -> val <= remain-> val && tail-> next -> val >= remain -> val)
{
tmp = remain;
remain = remain -> next;
tmp -> next = tail -> next;
tail -> next = tmp;
tail = tail -> next;
} else {
tail = tail -> next;
}
}
if(remain!=NULL) tail -> next = remain;
return head;
}
题目7: 链表的归并排序 O(nlogn)
基于上述合并两个已排序链表的操作,我们可以引入链表的归并排序代码:
int getListLen(ListNode *head) //get how many nodes in the list starting from head.
{
int i = 0;
while( head != NULL)
{
i++;
head = head -> next;
}
return i;
}
//The main function which deals with merge sort.
ListNode *mergeSort(ListNode *&head, int start, int end)
{
ListNode *tmp = head;
if( start == end)
{
head = head -> next;
tmp -> next = NULL;
return tmp;
}
if( start > end)
return NULL;
int mid = start + (end - start ) / 2;
ListNode *lhead = mergeSort(head, start, mid);
ListNode *rhead = mergeSort(head,mid+1,end);
return mergeTwoLists(lhead,rhead);
}
// The sort interface.
ListNode *sortList(ListNode *head) {
int len = getListLen(head);
return mergeSort(head,0,len-1);
}
我们注意到在进行链表的归并排序的过程中,我们利用引用来改变head的值,那么调用过mergeSort之后,head将指向未排序的另一半。
题目8: 检测链表是否存在环路
检测链表环路的方法类似于龟兔赛跑,如果跑道是环行的,那么龟兔必然在某一点相遇,利用这一特性,我们以写出如下代码。
bool hasCycle(ListNode *head)
{
int result = false;
ListNode *sp = head;
ListNode *fp = head;
while(fp!=NULL && fp -> next != NULL)
{
fp = fp ->next -> next;
sp = sp -> next;
if(fp == sp)
return true;
}
return false;
}
题目9: 找出带环链表中环的起点
这道题有些tricky的地方,需要用到一点点数学知识来分析它。
前面一题我们已经看到,当快指针与慢指针相遇时,快指针走过的距离是慢指针的2倍,分别标记为DF和DS,DF=2DS。
假设链表头至环路起点距离为D1,慢指针在环路中走过的距离为D2,环路总长度为DL则可以得出以下关系式:
2(D1+D2) = D1+D2+nDL --> D1=(n-1)DL + DL-D2;
那么可以看到当将某一指针A放在开头,而将另一指针B放在两指针相遇的位置,两指针以相的速度向前移动,则A走过D1的距离后,B走过多次环路与DL-D2(即初次相遇点到环路起点的距离),他们再次相遇,而该相遇到点即为环路起点。
ListNode *detectCycleHead(ListNode *head)
{
int result = false;
ListNode *sp = head;
ListNode *fp = head;
while(fp!=NULL && fp -> next != NULL)
{
fp = fp ->next -> next;
sp = sp -> next;
// we need to keep the step of the two pointers to make the algorithm work.
if(fp == sp)
{
//The next step;
sp = head;
while(sp!=fp)
{
sp = sp -> next;
fp = fp -> next;
}
return sp;
}
}
return NULL;
}
题目10: 判断两单链表是否相交
这题是《编程之美》上的,我觉得有两种方法比较巧妙。
解法1: 将其中一个链表首尾相连,然后根据题目8中的算法来验证另一条链表是否存在环路,存在则两链表相交,不存在,则两链表不相交。
解法2:利用单链表的特性,一个结点的出度只能为1,因此两个链表相交,则某一点之后的所有结点应该是一样的,那么最简单的就是遍历两链表找到尾结点,判断尾结点指针是否一致即可。代码如下:
ListNode *getTailNode(struct ListNode *head)
{
while(head->next!=NULL)
{
head = head -> next;
}
return head;
}
bool crossedLinkedList(struct ListNode *head1, struct ListNode *head2)
{
return getTailNode(head1) == getTailNode(head2);
}
题目11: 若两链表相交,求两链表的交点
解法1:这道题同样可以参考题目9的方法来找到环路的起点即为相交的起点。
解法2:利用hash map, 由其中一个链表构造hash map,然后从另一个链表开始逐个检查每一个结点是否在hash map中,第一个存在的即为交点。引入O(n)的空间复杂度。
解法3:有些tricky,即假设两个链L1和L2表长度分别是 A,B,并假设A>=B,那么我们可以求得差值 D=A-B, 那么我们首先将L1移动D个结点,然后同时将L1,L2向后遍历并比较结点指针是否一致,如果一致则中断循环,该结点即为首个相交点。该解法巧妙地利用了两个链表的长度差,以获得两个链表距离尾部距离相同的起点,该起点也即跑离相交点相同的起点,因为单链表相并,后面的结点都是一样的,依此结点开始逐个判断即可得到相交结点的位置。代码如下:
ListNode *getFirstCrossingNode(ListNode *head1, ListNode *head2)
{
int A = getListLen(head1);
int B = getListLen(head2);
ListNode *longhead = (A>=B)?head1:head2;
ListNode *shorthead = (A=B)?A-B:B-A;
while(D>0)
{
longhead = longhead->next;
D--;
}
while(longhead!=NULL && shorthead!=NULL && longhead!=shorthead)
{
longhead = longhead->next;
shorthead = shorthead -> next;
}
return (longhead==shorthead)?longhead:NULL;
}
题目12: Linux 内核链表的实现
我们一般看到的链表形式是这样的: