diff --git a/DIRECTORY.md b/DIRECTORY.md index 465438f2..0fd346b1 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -412,14 +412,17 @@ * Circular * [Node](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/circular/node.py) * [Test Circular Linked List](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/circular/test_circular_linked_list.py) + * [Test Circular Linked List Split](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/circular/test_circular_linked_list_split.py) * Doubly Linked List * [Node](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/doubly_linked_list/node.py) * [Test Doubly Linked List](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/doubly_linked_list/test_doubly_linked_list.py) * [Test Doubly Linked List Move Tail To Head](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/doubly_linked_list/test_doubly_linked_list_move_tail_to_head.py) * [Test Doubly Linked List Palindrome](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/doubly_linked_list/test_doubly_linked_list_palindrome.py) + * [Linked List](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/linked_list.py) * [Linked List Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/linked_list_utils.py) * Mergeklinkedlists * [Test Merge](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/mergeklinkedlists/test_merge.py) + * [Node](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/node.py) * Reorder List * [Test Reorder List](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/reorder_list/test_reorder_list.py) * Singly Linked List @@ -438,6 +441,8 @@ * [Test Singly Linked Move Tail To Head](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/test_singly_linked_move_tail_to_head.py) * [Test Singly Linked Palindrome](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/test_singly_linked_palindrome.py) * [Test Singly Linked Revese](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/singly_linked_list/test_singly_linked_revese.py) + * [Test Linked List Utils](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/test_linked_list_utils.py) + * [Types](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/linked_lists/types.py) * Lists * Bus Stops * [Test Bus Stops](https://github.com/BrianLusina/PythonSnips/blob/master/datastructures/lists/bus_stops/test_bus_stops.py) diff --git a/datastructures/linked_lists/__init__.py b/datastructures/linked_lists/__init__.py index 8313fe07..e8b8e6a9 100755 --- a/datastructures/linked_lists/__init__.py +++ b/datastructures/linked_lists/__init__.py @@ -1,699 +1,5 @@ -from typing import Any, Union, Optional, Generic, TypeVar, List, Tuple -from abc import ABCMeta, abstractmethod +from datastructures.linked_lists.node import Node +from datastructures.linked_lists.linked_list import LinkedList +from datastructures.linked_lists.types import T -from datastructures.linked_lists.exceptions import EmptyLinkedList -from datastructures.linked_lists.linked_list_utils import ( - has_cycle, - detect_node_with_cycle, - cycle_length, - remove_cycle, -) - -T = TypeVar("T") - - -class Node(Generic[T]): - """ - Node object in the Linked List - """ - - __metaclass__ = ABCMeta - - def __init__( - self, - data: Optional[T] = None, - next_: Optional["Node[Generic[T]]"] = None, - key: Any = None, - ): - self.data = data - self.next = next_ - # if no key is provided, the hash of the data becomes the key - self.key = key or hash(data) - - def __str__(self) -> str: - return f"Node(data={self.data}, key={self.key})" - - def __repr__(self) -> str: - return f"Node(data={self.data}, key={self.key})" - - def __eq__(self, other: "Node") -> bool: - return self.key == other.key - - -class LinkedList(Generic[T]): - """ - The most basic LinkedList from which other types of Linked List will be subclassed - """ - - __metaclass__ = ABCMeta - - def __init__(self, head: Optional[Node[Generic[T]]] = None): - self.head: Optional[Node[Generic[T]]] = head - - def __iter__(self): - current = self.head - if not current: - return - - if current: - if current.data: - yield current.data - - if current.next: - node = current.next - while node: - if node.data: - yield node.data - node = node.next - - @abstractmethod - def __str__(self): - return "->".join([str(item) for item in self]) - - @abstractmethod - def __repr__(self): - return "->".join([str(item) for item in self]) - - def __len__(self) -> int: - """ - Implements the len() for a linked list. This counts the number of nodes in a Linked List - This uses an iterative method to find the length of the LinkedList - Returns: - int: Number of nodes - """ - return len(tuple(iter(self))) - - @abstractmethod - def append(self, data): - """ - Add a node to the end of the linked list - :param data: the node to add to the list - """ - raise NotImplementedError("Not yet implemented") - - @abstractmethod - def prepend(self, data): - """ - Add a node to the beginning of the linked list - :param data: the node to add to the list - """ - raise NotImplementedError("Not yet implemented") - - def search(self, node): - """ - Search method to search for a node in the LinkedList - :param node: the node being sought - :return: the node being sought - """ - head = self.head - if head is not None: - while head.next is not None: - if head.data == node: - return head - head = head.next - if head.data == node: - return head - return None - - def count_occurrences(self, data: Any) -> int: - """ - Counts the number of occurrences of a data in a LinkedList. If the linked list is empty(no head). 0 is returned. - otherwise the occurrences of the data element will be sought using the equality operator. This assumes that the - data element in each node already implements this operator. - - Complexity: - The assumption here is that n is the number of nodes in the linked list. - - Time O(n): This is because the algorithm iterates through each node in the linked list to find data values in - each node that equal the provided data argument in the function. This is both for the worst and best case as - each node in the linked list has to be checked - - Space O(1): no extra space is required other than the value being incremented for each node whose data element - equals the provided data argument. - - Args: - data(Any): the data element to count. - Returns: - int: the number of occurrences of an element in the linked list - """ - if self.head is None: - return 0 - else: - occurrences = 0 - current = self.head - while current: - if current.data == data: - occurrences += 1 - current = current.next - return occurrences - - def get_last(self): - """ - Gets the last node in the Linked List. check each node and test if it has a successor - if it does, continue checking - :return: The last Node element - :rtype: Node - """ - if not self.head or not self.head.next: - return self.head - - node = self.head - while node.next: - node = node.next - - return node - - def get_position(self, position: int) -> Union[Node, None]: - """ - Returns the current node in the linked list if the current position of the node is equal to the position. Assume - counting starts from 1 - :param position: Used to get the node at the given position - :type position int - :raises ValueError - :return: Node - :rtype: Node - """ - if position < 0 or not isinstance(position, int): - raise ValueError("Position should be a positive integer") - counter = 1 - current = self.head - - if position == 0 and current is not None: - return current - if position < 1 and current is None: - return None - while current and counter <= position: - if counter == position: - return current - current = current.next - counter += 1 - return None - - def is_empty(self): - """ - Check if the linked list is empty, essentially if the linked list's head's successor is None - then the linked list is empty - :return: True or False - :rtype: bool - """ - return self.head is None - - @abstractmethod - def reverse(self): - """ - Reverses the linked list, such that the Head points to the last item in the - LinkedList and the tail points to its predecessor. The first node becomes the tail - :return: New LinkedList which is reversed - :rtype: LinkedList - """ - raise NotImplementedError("Not Yet Implemented") - - @abstractmethod - def insert(self, node, pos): - """ - Insert node at a particular position in the list - :param node: node to insert - :param pos: position to insert the node - :type pos int - :return: inserted node in the list along with the predecessor and successor - :rtype: Node - """ - raise NotImplementedError() - - @abstractmethod - def insert_after_node(self, prev_key: Any, data: T): - """ - Inserts a given node data after a node's key in the Linked List. First find the node in the LinkedList with the - provided key. Get its successor, store in temp variable and insert this node with data in the position, - get this node's next as the successor of the current node - Args: - prev_key Any: The node's previous key to find - data T: The data to insert - """ - raise NotImplementedError("Not yet implemented") - - @abstractmethod - def insert_before_node(self, next_key: Any, data: T): - """ - Inserts a given node data before a node's key in the Linked List. - Args: - next_key Any: The node's next key to find - data T: The data to insert - """ - raise NotImplementedError("Not yet implemented") - - @abstractmethod - def unshift(self, node): - """ - Insert a node at the beginning of the list - :return: Inserted node, its predecessor and successor - :rtype: LinkedList object - """ - pass - - @abstractmethod - def shift(self): - """ - Deletes a node from the beginning of the linked list, sets the new head to the successor of the deleted - head node - :return: the deleted node - :rtype: Node - """ - # check if the LinkedList is empty, return None - if self.is_empty(): - return None - - @abstractmethod - def pop(self) -> Optional[Node]: - """ - Deletes the last node element from the LinkedList - :return: Deleted node element - :rtype: Node object - """ - raise NotImplementedError("Not yet implemented") - - def delete_head(self) -> Union[Node, None]: - """ - Delete the first node in the linked list - """ - if self.head: - deleted_node = self.head - self.head = self.head.next - return deleted_node - else: - return None - - @abstractmethod - def delete_node(self, node: Node): - """ - Finds the node from the linked list and deletes it from the LinkedList - Moves the node's next to this node's previous link - Moves this node's previous link to this node's next link - :param node: Node element to be deleted - """ - raise NotImplementedError("Not yet implemented") - - @abstractmethod - def delete_node_at_position(self, position: int): - """ - Deletes a node from the given provided position. - :param position: Position of node to delete - """ - if not 0 <= position <= len(self) - 1: - raise ValueError(f"Position ${position} out of bounds") - - if self.head is None: - return None - - # rest of the implementation is at the relevant subclasses - - @abstractmethod - def delete_node_by_key(self, key: Any): - """ - traverses the LinkedList until we find the data in a Node that matches and deletes that node. This uses the same - approach as self.delete_node(node: Node) but instead of using the node to traverse the linked list, we use the - data attribute of a node. Note that if there are duplicate Nodes in the LinkedList with the same data attributes - only, the first Node is deleted. - Args: - key Any: Key of Node element to be deleted - """ - raise NotImplementedError("Not yet implemented") - - @abstractmethod - def delete_nodes_by_key(self, key: Any): - """ - traverses the LinkedList until we find the key in a Node that matches and deletes those nodes. This uses the - same approach as self.delete_node_by_key(key) but instead deletes multiple nodes with the same key - Args: - key Any: Key of Node elements to be deleted - """ - raise NotImplementedError("Not yet implemented") - - @abstractmethod - def delete_middle_node(self) -> Optional[Node]: - """ - Deletes the middle node in the linked list and returns the deleted node - """ - raise NotImplementedError("Not yet implemented") - - @abstractmethod - def delete_nth_last_node(self, n: int) -> Optional[Node]: - """ - Deletes the nth last node of the linked list and returns the head of the linked list. - Example: - n = 1 - 43 -> 68 -> 11 -> 5 -> 69 -> 37 -> 70 -> None - 43 -> 68 -> 11 -> 5 -> 69 -> 37 -> None - Args: - n (int): the position from the last node of the node to delete - Returns: - Node: Head of the linked list - """ - raise NotImplementedError("Not yet implemented") - - @abstractmethod - def display(self): - """ - Displays the whole of the LinkedList - :return: LinkedList data structure - """ - pass - - @abstractmethod - def display_forward(self): - """ - Display the complete list in a forward manner - :return:The LinkedList displayed in 'ascending order' or in order of insertion - """ - pass - - @abstractmethod - def display_backward(self): - """ - Display the complete list in a backward manner - :return: The LinkedList displayed in 'descending order', not in the order of insertion - """ - pass - - def has_cycle(self): - """ - Detects if linked list has cycle, i.e. a node in the linked list points to a node already traversed. - Will use Floyd-Cycle Detection algorithm to check for cycles. Will have a fast and slow pointer and check if - the fast pointer - catches up to the slow pointer. If this happens, there is a cycle. The fast pointer will move at twice the speed - of slow pointer - :return: True if there is a cycle, False otherwise - :rtype: bool - """ - return has_cycle(self.head) - - def cycle_length(self) -> int: - """ - Determines the length of the cycle in a linked list if it has one. The length of the cycle is the number - of nodes that are 'caught' in the cycle - Returns: - int: length of the cycle or number of nodes in the cycle - """ - return cycle_length(self.head) - - def detect_node_with_cycle(self) -> Optional[Node]: - """ - Detects the node with a cycle and returns it - """ - return detect_node_with_cycle(self.head) - - def remove_cycle(self) -> Optional[Node]: - """ - Removes cycle if there exists. This will use the same concept as has_cycle method to check if there is a loop - and remove the cycle - Returns: - Node: head node with cycle removed - """ - if not self.head or not self.head.next: - return self.head - - return remove_cycle(self.head) - - @abstractmethod - def alternate_split(self): - """ - Alternate split a linked list such that a linked list such as a->b->c->d->e becomes a->c->e->None and b->d->None - """ - pass - - @abstractmethod - def is_palindrome(self) -> bool: - """ - Checks if the linked list is a Palndrome. That is, can be read from both back & front - :return: boolean data. True if the LinkedList is a Palindrome - """ - raise NotImplementedError("Method has not been implemented") - - def swap_nodes(self, data_one: Any, data_two: Any): - """ - Swaps two nodes based on the data they contain. We search through the LinkedList looking for the data item in - each node. Once the first is found, we keep track of it and move on until we find the next data item. Once that - is found, we swap the two nodes' data items. - - If we can't find the first data item nor the second. No need to perform swap. If the 2 data items are similar - no need to perform swap as well. - - If the LinkedList is empty (i.e. has no head node), return, no need to swap when we have no LinkedList :) - """ - if not self.head: - raise EmptyLinkedList("Empty LinkedList") - - # if they are the same, we do not need to swap - if data_one == data_two: - return - - # set the 2 pointers we will use to traverse the linked list - current_one = self.head - current_two = self.head - - # move the pointer down the LinkedList while the data item is not the same as the data item we are searching for - while current_one and current_one.data != data_one: - current_one = current_one.next - - # we look for the second data item - while current_two and current_two.data != data_two: - current_two = current_two.next - - # the data items do not exist in the LinkedList or only one of them exists, therefore we can not perform a swap - if not current_one or not current_two: - return - - # swap the data items of the nodes - current_one.data, current_two.data = current_two.data, current_one.data - - @abstractmethod - def pairwise_swap(self) -> Node: - """ - Swaps nodes in a linked list in pairs. - As there are different kinds of LinkedLists, it is up to the child class to implement this - - The premise(idea) is to swap the data of each node with the data of the next node. This is while using - an iterative approach - Example: - 1 -> 2 -> 3 -> 4 - becomes - 2 -> 1 -> 4 -> 3 - :return: New head of node - """ - raise NotImplementedError("Method has not been implemented") - - @abstractmethod - def swap_nodes_at_kth_and_k_plus_1(self, k: int) -> Node: - """ - Return the head of the linked list after swapping the datas of the kth node from the beginning and the kth node - from the end (the list is 1-indexed). - - Input: head = [7,9,6,6,7,8,3,0,9,5], k = 5 - Output: [7,9,6,6,8,7,3,0,9,5] - """ - raise NotImplementedError("Method has not been implemented") - - def get_kth_to_last_node(self, k: int) -> Optional[Node]: - """ - Gets the kth to the last node in a Linked list. - - Assumptions: - - k can not be an invalid integer, less than 0. A ValueError will be raised - - Algorithm: - - set 2 pointers; fast_pointer & slow_pointer - - Move fast_pointer k steps ahead - - increment both pointers(fast_pointer & slow_pointer) until fast_pointer reaches end - - return the slow_pointer - - Complexity Analysis: - - Time Complexity O(n) where n is the number of nodes in the linked list to traverse - - Space Complexity O(1). No extra space is needed - - @param k: integer value which will enable us to get the kth node from the end of the LinkedList - @return: Kth node from the end - @rtype: Node - """ - if k < 0: - raise ValueError("Invalid K value") - if k > len(self): - raise IndexError("K longer than linked list") - fast_pointer, slow_pointer = self.head, self.head - - for _ in range(k - 1): - fast_pointer = fast_pointer.next - - if not fast_pointer: - return None - - while fast_pointer.next: - fast_pointer = fast_pointer.next - slow_pointer = slow_pointer.next - - return slow_pointer - - @abstractmethod - def move_to_front(self, node: Node): - """ - Moves a node from it's current position to the head of the linked list - @param node: - @return: - """ - raise NotImplementedError("Not yet implemented") - - @abstractmethod - def move_tail_to_head(self): - """ - Moves the tail node to the head node making the tail node the new head of the linked list - Uses two pointers where last pointer will be moved until it points to the last node in the linked list. - The second pointer, previous, will point to the second last node in the linked list. - - Complexity Analysis: - - An assumption is made where n is the number of nodes in the linked list - - Time: O(n) as the the pointers have to be moved through each node in the linked list until both point to the - last and second last nodes in the linked list - - - Space O(1) as no extra space is incurred in the iteration. Only pointers are moved at the end to move the tail - node to the head and make the second to last node the new tail - """ - raise NotImplementedError("Not yet implemented") - - @abstractmethod - def partition(self, data: Any) -> Union[Node, None]: - """ - Partitions a LinkedList around a data point such that all nodes with values less than this data point come - before all nodes greater than or equal to that data point - - Algorithm: - - Create left & right LinkedLists - - For each element/node in the linked list: - - if element data < data: - - append to the left list - - if element data is equal to the data: - - prepend to the right list - - if element data is greater than the data: - - append to the right list - - Check if the left list is empty. Return right list if left is empty - - Move the pointer of the left list up to the last node (O(n) operation) - - set the next pointer of the last node in the left list to the head of the right list - - return the head of the left list - - Complexity Analysis: - - Space Complexity: O(n) - left & right lists to hold the partitions of the linked list with nodes n - - Time Complexity: O(n) - where n is the number of nodes in the linked list - - @param data: Data point to compare nodes and create partitions - @return: Head of the partitioned linked list - @rtype: Node - """ - raise NotImplementedError("Not yet implemented") - - @abstractmethod - def remove_tail(self): - """ - Remotes the tail of a linked list - """ - raise NotImplementedError("Not yet implemented") - - @abstractmethod - def remove_duplicates(self) -> Optional[Node]: - """ - Remotes the duplicates from a linked list - """ - raise NotImplementedError("Not yet implemented") - - @abstractmethod - def rotate(self, k: int): - """ - Rotates a linked list by k nodes - """ - raise NotImplementedError("Not yet implemented") - - @abstractmethod - def reverse_groups(self, k: int): - """ - Reverses every k groups of a linked list - """ - raise NotImplementedError("Not yet implemented") - - def middle_node(self) -> Optional[Node]: - """ - Traverse the linked list to find the middle node - Time Complexity: O(n) where n is the number of nodes in the linked list - Space Complexity: O(1) as constant extra space is needed - @return: Middle Node or None - """ - if not self.head: - return None - - fast_pointer, slow_pointer = self.head, self.head - - while fast_pointer and fast_pointer.next: - slow_pointer = slow_pointer.next - fast_pointer = fast_pointer.next.next - - return slow_pointer - - @abstractmethod - def odd_even_list(self) -> Optional[Node]: - """ - Returns the odd even list where the even indexed nodes are grouped first and then the even indexed nodes - @return: New head node - """ - raise NotImplementedError("not yet implemented") - - @abstractmethod - def maximum_pair_sum(self) -> int: - """ - Returns the maximum twin sum of a node and its twin, where a node's twin is at the index (n-1-i) where n is the - number of nodes in the linked list. - For example, if n = 4, then node 0 is the twin of node 3, and node 1 is the twin of node 2. These are the only - nodes with twins for n = 4. - @return: maximum twin sum of a node and it's twin - """ - raise NotImplementedError("not yet implemented") - - @abstractmethod - def pairs_with_sum(self, target: T) -> List[Tuple[Node, Node]]: - """ - Returns a list of tuples which contain nodes whose data sum equal the given target. - Args: - target T: the target with which each pair's data sums up to - Return: - List: list of pairs - """ - raise NotImplementedError("not yet implemented") - - @staticmethod - def reverse_list(head: Node) -> Optional[Node]: - """ - Reverses a linked list given the head node - Args: - head Node: the head node of the linked list - Returns: - Optional[Node]: the new head node of the reversed linked list - """ - if head is None or head.next is None: - return head - - # track previous node, so we can point our next pointer to it - previous = None - # track node to loop through - current_node = head - - while current_node: - # track the next node to not lose it while adjusting pointers - nxt = current_node.next - - # set the next pointer to the node behind it, previous - current_node.next = previous - - # adjust the new previous node to the current node for subsequent loops - previous = current_node - - # move our node pointer up to the next node in front of it - current_node = nxt - - # return the new tail of the k-group which is our head - return previous +__all__ = ["LinkedList", "Node", "T"] diff --git a/datastructures/linked_lists/circular/__init__.py b/datastructures/linked_lists/circular/__init__.py index 2a87f13f..5a693d6c 100644 --- a/datastructures/linked_lists/circular/__init__.py +++ b/datastructures/linked_lists/circular/__init__.py @@ -206,34 +206,54 @@ def split_list(self) -> Optional[Tuple[CircularNode, Optional[CircularNode]]]: Returns: Tuple: tuple with two circular linked lists """ - size = len(self) - - if size == 0: + # If there is no head node, there is nothing more to do here + if not self.head: return None - if size == 1: - return self.head, None - mid = size // 2 - count = 0 + # Get the middle of the linked list + middle_node = self.middle_node() - previous: Optional[CircularNode] = None - current = self.head + # Set the head node to head_one and head_two will be the head node of the second linked list + head_one = self.head + head_two = middle_node.next + # Set the middle node's next pointer to cycle back to the head_one node + middle_node.next = head_one - while current and count < mid: - count += 1 - previous = current + # Assign a pointer to the second head node and move it along the linked list as long as it's next pointer is not + # equal to the head node + current = head_two + while current.next is not self.head: current = current.next - previous.next = self.head + # Now since we are at the tail of the linked list, we assign the next pointer to point to the second head node + current.next = head_two - second_list = CircularLinkedList() - while current.next != self.head: - second_list.append(current.data) - current = current.next + # Now we have split the linked list into two halves. + return head_one, head_two + + def middle_node(self) -> Optional[CircularNode]: + """ + Traverse the linked list to find the middle node. For a circular linked list, the tail node points back to the + head node, to prevent this from continuously looping, we have to break the cycle once the tail node's next pointer + points back to the head node + Time Complexity: O(n) where n is the number of nodes in the linked list + Space Complexity: O(1) as constant extra space is needed + Return: + CircularNode: middle node of linked list + """ + if not self.head: + return None + + fast_pointer, slow_pointer = self.head, self.head - second_list.append(current.data) + while ( + fast_pointer.next is not self.head + and fast_pointer.next.next is not self.head + ): + slow_pointer = slow_pointer.next + fast_pointer = fast_pointer.next.next - return self.head, second_list + return slow_pointer def is_palindrome(self) -> bool: pass diff --git a/datastructures/linked_lists/circular/test_circular_linked_list.py b/datastructures/linked_lists/circular/test_circular_linked_list.py index 6e3fd85f..aa63515a 100644 --- a/datastructures/linked_lists/circular/test_circular_linked_list.py +++ b/datastructures/linked_lists/circular/test_circular_linked_list.py @@ -55,24 +55,5 @@ def test_1(self): self.assertEqual(expected, list(circular_linked_list)) -class CircularLinkedListSplitListTestCase(unittest.TestCase): - def test_1(self): - """should split a linked list [1,2,3,4,5,6] to become ([1,2,3],[4,5,6])""" - data = [1, 2, 3, 4, 5, 6] - expected = ([1, 2, 3], [4, 5, 6]) - circular_linked_list = CircularLinkedList() - - for d in data: - circular_linked_list.append(d) - - result = circular_linked_list.split_list() - self.assertIsNotNone(result) - - first_list_head, second_list_head = result - - self.assertEqual(expected[0], list(first_list_head)) - self.assertEqual(expected[1], list(second_list_head)) - - if __name__ == "__main__": unittest.main() diff --git a/datastructures/linked_lists/circular/test_circular_linked_list_split.py b/datastructures/linked_lists/circular/test_circular_linked_list_split.py new file mode 100644 index 00000000..9e916c34 --- /dev/null +++ b/datastructures/linked_lists/circular/test_circular_linked_list_split.py @@ -0,0 +1,51 @@ +import unittest +from typing import List, Tuple +from parameterized import parameterized +from datastructures.linked_lists.circular import CircularLinkedList +from datastructures.linked_lists.circular.node import CircularNode + + +CIRCULAR_LINKED_LIST_SPLIT_LIST_TEST_CASES = [ + ([1, 2, 3, 4, 5, 6], ([1, 2, 3], [4, 5, 6])), + ([], ([], [])), + ([1, 5, 7], ([1, 5], [7])), + ([2, 6, 1, 5], ([2, 6], [1, 5])), + ([3, 1, 4, 2, 5], ([3, 1, 4], [2, 5])), + ([8, 10, 12, 14, 16, 18], ([8, 10, 12], [14, 16, 18])), + ([9, 10], ([9], [10])), +] + + +class CircularLinkedListSplitListTestCase(unittest.TestCase): + def assert_data(self, head: CircularNode, expected: List[int]): + current = head + actual = [] + while current: + actual.append(current.data) + if current.next is head: + break + current = current.next + + self.assertListEqual(expected, actual) + + @parameterized.expand(CIRCULAR_LINKED_LIST_SPLIT_LIST_TEST_CASES) + def test_split_list(self, data: List[int], expected: Tuple[List[int], List[int]]): + circular_linked_list = CircularLinkedList() + if not data: + result = circular_linked_list.split_list() + self.assertIsNone(result) + return + + for d in data: + circular_linked_list.append(d) + + result = circular_linked_list.split_list() + self.assertIsNotNone(result) + + first_list_head, second_list_head = result + self.assert_data(first_list_head, expected[0]) + self.assert_data(second_list_head, expected[1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/datastructures/linked_lists/linked_list.py b/datastructures/linked_lists/linked_list.py new file mode 100644 index 00000000..1fbe43ff --- /dev/null +++ b/datastructures/linked_lists/linked_list.py @@ -0,0 +1,669 @@ +from typing import Any, Union, Optional, Generic, List, Tuple +from abc import ABCMeta, abstractmethod + +from datastructures.linked_lists.exceptions import EmptyLinkedList +from datastructures.linked_lists.linked_list_utils import ( + has_cycle, + detect_node_with_cycle, + cycle_length, + remove_cycle, +) +from datastructures.linked_lists.node import Node +from datastructures.linked_lists.types import T + + +class LinkedList(Generic[T]): + """ + The most basic LinkedList from which other types of Linked List will be subclassed + """ + + __metaclass__ = ABCMeta + + def __init__(self, head: Optional[Node[Generic[T]]] = None): + self.head: Optional[Node[Generic[T]]] = head + + def __iter__(self): + current = self.head + if not current: + return + + if current: + if current.data: + yield current.data + + if current.next: + node = current.next + while node: + if node.data: + yield node.data + node = node.next + + @abstractmethod + def __str__(self): + return "->".join([str(item) for item in self]) + + @abstractmethod + def __repr__(self): + return "->".join([str(item) for item in self]) + + def __len__(self) -> int: + """ + Implements the len() for a linked list. This counts the number of nodes in a Linked List + This uses an iterative method to find the length of the LinkedList + Returns: + int: Number of nodes + """ + return len(tuple(iter(self))) + + @abstractmethod + def append(self, data): + """ + Add a node to the end of the linked list + :param data: the node to add to the list + """ + raise NotImplementedError("Not yet implemented") + + @abstractmethod + def prepend(self, data): + """ + Add a node to the beginning of the linked list + :param data: the node to add to the list + """ + raise NotImplementedError("Not yet implemented") + + def search(self, node): + """ + Search method to search for a node in the LinkedList + :param node: the node being sought + :return: the node being sought + """ + head = self.head + if head is not None: + while head.next is not None: + if head.data == node: + return head + head = head.next + if head.data == node: + return head + return None + + def count_occurrences(self, data: Any) -> int: + """ + Counts the number of occurrences of a data in a LinkedList. If the linked list is empty(no head). 0 is returned. + otherwise the occurrences of the data element will be sought using the equality operator. This assumes that the + data element in each node already implements this operator. + + Complexity: + The assumption here is that n is the number of nodes in the linked list. + + Time O(n): This is because the algorithm iterates through each node in the linked list to find data values in + each node that equal the provided data argument in the function. This is both for the worst and best case as + each node in the linked list has to be checked + + Space O(1): no extra space is required other than the value being incremented for each node whose data element + equals the provided data argument. + + Args: + data(Any): the data element to count. + Returns: + int: the number of occurrences of an element in the linked list + """ + if self.head is None: + return 0 + else: + occurrences = 0 + current = self.head + while current: + if current.data == data: + occurrences += 1 + current = current.next + return occurrences + + def get_last(self): + """ + Gets the last node in the Linked List. check each node and test if it has a successor + if it does, continue checking + :return: The last Node element + :rtype: Node + """ + if not self.head or not self.head.next: + return self.head + + node = self.head + while node.next: + node = node.next + + return node + + def get_position(self, position: int) -> Union[Node, None]: + """ + Returns the current node in the linked list if the current position of the node is equal to the position. Assume + counting starts from 1 + :param position: Used to get the node at the given position + :type position int + :raises ValueError + :return: Node + :rtype: Node + """ + if position < 0 or not isinstance(position, int): + raise ValueError("Position should be a positive integer") + counter = 1 + current = self.head + + if position == 0 and current is not None: + return current + if position < 1 and current is None: + return None + while current and counter <= position: + if counter == position: + return current + current = current.next + counter += 1 + return None + + def is_empty(self): + """ + Check if the linked list is empty, essentially if the linked list's head's successor is None + then the linked list is empty + :return: True or False + :rtype: bool + """ + return self.head is None + + @abstractmethod + def reverse(self): + """ + Reverses the linked list, such that the Head points to the last item in the + LinkedList and the tail points to its predecessor. The first node becomes the tail + :return: New LinkedList which is reversed + :rtype: LinkedList + """ + raise NotImplementedError("Not Yet Implemented") + + @abstractmethod + def insert(self, node, pos): + """ + Insert node at a particular position in the list + :param node: node to insert + :param pos: position to insert the node + :type pos int + :return: inserted node in the list along with the predecessor and successor + :rtype: Node + """ + raise NotImplementedError() + + @abstractmethod + def insert_after_node(self, prev_key: Any, data: T): + """ + Inserts a given node data after a node's key in the Linked List. First find the node in the LinkedList with the + provided key. Get its successor, store in temp variable and insert this node with data in the position, + get this node's next as the successor of the current node + Args: + prev_key Any: The node's previous key to find + data T: The data to insert + """ + raise NotImplementedError("Not yet implemented") + + @abstractmethod + def insert_before_node(self, next_key: Any, data: T): + """ + Inserts a given node data before a node's key in the Linked List. + Args: + next_key Any: The node's next key to find + data T: The data to insert + """ + raise NotImplementedError("Not yet implemented") + + @abstractmethod + def unshift(self, node): + """ + Insert a node at the beginning of the list + :return: Inserted node, its predecessor and successor + :rtype: LinkedList object + """ + pass + + @abstractmethod + def shift(self): + """ + Deletes a node from the beginning of the linked list, sets the new head to the successor of the deleted + head node + :return: the deleted node + :rtype: Node + """ + # check if the LinkedList is empty, return None + if self.is_empty(): + return None + + @abstractmethod + def pop(self) -> Optional[Node]: + """ + Deletes the last node element from the LinkedList + :return: Deleted node element + :rtype: Node object + """ + raise NotImplementedError("Not yet implemented") + + def delete_head(self) -> Union[Node, None]: + """ + Delete the first node in the linked list + """ + if self.head: + deleted_node = self.head + self.head = self.head.next + return deleted_node + else: + return None + + @abstractmethod + def delete_node(self, node: Node): + """ + Finds the node from the linked list and deletes it from the LinkedList + Moves the node's next to this node's previous link + Moves this node's previous link to this node's next link + :param node: Node element to be deleted + """ + raise NotImplementedError("Not yet implemented") + + @abstractmethod + def delete_node_at_position(self, position: int): + """ + Deletes a node from the given provided position. + :param position: Position of node to delete + """ + if not 0 <= position <= len(self) - 1: + raise ValueError(f"Position {position} out of bounds") + if self.head is None: + return None + + # rest of the implementation is at the relevant subclasses + + @abstractmethod + def delete_node_by_key(self, key: Any): + """ + traverses the LinkedList until we find the data in a Node that matches and deletes that node. This uses the same + approach as self.delete_node(node: Node) but instead of using the node to traverse the linked list, we use the + data attribute of a node. Note that if there are duplicate Nodes in the LinkedList with the same data attributes + only, the first Node is deleted. + Args: + key Any: Key of Node element to be deleted + """ + raise NotImplementedError("Not yet implemented") + + @abstractmethod + def delete_nodes_by_key(self, key: Any): + """ + traverses the LinkedList until we find the key in a Node that matches and deletes those nodes. This uses the + same approach as self.delete_node_by_key(key) but instead deletes multiple nodes with the same key + Args: + key Any: Key of Node elements to be deleted + """ + raise NotImplementedError("Not yet implemented") + + @abstractmethod + def delete_middle_node(self) -> Optional[Node]: + """ + Deletes the middle node in the linked list and returns the deleted node + """ + raise NotImplementedError("Not yet implemented") + + @abstractmethod + def delete_nth_last_node(self, n: int) -> Optional[Node]: + """ + Deletes the nth last node of the linked list and returns the head of the linked list. + Example: + n = 1 + 43 -> 68 -> 11 -> 5 -> 69 -> 37 -> 70 -> None + 43 -> 68 -> 11 -> 5 -> 69 -> 37 -> None + Args: + n (int): the position from the last node of the node to delete + Returns: + Node: Head of the linked list + """ + raise NotImplementedError("Not yet implemented") + + @abstractmethod + def display(self): + """ + Displays the whole of the LinkedList + :return: LinkedList data structure + """ + pass + + @abstractmethod + def display_forward(self): + """ + Display the complete list in a forward manner + :return:The LinkedList displayed in 'ascending order' or in order of insertion + """ + pass + + @abstractmethod + def display_backward(self): + """ + Display the complete list in a backward manner + :return: The LinkedList displayed in 'descending order', not in the order of insertion + """ + pass + + def has_cycle(self): + """ + Detects if linked list has cycle, i.e. a node in the linked list points to a node already traversed. + Will use Floyd-Cycle Detection algorithm to check for cycles. Will have a fast and slow pointer and check if + the fast pointer + catches up to the slow pointer. If this happens, there is a cycle. The fast pointer will move at twice the speed + of slow pointer + :return: True if there is a cycle, False otherwise + :rtype: bool + """ + return has_cycle(self.head) + + def cycle_length(self) -> int: + """ + Determines the length of the cycle in a linked list if it has one. The length of the cycle is the number + of nodes that are 'caught' in the cycle + Returns: + int: length of the cycle or number of nodes in the cycle + """ + return cycle_length(self.head) + + def detect_node_with_cycle(self) -> Optional[Node]: + """ + Detects the node with a cycle and returns it + """ + return detect_node_with_cycle(self.head) + + def remove_cycle(self) -> Optional[Node]: + """ + Removes cycle if there exists. This will use the same concept as has_cycle method to check if there is a loop + and remove the cycle + Returns: + Node: head node with cycle removed + """ + if not self.head or not self.head.next: + return self.head + + return remove_cycle(self.head) + + @abstractmethod + def alternate_split(self): + """ + Alternate split a linked list such that a linked list such as a->b->c->d->e becomes a->c->e->None and b->d->None + """ + pass + + @abstractmethod + def is_palindrome(self) -> bool: + """ + Checks if the linked list is a Palndrome. That is, can be read from both back & front + :return: boolean data. True if the LinkedList is a Palindrome + """ + raise NotImplementedError("Method has not been implemented") + + def swap_nodes(self, data_one: Any, data_two: Any): + """ + Swaps two nodes based on the data they contain. We search through the LinkedList looking for the data item in + each node. Once the first is found, we keep track of it and move on until we find the next data item. Once that + is found, we swap the two nodes' data items. + + If we can't find the first data item nor the second. No need to perform swap. If the 2 data items are similar + no need to perform swap as well. + + If the LinkedList is empty (i.e. has no head node), return, no need to swap when we have no LinkedList :) + """ + if not self.head: + raise EmptyLinkedList("Empty LinkedList") + + # if they are the same, we do not need to swap + if data_one == data_two: + return + + # set the 2 pointers we will use to traverse the linked list + current_one = self.head + current_two = self.head + + # move the pointer down the LinkedList while the data item is not the same as the data item we are searching for + while current_one and current_one.data != data_one: + current_one = current_one.next + + # we look for the second data item + while current_two and current_two.data != data_two: + current_two = current_two.next + + # the data items do not exist in the LinkedList or only one of them exists, therefore we can not perform a swap + if not current_one or not current_two: + return + + # swap the data items of the nodes + current_one.data, current_two.data = current_two.data, current_one.data + + @abstractmethod + def pairwise_swap(self) -> Node: + """ + Swaps nodes in a linked list in pairs. + As there are different kinds of LinkedLists, it is up to the child class to implement this + + The premise(idea) is to swap the data of each node with the data of the next node. This is while using + an iterative approach + Example: + 1 -> 2 -> 3 -> 4 + becomes + 2 -> 1 -> 4 -> 3 + :return: New head of node + """ + raise NotImplementedError("Method has not been implemented") + + @abstractmethod + def swap_nodes_at_kth_and_k_plus_1(self, k: int) -> Node: + """ + Return the head of the linked list after swapping the datas of the kth node from the beginning and the kth node + from the end (the list is 1-indexed). + + Input: head = [7,9,6,6,7,8,3,0,9,5], k = 5 + Output: [7,9,6,6,8,7,3,0,9,5] + """ + raise NotImplementedError("Method has not been implemented") + + def get_kth_to_last_node(self, k: int) -> Optional[Node]: + """ + Gets the kth to the last node in a Linked list. + + Assumptions: + - k can not be an invalid integer, less than 0. A ValueError will be raised + + Algorithm: + - set 2 pointers; fast_pointer & slow_pointer + - Move fast_pointer k steps ahead + - increment both pointers(fast_pointer & slow_pointer) until fast_pointer reaches end + - return the slow_pointer + + Complexity Analysis: + - Time Complexity O(n) where n is the number of nodes in the linked list to traverse + - Space Complexity O(1). No extra space is needed + + @param k: integer value which will enable us to get the kth node from the end of the LinkedList + @return: Kth node from the end + @rtype: Node + """ + if k < 1: + raise ValueError("K must be >= 1") + if k > len(self): + raise IndexError("K longer than linked list") + fast_pointer, slow_pointer = self.head, self.head + + for _ in range(k - 1): + fast_pointer = fast_pointer.next + + if not fast_pointer: + return None + + while fast_pointer.next: + fast_pointer = fast_pointer.next + slow_pointer = slow_pointer.next + return slow_pointer + + @abstractmethod + def move_to_front(self, node: Node): + """ + Moves a node from it's current position to the head of the linked list + @param node: + @return: + """ + raise NotImplementedError("Not yet implemented") + + @abstractmethod + def move_tail_to_head(self): + """ + Moves the tail node to the head node making the tail node the new head of the linked list + Uses two pointers where last pointer will be moved until it points to the last node in the linked list. + The second pointer, previous, will point to the second last node in the linked list. + + Complexity Analysis: + + An assumption is made where n is the number of nodes in the linked list + - Time: O(n) as the the pointers have to be moved through each node in the linked list until both point to the + last and second last nodes in the linked list + + - Space O(1) as no extra space is incurred in the iteration. Only pointers are moved at the end to move the tail + node to the head and make the second to last node the new tail + """ + raise NotImplementedError("Not yet implemented") + + @abstractmethod + def partition(self, data: Any) -> Union[Node, None]: + """ + Partitions a LinkedList around a data point such that all nodes with values less than this data point come + before all nodes greater than or equal to that data point + + Algorithm: + - Create left & right LinkedLists + - For each element/node in the linked list: + - if element data < data: + - append to the left list + - if element data is equal to the data: + - prepend to the right list + - if element data is greater than the data: + - append to the right list + - Check if the left list is empty. Return right list if left is empty + - Move the pointer of the left list up to the last node (O(n) operation) + - set the next pointer of the last node in the left list to the head of the right list + - return the head of the left list + + Complexity Analysis: + - Space Complexity: O(n) - left & right lists to hold the partitions of the linked list with nodes n + - Time Complexity: O(n) - where n is the number of nodes in the linked list + + @param data: Data point to compare nodes and create partitions + @return: Head of the partitioned linked list + @rtype: Node + """ + raise NotImplementedError("Not yet implemented") + + @abstractmethod + def remove_tail(self): + """ + Remotes the tail of a linked list + """ + raise NotImplementedError("Not yet implemented") + + @abstractmethod + def remove_duplicates(self) -> Optional[Node]: + """ + Remotes the duplicates from a linked list + """ + raise NotImplementedError("Not yet implemented") + + @abstractmethod + def rotate(self, k: int): + """ + Rotates a linked list by k nodes + """ + raise NotImplementedError("Not yet implemented") + + @abstractmethod + def reverse_groups(self, k: int): + """ + Reverses every k groups of a linked list + """ + raise NotImplementedError("Not yet implemented") + + def middle_node(self) -> Optional[Node]: + """ + Traverse the linked list to find the middle node + Time Complexity: O(n) where n is the number of nodes in the linked list + Space Complexity: O(1) as constant extra space is needed + @return: Middle Node or None + """ + if not self.head: + return None + + fast_pointer, slow_pointer = self.head, self.head + + while fast_pointer and fast_pointer.next: + slow_pointer = slow_pointer.next + fast_pointer = fast_pointer.next.next + + return slow_pointer + + @abstractmethod + def odd_even_list(self) -> Optional[Node]: + """ + Returns the odd even list where the even indexed nodes are grouped first and then the even indexed nodes + @return: New head node + """ + raise NotImplementedError("not yet implemented") + + @abstractmethod + def maximum_pair_sum(self) -> int: + """ + Returns the maximum twin sum of a node and its twin, where a node's twin is at the index (n-1-i) where n is the + number of nodes in the linked list. + For example, if n = 4, then node 0 is the twin of node 3, and node 1 is the twin of node 2. These are the only + nodes with twins for n = 4. + @return: maximum twin sum of a node and it's twin + """ + raise NotImplementedError("not yet implemented") + + @abstractmethod + def pairs_with_sum(self, target: T) -> List[Tuple[Node, Node]]: + """ + Returns a list of tuples which contain nodes whose data sum equal the given target. + Args: + target T: the target with which each pair's data sums up to + Return: + List: list of pairs + """ + raise NotImplementedError("not yet implemented") + + @staticmethod + def reverse_list(head: Node) -> Optional[Node]: + """ + Reverses a linked list given the head node + Args: + head Node: the head node of the linked list + Returns: + Optional[Node]: the new head node of the reversed linked list + """ + if head is None or head.next is None: + return head + + # track previous node, so we can point our next pointer to it + previous = None + # track node to loop through + current_node = head + + while current_node: + # track the next node to not lose it while adjusting pointers + nxt = current_node.next + + # set the next pointer to the node behind it, previous + current_node.next = previous + + # adjust the new previous node to the current node for subsequent loops + previous = current_node + + # move our node pointer up to the next node in front of it + current_node = nxt + + # return the new tail of the k-group which is our head + return previous diff --git a/datastructures/linked_lists/linked_list_utils.py b/datastructures/linked_lists/linked_list_utils.py index 7ed97a37..0c23e387 100644 --- a/datastructures/linked_lists/linked_list_utils.py +++ b/datastructures/linked_lists/linked_list_utils.py @@ -173,3 +173,56 @@ def remove_nth_from_end(head: Optional[Node], n: int) -> Optional[Node]: # Return the modified head node of the linked list with the node removed return head + + +def sum_of_linked_lists( + head_one: Optional[Node], head_two: Optional[Node] +) -> Optional[Node]: + """ + Sums two linked lists together to create a new linked list. The two heads of the two linked lists provided represent + two non-negative integers. The digits are in reverse order and each of their nodes contains a single digit. This + adds the two numbers and returns the sum as a linked list. This assumes that the two numbers do not contain any + leading zero, except the number 0 itself. + + Example: + Input: l1 = [2,4,3], l2 = [5,6,4] + Output: [7,0,8] + Explanation: 342 + 465 = 807. + + Complexity: + Time: O(max(m, n)). Assume that m and n represent the lengths of the first and the second linked lists respectively, + the algorithm iterates at most max(m, n) times + + Space: O(1). The length of the new linked list is at most max(m, n) + 1 + + Args: + head_one(Node): head node of the first linked list + head_two(Node): head node of the second linked list + Returns: + Node: head node of newly created linked list + """ + if not head_one and not head_two: + return None + + dummy_head = Node(0) + carry = 0 + pointer_one, pointer_two = head_one, head_two + current = dummy_head + + while pointer_one or pointer_two: + x = pointer_one.data if pointer_one else 0 + y = pointer_two.data if pointer_two else 0 + current_sum = carry + x + y + carry = current_sum // 10 + current.next = Node(current_sum % 10) + current = current.next + + if pointer_one: + pointer_one = pointer_one.next + if pointer_two: + pointer_two = pointer_two.next + + if carry > 0: + current.next = Node(carry) + + return dummy_head.next diff --git a/datastructures/linked_lists/node.py b/datastructures/linked_lists/node.py new file mode 100644 index 00000000..3d020e5e --- /dev/null +++ b/datastructures/linked_lists/node.py @@ -0,0 +1,31 @@ +from typing import Any, Optional, Generic, TypeVar +from abc import ABCMeta +from datastructures.linked_lists.types import T + + +class Node(Generic[T]): + """ + Node object in the Linked List + """ + + __metaclass__ = ABCMeta + + def __init__( + self, + data: Optional[T] = None, + next_: Optional["Node[Generic[T]]"] = None, + key: Any = None, + ): + self.data = data + self.next = next_ + # if no key is provided, the hash of the data becomes the key + self.key = key or hash(data) + + def __str__(self) -> str: + return f"Node(data={self.data}, key={self.key})" + + def __repr__(self) -> str: + return f"Node(data={self.data}, key={self.key})" + + def __eq__(self, other: "Node") -> bool: + return self.key == other.key diff --git a/datastructures/linked_lists/singly_linked_list/node.py b/datastructures/linked_lists/singly_linked_list/node.py index be4436af..3e0d4711 100644 --- a/datastructures/linked_lists/singly_linked_list/node.py +++ b/datastructures/linked_lists/singly_linked_list/node.py @@ -1,9 +1,9 @@ -from .. import Node +from datastructures.linked_lists import Node class SingleNode(Node): """ - SingleNode implementation in a single linked list + SingleNode implementation in a single-linked list """ def __init__(self, value, next_=None): diff --git a/datastructures/linked_lists/test_linked_list_utils.py b/datastructures/linked_lists/test_linked_list_utils.py new file mode 100644 index 00000000..4fc73062 --- /dev/null +++ b/datastructures/linked_lists/test_linked_list_utils.py @@ -0,0 +1,94 @@ +import unittest +from typing import List +from parameterized import parameterized +from datastructures.linked_lists.singly_linked_list.node import SingleNode +from datastructures.linked_lists.linked_list_utils import ( + remove_nth_from_end, + sum_of_linked_lists, +) + +REMOVE_NTH_NODE_FROM_END_TEST_CASES = [ + ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 4, [0, 1, 2, 3, 4, 5, 7, 8, 9]), + ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], 10, [1, 2, 3, 4, 5, 6, 7, 8, 9]), +] + + +class RemoveNthNodeFromEndTestCase(unittest.TestCase): + @parameterized.expand(REMOVE_NTH_NODE_FROM_END_TEST_CASES) + def test_remove_nth_node_from_end( + self, data: List[int], n: int, expected: List[int] + ): + if not data: + actual = remove_nth_from_end(head=None, n=n) + self.assertIsNone(actual) + + head = SingleNode(data[0]) + current = head + for d in data[1:]: + current.next = SingleNode(d) + current = current.next + + actual = remove_nth_from_end(head=head, n=n) + actual_current_head = actual + actual_data = [] + while actual_current_head: + actual_data.append(actual_current_head.data) + actual_current_head = actual_current_head.next + + self.assertListEqual(expected, actual_data) + + +SUM_LINKED_LISTS_TEST_CASES = [ + ([2, 4, 3], [5, 6, 4], [7, 0, 8]), + ([0], [0], [0]), + ([9, 9, 9, 9, 9, 9, 9], [9, 9, 9, 9], [8, 9, 9, 9, 0, 0, 0, 1]), +] + + +class SumLinkedListsTestCase(unittest.TestCase): + @staticmethod + def construct_linked_list(data: List[int]) -> SingleNode: + head = SingleNode(data[0]) + current = head + for d in data[1:]: + current.next = SingleNode(d) + current = current.next + return head + + def assert_data(self, actual: SingleNode, expected: List[int]): + actual_current_head = actual + actual_data = [] + while actual_current_head: + actual_data.append(actual_current_head.data) + actual_current_head = actual_current_head.next + + self.assertListEqual(expected, actual_data) + + @parameterized.expand(SUM_LINKED_LISTS_TEST_CASES) + def test_sum_linked_lists( + self, data_one: List[int], data_two: List[int], expected: List[int] + ): + if not data_one and not data_two: + actual = sum_of_linked_lists(head_one=None, head_two=None) + self.assertIsNone(actual) + + if not data_one and data_two: + head = self.construct_linked_list(data_two) + actual = sum_of_linked_lists(head_one=None, head_two=head) + self.assertEqual(expected, actual) + + if data_one and not data_two: + head = self.construct_linked_list(data_one) + actual = sum_of_linked_lists(head_one=None, head_two=head) + self.assertEqual(expected, actual) + + head_one = self.construct_linked_list(data_one) + head_two = self.construct_linked_list(data_two) + + actual = sum_of_linked_lists(head_one=head_one, head_two=head_two) + self.assertIsNotNone(actual) + self.assert_data(actual, expected) + + +if __name__ == "__main__": + unittest.main() diff --git a/datastructures/linked_lists/types.py b/datastructures/linked_lists/types.py new file mode 100644 index 00000000..b186d5ad --- /dev/null +++ b/datastructures/linked_lists/types.py @@ -0,0 +1,3 @@ +from typing import TypeVar + +T = TypeVar("T")