Files
algorithms/python_hw1_notebook.ipynb
T
2025-02-02 14:27:24 -06:00

28 KiB

Linked list with Collections

I. Introduction

Thank you for choosing Python as your main programming language. This is our first homework assignment, and we will write the API for the data structures we will need later in this course.

You will submit the notebook file (.ipynb) and a brief report (pdf file) showcasing the implementation process and outcomes of the code.

Due date of HW1: 12PM (Noon) Feb 2, 2025

If you have any questions please contact us via Teams or email: arpanp@uark.edu and maalexra@uark.edu

Several fundamental data type involves collections of objects, e.g. Python has list, tuple, set and dictionary, and the operations revolving around adding, removing, or examining the objects in the collection. In this assignment, we will look into 3 popular data types, i.e. bag, queue and stack. The main difference among them is the specifications of which objects is to be removed and examined next.

Although we could utilize the Python built-in data type such as list to ease the implementation, it is our goal to introduce and show the importance of linked data structures in implementing bags, queues and stacks. Understanding linked lists is pivotal to understanding algorithms and data structures.

II.1. Linked List

A linked list is a recursive data structure that is either empty or a reference to a node having a generic item and a reference to a linked list.

In this section, we will introduce linked list, a data structure that is not directly supported by the Python language. As defined above, linked lists make use of node, an abstract entity that could contain any kind of data, and node reference that aids in building linked lists. This fundamental data structure is very efficient when it comes to writing recursive program.

Python is an object-oriented programming language and we will utilize class to define node abstraction.

In [1]:
# implementation of Node abstraction
class Node:
  def __init__(self):
    self.item = None   # an item
    self.next = None   # a node

a Node has two attributes: an item and a node. The code looks way too simple as we only override the default constructor by creating 2 variables item and next, and set them both to None. item is a placeholder for any data that we want to structure with a linked list. next is a reference to the next node.

##Q&A: [Q]: This node definition only works for singly linked list. What if I want node for doubly linked list?

[A]: We can add another attribute prev that points to previous node to build doubly linked list. Implementation of our considered data types only requires singly linked list, thus having prev is a waste of memory. Our Node definition will be nested inside class definition of other data types, as you will see when we define bag.

With Node definition above, we will construct a very simple linked list that has 3 nodes: first, second and third. Structure of the linked list can be seen below:

                    first
                |item: "to"  |
                |next: second| ->   second
                                 |item: "be"  |
                                 |next: third | ->   third    
                                                  |item: "or" |
                                                  |next: None |  ->  None
In [50]:
# initialize 3 nodes
first  = Node()
second = Node()
third  = Node()

# add content to the node
first.item  = "to"   # first node
second.item = "be"  # second node
third.item  = "or"   # third node

# building the linked list
first.next  = second
second.next = third

# iterate through this linked list and print the items
node = first
while node is not None:
  print(node.item)
  node = node.next
del node # delete the temporary node
to
be
or

One big advantage of linked lists is we don't need to tell the size of the list before creating it. The list can always take in new value and remove the current value. We can easily iterate through the list using while loop with the stop conditon that the current node is None. This demo should be enough for us to understand how to build linked list in Python. Let's talk about Bag, Queue and Stack.

II.2. Bag

A bag is a collection where removing items is not supported. Its purpose is to provide clients with the ability to collect items and iterate through all the items in the bag. Before we implement this data type, let's see exactly what we need for bag.

Bag API

class Bag():
    __init__()->None       # create an empty bag
    add(item: Item)->None  # add an item
    isEmpty()->bool        # is the bag empty?
    __len__()->int         # number of items in the bag

From the API, we need 4 methods to replicate the characteristic of bag. In fact, we might need more than 4 methods but these are the most important for this data type. The implementation code can be seen in the cell below

In [3]:
from typing import TypeVar
Item = TypeVar('Item')  # don't worry about this, it makes code more readable

class Bag:
  # define the constructor with 2 private attributes
  def __init__(self) -> None:
    self.__N     = 0     # number of items - private
    self.__first = None  # point to first item in the bag - private

  # add an item to bag
  def add(self, item: Item) -> None:
    oldFirst = self.__first
    self.__first = self.Node()   # create a new node
    self.__first.item = item     # add content to that node
    self.__first.next = oldFirst # create a reference to next node
    self.__N += 1                # increment the size of bag by 1

  # nested class Node inside class Bag
  class Node:
    def __init__(self) -> None:
      self.item = None
      self.next = None

  # method to check if bag is empty
  def isEmpty(self) -> bool:
    return self.__N == 0

  # method to return the size of bag
  def __len__(self) -> int:
    return self.__N

  # print the content of the bag
  def __str__(self) -> str:
    output_str = "["
    self.__iter_node = self.__first
    while True:
      if self.__iter_node is not None:
        output_str += f'{self.__iter_node.item}'
        self.__iter_node = self.__iter_node.next
      if self.__iter_node is not None:
        output_str += ','
      else:
        break
    output_str += ']'
    return output_str

  # The 2 methods below make the objects iterable
  def __iter__(self):
    self.__iter_node = self.__first
    return self

  def __next__(self):
    if self.__iter_node is not None:
      x = self.__iter_node.item
      self.__iter_node = self.__iter_node.next
      return x
    else:
      raise StopIteration

The code implementation above is self-explanatory. Let's create a bag example and see what we can do with it.

In [52]:
# create a variable of type Bag
bag = Bag()
print(f'The bag after initialization is {bag}')

# check if the bag is empty
print(f"bag is empty: {bag.isEmpty()} \n")

# let's add some items to this bag. Items could be of any data type
bag.add("University of Arkansas")
bag.add(1871)
bag.add("Fayetteville")

# print the content of bag
print(f"The size of the bag after adding is {len(bag)}")   # thanks to __len__()
print('The items in the bag after adding are')
print(bag)   #  thanks to __str__()

# iterate through the bags and print each item
print("\nIterate through this bag")
for item in bag:    # thanks to __iter__ and __next__
  print(item, end='  ')
The bag after initialization is []
bag is empty: True 

The size of the bag after adding is 3
The items in the bag after adding are
[Fayetteville,1871,University of Arkansas]

Iterate through this bag
Fayetteville  1871  University of Arkansas  

III. FIFO Queue (25 pts)

A queue is a collection that is based on the first-in-first-out (FIFO) policy. Whatever item comes in the queue first must leave first. Queues are a natural model for many everyday phenomena, and they play a very important role in many applications. When a client iterates through the items in a queue, the items are processed in the order they were added to the queue. In this section, you have to

  1. Write the class definition of Queue
  2. Run the test cases that we provide below

You will get full 25 pts upon successful run of all test cases (including the ones hidden from this file). You have to use linked list to implement queue.

Queue API

class Queue():
    __init__()->None           # create an empty queue
    enqueue(item: Item)->None  # add an item
    dequeue()->Item            # remove the least recently added item
    isEmpty()->bool            # is the queue empty?
    peek()-> Item              # the element at the front without removing it    
    __len__()->int             # number of items in the queue
In [22]:
# Write class definition of queue below this line
class Queue:

    head: Node = None

    def enqueue(self, item: Item) -> None:
        if not self.head:
            self.head = Node()
            self.head.item = item
            return None

        next: Node | None = self.head.next
        if not next:
            self.head.next = Node()
            self.head.next.item = item
            return None

        while next:
            if not next.next:
                next.next = Node()
                next.next.item = item
                return None
            next = next.next

    def dequeue(self) -> Item:
        item = self.head.item
        self.head = self.head.next
        return item

    def isEmpty(self) -> bool:
        return not self.head

    def peek(self) -> Item | None:
        return self.head.item if self.head else None

    def __len__(self) -> int:
        l = 0
        for i in self:
            l += 1

        return l

    def __str__(self) -> str:
        s = "["

        for item in self:
            s += f'{str(item)}, '

        if len(self) > 0:
            s = s[:-2]

        s += "]"

        return s

    def __iter__(self):
        self.__iter_node = self.head
        return self

    def __next__(self):
        if self.__iter_node is not None:
            x = self.__iter_node.item
            self.__iter_node = self.__iter_node.next
            return x
        else:
            raise StopIteration

Test case 1

In [23]:
# initializing queue
queue = Queue()
print(f'The queue after initialization is {queue}')
assert(queue.isEmpty() == True)  # check if isEmpty works properly

# add the items to queue
for i in range(5):
  queue.enqueue(i)
print(f'The queue after enqueue is {queue}')

assert(queue.isEmpty() == False)  # check if isEmpty works properly
assert(len(queue) == 5)           # check if __len__ works properly
# Peek at the front item
front_item = queue.peek()
assert(front_item == 0)           #check if peek is working properly
The queue after initialization is []
The queue after enqueue is [0, 1, 2, 3, 4]

Test case 2

In [24]:
# dequeue the queue
print("Dequeue the queue")
while len(queue) > 0:
  item = queue.dequeue()
  print(f'item dequeued from queue: {item}')
  print(f'queue: {queue}')
  print(f'length of queue: {len(queue)}')

assert(queue.isEmpty() == True)

# enqueue the queue
queue.enqueue(5)
print(f"\nQueue after enqueue: {queue}")
print(f"Length of queue: {len(queue)}")
Dequeue the queue
item dequeued from queue: 0
queue: [1, 2, 3, 4]
length of queue: 4
item dequeued from queue: 1
queue: [2, 3, 4]
length of queue: 3
item dequeued from queue: 2
queue: [3, 4]
length of queue: 2
item dequeued from queue: 3
queue: [4]
length of queue: 1
item dequeued from queue: 4
queue: []
length of queue: 0

Queue after enqueue: [5]
Length of queue: 1

IV. Pushdown (LIFO) stack (25 pts)

A stack is a collection that is based on the last-in-first-out (LIFO) policy. When we click a hyperlink from a current page, our browser displays the new page. We can keep clicking on hyperlinks to visit new pages, but we can visit the previous page just by clicking the back button. This is one application of stack. In this section, you have to

  1. Write the class definition of Stack
  2. Run the test cases that we provide below

You will get full 25 pts upon successful run of all test cases (including the ones hidden from this file). You have to use linked list to implement stack.

Stack API

class Stack():
    __init__()->None       # create an empty stack
    push(item: Item)->None # add an item
    pop()->Item            # remove the most recently added item
    isEmpty()->bool        # is the stack empty?
    peek()-> Item          # get the item at the top without removing it.
    __len__()->int         # number of items in the stack
In [35]:
# Write class definition of stack below this line
class Stack:

    items: list[Item] = []
    head: Node = None

    def push(self, item: Item) -> None:
        if not self.head:
            self.head = Node()
            self.head.item = item
            return None

        next: Node | None = self.head.next
        if not next:
            self.head.next = Node()
            self.head.next.item = item
            return None

        while next:
            if not next.next:
                next.next = Node()
                next.next.item = item
                return None
            next = next.next

    def pop(self) -> Item:
        node = self.head
        last = None

        while node.next:
            last = node
            node = node.next

        if not last:
            self.head = None
        else:
            last.next = None
        return node.item

    def isEmpty(self) -> bool:
        return not self.head

    def peek(self) -> Item:
        node = None
        for i in self:
            node = i

        return node

    def __len__(self) -> int:
        l = 0
        for i in self:
            l += 1

        return l

    def __str__(self) -> str:
        s = "["

        for item in self:
            s += f'{str(item)}, '

        if len(self) > 0:
            s = s[:-2]

        s += "]"

        return s

    def __iter__(self):
        self.__iter_node = self.head
        return self

    def __next__(self):
        if self.__iter_node is not None:
            x = self.__iter_node.item
            self.__iter_node = self.__iter_node.next
            return x
        else:
            raise StopIteration


Test case 1

In [36]:
# initializing stack
stack = Stack()
print(f'The stack after initialization is {stack}')
assert(stack.isEmpty() == True)  # check if isEmpty works properly

# add the items to stack
for i in range(5):
  stack.push(i)
print(f'The stack after push is {stack}')

assert(stack.isEmpty() == False)  # check if isEmpty works properly
assert(len(stack) == 5)           # check if __len__ works properly
#check if peek is working properly
top_item = stack.peek()
assert(top_item==4)
The stack after initialization is []
The stack after push is [0, 1, 2, 3, 4]

Test case 2

In [37]:
# pop the stack
print("Pop the stack")
while len(stack) > 0:
  item = stack.pop()
  print(f'item dequeued from stack: {item}')
  print(f'stack: {stack}')
  print(f'length of stack: {len(stack)}')

assert(stack.isEmpty() == True)

# enqueue the queue
stack.push(5)
print(f"\nstack after push: {stack}")
print(f"length of stack: {len(stack)}")
Pop the stack
item dequeued from stack: 4
stack: [0, 1, 2, 3]
length of stack: 4
item dequeued from stack: 3
stack: [0, 1, 2]
length of stack: 3
item dequeued from stack: 2
stack: [0, 1]
length of stack: 2
item dequeued from stack: 1
stack: [0]
length of stack: 1
item dequeued from stack: 0
stack: []
length of stack: 0

stack after push: [5]
length of stack: 1

V. Applications (50 pts)

Browser History Management

The Browser History Management is designed to mimic the navigation history functionality of a web browser. It allows users to:

  1. Visit new pages.
  2. Go back to previously visited pages for given steps.
  3. Go forward to pages they have navigated back for given steps.
  4. Retrieve the current page.

Hint:

Initializes an two empty stack to store the history of pages visited before and to store pages that can be navigated forward to after going back.

  1. To visit a page:
  • Push Current Page to back stack
  • Update Current Page
  • Clear forward stack
  1. To go back:
  • iterate until steps is greater than 0 and back stack is not empty
    1. Push Current Page to forward stack
    2. Pop from back Stack and Update Current Page
    3. Decrement steps
  1. To go forward:
  • iterate until steps is greater than 0 and forward stack is not empty
    1. Push Current Page to back stack
    2. Pop from forward stack and Update Current Page
    3. Decrement Steps
  1. To get current page:
  • Return the current page

Notes

  • You have to use stacks implemented before.
  • Each test case is worth 10 points. The other 20 points are in hidden test cases.
In [38]:
class BrowserHistory:
    def __init__(self, homepage: str):
        self.current_page = homepage
        # create a backward stack
        self.backward = Stack()
        # create a forward stack
        self.forward_h = Stack()

    def visit(self, url: str) -> None:
        # function to visit a new page, updating the history accordingly.
        self.backward.push(self.current_page)
        self.current_page = url

    def back(self, steps: int) -> str:
        # function to oves back a specified number of steps in the browsing history.
        for i in range(steps):
            if not self.backward.isEmpty():
                self.forward_h.push(self.current_page)
                self.current_page = self.backward.pop()


    def forward(self, steps: int) -> str:
        # function to moves forward a specified number of steps in the browsing history.
        for i in range(steps):
            if not self.forward_h.isEmpty():
                self.backward.push(self.current_page)
                self.current_page = self.forward_h.pop()

    def current(self) -> str:
        # function to get URL of the current active page
        return self.current_page

Test case 1

In [39]:
history = BrowserHistory("uark-aicv.github.io")
history.visit("uark-aicv.github.io/team")
history.visit("uark-aicv.github.io/research")
history.visit("uark-aicv.github.io/publications")
history.visit("uark-aicv.github.io/contact")
assert(history.current()=='uark-aicv.github.io/contact')

Test case 2

In [40]:
history.back(2)
assert(history.current()=='uark-aicv.github.io/research')

Test case 3

In [41]:
history.forward(1)
assert(history.current()=='uark-aicv.github.io/publications')