检查两个链表是不是合并。如果有,在哪里?

Posted

技术标签:

【中文标题】检查两个链表是不是合并。如果有,在哪里?【英文标题】:Check if two linked lists merge. If so, where?检查两个链表是否合并。如果有,在哪里? 【发布时间】:2010-12-08 07:41:59 【问题描述】:

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

比如说,有两个不同长度的列表,在一点合并;我们怎么知道合并点在哪里?

条件:

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

【问题讨论】:

合并意味着从那时起将只有一个列表。 是否允许修改列表? 我很确定如果不修改列表,它就无法工作。 (或者只是将其复制到其他地方以避免仅解析一次的限制。) 可能是重点。该死的面试官!呵呵 我有一个有趣的提议……假设列表的共同尾部是无限长的。如何使用常量内存找到节点交集? 【参考方案1】:

按照简单的逻辑来解决这个问题: 因为指针 A 和 B 都以相同的速度行进。要在同一点相遇,它们必须覆盖相同的距离。我们可以通过将一个列表的长度添加到另一个列表中来实现这一点。

【讨论】:

【参考方案2】:

如果允许编辑链表,

    那么只要将list 2的所有节点的next节点指针设为null即可。 查找列表1的最后一个节点的数据值。 这将在两个列表的单次遍历中为您提供相交节点,“无高保真逻辑”。

【讨论】:

【参考方案3】:

使用 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;

【讨论】:

【参考方案4】:

您可以将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;

【讨论】:

【参考方案5】:

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

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;

【讨论】:

【参考方案6】:
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;

【讨论】:

在其原始版本中,这只是拼写为the highest voted answer (Pavel Radzivilovsky, 2013)。【参考方案7】:

O(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;
    


【讨论】:

【参考方案8】:

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

【讨论】:

【参考方案9】:

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

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

【讨论】:

【参考方案10】:

使用 Map 或 Dictionary 存储节点的地址与值。如果地址已经存在于 Map/Dictionary 中,那么键的值就是答案。 我这样做了:

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;

【讨论】:

【参考方案11】:
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;

【讨论】:

您需要为您的答案添加一些解释。仅代码答案可能会被删除。 这只是将第一个列表的每个元素与第一个列表的每个元素进行比较的蛮力方法。这给出了 O(n*m) 的时间复杂度。【参考方案12】:

第 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

【讨论】:

(我更喜欢每个项目开始一行的列表。考虑使用拼写检查器。)【参考方案13】:

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

【讨论】:

【参考方案14】:

以下是迄今为止我所见过的最伟大的 - O(N),没有计数器。我是在面试候选人 S.N. 时得到的。在VisionMap。

制作一个像这样的交互指针:它每次都向前直到结束,然后跳转到相反列表的开头,依此类推。 创建其中两个,指向两个头。 每次将每个指针推进 1,直到它们相遇。这将发生在一或两次通过后。

我仍然在面试中使用这个问题 - 但要看看人们需要多长时间才能理解为什么这个解决方案有效。

【讨论】:

太棒了! 这是一个很好的答案,但是您必须通过列表两次,这违反了条件 #2。 如果保证存在合并点,我发现这个解决方案非常优雅。无法检测合并点,就好像没有合并点一样会无限循环。 太棒了!说明:我们有 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的路径 太棒了。不明白的可以数一下从head1->tail1->head2->交点和head2->tail2->head1->交点经过的节点数。两者将相等(绘制不同类型的链表来验证这一点)。原因是两个指针在再次到达 IP 之前必须经过相同的距离 head1-> IP + head2->IP。所以当它到达 IP 时,两个指针将相等,我们就有了合并点。【参考方案15】:

Java 中的步骤:

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

【讨论】:

如果合并部分有循环怎么办? 但是对于错误处理周期,这看起来很像isyi's answer。【参考方案16】:

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

【讨论】:

恐怕(因为 Ω(n) 额外空间)这是唯一的方法(不是重建列表和)不多次解析列表。检测列表中的循环对于第一个列表来说是微不足道的(检查节点是否在集合中) - 使用第二个列表上的任何循环检测方法来确保终止。 (面试问题可能是关于仔细听问题陈述,并且跳进去使用你碰巧知道的锤子东西不完全是钉子。)【参考方案17】:

这个怎么样:

    如果只允许每个链表只遍历一次,可以新建一个节点,遍历第一个链表让每个节点都指向这个新节点,再遍历第二个链表看是否有节点指向你的新节点(那是你的合并点)。如果第二次遍历没有导致你的新节点,那么原始列表没有合并点。

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

【讨论】:

1.不仅修改而且破坏了第一个列表。 2. 被反复推荐。【参考方案18】:

这是一个解决方案,计算速度很快(迭代每个列表一次),但会占用大量内存:

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

【讨论】:

这相当于处理一个列表两次。 我想,从技术上讲,您对列表进行了两次处理,但这是对 Kyle Rozendo 解决方案的重大改进。现在,如果“处理列表”被定义为“读取链接值并跟随指针”,则可以说它确实处理了一次列表 - 它读取每个链接值一次,存储它,然后比较它们。 毫无疑问,肯定会比我快。【参考方案19】:

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

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

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. 

【讨论】:

【参考方案20】:

这个解决方案只对每个列表进行一次迭代……也不需要修改列表……尽管您可能会抱怨空间问题…… 1) 基本上,您在 list1 中进行迭代并将每个节点的地址存储在一个数组中(该数组存储无符号整数值) 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;
 

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

【讨论】:

这几乎不止一次地迭代其中一个列表,尽管是以数组的形式而不是列表本身。【参考方案21】:

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

迭代两个列表并计算长度 A 和 B 计算长度差 C = |A-B|; 同时开始迭代两个列表,但在列表上执行额外的 C 步骤,这会更大 这两个指针会在合并点相遇

【讨论】:

【参考方案22】:

我在我的 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() 一个节点时,地址对齐 w/ 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,这就是“只迭代一次”自然想到的,不知道为什么这从来没有投票!漂亮的解决方案。【参考方案23】:

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

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

【讨论】:

+1 这样也不需要对列表进行任何修改,而且大多数链表实现通常都提供长度 我们的 Pavel 太多了。我的解决方案不需要修改列表。 好答案。不过,这会有什么时间复杂度。 0(n + m) ?其中 n = 列表 1 中的节点,m = 列表 2 中的节点? 而不是移动两个列表中的两个指针:我们可以只查看两个路径的差异 >= 小,如果是,则在小列表中移动小值,否则在小列表中移动差异 + 1 值;如果 diff 为 0,那么最后一个节点就是答案。 事实上,这甚至可以用来计算是否存在合并点,因为一旦到达一个列表的末尾,我们就可以存储结束节点并在另一个列表到达时进行比较。由于我们只创建虚拟循环而不是真实循环,因此效果很好。【参考方案24】:

如果

“不允许修改”的意思是“您可以更改,但最终应该恢复它们”,并且 我们可以准确地迭代列表两次

以下算法将是解决方案。

首先,数字。假设第一个列表的长度为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

这解决了问题。

【讨论】:

评论问题状态不允许修改列表! 我喜欢这个答案(很有创意)。我唯一遇到的问题是它假设你知道两个列表的长度。 你不能修改列表,我们不知道长度——这些是限制......无论如何,谢谢你的创造性回答。 @tster , @calvin ,答案不假设,我们需要长度。可以内联计算。为我的答案添加解释。 @Forethinker 散列访问过的节点和/或将它们标记为已见需要 O(list length) 内存,而许多解决方案(包括我的,无论多么不完美和复杂)都需要 O(1) 内存。 【参考方案25】:

这可以说违反了“每个列表只解析一次”的条件,但实现了tortoise and hare algorithm(用于查找循环列表的合并点和循环长度),因此您从列表 A 开始,当您到达 NULL最后,您假装它是指向列表 B 开头的指针,从而创建了循环列表的外观。然后,该算法将准确地告诉您合并列表 A 的位置(根据 Wikipedia 描述的变量“mu”)。

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

【讨论】:

和我说的差不多,只是用了更花哨的名字。 :P 一点也不。这个解决方案在操作上是 O(n),在内存使用上是 O(1)(实际上只需要两个指针变量)。 是的,应该删除我之前的评论,因为我的解决方案发生了一些变化。呵呵。 但我一开始不明白它是如何适用的? 是您的解释,而不是算法本身。也许我的看法不同,但是嘿。【参考方案26】:

好吧,如果你知道它们会合并:

假设你开始:

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

1) 遍历第一个列表,将每个 next 指针设置为 NULL。

现在你有:

A   B   C

1-->2-->3   4   5

2) 现在遍历第二个列表并等到您看到 NULL,这就是您的合并点。

如果您不能确定它们是否合并,您可以为指针值使用哨兵值,但这并不优雅。

【讨论】:

但是,你在这个过程中破坏了列表,再也不会被使用了:P @Kyle Rozendo ,好吧,我的解决方案更改了列表,使其在处理后可以恢复。但这更清楚地展示了这个概念 我没有看到不允许修改列表。我会考虑一下,但如果不存储看到的每个节点,什么都不会出现。 来吧,这是正确的答案!我们只需要调整问题:) 创建内存泄漏的优秀算法。【参考方案27】:

也许我过于简化了,只是简单地迭代最小的列表并使用最后一个节点 Link 作为合并点?

所以,Data-&gt;Link-&gt;Link == NULL 是终点,Data-&gt;Link 作为合并点(在列表的末尾)。

编辑:

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

你有一个链表(参考如下):

A-&gt;B-&gt;C-&gt;D-&gt;E

你有第二个链表:

1-&gt;2-&gt;

使用合并列表,引用将如下所示:

1-&gt;2-&gt;D-&gt;E-&gt;

因此,您映射第一个“较小”列表(作为合并列表,我们正在计算的列表的长度为 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 似乎想要的来看,这可以解决问题。 现在更清楚了。但在内存使用方面是线性的。我不喜欢那样。 问题没有多问,否则整个过程可以多线程。这仍然是解决方案的简单“***”视图,代码可以通过多种方式实现。 :) 呃,什么?多线程是一种更好地利用处理能力的方法,而不是降低算法所需的总处理能力。说代码可以通过多种方式实现只是一个借口。 这确实将“每个列表只解析一次”弯曲到接近断点。您所做的只是复制一个列表,然后对照副本检查另一个列表。

以上是关于检查两个链表是不是合并。如果有,在哪里?的主要内容,如果未能解决你的问题,请参考以下文章

合并两个排序的链表

合并两个有序链表【递归、迭代】

leetcode-合并两个排序的链表-66

(链表)链表的一些合并问题

剑指offer:合并两个排序的链表

LeetCode-021-合并两个有序链表