将3个链表合并为1个(Java)

7

我有一个关于编程课程的最终审查问题。它要求将3个链表合并为1个链表。当我尝试按升序合并三个链表时,我发现少了第二个链表中的最后两个节点23和25。我不知道为什么会停在那里。题目如下:

编写一个名为LinkedTest的程序,该程序应该:

  1. 创建三个按顺序排列的整数单向链表,如下所示
First List:  2 11 19 21 24

Second List: 14 15 18 23 25

Third List:  3 9 17 20 22
  1. 将三个链表合并成一个新的有序链表,如下所示:2 3 9 11 14 15 17 18 19 20 21 22 23 24 25
  2. 返回新的有序链表 要求:您的程序时间复杂度必须小于或等于O(nlog n)

这是我的代码:

public class LinkedTest {

public static class ListNode {

    private int data;
    ListNode next;

    public ListNode(int data) {
        this.data = data;
        next = null;
    }
}

ListNode head;

public static void main(String[] args) {

    LinkedTest list = new LinkedTest();

        int[] data1 = { 2, 11, 19, 21, 24 };

        ListNode head1 = new ListNode(data1[0]);

        for (int i = 1; i < data1.length; i++)
            list.push(head1, data1[i]);

        System.out.print("First List: ");
        list.display(head1);

        int[] data2 = { 14, 15, 18, 23, 25 };

        ListNode head2 = new ListNode(data2[0]);

        for (int count = 1; count < data2.length; count++)
            list.push(head2, data2[count]);

        System.out.println(" Second List: ") ;

        list.display(head2);

        int[] data3 = { 3, 9, 17, 20, 22 };

        ListNode head3 = new ListNode(data3[0]);

        for (int count = 1; count < data3.length; count++)
            list.push(head3, data3[count]);

        System.out.println(" Third List: ") ;

        list.display(head3);

        ListNode n = list.LinkedTest(head1, head2, head3);

        System.out.print(" Merged List: ");

        list.display(n);
    }



public ListNode LinkedTest(ListNode first, ListNode second, ListNode third) { 
      ListNode head = null;

        if (first == null && second != null && third != null)

            return second;

        else if (second == null && third != null && first != null)

            return third;

        else if (third == null && first != null && second != null)

            return first;

        else if (first.data < second.data && first.data < third.data) 
        {
            head = first;
            head.next = LinkedTest(first.next, second, third);
        } 
        else if (second.data < third.data && second.data < first.data)
        {
            head = second;
            head.next = LinkedTest(first, second.next, third);
        }

        else if (third.data < first.data && third.data < second.data)
        {
            head = third;
            head.next = LinkedTest(first, second, third.next);
        }

        return head;
    }

    public void push(ListNode head, int n) 
    {
        while (head.next != null)
            head = head.next;
        head.next = new ListNode(n);
    }

    public void display(ListNode head)
    {
        ListNode tempDisplay = head; 
        while (tempDisplay != null) 
        {
            System.out.print(tempDisplay.data);
            tempDisplay = tempDisplay.next; 
    }

    }
}

输出:

First List:   2 11 19 21 24 
Second List:  14 15 18 23 25 
Third List:   3 9 17 20 22 
Merged List:  2 3 9 11 14 15 17 18 19 20 21 22 24

@ScaryWombat 这在学术界中是将多个类包含在一个Java文件中的常见做法。我认为这没有什么大问题,因为OP的课程似乎专注于数据结构和算法。 - T Tse
@ShioT 也许楼主没有注意到,但他的“第三个列表”的输入和输出不匹配 - 顺便说一句,在全世界都是通行做法。 - Scary Wombat
也许我在声明它为静态时是错误的?在之前的一道作业问题中,我有一个合并两个链表的程序。所以我把它作为三个链表问题的基础。但是我无法让第三个链表通过第二个节点。有什么想法吗? - Daniel Rogers
@ScaryWombat 编码风格和规范不是这个问题的主题,因为它是基于个人意见的。我找不到更好的参考资料,但在这里可以找到一些信息。 - T Tse
@DanielRogers 输入 int[] data3 = { 3, 9, 17, 20, 22 }; 输出 第三个列表:149172022 在你理解这部分之前不要考虑合并。 - Scary Wombat
显示剩余2条评论
4个回答

2

为什么要限制自己只使用三路合并?让我们来看看一般情况下的N路合并,然后将其应用于三路(或平凡无奇的二路)。

// drop-in replacement for your LinkedTest(), but I like the name better:
// ListNode n = list.merge(head1, head2, head3);
public ListNode merge(ListNode... nodes) {

    // find smallest, keep its index
    int firstIndex = -1;
    int firstValue = 0;
    for (int i=0; i<nodes.length; i++) {
        ListNode n = nodes[i];
        if (n != null && (firstIndex == -1 || n.data < firstValue)) {
            firstIndex = i;
            firstValue = n.data;
        }
    }

    if (firstIndex == -1) {
        // reached the end of all lists
        return null;
    } else {
        // use node with smallest as next head
        ListNode head = nodes[firstIndex];

        // call again with all lists, but skipping head
        // because we are already using it in the result list
        nodes[firstIndex] = head.next;
        head.next = merge(nodes);
        return head;
    }
}

您可以将其看作具有数字链的许多链,其中每个链的数字按升序排列。为构建一个具有所有数字顺序的大链,您需要:
  • 抓住剩余链的所有小尾巴
  • 选择最小的链接(如果没有剩余,则完成)
  • 从其链中取出该链接,并将其添加到新链的末尾
  • 返回选择旧链的最小链接
在您的答案中,正如Jacob_G所指出的那样,您缺少了一些在一个或多个列表为空时选择正确的最小元素的逻辑。Jacob的答案很好,但我想向您展示更大的图片:如果您理解了N,那么3应该就不会有困难了(此外,您还可以获得有关一般思想的洞察)。

关于合并列表,这对我来说没有返回任何内容? - Daniel Rogers
它对我有效,按照第一个评论的建议使用它。 - tucuxi

0

你的问题在于你没有编写所有的条件。让我们看看你的用例。

第一个列表: 2 11 19 21 24

第二个列表: 14 15 18 23 25

第三个列表: 3 9 17 20 22

根据你的代码逻辑,在第一次合并中,我们得到了first.data < second.data中的2。接下来,我们在同一位置上有11 < 14。但是之后我们现在有了
19 21 24
14 15 18 23 25
3 9 17 20 22
在接下来的轮次中,没有任何一个条件会被满足。


同意,在第二个位置上的11实际上应该是数字3。所以我百分之百确定错误在于我的if else语句中的驱动程序。 - Daniel Rogers
还要注意,你处理空链表的逻辑是错误的。如果 first 为空,但 secondthird 都不为空呢? - lincr
我已经对原帖进行了一些修改。现在我正在获取合并后的列表,以升序合并13个节点。唯一的问题是最后两个节点23和25没有合并。我猜想这个过程在列表3中合并最后一个节点后就停止了。 - Daniel Rogers
很明显,if (first == null && second != null) return second; 那么你的第三个链表呢?对于另外两个空分支使用相同的逻辑。你可能需要一个 mergeTwo(ListNode a, ListNode b) 来处理这种情况。 - lincr

0
给你三个已排序的列表,要求将它们合并成一个已排序的列表。实现这个算法非常简单。
我建议跟踪三个索引(每个输入列表一个),并将它们全部初始化为0(每个列表的第一个元素)。因为每个列表包含相同数量的元素,所以可以从0到3n进行简单的迭代,其中n是其中一个列表的大小。
在循环内部,您需要使用各自的索引找到每个列表的3个头元素之间的最小元素。为此,只需查看元素并将它们相互比较即可。一旦找到最小值,就可以将该元素附加到合并列表中并递增相应列表的索引(这很重要,以免重复附加相同的元素)。
你将重复这个步骤 3n 次(每个列表中的元素都要遍历一次),最终得到一个按升序排列的合并后的单一列表。
假设每个输入的列表大小总是相同的,此算法的时间复杂度为O(3n),简化为O(n)
伪代码实现如下:
list1 = [2, 11, 19, 21, 24]
list2 = [14, 15, 18, 23, 25]
list3 = [3, 9, 17, 20, 22]
merged_list = []

indexes = [0, 0, 0]

for i in 0..15:
    minValue = Integer.MAX_VALUE
    minIndex = -1

    if indexes[0] < len(list1) and list1[indexes[0]] < minValue:
        minValue = list1[indexes[0]]
        minIndex = 0

    if indexes[1] < len(list2) and list2[indexes[1]] < minValue:
        minValue = list2[indexes[1]]
        minIndex = 1

    if indexes[2] < len(list3) and list3[indexes[2]] < minValue:
        minValue = list3[indexes[2]]
        minIndex = 2

    merged_list += minValue
    indexes[minIndex]++

我正在尝试实现这个方法以及其他一些建议。它不允许我发布我尝试使您的伪代码工作的尝试。 - Daniel Rogers
你不能编辑你的问题来发布吗?也许你可以将你的代码上传到像 https://gist.github.com/ 这样的网站上。 - Jacob G.
好的,我编辑了原帖。我对代码做了一些更改。现在我要让合并后的列表返回2、3、9、11、14、15、17、18、19、20、21、22、24。它缺少第二个链表的最后两个节点,即23和25。我正在费尽心思想办法解决这个问题。 - Daniel Rogers
@DanielRogers 看起来你根本没有尝试实现伪代码。 - Jacob G.
我尝试了但失败了。所以我试图调整您的伪代码以适应我现有的代码。大部分情况下它已经达到了90%的工作效果。我只需要弄清楚最后2个节点为什么在它们被添加之前就结束了。 - Daniel Rogers

0
我猜您的代码是从合并两个列表的代码修改而来。您应该回想一下代码中每个部分的作用,思考为什么您的代码没有按照您所期望的方式工作。
假设您最初的算法如下:
public ListNode LinkedTest(ListNode first, ListNode second) {

    ListNode head = null;

    if (first == null)

        return second;

    else if (second == null)

        return first;

    else if (first.data < second.data) 
    {
        head = first;
        head.next = LinkedTest(first.next, second);
    } 
    else if (second.data < first.data)
    {
        head = second;
        head.next = LinkedTest(first, second.next);
    }

    return head;
}

方法的前半部分检查递归终止的需要。在这里,当两个列表中的一个到达末尾时,代码就会终止。例如,如果您正在将列表[1, 3, 5, 7]与列表[2, 4, 6, 8, 9, 10]合并,您最终将合并列表[]和列表[8, 9, 10]。这时,您需要通过返回列表来终止递归,从而完成整个过程。
您对算法所做的修改是错误的,因为在三路合并中,当第一个列表耗尽节点时,不能简单地返回第二个列表。终止条件变得更加复杂,您应该注意何时真正需要结束递归。例如,如果您试图合并[1, 2][3, 4][5, 6],假设算法的其他部分工作正常,则最终会生成[][3, 4][5, 6] 的合并结果,然后您将返回[3, 4] 并丢弃 [5, 6],这是不正确的。
一个解决方案是,当任何一个列表用完时,调用两个列表版本的算法。这需要更多的代码(也相当重复),但是从这个特定的代码来看,更直接。另一种解决方案是仅在两个列表为空时终止,但这需要在递归情况下特别小心地检查空值。
方法的后半部分比较了列表的第一个元素,并使用较小的元素作为新列表的头部,然后将头结点的next数据成员分配给“合并其余列表的结果”。请注意,在此版本的代码中,每次仅执行一个代码块。(除非数据相等,否则会出现错误。修复它留作练习。)只要递归未终止,我们就需要深入进去,对吧!
您所做的修改无效,因为您将第一个数据与第二个数据进行比较,然后将第二个数据与第三个数据进行比较...然后呢?让我们制作一个表格,跟踪代码并记录其行为与您希望代码行为如何不同。
| Order | Your code   | Expected | Result  |
|-------|-------------|----------|---------|
| A<B<C | head = A    | head = A | Correct |
| A<C<B | head = A    | head = A | Correct |
| B<A<C | head = B    | head = B | Correct |
| B<C<A | head = B    | head = B | Correct |
| C<A<B | head = A    | head = C | WRONG!  |
| C<B<A | head = null | head = C | WRONG!  | * note: no recursion in this case

看到你的代码在C是最小元素时失败了吗?你也需要修复这个问题。


最后,有一种解决方案甚至不需要创建三路合并。从技术上讲,merge(A, merge(B, C))应该仍然以O(n)的时间复杂度运行,并且表现正确。

祝你期末考试好运!


好的,我在午休时编辑了原帖。我对代码进行了一些更改。现在我得到了合并后的列表,返回值为2,3,9,11,14,15,17,18,19,20,21,22,24。它缺少第二个链表的最后两个节点。23和25。我正在努力想出解决这部分问题的方法。 - Daniel Rogers
检查您的终止条件。您可能会在仅第三个列表耗尽时终止递归。 - T Tse

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接