链表问题主要是指针操作需要考虑周全。
链表和数组都是一种线性结构。
数组是一段连续的存储空间。可以直接通过下标获取值。
链表空间不一定保证连续,是临时分配的。只能从链表头部开始一个一个找下去。
链表的分类:
1. 按照连接方向分类:单链表和双链表。
单链表只能通过next指针寻找下一个节点。
双链表有next和previous指针,previous指针可以指向其前一个节点。
2. 按照有环无环分类:普通链表和循环链表。
循环链表是首尾相接。最后节点的next指针指向第一个节点。
而对于循环双链表来说,除了最后节点的next指针指向第一个节点之外,第一个元素的previous指针还要指向最后一个节点。
链表问题代码实现的关键点:
1. 链表调整函数的返回值类型,根据要求往往第节点类型。有时候头结点会被替换改变,这时候就要返回一个新的头结点。
2. 处理链表过程中,最好采用画图方式,查看修改了哪些指针,同时查看前后节点的变化。
3. 链表问题对于边界条件讨论要求严格。比如头结点,尾节点以及空节点,这些都是特殊值。要时刻注意判断节点是否为空。
链表的插入和删除:
1. 特殊处理链表为空,或者链表长度为1的情况。
2. 注意插入操作的调整过程。
插入一个节点的时候一定要先找到插入位置的前一个节点和后一个节点。
删除节点的时候要找到删除节点的前一个节点。
在头尾节点插入删除的时候要注意空节点的情况!!!
双链表的插入删除只是额外要考虑previous指针。
单链表的翻转操作:(链表翻转就是原来的头结点变成尾节点,原来的尾节点变成头结点)
1.当链表为空或者长度为1时 ,特殊处理。
大量链表问题可以使用额外数据结构来简化调整过程,比如栈队列等。
但链表问题最优解往往是不适用额外的数据结构,也就是额外空间复杂度可以为o(1)。
案例一:
给定给一个整num,如何在节点值有序的环形链表中插入一个节点值为num的节点,并且保证这个环形单链表依然有序。
时间复杂度为o(n).,额外空间复杂度为o(1)。
1. 形成值为num的node节点。
2. 如果链表为空,node自己形成环形链表。
3. 如果链表不为空,那么previous和current指针找到插入位置进行插入。
4. 如果previous和current转一圈都没有返现应该插入的位置,此时node应该插入头节点的前面。这种情况一般是
node值比所有的值都大或者node值比所有的值都小。但是这两种情况的返回值是不同的。
当num比所有值都大的时候,返回的应该是原来的头节点。
当num比所有值都小的时候,返回的应该是新节点node。
只有这样才能保证链表有序。
案例二:
给定给一个链表中的节点node,但不给定整个链表的头节点,如何在链表中删除node.要求时间复杂度为o(1).
对于双链表比较简单,注意previous以及next就可以。
但是对于单链表来说,无法通过当前节点找到之前的节点,这时候的删除方式为:
将要删除的节点node的下一个next节点node1的值复制到node上,然后删除node1,但是这种方式无法删除最后一个节点!!!
这种情况下如果要删除最后一个节点,只能将要删除的节点直接变成空,但是这是不对的,因为null在系统中是特定的一个区域,要让node变成空,
必须要找到node的前一个节点!
上面的删除方式并不是删除了该删除的节点,而是进行了值的拷贝。
1. 结构复杂且拷贝操作受限时,不可行。
2. 在工程上回影响外部依赖。
案例三:给定一个链表的头节点head,再给定一个数num,请把链表调整成节点值小于num的节点都放在链表左边,值等于num的节点都放在链表的中间,值大于num的节点,都放在链表的右边。
简单做法:
1. 将链表的所有节点都放入到数组中,然后将数组进行快排划分的调整过程。
2. 然后将数组中的节点一次重新串连。
最优解:
不需要任何额外的空间,把所有节点划分成三个小链表:分别是小于num的节点所在的链表,等于num的节点所在的链表,以及最后大于num的节点所在的链表。最后整体连接起来就可以了。相比于上面的简单做法,这种方法虽然不需要额外的空间,但是需要谨慎,更注重代码实现能力。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class Divide {
public:
ListNode* listDivide(ListNode* head, int val) {
// write code here
ListNode* right=NULL;//这是右边大于num的链表的尾节点
ListNode* left=NULL;//这是左边小于num的链表的尾节点
ListNode* lhead=NULL;//这是左边小于num的链表的头节点
ListNode* rhead=NULL;//这是右边大于num的链表的头节点
ListNode* next;//这是作为当前遍历到的节点
while(head!=NULL){//既然是链表就应该保证其中的节点的前一个节点只有一个,所以在程序中应该时刻保证current的next为空,否则的话会出现多个节点的next节点是同一个节点的
next=head->next;
head->next=NULL;//这一步非常关键
if(head->val<=val){
if(left!=NULL){
left->next=head;
left=head;
}else{
left=head;
lhead=head;
}
}
if(head->val>val){
if(right!=NULL){
right->next=head;
right=head;
}else{
right=head;
rhead=head;
}
}
if(next!=NULL){
head=next;
}else{
break;
}
}
if(lhead!=NULL)
left->next=rhead;
if(lhead!=NULL)
return lhead;
else if(rhead!=NULL)
return rhead;
else
return NULL;
}
};
对于上面的程序,最最重要的就是要时刻将当前遍历节点的next指定为NULL,否则的话会导致程序报错为超出内存的使用限制。个人理解为链表应该时刻保证每个节点的前一个节点的数量只有一个,也就是说如果我们不将当前节点的next设置为NULL的话,会导致有多个节点的next指向同一个节点,那么该节点的前一个节点就不止一个了。
案例四:给定两个有序链表的头节点head1和head2,打印两个有序链表的公共部分。
如果两个链表有任何一个为空,直接返回即可。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class Common {
public:
vector<int> findCommonParts(ListNode* headA, ListNode* headB) {
// write code here
ListNode* temp1;
ListNode* temp2;
ListNode* head;
vector<int> result;
temp1=headA;
temp2=headB;
while(temp1!=NULL&&temp2!=NULL){
if(temp1->val<temp2->val){
if(temp1->next!=NULL)
temp1=temp1->next;//注意凡是要用到next的都要先判断是否为空
else
break;
}
else if(temp1->val>temp2->val){
if(temp2->next!=NULL)
temp2=temp2->next;
else
break;
}
else if(temp1->val==temp2->val)
{
result.push_back(temp1->val);
if(temp1->next!=NULL&&temp2->next!=NULL){
temp1=temp1->next;
temp2=temp2->next;
}else
break;
}
}
return result;
}
};
需要注意的是上面的要求并不是说打印连续的公共部分,而是打印出两个链表中的相同的值。
案例五:给定给一个单链表的头节点head,实现一个调整单链表的函数,使得每K个节点之间逆序,如果最后不够K个节点一组,则不调整最后几个节点。
如果链表为空,或长度为1,或k<2,链表不用进行调整。
方法一:时间复杂度为O(n),额外空间复杂度为o(k)。
方法二:时间复杂度为o(n),额外空间复杂度为o(1)。
方法一:利用栈,每次都是k个元素进栈,然后出栈,最后出栈的元素的顺序肯定都是逆序的!但是需要注意的是,如果最后元素的数量不足k个,那么就需要特殊处理。
并且组与组之间的链接一定要注意,需要记录上一组中的最后一个元素的指针,方便下一组的元素与上一组进行链接。同时第一组需要特殊处理,因为第一组中没有上一组,所以不需要和上一组的元素进行重连。最后返回的头节点会发生改变,不是原来的头节点了。
方法二:方法二基本过程与方法一类似。依然是每收集k个元素就做逆序调整。需要更多的边界讨论以及代码实现技巧。不需要栈。需要记录每一组的第一个节点,方便之后满k个节点之后进行逆序。同时,也要注意逆序之后与上一组进行链接!
关键是反转了K个结点之后,要将它与前面的已有链表连接起来,即将k个结点的前一个结点pleft与链表的尾结点pend连接起来,然后将链表头结点pstart与k个结点的下一个结点pright连接起来,从而完成链表中局部K个结点的反转。
对于原始链表来说,需要不断隔离出K个结点,对其进行逆序然后再连接到原始链表上面,总是需要保留k个结点之前的结点pleft以及k个结点之后的结点pright。
特殊地:
对于K=0,1时不需要逆序,直接返回原来链表的头结点head即可;
对于第一组的k个结点,反转后的head变成pend,最终返回的是pend,pstart与下一组k个结点的第一个结点pstart进行连接。
/*struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class KInverse {
public:
ListNode* inverse(ListNode* head, int k) {
// write code here
int count=0;
ListNode* current=head;
ListNode* start=head;
ListNode* left=NULL;
ListNode* pre;
while(current!=NULL){
if(count==k){
if(left==NULL){
Listinverse(left,current,start,pre);
head=pre;
left=start;
start=current;
count=0;
}else{
Listinverse(left,current,start,pre);
left=start;
start=current;
count=0;
}
}else{
pre=current;
current=current->next;
count++;
}
}
if(count==k){
Listinverse(left,current,start,pre);
if(start==head)//假如正好链表长度为k,那么上面的循环结束之后,并不会进入到count==k这里面,所以需要在while循环外面加上if(count==k),并且此时head
//一定更要记得修改,否则,此时head还是原来的head,就错了!!!
head=pre;
}
return head;
}
void Listinverse(ListNode* left,ListNode* right,ListNode* start,ListNode* end){
ListNode* pre=start;
ListNode* cur=start->next;
ListNode* next=NULL;
pre->next=right;
while(cur!=right){
next=cur->next;
cur->next=pre;
pre=cur;
cur=next;
}
if(left!=NULL)
left->next=pre;
}
};
对于上面的程序来说,写成两个函数比较好,逻辑比较清晰!并且我是先写第二个函数的!!
案例六:给定给一个单链表的头节点head,链表中每个节点保存一个整数,再给定一个值val,把所有等于val的节点删掉。
需要时刻注意的是,接在最后的节点的next指针为null。一开始初始化head和tail指针,并且这两个指针都是null。然后第一个接上的节点既是head也是tail。并且tail节点在整个过程中都是没有next指针的。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class ClearValue {
public:
ListNode* clear(ListNode* head, int val) {
// write code here
ListNode* first=head;
ListNode* tail=head;
ListNode* current=head;
bool flag=true;
while(current!=NULL){
if(current->val!=val&&flag==true){//对于遇到的第一个不是val的值来说
first=current;
tail=first;
current=current->next;//这句话放在前面比较好,因为如果放在后面可能会找不到next,因为下面tail->next和这里的next其实是同一个值,它赋值为NULL了
tail->next=NULL;
flag=false;
}else if(current->val!=val&&flag==false){//对于遇到的其他的不是val的值来说
tail->next=current;
tail=current;
current=current->next;//这句话一定要放在下面那句话的前面,因为否则会导致current的next为NULL!!
tail->next=NULL;//这句话是一定更要写的,因为假如不写的话,最后可能最后的值都是和val相等的,那么就都连接到tail上面了。
}else{
current=current->next;
continue;
}
}
return first;
}
};
还有一种老师的用java写的解法,感觉更简洁,但是时间复杂度感觉是一样的:
import java.util.*; /* public class ListNode { int val; ListNode next = null; ListNode(int val) { this.val = val; } }*/ public class ClearValue { public ListNode clear(ListNode head, int num) { while (head != null) { if (head.val != num) { break; } head = head.next; } ListNode pre = head; ListNode cur = head; while (cur != null) { if (cur.val == num) { pre.next = cur.next; } else { pre = cur; } cur = cur.next; } return head; } }
案例七:判断一个链表是否为回文结构。
一共有三种实现方式。
方法一:时间复杂度为o(n),使用了n个额外的空间。
方法二:时间复杂度为o(n),使用了n/2个额外的空间。
方法三:时间复杂度为o(n),使用了o(1)空间复杂度。
也就是说方法一和方法二都是空间复杂度为o(n)。
方法一:
利用栈结构,先将链表里面的元素全部都入栈,这时候再将栈中元素出栈,这时候我们知道,栈中元素出站的顺序正好的链表中元素的逆序,所以对于栈中出的每一个元素都要和链表中的元素进行比较,一直到最后,一旦出现任何一个值不同的,那么就返回false。
方法二:
也利用栈,但是用快和慢两个指针进行遍历,快指针一次走两步,慢指针一次走一步。慢指针遍历时将遍历过的节点压入到栈中,那么当快正真走完整个链表的时候,慢指针正好走到链表中间的位置,同时从栈顶到栈底的顺序是链表左部分的逆序。此时如果链表的长度为奇数,那么就不把中间的节点压入到栈中。接下来慢指针继续遍历,栈也跟着进行元素的弹出,而且对比此时栈中弹出的节点和慢指针所指的节点是否相等,如果相等就继续,否则就结束,说明不是回文。这种方法的思想类似于将整个链表进行对折然后进行比较。这里面的快指针只是一个标志,标志着满指针什么时候走到链表的一半。
方法三:前半部分的思路是方法二一样,都是找到链表中间的节点,然后将链表右边进行逆序的调整,
接下来从链表的两头开始,依次进行对比,如果对比到中间位置都是一样的,那么说明链表是回文结构。
但是最后的结构不管是不是回文结构,链表在进行返回的时候都需要将链表右半部分的顺序进行恢复。也就是把链表的结构调整过来。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class Palindrome {
public:
bool isPalindrome(ListNode* pHead) {
// write code here
ListNode* low=pHead;
ListNode* quick=pHead;
//bool flag=true;//如果最后flag为true,代表有奇数个节点,且low指向的是中间的节点
while(quick->next!=NULL){
low=low->next;
quick=quick->next;
if(quick->next!=NULL)
quick=quick->next;
else{
//flag=false;//说明有偶数个节点,此时low指向的是右半部分的第一个节点
break;
}
}
ListNode* middle=low;
//下面是将右半部分逆序
ListNode* pre=low;
low=low->next;
ListNode* next;
while(low!=NULL){
next=low->next;
low->next=pre;
pre=low;
low=next;
}
ListNode* current=pHead;
while(current!=middle){
if(current->val==pre->val)
{
current=current->next;
pre=pre->next;
}else{
//这里应该加上将上面的链表的右半部分恢复原来的顺序,而不是逆序
return false;
}
}
return true;
}
};
上面的程序缺少将逆序的部分恢复的过程!!
下面是老师的程序:
import java.util.*; /* public class ListNode { int val; ListNode next = null; ListNode(int val) { this.val = val; } }*/ public class Palindrome { public boolean isPalindrome(ListNode head) { if (head == null || head.next == null) { return true; } ListNode n1 = head; ListNode n2 = head; while (n2.next != null && n2.next.next != null) { // find mid node n1 = n1.next; // n1 -> mid n2 = n2.next.next; // n2 -> end } n2 = n1.next; // n2 -> right part first node n1.next = null; // mid.next -> null ListNode n3 = null; while (n2 != null) { // right part convert n3 = n2.next; // n3 -> save next node n2.next = n1; // next of right node convert n1 = n2; // n1 move n2 = n3; // n2 move } n3 = n1; // n3 -> save last node n2 = head;// n2 -> left first node boolean res = true; while (n1 != null && n2 != null) { // check palindrome if (n1.val != n2.val) { res = false; break; } n1 = n1.next; // left to mid n2 = n2.next; // right to mid } n1 = n3.next; n3.next = null; while (n1 != null) { // recover list n2 = n1.next; n1.next = n3; n3 = n1; n1 = n2; } return res; } }对于偶数和节点来说,n2位于右半部分第一个节点;对于奇数个节点来说,n2位于中间节点的右边第一个节点。对于上面的程序来说,最重要的是最后它对链表的顺序进行了恢复!!用res保存最后的结果!!同时应该注意开头对边界条件的处理!
参考这个程序对自己的程序进行了修改,具体为:
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class Palindrome {
public:
bool isPalindrome(ListNode* pHead) {
// write code here
ListNode* low=pHead;
ListNode* quick=pHead;
//bool flag=true;//如果最后flag为true,代表有奇数个节点,且low指向的是中间的节点
while(quick->next!=NULL){
low=low->next;
quick=quick->next;
if(quick->next!=NULL)
quick=quick->next;
else{
//flag=false;//说明有偶数个节点,此时low指向的是右半部分的第一个节点
break;
}
}
ListNode* last=quick;//保存的是最后一个节点
ListNode* middle=low;
//下面是将右半部分逆序
ListNode* pre=low;
low=low->next;
ListNode* next;
while(low!=NULL){
next=low->next;
low->next=pre;
pre=low;
low=next;
}
middle->next=NULL;//中间节点的next为NULL
ListNode* current=pHead;
bool res=true;
while(current!=middle){
if(current->val==pre->val)
{
current=current->next;
pre=pre->next;
}else{
//这里应该加上将上面的链表的右半部分恢复原来的顺序,而不是逆序
//return false;
res=false;
break;
}
}//下面是对链表顺序进行恢复
current=last->next;
next=NULL;
pre=last;
while(pre!=middle){
next=current->next;
current->next=pre;
pre=current;
current=next;
}
return res;
}
};
案例八:
一个链表结构中,每个节点不仅含有一条指向下一个节点的next指针,同时含有一条rand指针,rand指针可能指向任何一个链表中的节点,请复制这种含有rand指针节点的链表。
下面介绍的方法不需要使用任何额外的数据结构。
首先如果链表的长度为0或者为NULL,则直接返回NULL;
然后从链表的头指针开始向下遍历,遍历的过程中,将当前的节点拷贝到当前节点和下一个节点之间;
然后再从头开始遍历链表,这回一下取出两个节点,也就是当前节点和它的拷贝节点;
然后通过当前节点的rand指针可以找到它的rand节点n,然后将当前节点的拷贝节点的rand指向n的拷贝节点就可以了;
最后将整个链表分流成两个链表就可以了!
/*
struct RandomListNode {
int label;
struct RandomListNode *next, *random;
RandomListNode(int x) :
label(x), next(NULL), random(NULL) {
}
};
*/
class Solution {
public:
RandomListNode* Clone(RandomListNode* pHead)
{
if(pHead==NULL)
return NULL;
RandomListNode* current=pHead;
RandomListNode* next;
while(current!=NULL){
RandomListNode* newnode=new RandomListNode(current->label);
next=current->next;
current->next=newnode;
newnode->next=next;
current=newnode->next;
}//这部分代码是将每个节点都复制一下并放到该节点的后面
current=pHead;
while(current!=NULL){
if(current->random!=NULL)
current->next->random=current->random->next;
else
current->next->random=NULL;
current=current->next->next;
}//这部分的代码是将复制节点的random指针进行赋值
current=pHead;
RandomListNode* start=pHead->next;//新链表的头指针
while(current!=NULL){
next=current->next;
current->next=current->next->next;
if(current->next!=NULL)//这里很重要
next->next=current->next->next;//这里一定更要注意,此时current->next已经改变了!!!!
else
next->next=NULL;
current=current->next;
}
return start;
}
};
注意上面的分流的时候,除了要将复制好的链表分离出来,还要保证原来的链表结构不变!!1
案例九:如何判断一个单链表是否有环?有环的话返回进入环的第一个节点,无环的话返回空。如果链表的长度为N,请做到时间复杂度为o(n),额外空间复杂度为o(1).
普通解法没有额外空间复杂度限制的时候可以利用哈希表来实现。第一个重复出现的节点一定共识第一个进入环的节点。没有环的时候第一个入环节点为空,返回空。
额外空间复杂度为o(1)的方法为:
有快慢两个指针,慢指针一次走一步,快指针一次走两步。如果无环,快指针会迅速发现末尾为空的位置,然后返回空即可。
如果有环,慢指针和快指针会在链表的某一处相遇,这之后再将快指针从链表头节点开始,一次走一步,同时慢指针也从当前节点开始一次走一步,然后他们第一次相遇的节点
就是第一个入环节点。
上面这个算法可以用数学方式证明。
总结一下这个问题:给定给一个单链表,只给出头指针h:
1、如何判断是否存在环
使用追赶的方法,设定两个指针slow、fast,从头指针开始,每次分别前进1步、2步。如存在环,则两者相遇;如不存在环,fast遇到NULL退出。
2. 如何知道环的长度
记录下问题1的碰撞点p,slow、fast从该点开始,再次碰撞所走过的操作数就是环的长度s,当然也可以利用slow再次走到p。
3. 如何找到环的连接点在哪里?或者说进入环的第一个节点在哪里?
记住定理:碰撞点p到连接点的距离=头指针到连接点的距离,因此分别从碰撞点和头指针开始走,速度一样,相遇的那个点就是连接点。
4. 带环链表的长度是多少?
用三种已经求出连接点距离头指针的长度,加上问题2中求出的环的长度,二者之和就是带环单链表的长度。
具体证明查看http://blog.csdn.net/sup_heaven/article/details/39896833。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class ChkLoop {
public:
int chkLoop(ListNode* head, int adjust) {
// write code here
if(head==NULL)
return -1;
ListNode* quick=head;
ListNode* low=head;
int flag=0;
while(quick!=NULL){
//不能再这里判断quick和low是否相等 ,因为它们的初始值就是相等的,都是head.
quick=quick->next;
if(quick!=NULL)
quick=quick->next;
else{
flag=-1;
break;
}
low=low->next;
if(quick==low)
break;
}
if(quick==NULL)
flag=-1;
if(flag==-1)
return flag;
else{//quick不为空,且quick==low说明有环
low=head;
while(low!=quick)
{
low=low->next;
quick=quick->next;
}
return low->val;
}
}
};
对于上面的程序来说有点繁琐,可以向下面的程序一样借助于do while循环!!
class
ChkLoop {
public
:
int
chkLoop(ListNode* head,
int
adjust) {
if
(head==NULL)
return
-
1
;
ListNode *quick=head,*slow=head;
do
{
if
(quick==NULL||quick->next==NULL)
return
-
1
;
quick=quick->next->next;
slow=slow->next;
}
while
(quick!=slow);
quick=head;
while
(quick!=slow)
{
quick=quick->next;
slow=slow->next;
}
return
slow->val;
}
};
案例十:如何判断两个无环单链表是否相交?相交的话返回第一个相交的节点,不相交的话返回空。如果两个链表长度分别为N和M,请做到时间复杂度为o(n+m),额外空间复杂度为o(1).
普通方法用哈希表实现,先遍历一个链表,再遍历另一个。但是这是需要额外空间复杂度的。
另一种方式是遍历两个链表,统计两个链表各自的长度m>n。
然后让长度为m的链表先走(m-n)步;
然后两个链表同时走,如果两个链表相交,那么它们共同走得时候一定会共同到达相交的节点;
如果一直不相交,那么返回空。
看看两个链表相交到底是怎么回事吧,有这样的的几个事实:(假设链表中不存在环)
(1)一旦两个链表相交,那么两个链表中的节点一定有相同地址。
(2)一旦两个链表相交,那么两个链表从相交节点开始到尾节点一定都是相同的节点。
对于上面第二个特点我们知道,仔细研究两个链表,如果他们相交的话,那么他们最后的一个节点一定是相同的,否则是不相交的。因此判断两个链表是否相交就很简单了,分别遍历到两个链表的尾部,然后判断他们是否相同,如果相同,则相交;否则不相交。但是这种方式不能得到两个链表的第一个交点。如果想要找到两个链表的第一个交点,最好的方式就是用上面介绍过的。
http://blog.csdn.net/jiary5201314/article/details/50990349这个网址里面与多种不同的解决方式。/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class CheckIntersect {
public:
bool chkIntersect(ListNode* headA, ListNode* headB) {
// write code here
int A=0;
int B=0;
int length=0;
ListNode* a=headA;
ListNode* b=headB;
while(a!=NULL){
A++;
a=a->next;
}
while(b!=NULL){
B++;
b=b->next;
}
if(A>B){
a=headA;
b=headB;
length=A-B;
}else{
a=headB;
b=headA;
length=B-A;
}
int c=1;
while(c<=length){
a=a->next;
c++;
}
while(a!=NULL&&b!=NULL){
if(a==b)
return true;
else{
a=a->next;
b=b->next;
continue;
}
}
return false;
}
};
案例十一:如何判断两个有环单链表是否相交?相交的话返回第一个相交的节点,不相交的话返回空。如果两个链表长度分别为N和M,请做到时间复杂度o(m+n),额外空间复杂度为o(1).
首先根据之前介绍的找到环形单链表第一个入环的题目,找到两个链表各自的入环节点。
如果入环节点是同一个节点,可以肯定两个链表是相交的。但是如果两个有环单链表在入环的时候就已经相交,那么需要找到更早的节点。这样的话就和上面的找两个无环单链表第一个相交节点很类似。只不过上面的无环单链表最后终止条件是null,而这里终止条件是它们共用的入环节点。
另一种情况是两个链表的入环节点不是同一个节点的情况。这种情况下只能是下面所示的两种拓扑结构。分辨这两种结构的方式为:
从第一个链表的入环节点往下走,如果能回到出发位置,说明为结构1;
如果在回到原来的位置之前,遇到了第二个链表的入环节点,说明为结构二。
对于结构1返回空;
对于结构2,返回链表1的入环节点或者链表2的入环节点都是可以的。它们都是链表第一次相交的节点,只不过第一个离链表1更近,第二个离链表2更近。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class ChkIntersection {
public:
bool chkInter(ListNode* head1, ListNode* head2, int adjust0, int adjust1) {
// write code here
//需要分两种情况!第一种是在环外相交
//先找到两个链表的入环节点
ListNode* quick=head1;
ListNode* low=head1;
ListNode* Intersection;
ListNode* Intersection1;//这是第一个链表的入环节点
do{
quick=quick->next->next;
low=low->next;
//这里一定是有环的,所以一定会找到quick==low的时候
}while(quick!=low);
quick=head1;
while(quick!=low){
quick=quick->next;
low=low->next;
}
Intersection1=low;
quick=head2;
low=head2;
ListNode* Intersection2;
do{
quick=quick->next->next;
low=low->next;
}while(quick!=low);
quick=head2;
while(quick!=low){
quick=quick->next;
low=low->next;
}
Intersection2=low;//这是第二个链表的入环节点
if(Intersection2==Intersection1){//入环之前就已经相交了
//统计两个链表到入环节点的长度
quick=head1;
int a=0;
int b=0;//a和b分别是两个链表到入环节点的长度
int length;
while(quick!=Intersection1){
a++;
quick=quick->next;
}
quick=head2;
while(quick!=Intersection2){
b++;
quick=quick->next;
}
if(a>b){
quick=head1;
low=head2;
length=a-b;
Intersection=Intersection1;
}
else{
quick=head2;
low=head1;
length=b-a;
Intersection=Intersection2;
}
int c=1;
while(c<=length){
quick=quick->next;
c++;
}
while(quick!=Intersection){
if(quick!=low){
quick=quick->next;
low=low->next;
}else{
//return low;//这里可以返回相交的节点的
return true;
}
}
return false;
}else{//入环之后才会相交
quick=Intersection1;
do{
quick=quick->next;
if(quick==Intersection2)
return true;
}while(quick!=Intersection1);
return false;
}
//第二种是在环内相交
//其实还有一种个就是不相交
}
};
上面的代码之所以很长是因为可以返回相交的节点;而不仅仅只能返回true或者false。
案例十二:给定两个单链表的头节点head1和head2,如何判断两个链表是否相交?相交的话返回第一个相交的节点,不相交的话返回空。(注意这里一定要先判断是不是有环!!!!)
1. 利用上面的方式找到两个链表各自的入环节点。无环返回NULL,有环返回入环节点。假设返回的入环节点分别为:node1和node2.
2. 如果node1和node2一个为空,一个不为空,那么不可能相交。
3. 如果都为空,说明都无环,可以用上面的方式解决。
4. 如果都不为空,则都有环,可以用上面的方式解决。
/*
struct ListNode {
int val;
struct ListNode *next;
ListNode(int x) : val(x), next(NULL) {}
};*/
class ChkIntersection {
public:
bool chkInter(ListNode* head1, ListNode* head2, int adjust0, int adjust1) {
// write code here
if(head1==NULL||head2==NULL)
return false;
ListNode* quick;
ListNode* low;
quick=head1;
low=head1;
bool flag1=false;//为false代表head1没有环
bool flag2=false;//为false代表head2没有环
ListNode* Intersection1;
ListNode* Intersection2;
int a;
int b;
int c;
int length;
do{
if(quick!=NULL||quick->next!=NULL)//这里由于不知道head1是否有环,所以需要判断,也就是说不一定有quick=low
{
low=low->next;
quick=quick->next->next;
}else{
break;
}
}while(quick!=low);
if(quick==low)
flag1=true;//说明有环
if(flag1==true)//如果有环就要立刻求出入环节点,否则low就会改变
{
quick=head1;
while(quick!=low){
quick=quick->next;
low=low->next;
}
Intersection1=low;//head1的入环节点
}
quick=head2;
low=head2;
do{
if(quick!=NULL||quick->next!=NULL)//这里由于不知道head1是否有环,所以需要判断,也就是说不一定有quick=low
{
low=low->next;
quick=quick->next->next;
}else{
break;
}
}while(quick!=low);
if(quick==low)
flag2=true;//说明有环
if(flag2==true){
quick=head2;
while(quick!=low){
quick=quick->next;
low=low->next;
}
Intersection2=low;//head2的入环节点
}
if((flag1==true&&flag2==false)||(flag2==true&&flag1==false))
return false;
else if(flag1==flag2&&flag1==true){//说明两个链表都有环
if(Intersection1==Intersection2)
return true;
else{
quick=Intersection2;
do{
if(quick==Intersection1)
return true;
else
quick=quick->next;
}while(quick!=Intersection2);
return false;
}
}else //说明两个链表都没有环
{
quick=head1;
a=0;
while(quick!=NULL){
a++;
quick=quick->next;
}
quick=head2;
b=0;
while(quick!=NULL){
b++;
quick=quick->next;
}
if(a>b)
{
length=a-b;
quick=head1;
low=head2;
}else {
length=b-a;
quick=head2;
low=head1;
}
c=1;
while(c<=length){
c=c+1;
quick=quick->next;
}
while(quick!=NULL&&low!=NULL){
if(quick==low)
return true;
else{
quick=quick->next;
low=low->next;
}
}
return false;
}
}
};
上面的程序中,可以把确定链表是否有环以及求入环点的部分合并起来放到一个函数中,返回值就是入环点,如果没有环,返回的入环点值为NULL,否则返回入环点指针。