算法分析:合并两个有序链表

153 阅读7分钟

【题目】 合并两个有序链表,将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

【方法一】 递归
思路是递归地定义两个链表里的 merge 操作,即两个链表头部值较小的一个节点与剩下元素的 merge 操作结果合并。
如果 l1 或者 l2 一开始就是空链表 ,那么没有任何操作需要合并,所以我们只需要返回非空链表。
否则,我们要判断 l1 和 l2 哪一个链表的头节点的值更小,然后递归地决定下一个添加到结果里的节点。
如果两个链表有一个为空,递归结束。

public class ListNode {
	int val;
	ListNode next;
	ListNode() {}
	ListNode(int val) { 
		this.val = val; 
	}
	ListNode(int val, ListNode next) {
		this.val = val; this.next = next; 
	}
}

class Solution {
	// 定义了一个名为mergeTwoLists的方法,用于将两个有序的链表合并为一个新的有序链表。
	// 方法接收两个链表的头节点l1和l2作为参数,返回合并后的新链表的头节点。
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        if (l1 == null) {
            return l2;
        } else if (l2 == null) {
            return l1;
        } else if (l1.val < l2.val) {
            l1.next = mergeTwoLists(l1.next, l2);
            return l1;
        } else {
            l2.next = mergeTwoLists(l1, l2.next);
            return l2;
        }
    }
}

对于上面的算法,其核心逻辑如下:
处理边界情况:如果l1为空链表,则直接返回l2;如果l2为空链表,则直接返回l1。
递归比较节点值:比较l1和l2当前节点的值,如果l1.val < l2.val,则将l1作为当前节点,并递归合并l1.next和l2;否则将l2作为当前节点,并递归合并l1和l2.next。
返回结果:每次递归返回当前处理好的节点,最终形成完整的有序链表。
对于上面的算法,分析其算法复杂度:
时间复杂度:O(n+m),其中 n 和 m 分别为两个链表的长度。因为每次调用递归都会去掉 l1 或者 l2 的头节点(直到至少有一个链表为空),函数 mergeTwoList 至多只会递归调用每个节点一次。因此,时间复杂度取决于合并后的链表长度,即 O(n+m)。
空间复杂度:O(n+m),其中 n 和 m 分别为两个链表的长度。递归调用 mergeTwoLists 函数时需要消耗栈空间,栈空间的大小取决于递归调用的深度。结束递归调用时 mergeTwoLists 函数最多调用 n+m 次,因此空间复杂度为 O(n+m)。
优缺点分析
优点:代码简洁明了,递归逻辑清晰;不需要额外的存储空间(除了递归栈)。
缺点:递归深度可能导致栈溢出,尤其是链表很长时;相比迭代实现,递归的性能开销略大(函数调用开销)
下面是C++代码实现:

struct ListNode {
	int val;
	ListNode *next;
	ListNode() : val(0), next(nullptr) {}
	ListNode(int x) : val(x), next(nullptr) {}
	ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        if (l1 == nullptr) {
            return l2;
        } else if (l2 == nullptr) {
            return l1;
        } else if (l1->val < l2->val) {
            l1->next = mergeTwoLists(l1->next, l2);
            return l1;
        } else {
            l2->next = mergeTwoLists(l1, l2->next);
            return l2;
        }
    }
};

【方法二】 迭代
思路:可以用迭代的方法来实现上述算法。 当 l1 和 l2 都不是空链表时,判断 l1 和 l2 哪一个链表的头节点的值更小,将较小值的节点添加到结果里,当一个节点被添加到结果里之后,将对应链表中的节点向后移一位。
算法:首先,设定一个哨兵节点 prehead ,这可以在最后比较容易地返回合并后的链表。
维护一个 prev 指针,需要做的是调整它的 next 指针。
然后,重复以下过程,直到 l1 或者 l2 指向了 null;
如果 l1 当前节点的值小于等于 l2 ,我们就把 l1 当前的节点接在 prev 节点的后面同时将 l1 指针往后移一位。
否则,我们对 l2 做同样的操作。
不管将哪一个元素接在了后面,都需要把 prev 向后移一位。
在循环终止的时候, l1 和 l2 至多有一个是非空的。
由于输入的两个链表都是有序的,所以不管哪个链表是非空的,它包含的所有元素都比前面已经合并链表中的所有元素都要大。 这意味着只需要简单地将非空链表接在合并链表的后面,并返回合并链表即可。

class Solution {
	// 代码实现了合并两个有序链表的迭代算法,通过虚拟头节点和双指针技巧高效地完成了链表合并。
	// 方法接收两个链表的头节点l1和l2作为参数,返回合并后的新链表的头节点。
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
		// 创建一个值为 - 1 的虚拟头节点prehead,用于简化边界处理
        ListNode prehead = new ListNode(-1);
		// 使用prev指针指向当前合并链表的末尾节点
        ListNode prev = prehead;
		// 当l1和l2都不为空时,比较它们当前节点的值
        while (l1 != null && l2 != null) {
            if (l1.val <= l2.val) {
				// 将较小值的节点连接到prev的后面,并移动相应指针
                prev.next = l1;
                l1 = l1.next;
            } else {
                prev.next = l2;
                l2 = l2.next;
            }
			// 更新prev指针到新连接的节点
            prev = prev.next;
        }

        // 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
		// 当其中一个链表遍历完后,将另一个链表的剩余部分直接连接到prev后面
        prev.next = l1 == null ? l2 : l1;
		// 虚拟头节点的下一个节点即为合并后链表的头节点
        return prehead.next;
    }
}

对上面算法的分析如下:
1)算法核心逻辑
创建虚拟头节点:创建一个值为 - 1 的虚拟头节点prehead,用于简化边界处理;使用prev指针指向当前合并链表的末尾节点。
迭代比较节点值:当l1和l2都不为空时,比较它们当前节点的值;将较小值的节点连接到prev的后面,并移动相应指针;更新prev指针到新连接的节点。
处理剩余节点:当其中一个链表遍历完后,将另一个链表的剩余部分直接连接到prev后面。 返回结果:虚拟头节点的下一个节点即为合并后链表的头节点。
2)复杂度分析
时间复杂度:O(n+m),其中 n 和 m 分别为两个链表的长度。因为每次循环迭代中,l1 和 l2 只有一个元素会被放进合并链表中, 因此 while 循环的次数不会超过两个链表的长度之和。所有其他操作的时间复杂度都是常数级别的,因此总的时间复杂度为 O(n+m)。
空间复杂度:O(1)。我们只需要常数的空间存放若干变量。
下面是C++代码实现迭代的方案:

class Solution {
public:
    ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
        ListNode* preHead = new ListNode(-1);

        ListNode* prev = preHead;
        while (l1 != nullptr && l2 != nullptr) {
            if (l1->val < l2->val) {
                prev->next = l1;
                l1 = l1->next;
            } else {
                prev->next = l2;
                l2 = l2->next;
            }
            prev = prev->next;
        }

        // 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
        prev->next = l1 == nullptr ? l2 : l1;

        return preHead->next;
    }
};