检查两个链接列表是否合并。如果是这样,在哪里?


102

这个问题可能很旧,但是我想不出答案。

假设有两个不同长度的列表,它们合并在一个点上;我们如何知道合并点在哪里?

条件:

  1. 我们不知道长度
  2. 我们应该只解析每个列表一次。

两个合并的链表的示例。


合并意味着从那时开始将只有一个列表。
rplusg

允许修改列表吗?
Artelius

1
我敢肯定,如果不修改列表,它是不会起作用的。(或者只是将其复制到其他地方,以避免限制只能解析一次。)
GeorgSchölly09年

2
可能是重点。该死的面试官!嘿
凯尔·罗森多

1
我有一个有趣的建议...假设列表的尾巴是无限长的。如何使用常量内存找到节点交点?
Akusete

Answers:


36

如果

  • “不允许修改”是指“您可以更改,但最终应将其还原”,并且
  • 我们可以对列表进行两次精确的迭代

以下算法将是解决方案。

首先,数字。假设第一个列表的长度为a+c,第二个列表的长度为b+c,其中c是它们共同的“尾巴”的长度(在合并点之后)。让我们将它们表示如下:

x = a+c
y = b+c

由于我们不知道长度,因此我们将进行计算xy而无需进行其他迭代;您将看到如何。

然后,我们迭代每个列表,并在迭代时反转它们!如果两个迭代器同时到达合并点,那么我们仅通过比较就可以找到它。否则,一个指针将在另一指针之前到达合并点。

此后,当另一个迭代器到达合并点时,它将不会进行到通用尾部。相反,它将返回到之前已达到合并点的列表的前一个开始!因此,在到达更改列表的末尾(即另一个列表的前一个开始)之前,他将进行a+b+1总计迭代。叫它z+1

首先到达合并点的指针将继续迭代,直到到达列表的末尾。计算出的迭代次数应等于x

然后,该指针进行迭代,并再次反转列表。但是现在它不会回到原来的列表的开头了!相反,它将转到另一个列表的开头!计算得出的迭代次数应等于y

因此,我们知道以下数字:

x = a+c
y = b+c
z = a+b

从中我们确定

a = (+x-y+z)/2
b = (-x+y+z)/2
c = (+x+y-z)/2

解决了问题。


2
对问题的评论指出不允许修改列表!
Skizz

1
我喜欢这个答案(非常有创意)。我唯一的问题是它假设您知道两个列表的长度。
tster

您无法修改列表,而且我们也不知道长度-这些都是限制因素...无论如何,谢谢您的创造性回答。
rplusg

2
@tster,@calvin,答案没有假设,我们需要长度。可以内联计算。在我的答案中添加解释。
P

2
@Forethinker散列访问的节点和/或将它们标记为可见节点需要O(列表长度)内存,而许多解决方案(包括我的,尽管不完善又复杂)都需要O(1)内存。
P Shved

156

以下是迄今为止我所见过的最棒的-O(N),没有计数器。我在VisionMap的候选人SN采访中得到了它。

像这样创建一个中间指针:它每次都向前移动直到结尾,然后跳到对面列表的开头,依此类推。创建其中两个,指向两个头。每次将每个指针前进1,直到它们相遇。一两次通过之后,就会发生这种情况。

我在访谈中仍然使用这个问题-但要了解需要多长时间才能理解该解决方案的有效性。


6
那真是太好了!
从聪

2
这是一个很好的答案,但您必须两次浏览列表,这违反了条件2。
tster

2
如果保证存在合并点,我发现此解决方案非常优雅。检测合并点将不起作用,就像没有合并点一样,它将无限循环。
交替方向

4
太棒了!说明:我们有2个列表:a-b-c-x-y-zp-q-x-y-z。第一个指针的a,b,c,x,y,z,p,q,x路径,第二个指针的路径p,q,x,y,z,a,b,c,x
Nikolai Golub,2016年

14
辉煌。对于那些不了解的人,计算从head1-> tail1-> head2->相交点和head2-> tail2-> head1->相交点经过的节点数。两者将相等(绘制链表的diff类型以进行验证)。原因是两个指针在再次到达IP之前必须经过head1-> IP + head2-> IP相同的距离。因此,到IP时,两个指针将相等,并且我们有了合并点。
adev

91

Pavel的答案需要修改列表以及对每个列表进行两次迭代。

这是一个需要迭代每个列表两次的解决方案(第一次计算它们的长度;如果给出了长度,则只需要迭代一次)。

这个想法是忽略较长列表的开始条目(合并点不能在那里),以便两个指针到列表末尾的距离相等。然后将它们向前移动,直到它们合并。

lenA = count(listA) //iterates list A
lenB = count(listB) //iterates list B

ptrA = listA
ptrB = listB

//now we adjust either ptrA or ptrB so that they are equally far from the end
while(lenA > lenB):
    ptrA = ptrA->next
    lenA--
while(lenB > lenA):
    prtB = ptrB->next
    lenB--

while(ptrA != NULL):
    if (ptrA == ptrB):
        return ptrA //found merge point
    ptrA = ptrA->next
    ptrB = ptrB->next

与我的其他答案渐近相同(线性时间),但常数较小,因此可能更快。但是我认为我的其他答案比较酷。


4
今天,当我们喝伏特加酒时,我向我的一个朋友提出了这个问题,他给出了与您相同的答案,并要求将其张贴在SO上。但是你似乎是第一位的。因此,我将为您做+1,希望我能再做+1。
P Shved

2
像这样+1,并且也不需要对列表进行任何修改,大多数链接列表实现通常也提供了长度
keshav84'9

3
我们的Pavel太多了。我的解决方案不需要修改列表。
Pavel Radzivilovsky 2015年

好答案。时间的复杂度如何?0(n + m)?其中n =列表1中的节点,m =列表2中的节点?
Vihaan Verma

而不是同时移动两个列表中的两个指针:我们可以仅查看diff> =两个路径的小,如果是,则以较小的值移动小列表,否则以diff + 1的值移动小列表;如果diff为0,则最后一个节点为答案。
Vishal Anand

30

好吧,如果您知道它们将合并:

假设您从以下内容开始:

A-->B-->C
        |
        V
1-->2-->3-->4-->5

1)浏览第一个列表,将每个下一个指针设置为NULL。

现在您有了:

A   B   C

1-->2-->3   4   5

2)现在浏览第二个列表,等待直到看到一个NULL,这就是您的合并点。

如果不能确定它们是否合并,可以将定点值用作指针值,但这并不那么优雅。


3
但是,您在此过程中销毁了该列表,从此不再使用:P
Kyle Rosendo

@Kyle Rozendo,好吧,我的解决方案更改了列表,以便在处理后将其还原。但这更清楚地说明了这一概念
P Shved

我没有看到不允许修改列表。我会考虑一下,但是如果不存储所看到的每个节点,什么都不会想到。
tster

10
来吧,这是正确的答案!我们只需要调整问题即可:)
P Shved

23
创建内存泄漏的出色算法。
Karoly Horvath

14

如果我们可以精确地迭代列表两次,那么我可以提供确定合并点的方法:

  • 遍历两个列表并计算长度A和B
  • 计算长度差C = | AB |;
  • 开始同时迭代两个列表,但是在列表上执行更多的C步骤
  • 这两个指针将在合并点相遇

8

这是一个解决方案,计算速度快(每个列表重复一次),但使用大量内存:

for each item in list a
  push pointer to item onto stack_a

for each item in list b
  push pointer to item onto stack_b

while (stack_a top == stack_b top) // where top is the item to be popped next
  pop stack_a
  pop stack_b

// values at the top of each stack are the items prior to the merged item

2
这相当于两次处理一个列表。
格奥尔格·舍利

从技术上讲,我想您要对列表进行两次处理,但这是对Kyle Rozendo解决方案的重大改进。现在,如果将“处理列表”定义为“读取链接值并跟随指针”,则可以说它确实处理了列表一次-它读取一次每个链接值,将其存储然后比较它们。
Skizz

毫无疑问,它肯定会比我的快。
凯尔·罗森多

7

您可以使用一组节点。遍历一个列表并将每个Node添加到集合中。然后遍历第二个列表,对于每次迭代,请检查节点是否存在于集合中。如果是这样,您已经找到了合并点:)


恐怕(由于Ω(n)个额外的空间),这是唯一的方法(不是某种形式的重建列表,并且)不会多次解析列表。在第一个列表中检测列表中的循环很简单(检查节点是否已设置)-在第二个列表中使用任何循环检测方法来确保终止。(采访中的问题可能是关于认真听取问题陈述,而不是突然跳入锤子,而您碰巧知道打的不是钉子。)
灰胡子

6

可以说这违反了“仅分析每个列表一次”的条件,但是实现了乌龟和野兔算法(用于查找循环列表的合并点和循环长度),因此您从列表A开始,并且在到达列表NULL时您假装它是指向列表B开头的指针,从而创建了循环列表的外观。然后,该算法将告诉您确切的合并列表A有多远(根据Wikipedia的描述,变量“ mu”)。

另外,“ lambda”值告诉您列表B的长度,如果需要,您可以在算法期间(重定向NULL链接时)计算出列表A的长度。


我说的差不多,只是名字比较贵。:P
Kyle Rosendo

一点也不。该解决方案在操作中为O(n),在内存使用中为O(1)(实际上只需要两个指针变量)。
Artelius

是的,应该删除我之前的评论,因为我的解决方案有所更改。呵呵。
凯尔·罗森多

但是我不知道这首先如何适用?
Artelius

您的解释是这样,而不是算法本身。也许我有不同的看法,但嘿。
凯尔·罗森多

3

也许我已经简化了这个工作,但是只是迭代最小的列表并将最后一个节点Link用作合并点?

因此,Data->Link->Link == NULL终点在哪里,Data->Link作为合并点(在列表的末尾)。

编辑:

好的,从您发布的图片中,您解析了两个列表,最小的列表。使用最小的列表,您可以维护对以下节点的引用。现在,当您解析第二个列表时,您将对引用进行比较,以找到引用[i]在LinkedList [i]-> Link处的引用。这将给出合并点。用图片解释的时间(将值叠加在OP上)。

您有一个链接列表(参考如下所示):

A->B->C->D->E

您还有第二个链接列表:

1->2->

对于合并的列表,引用将如下所示:

1->2->D->E->

因此,您映射了第一个“较小”列表(作为合并列表,这就是我们要计算的长度为4而主列表为5)

遍历第一个列表,维护引用的引用。

该列表将包含以下参考Pointers { 1, 2, D, E }

现在,我们浏览第二个列表:

-> A - Contains reference in Pointers? No, move on
-> B - Contains reference in Pointers? No, move on
-> C - Contains reference in Pointers? No, move on
-> D - Contains reference in Pointers? Yes, merge point found, break.

当然,您需要维护一个新的指针列表,但这并不超出规范范围。但是,第一个列表仅被解析一次,而第二个列表将仅在没有合并点的情况下被完全解析。否则,它将更快(在合并点)结束。


好吧,与起初我想说的略有不同,但是从OP看来想要的,这可以解决问题。
凯尔·罗森多

现在更清楚了。但是线性使用在内存中。我不喜欢
Artelius

这个问题并不需要更多,否则整个过程可以是多线程的。这仍然是该解决方案的简单化“顶层”视图,可以采用多种方式来实现代码。:)
Kyle Rosendo

1
嗯,什么?多线程是一种更好地利用处理能力而不降低算法所需总处理能力的方法。并且说代码可以以多种方式实现只是一个借口。
Artelius

1
这实际上使“仅解析每个列表一次”接近断点。您要做的只是复制一个列表,然后对照副本检查另一个列表。
Skizz

3

我已经在我的FC9 x86_64上测试了合并情况,并打印每个节点地址,如下所示:

Head A 0x7fffb2f3c4b0
0x214f010
0x214f030
0x214f050
0x214f070
0x214f090
0x214f0f0
0x214f110
0x214f130
0x214f150
0x214f170


Head B 0x7fffb2f3c4a0
0x214f0b0
0x214f0d0
0x214f0f0
0x214f110
0x214f130
0x214f150
0x214f170

请注意,如果我已经对齐了节点结构,那么当malloc()一个节点时,地址以16个字节对齐,请参见最低4位。最低位为0,即0x0或000b。因此,如果您也使用相同的特殊情况(对齐的节点地址),则可以使用这至少4位。例如,当两个列表都从头到尾旅行时,设置访问节点地址的4位中的1或2,即设置一个标志;

next_node = node->next;
node = (struct node*)((unsigned long)node | 0x1UL);

请注意,以上标志不会影响实际节点地址,而只会影响您的SAVED节点指针值。

一旦发现有人设置了标志位,则发现的第一个节点应为合并点。完成后,您将通过清除已设置的标志位来还原节点地址。重要的是,在进行迭代(例如,node = node-> next)进行清理时应该小心。记住你已经设置了标志位,所以这样做

real_node = (struct node*)((unsigned long)node) & ~0x1UL);
real_node = real_node->next;
node = real_node;

因为此建议将还原已修改的节点地址,所以可以将其视为“未修改”。


+1,这就是自然而然的想到“只重复一次”不知道为什么从来没有投票过!美丽的解决方案。
jman

3

可以有一个简单的解决方案,但需要一个辅助空间。这个想法是遍历一个列表并将每个地址存储在一个哈希图中,现在遍历另一个列表并匹配该地址是否在哈希图中。每个列表仅被遍历一次。没有任何列表的修改。长度仍然未知。使用的辅助空间:O(n),其中“ n”是所遍历的第一个列表的长度。


2

此解决方案仅迭代每个列表一次...也无需修改列表。.尽管您可能会抱怨空间。.1
)基本上,您在list1中进行迭代并将每个节点的地址存储在一个数组中(该数组存储了unsigned int值)
2)然后迭代list2,并为每个节点的地址--->搜索是否找到匹配项的数组...如果您这样做,则这是合并节点

//pseudocode
//for the first list
p1=list1;
unsigned int addr[];//to store addresses
i=0;
while(p1!=null){
  addr[i]=&p1;
  p1=p1->next;
}
int len=sizeof(addr)/sizeof(int);//calculates length of array addr
//for the second list
p2=list2;
while(p2!=null){
  if(search(addr[],len,&p2)==1)//match found
  {
    //this is the merging node
    return (p2);
  }
  p2=p2->next;
}

int search(addr,len,p2){
  i=0;  
  while(i<len){
    if(addr[i]==p2)
      return 1;
    i++;
  }
 return 0;
} 

希望这是一个有效的解决方案...


尽管以数组的形式而不是列表本身,这几乎重复了列表中的一个。
syockit

1

无需修改任何列表。有一种解决方案,我们只需要遍历每个列表一次。

  1. 创建两个堆栈,比如说stck1和stck2。
  2. 遍历第一个列表,并在stck1中推送遍历的每个节点的副本。
  3. 与第二步相同,但是这次遍历第二个列表,并将节点的副本推送到stck2中。
  4. 现在,从两个堆栈中弹出,检查两个节点是否相等,如果是,则保留对其的引用。如果否,则相等的先前节点实际上是我们正在寻找的合并点。

1
int FindMergeNode(Node headA, Node headB) {
  Node currentA = headA;
  Node currentB = headB;

  // Do till the two nodes are the same
  while (currentA != currentB) {
    // If you reached the end of one list start at the beginning of the other
    // one currentA
    if (currentA.next == null) {
      currentA = headA;
    } else {
      currentA = currentA.next;
    }
    // currentB
    if (currentB.next == null) {
      currentB = headB;
    } else {
      currentB = currentB.next;
    }
  }
  return currentB.data;
}

在其最初的修订版中,这只是阐明了投票率最高的答案(Pavel Radzivilovsky,2013)
灰胡子

0

这是幼稚的解决方案,无需遍历整个列表。

如果您的结构化节点具有三个字段,例如

struct node {
    int data;   
    int flag;  //initially set the flag to zero  for all nodes
    struct node *next;
};

假设您有两个标头(head1和head2)指向两个列表的标头。

以相同的速度遍历两个列表,并为该节点添加标志= 1(已访问标志),

  if (node->next->field==1)//possibly longer list will have this opportunity
      //this will be your required node. 

0

这个怎么样:

  1. 如果只允许遍历每个列表一次,则可以创建一个新节点,遍历第一个列表以使每个节点都指向该新节点,然后遍历第二个列表以查看是否有任何节点指向您的新节点(这是您的合并点)。如果第二次遍历没有导致您的新节点,则原始列表没有合并点。

  2. 如果允许您遍历列表一次以上,则可以遍历每个列表以查找它们的长度,如果它们不同,则在较长列表的开头省略“多余”节点。然后,一次遍历两个列表一次,找到第一个合并节点。


1.不仅修改而且破坏了第一个列表。2.建议一次又一次。
greybeard

0

Java步骤:

  1. 创建地图。
  2. 在列表的两个分支中开始遍历,并使用与节点相关的某些唯一事物(例如,节点Id)作为键,并将列表中的所有遍历节点放入Map中,并将所有值的开头都设为1。
  3. 当第一个重复的密钥出现时,增加该密钥的值(现在说它的值变为2,即> 1)。
  4. 获取值大于1的Key,该Key应该是两个列表合并的节点。

1
如果我们在合并部分中有循环怎么办?
Rohit

但是对于错误处理周期,这很像isyi的答案
灰胡子,2016年

0

我们可以通过引入“ isVisited”字段来有效地解决它。遍历第一个列表,并将所有节点的“ isVisited”值设置为“ true”,直到结束。现在从第二个开始,找到标记为true的第一个节点,然后找到Boom,这是您的合并点。


0

步骤1:找到两个列表的长度步骤2:找到差异并移动相差最大的列表步骤3:现在两个列表的位置相似。步骤4:遍历列表以找到合并点

//Psuedocode
def findmergepoint(list1, list2):
lendiff = list1.length() > list2.length() : list1.length() - list2.length() ? list2.lenght()-list1.lenght()
biggerlist = list1.length() > list2.length() : list1 ? list2  # list with biggest length
smallerlist = list1.length() < list2.length() : list2 ? list1 # list with smallest length


# move the biggest length to the diff position to level both the list at the same position
for i in range(0,lendiff-1):
    biggerlist = biggerlist.next
#Looped only once.  
while ( biggerlist is not None and smallerlist is not None ):
    if biggerlist == smallerlist :
        return biggerlist #point of intersection


return None // No intersection found

(我更喜欢列表中的每个项目都以更好的开头。考虑使用拼写检查器。)
greybeard

0
int FindMergeNode(Node *headA, Node *headB)
{
    Node *tempB=new Node;
    tempB=headB;
   while(headA->next!=NULL)
       {
       while(tempB->next!=NULL)
           {
           if(tempB==headA)
               return tempB->data;
           tempB=tempB->next;
       }
       headA=headA->next;
       tempB=headB;
   }
    return headA->data;
}

您需要在答案中添加一些解释。仅代码答案可能会被删除。
rghome

0

使用“映射”或“字典”存储节点的地址与值。如果地图/字典中已经存在该地址,则键的值就是答案。我这样做:

int FindMergeNode(Node headA, Node headB) {

Map<Object, Integer> map = new HashMap<Object, Integer>();

while(headA != null || headB != null)
{
    if(headA != null && map.containsKey(headA.next))
    {
        return map.get(headA.next);
    }

    if(headA != null && headA.next != null)
    {
         map.put(headA.next, headA.next.data);
         headA = headA.next;
    }

    if(headB != null && map.containsKey(headB.next))
    {
        return map.get(headB.next);
    }

    if(headB != null && headB.next != null)
    {
        map.put(headB.next, headB.next.data);
        headB = headB.next;
    }
}

return 0;
}

0

AO(n)复杂度解决方案。但是基于一个假设。

假设是:两个节点都只有正整数。

逻辑:将list1的所有整数都设为负数。然后遍历list2,直到得到一个负整数。一旦找到=>接受它,将符号改回正号并返回。

static int findMergeNode(SinglyLinkedListNode head1, SinglyLinkedListNode head2) {

    SinglyLinkedListNode current = head1; //head1 is give to be not null.

    //mark all head1 nodes as negative
    while(true){
        current.data = -current.data;
        current = current.next;
        if(current==null) break;
    }

    current=head2; //given as not null
    while(true){
        if(current.data<0) return -current.data;
        current = current.next;
    }

}

0

我们可以使用两个指针并以某种方式移动,如果其中一个指针为null,则将其指向另一个列表的头部,另一个指向相同的指针,这样,如果列表长度不同,它们将在第二遍通过。如果list1的长度为n,list2的长度为m,则它们的差为d = abs(nm)。他们将覆盖此距离并在合并点会合。
码:

int findMergeNode(SinglyLinkedListNode* head1, SinglyLinkedListNode* head2) {
    SinglyLinkedListNode* start1=head1;
    SinglyLinkedListNode* start2=head2;
    while (start1!=start2){
        start1=start1->next;
        start2=start2->next;
        if (!start1)
        start1=head2;
        if (!start2)
        start2=head1;
    }
    return start1->data;
}

0

您可以将的节点添加list1到哈希集中,然后通过第二个循环进行循环,如果list2集合中已存在的任何节点。如果是,则为合并节点

static int findMergeNode(SinglyLinkedListNode head1, SinglyLinkedListNode head2) {
    HashSet<SinglyLinkedListNode> set=new HashSet<SinglyLinkedListNode>();
    while(head1!=null)
    {
        set.add(head1);
        head1=head1.next;
    }
    while(head2!=null){
        if(set.contains(head2){
            return head2.data;
        }
    }
    return -1;
}

0

使用JavaScript的解决方案

var getIntersectionNode = function(headA, headB) {
    
    if(headA == null || headB == null) return null;
    
    let countA = listCount(headA);
    let countB = listCount(headB);
    
    let diff = 0;
    if(countA > countB) {

        diff = countA - countB;
        for(let i = 0; i < diff; i++) {
            headA = headA.next;
        }
    } else if(countA < countB) {
        diff = countB - countA;
        for(let i = 0; i < diff; i++) {
            headB = headB.next;
        }
    }

    return getIntersectValue(headA, headB);
};

function listCount(head) {
    let count = 0;
    while(head) {
        count++;
        head = head.next;
    }
    return count;
}

function getIntersectValue(headA, headB) {
    while(headA && headB) {
        if(headA === headB) {
            return headA;
        }
        headA = headA.next;
        headB = headB.next;
    }
    return null;
}

0

如果允许编辑链接列表,

  1. 然后,使列表2的所有节点的下一个节点指针为null。
  2. 查找列表1的最后一个节点的数据值。这将为您提供遍历两个列表的相交节点,并且带有“ no hi fi logic”。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.