[data structure] linked list 04: LeetCode 21. Merge two ordered linked lists, LeetCode 23. Merge K ascending linked lists

Posted by troybtj on Sun, 28 Nov 2021 09:21:52 +0100

1, LeetCode 21. Merge two ordered linked lists

Method 1: iteration

Code and performance

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(-1);
        ListNode p = dummy;
        ListNode p1 = l1;
        ListNode p2 = l2;
        while(p1 != null && p2 != null){
            if(p1.val < p2.val){
                p.next = p1;
                p1 = p1.next;
            }else{
                p.next = p2;
                p2 = p2.next;
            }
            p = p.next;
        }
        p.next = p1 == null ? p2 : p1;
        return dummy.next;
    }
}

Time complexity: O(m+n)
Space complexity: O(1)

Development: if de duplication is required

In the case of l1.val == l2.val, only one is output, and then two linked lists go down at the same time.

Input: l1 = [1,2,4], l2 = [1,3,4]
Output:[1,2,3,4]

code:

class Solution {
    public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
        ListNode dummy = new ListNode(-1);
        ListNode p = dummy;
        ListNode p1 = l1;
        ListNode p2 = l2;
        while(p1 != null && p2 != null){
            if(p1.val < p2.val){
                p.next = p1;
                p1 = p1.next;
            }else if(p1.val == p2.val){
                p.next = p1;
                p1 = p1.next;
                p2 = p2.next;
            }else{
                p.next = p2;
                p2 = p2.next;
            }
            p = p.next;
        }
        p.next = p1 == null ? p2 : p1;
        return dummy.next;
    }
}

Method 2: recursion

Code and performance

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if(list1 == null) return list2;
        if(list2 == null) return list1;
        if(list1.val < list2.val){
            list1.next = mergeTwoLists(list1.next, list2);
            return list1;
        }else{
            list2.next = mergeTwoLists(list1, list2.next);
            return list2;
        }
    }
}

**Time complexity: * * each recursive operation: remove the head node of l1 or l2 (until at least one linked list is empty), and the function mergeTwoList will call each node recursively at most once. Therefore, the time complexity depends on the length of the combined linked list, i.e. O(n+m).
**Space complexity: * * at the end of recursive call, the mergeTwoLists function can be called at most n+m times. Recursion needs to consume stack space, so the space complexity is O(n+m).

To reprint code

Follow the same idea and change the operation when l1.val == l2.val.

class Solution {
    public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if(list1 == null) return list2;
        if(list2 == null) return list1;
        if(list1.val < list2.val){
            list1.next = mergeTwoLists(list1.next, list2);
            return list1;
        }else if(list1.val == list2.val){
            list1.next = mergeTwoLists(list1.next, list2.next);
            return list1;
        }else{
            list2.next = mergeTwoLists(list1, list2.next);
            return list2;
        }
    }
}

2, LeetCode 23. Merge K ascending linked lists

Method 1: Merge

1. Recursive version

The atomic operation of "merging two ordered linked lists" is the same as merging and sorting.
Note the merged base case: if(left == right) return lists[left];, It can not be written as left < = right.
Because different from bisection, the inter partition operations of merging are left = mid + 1 and right = mid, and there is no right = mid - 1; Because mid = left + (right - left) / 2 is rounded down, the case of left > right will not occur.

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        int n = lists.length;
        if(n == 0) return null;
        if(n == 1) return lists[0];
        return merge(lists, 0, n-1);
    }
    ListNode merge(ListNode[] lists, int left, int right){
        if(left == right) return lists[left];
        int mid = left + (right - left) / 2;
        ListNode l1 = merge(lists, left, mid);
        ListNode l2 = merge(lists, mid+1, right);
        return mergeTwoLists(l1, l2);
    }
    //The mergeTwoLists method code directly copies the previous question
    ListNode mergeTwoLists(ListNode l1, ListNode l2);
}

Time complexity:
Merging and sorting is an analytical idea. Let the maximum length of the linked list be n. The first time K/2 groups are merged, each group takes 2n, and the second time K/4 groups are merged, each group takes 4n... There are logK times in total, and each time takes Kn, so the time complexity is O(nK*logK). Where n is the maximum length of the linked list and K is the number of linked list entries.
Space complexity: mainly recursive stack space, O(logK). (Note: it is better to use the iterative mergeTwoLists method, which will not occupy additional stack space)

If de duplication is required, just change the mergeTwoLists method to de duplication.

2. Iterative version

The implementation idea of iteration is similar to merge sorting, but the specific implementation is very different.
After the first traversal, the combined results of K/2 groups of linked lists are stored in order in the first half of the linked list array.
This is the case for each cycle after that. See the following code for details:

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        int k = lists.length;
        if(k == 0) return null;
        if(k == 1) return lists[0];
        while(k > 1){
            //For the pointer following i, i walk twice and newRight walk once
            //Why is it called because it will become a new right boundary (open, not closed) at the end of the for loop
            int newRight = 0;
            for(int i=0; i<k; i+=2){
                //If K is an odd number, lists[k-1] will be left alone and saved directly without merging
                if(i == k-1){
                    lists[newRight++] = lists[i];
                }else{
                    lists[newRight++] = mergeTwoLists(lists[i],lists[i+1]);
                }
            }
            //Assign newRight to K to make k a new right boundary (on)
            k = newRight;
        }
        //Finally, the first element of the linked list array lists is the merged array
        return lists[0];
    }
    //The mergeTwoLists method code directly copies the previous question
    ListNode mergeTwoLists(ListNode l1, ListNode l2);
}

Time complexity:
It is exactly the same as the recursive version, O(nKlogK).
Space complexity: mainly recursive stack space, O(1). (Note: it is better to use the iterative mergeTwoLists method, which will not occupy additional stack space)

If de duplication is required, just change the mergeTwoLists method to de duplication.

Method 2: priority queue

1. Implement priority queue by yourself

The merging operation is similar to the previous question.
The MinPQ class in the problem solution implements a small top heap.
If it is only used in this problem, the MinPQ class can have no comparator < k > attribute. Just implement the more() method by directly comparing the val attribute of the ListNode. However, in order to make MinPQ more general, I chose to imitate the PriorityQueue of JDK. The commonality of MinPQ is reflected in that its constructor parameters have a comparator < k > comparator, and comparator < T > is a typical functional interface( See this article ). Therefore, when constructing a MinPQ, you can pass in a Lambda expression to replace the comparator < k > comparator in the parameter list. You can customize the Lambda expression at will to change the comparison mechanism of the more() function. For example, in the code of this problem, the Lambda expression is as follows: (node1, node2) - > (node1.val - node2. VAL).
MinPQ does not write the growth () method and cannot be expanded. However, this is not necessary in this problem.
swim, sink, insert and delMin are the four methods. The code is implemented from the official account labuladong, which is "the two fork heap and the priority queue".
In addition, if the heap is stored from queue[0], the left, right and parent methods need to be changed to:

private int parent(int k){
    return (k-1) / 2;
}
private int left(int k){
    return 2*k + 1;
}
private int right(int k){
    return 2*k + 2;
}

The following is the complete solution:

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if(lists.length == 0) return null;
        ListNode dummy = new ListNode(-1);
        ListNode p = dummy;
        MinPQ<ListNode> mp = new MinPQ<>(
                lists.length, (node1, node2)->(node1.val - node2.val));
        //Put the head nodes of k linked lists into the minimum heap
        for(ListNode head : lists){
            if(head != null) mp.insert(head);
        }
        while(!mp.isEmpty()){
            //Find and connect the next node
            ListNode node = mp.delMin();
            p.next = node;
            //Move pointer
            if(node.next != null) mp.insert(node.next);
            //The p pointer keeps moving forward
            p = p.next;
        }
        return dummy.next;
    }
}
//Priority queue. When inserting or deleting elements, it will be sorted automatically, and the head of the queue will become the largest element
//The bottom layer is a small top pile
//Small top heap: a complete binary tree, and the value of each node is less than or equal to the value of its left and right child nodes
class MinPQ<K> {
    //An array of storage elements
    private K[] queue;
    //Number of elements in the current heap
    private int n;
    //Maximum heap capacity
    private int capacity;
    //Comparator: typical functional interface
    private Comparator<K> comparator;
    //constructor 
    public MinPQ(int cap, Comparator<K> comparator){
        //Index 0 is not used, so it is cap+1
        this.queue = (K[]) new Object[cap + 1];
        this.n = 0;
        this.capacity = cap;
        this.comparator = comparator;
    }
    //Returns the smallest element in the heap
    public K min(){
        return queue[1];
    }
    public boolean isEmpty(){
        return n == 0;
    }
    public void insert(K e){
        //If it is full, it cannot be added
        if(n >= capacity) return;
        n++;
        queue[n] = e;
        swim(n);
    }
    public K delMin(){
        //If the heap is empty, it cannot be deleted and null is returned
        if(n == 0) return null;
        //Change the smallest element to the last
        exch(1,n);
        //Save the value of the original minimum element and delete it
        K min = queue[n];
        queue[n] = null;
        n--;
        sink(1);
        return min;
    }
    private void swim(int k){
        while(k > 1 && more(parent(k),k)){
            exch(k, parent(k));
            k = parent(k);
        }
    }
    private void sink(int k){
        while(left(k) <= n){
            int smaller = left(k);
            if(right(k) <= n && more(left(k),right(k)))
                smaller = right(k);
            if(more(smaller,k))
                break;
            exch(k, smaller);
            k = smaller;
        }
    }
    //Swap two elements in pq array
    private void exch(int i, int j){
        K temp = queue[i];
        queue[i] = queue[j];
        queue[j] = temp;
    }
    //Judge whether pq[i] is greater than pq[j]
    private boolean more(int i, int j){
        //Cannot use ">" directly
        return comparator.compare(queue[i],queue[j]) > 0;
    }
    //Calculate the index values of the parent node and the left and right child nodes
    private int parent(int k){
        return k / 2;
    }
    private int left(int k){
        return 2*k;
    }
    private int right(int k){
        return 2*k + 1;
    }
}

2. Use the PriorityQueue of JDK

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        if(lists.length == 0) return null;
        ListNode dummy = new ListNode(-1);
        ListNode p = dummy;
        PriorityQueue<ListNode> pq = new PriorityQueue<>(
                lists.length, (node1, node2)->(node1.val - node2.val));
        //Put the head nodes of k linked lists into the minimum heap
        for(ListNode head : lists){
            if(head != null) pq.add(head);
        }
        while(!pq.isEmpty()){
            //Find and connect the next node
            ListNode node = pq.poll();
            p.next = node;
            //Move pointer
            if(node.next != null) pq.add(node.next);
            //The p pointer keeps moving forward
            p = p.next;
        }
        return dummy.next;
    }
}

3. Performance

Time complexity: the maximum number of elements in the priority queue is K (number of linked list entries), so the time complexity of a delMin or insert is O(logK). All linked list nodes will be added to and popped out of the priority queue. This part takes O(NlogK) in total. K is the number of linked list nodes and N is the total number of linked list nodes. Other operations, such as pointer movement, are not as time-consuming as join and pop-up operations, so the overall time complexity is O(NlogK).
Space: store one more priority queue, O(K).
Of course, this is not very convenient for de duplication. If de duplication, you need to change the heap mechanism.

Topics: Java Algorithm data structure leetcode linked list