Close-up of two individuals holding hands bound by chains, symbolizing connections and relationships.

Linked list

What is linked list?

— Linked list is also an data structure like any other data structure.
— It is composed of Nodes. So, what is Node?
— Node is an container which contains data & an pointer named next which contains the address of next Node.
— First Node is called as head & last Node is tail

Let’s create an linked list & print values inside it as an warm up excercise

Solution:.

class Node:
    def __init__(self, val):
        self.val = val
        self.next = None
a = Node("A")
b = Node("B")
c = Node("C")
d = Node("D")

a.next = b
b.next = c
c.next = d

def print_list_iterative(head):
    current = head
    while current is not None:
        print(current.val)
        current = current.next
print_list_iterative(a)

def print_list_recursive(head):
    if head is None:
        return
    print(head.val)
    print_list_recursive(head.next)
print_list_recursive(a)
Linked list values

Problem:
Write a function, linked_list_values, that takes in the head of a linked list as an argument. The function should return a list containing all values of the nodes in the linked list.
Hey. This is our first linked list problem, so you should be liberal with watching the Approach and Walkthrough. Be productive! -AZ

# test_00:
a = Node("a")
b = Node("b")
c = Node("c")
d = Node("d")

a.next = b
b.next = c
c.next = d

# a -> b -> c -> d
linked_list_values(a) # -> [ 'a', 'b', 'c', 'd' ]

Solution

Iterative

def linked_list_values(head):
    values = []
    current = head
    while current is not None:
        values.append(current.val)
        current = current.next
    return values

Recursive

def linked_list_values(head):
    values = []
    _linked_list_values(head, values)
    return values

def _linked_list_values(head, values):
    if head is None:
        return
    values.append(head.val)
    _linked_list_values(head.next, values)
Sum list

Problem:
Write a function, sum_list, that takes in the head of a linked list containing numbers as an argument.
The function should return the total sum of all values in the linked list.

# test_00:
a = Node(2)
b = Node(8)
c = Node(3)
d = Node(-1)
e = Node(7)

a.next = b
b.next = c
c.next = d
d.next = e

# 2 -> 8 -> 3 -> -1 -> 7
sum_list(a) # 19

Solution

Iterative

def sum_list(head):
    total_sum = 0
    current = head
    while current is not None:
        total_sum += current.val
        current = current.next
    return total_sum

Recursive

def sum_list(head):
    if head is None:
        return 0
    return head.val + sum_list(head.next)
Linked list find

Problem:
Write a function, linked_list_find, that takes in the head of a linked list and a target value. The function should return a boolean indicating whether or not the linked list contains the target.

# test_00:
a = Node("a")
b = Node("b")
c = Node("c")
d = Node("d")

a.next = b
b.next = c
c.next = d

# a -> b -> c -> d

linked_list_find(a, "c") # True

Solution
The key to solve this problem is thinking like this 🤔. You are traversing through linked list & at any point of time, if you come across head.val == target, return True & make sure that True is carried forward back to the parent calls.
If you did not find True until end of the linked list, then return False & make sure that True is carried forward back to the parent calls.

Iterative

def linked_list_find(head, target):
    current = head
    while current is not None:
        if current.val == target:
            return True
        current = current.next
    return False

Recursive

def linked_list_find(head, target):
    if head is None:
        return False
    if head.val == target:
        return True
    return linked_list_find(head.next, target)
Get node value

Problem:
Write a function, get_node_value, that takes in the head of a linked list and an index. The function should return the value of the linked list at the specified index.
If there is no node at the given index, then return None.

# test_00:
a = Node("a")
b = Node("b")
c = Node("c")
d = Node("d")

a.next = b
b.next = c
c.next = d

# a -> b -> c -> d
get_node_value(a, 2) # 'c'

Solution
Tip:
Most of the Linked List problems will ask to do the work in place. So, the solution revolves around modifying the pointers, not creating new Linked List. Aware of this fact.

Iterative

def get_node_value(head, index):
    count = 0
    current = head
    while current is not None:
        if count == index:
            return current.val
        current = current.next
        count += 1
    return None

Recursive

def get_node_value(head, index):
    if head is None:
        return None
    if index == 0:
        return head.val
    return get_node_value(head.next, index - 1)
Reverse list

Problem:
Write a function, reverse_list, that takes in the head of a linked list as an argument. The function should reverse the order of the nodes in the linked list in-place and return the new head of the reversed linked list.

# test_00:
a = Node("a")
b = Node("b")
c = Node("c")
d = Node("d")
e = Node("e")
f = Node("f")

a.next = b
b.next = c
c.next = d
d.next = e
e.next = f

# a -> b -> c -> d -> e -> f
reverse_list(a) # f -> e -> d -> c -> b -> a

Solution
— How to think 🤔
— This is how the linked list looks.

{de455e87 49ac 4628 b81e cc0371938be0}

Observe that the question says, Linked List has to be reversed in place. So, the whole idea is to reverse the pointer of each node. Let’s see how the picture looks like when we reverse the pointer of first node.

{0cc35667 5295 47fb b21f 3cd8eeb2cae5}

We are done with the first node. Let’s move the head to next node & do the same process. Then the picture looks like below & This process has to be continued until the head is None.

{64803224 0706 4bf0 b5c7 f0894eb7116b}

Let’s solve this problem iteratively to get some sense & then will move to recursive way.

Iterative

def reverse_list(head):
    prev = None  # see the picture 1
    while head is not None:
        # upcoming 2 lines are to make the head.next to point towards prev
        temp = head.next  # before reversing the pointer of node,
        # first save the next node pointer to an temp variable.
        # If you just reverse the pointer without doing this,
        # you dont know what is next node. Got it?
        head.next = prev

        # moving to the next node
        prev = head
        head = temp
    return prev

Recursive

def reverse_list(head, prev=None):
    # base case
    if head is None:
        return prev
    # recursive step
    # reassign the pointer of head to prev node
    temp = head.next
    head.next = prev
    # move the head to next node
    prev = head
    head = temp
    return reverse_list(head, prev)
Zipper list

Problem:
Write a function, zipper_lists, that takes in the head of two linked lists as arguments. The function should zipper the two lists together into single linked list by alternating nodes. If one of the linked lists is longer than the other, the resulting list should terminate with the remaining nodes. The function should return the head of the zippered linked list.
Do this in-place, by mutating the original Nodes.
You may assume that both input lists are non-empty.

# test_00:
a = Node("a")
b = Node("b")
c = Node("c")
a.next = b
b.next = c
# a -> b -> c

x = Node("x")
y = Node("y")
z = Node("z")
x.next = y
y.next = z
# x -> y -> z

zipper_lists(a, x)
# a -> x -> b -> y -> c -> z

Solution

Iterative

def zipper_lists(head_1, head_2):
    tail = head_1  # output LL, which already includes one node from LL 1
    # Let’s take 2 pointers, to identify the current location
    # when we are working on these lists.
    current_1 = head_1.next  # LL 1
    current_2 = head_2  # LL 2

    count = 0  # if this count is even, we will take a node from LL 2
    # if this count is odd, we will take a node from LL 1
    while current_1 is not None and current_2 is not None:
        if count % 2 == 0:
            tail.next = current_2
            current_2 = current_2.next
        else:
            tail.next = current_1
            current_1 = current_1.next
        tail = tail.next
        count += 1

    if current_1 is not None:
        tail.next = current_1
    if current_2 is not None:
        tail.next = current_2

    return head_1

Recursive

def zipper_lists(head_1, head_2):
    if head_1 is None and head_2 is None:
        return None
    if head_1 is None:
        return head_2
    if head_2 is None:
        return head_1
    next_1 = head_1.next
    next_2 = head_2.next
    head_1.next = head_2
    head_2.next = zipper_lists(next_1, next_2)
    return head_1
Merge lists

Problem:
Write a function, merge_lists, that takes in the head of two sorted linked lists as arguments. The function should merge the two lists together into single sorted linked list. The function should return the head of the merged linked list.
Do this in-place, by mutating the original Nodes.
You may assume that both input lists are non-empty and contain increasing sorted numbers.

# test_00:
a = Node(5)
b = Node(7)
c = Node(10)
d = Node(12)
e = Node(20)
f = Node(28)
a.next = b
b.next = c
c.next = d
d.next = e
e.next = f
# 5 -> 7 -> 10 -> 12 -> 20 -> 28

q = Node(6)
r = Node(8)
s = Node(9)
t = Node(25)
q.next = r
r.next = s
s.next = t
# 6 -> 8 -> 9 -> 25

merge_lists(a, q)
# 5 -> 6 -> 7 -> 8 -> 9 -> 10 -> 12 -> 20 -> 25 -> 28 

Solution

Iterative

class Node:
    def __init__(self, val):
        self.val = val
        self.next = None

def merge_lists(head_1, head_2):
    # Let's create an dummy node.
    # This is the node which we will evolve during our solution.
    # So, eventually it becomes our output Linked List.
    dummy_head = Node(None)

    # Initially, we point our tail to dummy node.
    # This tail, we will keep pointing to other nodes
    # in Linked List 1 & 2 while we work our way through the problem.
    # That's how we will arrive at the
    # output Linked List.
    tail = dummy_head

    # We take two pointers to track out current location on
    # Linked List 1 & 2, while we are working our way to the final solution.
    current_1 = head_1
    current_2 = head_2

    # This while loop runs until, any one of the Linked List becomes empty.
    while current_1 is not None and current_2 is not None:
        # This is the block of code where we are doing the core logic
        # of working our way towards the output list.
        if current_1.val < current_2.val:
            tail.next = current_1
            current_1 = current_1.next
        else:
            tail.next = current_2
            current_2 = current_2.next
        tail = tail.next
    # When any one of the Linked List becomes empty,
    # will just attach the left over other Linked List to our output
    # Linked List.
    if current_1 is not None:
        tail.next = current_1
    if current_2 is not None:
        tail.next = current_2

    return dummy_head.next

Recursive

def merge_lists(head_1, head_2):
    # Base cases -- They are here to stop the recursion,
    # whenever one of the Linked List becomes empty.
    if head_1 is None and head_2 is None:
        return None
    if head_1 is None:
        return head_2
    if head_2 is None:
        return head_1

    # Recursive step
    if head_1.val < head_2.val:
        next_1 = head_1.next
        head_1.next = merge_lists(next_1, head_2)
        return head_1
    else:
        next_2 = head_2.next
        head_2.next = merge_lists(head_1, next_2)
        return head_2
Is univalue list

Problem:
Write a function, is_univalue_list, that takes in the head of a linked list as an argument. The function should return a boolean indicating whether or not the linked list contains exactly one unique value.
You may assume that the input list is non-empty.

# test_00:
a = Node(7)
b = Node(7)
c = Node(7)

a.next = b
b.next = c

# 7 -> 7 -> 7
is_univalue_list(a) # True 

Solution

Iterative

def is_univalue_list(head):
    current = head
    while current is not None:
        if current.val != head.val:
            return False
        current = current.next
    return True

Recursive

def is_univalue_list(head, prev_val=None):
    if head is None:
        return True
    if prev_val is None or head.val == prev_val:
        return is_univalue_list(head.next, head.val)
    else:
        return False
Longest streak

Problem:
Write a function, longest_streak, that takes in the head of a linked list as an argument. The function should return the length of the longest consecutive streak of the same value within the list.

# test_00:
a = Node(5)
b = Node(5)
c = Node(7)
d = Node(7)
e = Node(7)
f = Node(6)

a.next = b
b.next = c
c.next = d
d.next = e
e.next = f

# 5 -> 5 -> 7 -> 7 -> 7 -> 6
longest_streak(a) # 3

Solution

Iterative

def longest_streak(head):
    max_streak = 0
    current_streak = 0
    prev_val = None

    current_node = head
    while current_node is not None:
        if current_node.val == prev_val:
            current_streak += 1
        else:
            current_streak = 1
        prev_val = current_node.val
        if current_streak > max_streak:
            max_streak = current_streak
        current_node = current_node.next
    return max_streak
Remove node

Problem:
Write a function, remove_node, that takes in the head of a linked list and a target value as arguments. The function should delete the node containing the target value from the linked list and return the head of the resulting linked list. If the target appears multiple times in the linked list, only remove the first instance of the target in the list.
Do this in-place.
You may assume that the input list is non-empty.
You may assume that the input list contains the target.

# test_00:
a = Node("a")
b = Node("b")
c = Node("c")
d = Node("d")
e = Node("e")
f = Node("f")

a.next = b
b.next = c
c.next = d
d.next = e
e.next = f

# a -> b -> c -> d -> e -> f
remove_node(a, "c")
# a -> b -> d -> e -> f

Solution
— Let’s have 2 pointers prev & current, and will start traversing through the Linked List.
— Why you need 2 pointers?
Assume that, you are traversing through the Linked List & on the process, you found current.val == value of the node which needs to be removed.
Then, because of the reason you have 2 pointers named prev & current, you can easily do prev.next = current.next,
which removes the current node from Linked List.

Iterative

def remove_node(head, target_val):
    # Edge case: what if the node which needs to be removed is head node?
    # Our regular logic which we wrote is side the while loop will fail,
    # in this case. Hence, that case needs to be dealt as an edge case.
    if head.val == target_val:
        return head.next

    current = head
    prev = None
    while current is not None:
        if current.val == target_val:
            prev.next = current.next
            break
        prev = current
        current = current.next
    return head

Recursive

def remove_node(head, target_val):
    # Base case
    if head is None:
        return None

    # Remember that we saw an edge case in iterative code.
    # This base case is to deal with that edge case.
    if head.val == target_val:
        return head.next

    head.next = remove_node(head.next, target_val)
    return head
Insert node

Problem:
Write a function, insert_node, that takes in the head of a linked list, a value, and an index. The function should insert a new node with the value into the list at the specified index. Consider the head of the linked list as index 0. The function should return the head of the resulting linked list.
Do this in-place.
You may assume that the input list is non-empty and the index is not greater than the length of the input list.

# test_00:
a = Node("a")
b = Node("b")
c = Node("c")
d = Node("d")

a.next = b
b.next = c
c.next = d

# a -> b -> c -> d
insert_node(a, 'x', 2)
# a -> b -> x -> c -> d

Solution
— Let’s have 2 pointers prev & current, and will start traversing through the Linked List.
— Why you need 2 pointers?
Assume that, you are traversing through the Linked List & on the process, you found current.val == value of the node which needs to be removed.
Then, because of the reason you have 2 pointers named prev & current, you can easily do prev.next = current.next,
which removes the current node from Linked List.

Iterative

def insert_node(head, value, index):
    # This is an edge case. First read the remaining code.
    # At the end, read this.
    # Even when you are coding, do it the same way. First write the solution
    # Then think about the base case.
    if index == 0:
        new_head = Node(value)
        new_head.next = head
        return new_head
    # These 2 variables are necessary to keep track on which node you are on &
    # what's the corresponding index.
    count = 0
    current = head

    # These 2 lines are classic way of looping through each node of Linked List
    # while current is not None:
    #   <what_ever_logic_you_want_to_execute>
    #   current = current.next
    while current is not None:
        if count == index - 1:
            temp = current.next
            current.next = Node(value)
            current.next.next = temp

        count += 1
        current = current.next
    return head

Recursive

# If you observe the iterative approach, we took 2 variables named
# "current" & "count", They are to track the node at which we are
# currently at is index - 1 ?
# Because, that's where we have to insert the new node.
# In iterative approach, we kept increasing the count inside the while loop.
# Here, once you increase the count,
# the variable needs to be passed to recursive fn.
# That's why we have count parameter.
def insert_node(head, value, index, count=0):
    # This is edge case, which is same to same as that of iterative approach.
    if index == 0:
        new_head = Node(value)
        new_head.next = head
        return new_head

    # Base case -- While you are traversing the Linked List,
    # you have reached the end but did not found the index yet.
    # Which means, nothing you can do. Just return None.
    if head is None:
        return None

    # Recursive step
    # The core logic is once again same to same as that of iterative.
    if count == index - 1:
        temp = head.next
        head.next = Node(value)
        head.next.next = temp
        return

    insert_node(head.next, value, index, count + 1)
    return head