Data structure refresher: Day 7

Table of contents

1. Circular linked list

 1. Hash table

Ideas and algorithms

Complexity analysis

2. Speed ​​pointer

Ideas and algorithms

detail

Complexity analysis

2. Merge two ordered linked lists

 1. Recursion

algorithm

Complexity analysis

2. Iteration

Ideas

algorithm

Complexity analysis

3. Remove linked list elements

1. Recursion

Complexity analysis

2. Iteration

Complexity analysis

1. Circular linked list

141. Ring linked list - LeetCode https://leetcode.cn/problems/linked-list-cycle/?plan=data-structures&plan_progress=ggfacv7

 1. Hash table

Ideas and algorithms

The easiest way to think of is to traverse all nodes, and each time a node is traversed, determine whether the node has been visited before.

Specifically, we can use a hash table to store all visited nodes. Every time we reach a node, if the node already exists in the hash table, then the linked list is a circular linked list, otherwise the node is added to the hash table. Repeat this process until we have traversed the entire linked list.

class Solution {
public:
    bool hasCycle(ListNode *head) {
        unordered_set<ListNode*> seen;
        while (head != nullptr) {
            if (seen.count(head)) {
                return true;
            }
            seen.insert(head);
            head = head->next;
        }
        return false;
    }
};

Complexity analysis

Time complexity: O(N), where N is the number of nodes in the linked list. In the worst case we need to traverse each node once.

Space complexity: O(N), where N is the number of nodes in the linked list. Mainly due to the overhead of the hash table, in the worst case we need to insert each node into the hash table once.

2. Speed ​​pointer

Ideas and algorithms

This method requires readers to have some understanding of "Floyd's Circle Algorithm" (also known as the Tortoise and the Hare Algorithm).

Imagine that "tortoise" and "rabbit" are moving on the linked list. "Rabbit" runs fast and "tortoise" runs slowly. When "tortoise" and "rabbit" start moving from the same node on the linked list, if there is no cycle in the linked list, then "rabbit" will always be in front of "tortoise"; if there is a cycle in the linked list, then "rabbit" It will enter the ring before the "turtle" and keep moving within the ring. When the "tortoise" enters the ring, due to the fast speed of the "rabbit", it will definitely meet the tortoise at some point, that is, it will trap the "tortoise" several times.

We can solve this problem based on the above ideas. Specifically, we define two pointers, one fast and one slow. The slow pointer moves only one step at a time, while the fast pointer moves two steps at a time. Initially, the slow pointer is at position head, and the fast pointer is at position head.next. In this way, if the fast pointer catches up with the slow pointer during the movement, it means that the linked list is a circular linked list. Otherwise, the fast pointer will reach the end of the linked list, and the linked list is not a circular linked list.

detail

Why do we need to stipulate that the slow pointer is initially at position head and the fast pointer is at position head.next, instead of both pointers being at position head (that is, the same description as in "Turtle" and "Rabbit")?

Observe the following code, we are using a while loop, and the loop condition precedes the loop body. Since the loop condition must be to determine whether the fast and slow pointers overlap, if we initially place both pointers at head, the while loop will not be executed. Therefore, we can imagine a virtual node before head, the slow pointer moves one step from the virtual node to head, and the fast pointer moves two steps from the virtual node to head.next, so we can use a while loop.

Of course, we can also use do-while loops. At this point, we can set the initial values ​​of the fast and slow pointers to head.

class Solution {
public:
    bool hasCycle(ListNode* head) {
        if (head == nullptr || head->next == nullptr) {
            return false;
        }
        ListNode* slow = head;
        ListNode* fast = head->next;
        while (slow != fast) {
            if (fast == nullptr || fast->next == nullptr) {
                return false;
            }
            slow = slow->next;
            fast = fast->next->next;
        }
        return true;
    }
};

Complexity analysis

Time complexity: O(N), where N is the number of nodes in the linked list.

When there is no ring in the linked list, the fast pointer will reach the end of the linked list before the slow pointer, and each node in the linked list will be visited at most twice.

When there is a ring in the linked list, the distance between the fast and slow pointers will decrease by one after each round of movement. The initial distance is the length of the ring, so it moves at most N rounds.

Space complexity: O(1). We only use the extra space of two pointers.

2. Merge two ordered linked lists

21. Merge two ordered linked lists - LeetCode https://leetcode.cn/problems/merge-two-sorted-lists/?plan=data-structures&plan_progress=ggfacv7

 1. Recursion

We can recursively define the merge operation in two linked lists as follows
(ignoring edge cases, such as empty linked lists, etc.):


list1[0] + merge(list1[1:],list2) list1[0] < list2[0]
list2[0] + merge(list1, list2[1:) otherwise
, that is to say, the header values ​​of the two linked lists are relatively The smaller node is merged with the result of the merge operation of the remaining elements.

algorithm


We model the above recursive process directly, and we need to consider boundary cases.
If 11 or 12 is an empty linked list from the beginning, then no operation needs to be merged, so we only need to return a non-empty linked list. Otherwise, we have to determine which linked list's head node has a smaller value, 11 or 12, and then recursively determine the next node to add to the result. If one of the two linked lists is empty, the recursion ends.
 

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;
        }
    }
};

Complexity analysis


●Time complexity: O(n+m), where n and m are the lengths of the two linked lists respectively. Because each recursive call will remove
the head node 11 or 12 (until at least one linked list is empty), the function mergeTwoList will only call each node recursively once at most. Therefore, the time complexity depends on the length of the merged linked list, which is O(n + m).
●Space complexity: O(n+n), where n and 1 are the lengths of the two linked lists respectively. Recursively calling the mergeTwoLists function requires stack space. The size of the stack space depends on the depth of the recursive call. When ending the recursive call, the mergeTwoLists function
can be called at most n+m times, so the space complexity is O(n + m).
 

2. Iteration

Ideas

We can implement the above algorithm using an iterative method. When neither l1 nor l2 is an empty linked list, determine which linked list's head node has a smaller value, and add the node with the smaller value to the result. When a node is added to the result, add the corresponding linked list to the result. The node is moved backward one position.

algorithm

First, we set a sentinel node prehead, which allows us to return the merged linked list more easily at the end. We maintain a prev pointer, and what we need to do is adjust its next pointer. Then, we repeat the following process until l1 or l2 points to null: if the value of the current node of l1 is less than or equal to l2, we connect the current node of l1 to the back of the prev node and move the l1 pointer back one bit. Otherwise, we do the same for l2. No matter which element we connect to the back, we need to move prev backward one.

When the loop terminates, at least one of l1 and l2 is non-empty. Since the two input linked lists are both ordered, no matter which linked list is non-empty, all the elements it contains are larger than all the elements in the previously merged linked lists. This means we simply append the non-empty linked list to the merged list and return the merged list.

The figure below shows the process of iterative merging of two linked lists 1->4->5 and 1->2->3->6:

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;
    }
};

Complexity analysis

Time complexity: O(n+m), where n and m are the lengths of the two linked lists respectively. Because only one element of l1 and l2 will be put into the merged linked list in each loop iteration, the number of while loops will not exceed the sum of the lengths of the two linked lists. All other operations have constant time complexity, so the total time complexity is O(n+m).

Space complexity: O(1). We only need constant space to store several variables.

3. Remove linked list elements

203. Remove linked list elements - LeetCode https://leetcode.cn/problems/remove-linked-list-elements/?plan=data-structures&plan_progress=ggfacv7

1. Recursion

The definition of linked list is recursive, so linked list problems can often be solved using recursive methods. This question requires deleting all nodes in the linked list whose node value is equal to a specific value, which can be achieved using recursion.

For a given linked list, first delete the nodes except the head node head, and then determine whether the node value of head is equal to the given val. If the node value of head is equal to val, head needs to be deleted, so the head node after the deletion operation is head.next; if the node value of head is not equal to val, head is retained, so the head node after the deletion operation is still head. The above process is a recursive process.

The termination condition of recursion is that head is empty, in which case head is returned directly. When head is not empty, perform the deletion operation recursively, then determine whether the node value of head is equal to val and decide whether to delete head.

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        if (head == nullptr) {
            return head;
        }
        head->next = removeElements(head->next, val);
        return head->val == val ? head->next : head;
    }
};

Complexity analysis

Time complexity: O(n), where n is the length of the linked list. The linked list needs to be traversed once during the recursive process.

Space complexity: O(n), where n is the length of the linked list. The space complexity mainly depends on the recursive call stack, which will not exceed n levels at most.

2. Iteration

You can also use iteration to delete all nodes in the linked list whose node value is equal to a specific value.
Use temp to represent the current node. If the next node of temp is not empty and the node value of the next node is equal to the given val, then
the next node needs to be deleted. Deleting the next node can be done by

The following method is implemented:
temp.next = temp.next. next
If the node value of the next node of temp is not equal to the given val, then keep the next node and move temp to the next
node. When the next node of temp is empty, the linked list traversal ends, and all nodes with node values ​​equal to val are deleted. In terms of specific implementation, the head node head of the linked list may need to be deleted, so create a dummy node dummyHead, set
dummyHead.next= head, initialize temp =dummyHead, and then traverse the linked list to perform the deletion operation. The final
returned dummyHead.next is the head node after the deletion operation.

 

class Solution {
public:
    ListNode* removeElements(ListNode* head, int val) {
        struct ListNode* dummyHead = new ListNode(0, head);
        struct ListNode* temp = dummyHead;
        while (temp->next != NULL) {
            if (temp->next->val == val) {
                temp->next = temp->next->next;
            } else {
                temp = temp->next;
            }
        }
        return dummyHead->next;
    }
};

Complexity analysis

  • Time complexity: O(n), where n is the length of the linked list. The linked list needs to be traversed once.

  • Space complexity: O(1).

Guess you like

Origin blog.csdn.net/m0_63309778/article/details/126743391