Python Exercises with Data Structures and Algorithms
Python Exercises with Data Structures and Algorithms
ISBN: 9798326448439
Copyright © 2024 by Haris Tsetsekas
Table of Contents
1. University Courses
2. Restaurant Reservations
3. Library
4. Contact List
5. Priority Todo List
6. Songs List
7. Task allocation
8. Word Frequencies
9. Syntax Checker
10. Maze Solver
11. File Indexer
12. Inventory with AVL Tree
13. Social Network
14. Flights
15. MNIST Image Comparison
16. HTTP Server with Caching
17. Distributed Auction
1. University Courses
Let’s create a program that will handle the enrollment for
university courses. Each course has one or more prerequisites, i.e.
courses that must have been completed by a student in order to be
able to enroll in the specific one.
Proposed Solution
First of all, we will define the class for a student in the university:
class Student:
def __init__(self, id, name, course_count, courses):
self.id = id
self.name = name
self.course_count = course_count
self.courses = courses
This class contains information about the student and the courses
that have been completed successfully. The courses list contains
the ID of the course.
Next, we define a class for the university courses:
class Course:
def __init__(self, id, name, prereq_count, prereq_ids):
self.id = id
self.name = name
self.prereq_count = prereq_count
self.prereq_ids = prereq_ids
#Initialize a student
student = Student(1, "John Doe", 5, [0, 1, 2, 3, 4])
if __name__ == "__main__":
main()
This class contains the name of the customer. It could also include
the customer phone number or other details.
Next, we define a class for the restaurant tables:
class Table:
def __init__(self, id: int, capacity: int):
self.id = id
self.capacity = capacity
...
Method add_table() adds the reference of a table into the tables list.
In method is_table_available() we run a loop in the reservations list
to determine if there exists a table that is not reserved in the
specified timeslot.
def is_table_available(self, table: Table, start_time_slot: int,
end_time_slot: int):
#Check if the table is available for the given time slot
for reservation in self.reservations:
if reservation.table.id == table.id and (
(start_time_slot >= reservation.start_time_slot
and start_time_slot < reservation.end_time_slot) or
(end_time_slot > reservation.start_time_slot
and end_time_slot <= reservation.end_time_slot)
or
(start_time_slot <= reservation.start_time_slot
and end_time_slot >= reservation.end_time_slot)
):
return False
return True
We filter the list of all tables to find those that have a capacity
greater than or equal to the required capacity and are available
during the specified time slot. This is done using a list
comprehension that iterates over all tables and applies the
is_table_available() method to check each table's availability.
After filtering the tables, we sort the list of available tables by their
capacity in ascending order. This ensures that we prioritize smaller
tables that fit the required capacity, which can help optimize the
usage of larger tables for larger groups.
Next, the add_reservation() method will create a new reservation on
the fly and will insert it into the reservations list. That’s of course,
if a suitable table is found. Note that we get the first table in the
sorted list, i.e. the table with the smallest capacity.
def add_reservation(self, name: str, capacity: int, start_slot: int,
end_slot: int):
#Try to find available tables for the given capacity and time slot
available_tables = self.find_available_tables(capacity, start_slot,
end_slot)
if available_tables:
#If available, add a new reservation
self.reservations.append(Reservation(
Customer(name), available_tables[0], start_slot, end_slot))
print("Reservation successfully added.")
else:
print("No available tables for the requested time slot.")
if __name__ == "__main__":
main()
class LendingEvent:
def __init__(self, book_title: str, user_name: str,
lending_date: datetime, returned: int):
self.book_title = book_title
self.user_name = user_name
self.lending_date = lending_date
self.returned = returned
For each book, we record the title and the author. We also keep
information about whether it is available or is currently lent (1 or 0
respectively).
For each lending event we record the book title and the name of the
library user that has borrowed it. We also keep the lending date as
well as an integer value of whether it has been returned or not (1 or
0 respectively).
We also define the Library class, that contains all the functionality
for adding and displaying book information:
from datetime import datetime
import os
class Library:
BooksFilename = "books.txt"
LendingFilename = "lending_events.txt"
...
After we get the book title and author from the user, we create a
new Book object, Then, we open the books file with open() . The
second parameter to the function is ‘a’ , which means that we will
append to the end of the file and the current contents will not be
erased. Finally, we write the contents of the new book object at the
end of the file. The following format is used:
book title|book author|Available (0 or 1)
Here, we open the file from the beginning, and we use a loop to
read the details of each book from the text file.
We proceed with the book lending functionality:
def lend_book(self):
#Check if books file exists
if not os.path.exists(self.BooksFilename):
print("No books entered so far")
return
book_found = False
if not book_found:
print(f"Book '{book_title}' not found or not
available.")
We first try to find the requested book by reading all the entries
from the books file in a list of strings.
We iterate this list and we split each entry into the book details
(title, author, availability). When we find the book and (if it is
available), we mark it as not currently available.
Afterwards, we record the lending event by writing a new entry at
the end of the respective file. Finally, we write all the entries in the
books list back to the books text file, overwriting the file.
Next, we present the functionality for returning a book:
def return_book(self):
if not os.path.exists(self.LendingFilename):
print("No lending events entered so far")
return
book_found = False
if not book_found:
print(f"Book '{book_title}' not found or already
returned.")
Here, we read again all the book file entries into a list of strings,
and we try to find the requested file. Then, we read all the book
lending events from the respective file and we search for the
lending event of the specific book.
If we find the event, then we change its status to “returned” and we
write all the entries back to the lending file. Finally, we also store
all the book entries into the respective file, after having updated the
book availability status back to 1 again.
Now, let’s see the events listing method:
def list_lending_events(self):
if not os.path.exists(self.LendingFilename):
print("No lending events entered so far")
return
print("Lending events:")
with open(self.LendingFilename, 'r') as file:
for line in file:
parts = line.strip().split('|')
print(f"Book Title: {parts[0]}")
print(f"User Name: {parts[1]}")
print(f"Lending Date: {parts[2]}")
print(f"Returned: {'True' if parts[3] == '1'
else 'False'}")
print("------------------------------")
And finally, the main() function that handles the interaction with
the user:
import sys
from library import Library
def main():
lib = Library()
while True:
print("\n1. Add a book\n2. List all books\n3. Lend a book"
"\n4. Return a book\n5. List lending events\n0. Exit")
choice = input("Enter your choice: ")
try:
choice = int(choice)
except ValueError:
print("Invalid choice. Please enter a number.")
continue
if choice == 1:
lib.add_book()
elif choice == 2:
lib.list_books()
elif choice == 3:
lib.lend_book()
elif choice == 4:
lib.return_book()
elif choice == 5:
lib.list_lending_events()
elif choice == 0:
print("Exiting.")
sys.exit()
else:
print("Invalid choice. Please try again.")
if __name__ == "__main__":
main()
class ContactList:
HASH_SIZE = 100
def __init__(self):
self.bucket_table = [None] * self.HASH_SIZE
...
To remove an entry, we first need to get its hash value. We use this
integer value as the index to get the respective bucket. We then
search the bucket entries, one by one, until we locate the specific
contact. We then remove the entry from the buckets, in the same
way we remove a node from a linked list.
Next, the code for contact search is presented:
def contact_search(self, name):
hash_index = self.hash(name)
contact = self.bucket_table[hash_index]
while contact is not None:
if contact.name == name:
print(f"Name: {contact.name}\nPhone Number:
{contact.phone}")
return
contact = contact.next
print(f"Contact '{name}' not found.")
phonebook.contact_search("John")
phonebook.contact_search("Alex")
phonebook.contact_search("George")
phonebook.contact_remove("Jake")
phonebook.contact_remove("Jane")
phonebook.contact_search("Jane")
if __name__ == "__main__":
todo_list = TodoList()
while True:
print("\nTo-Do List Manager")
print("1. Add Task")
print("2. Remove Task")
print("3. Display Tasks")
print("4. Sort Tasks by Priority")
print("0. Exit")
choice = input("Enter your choice: ")
try:
choice = int(choice)
if choice == 1:
description = input("Enter task description:
")
priority = int(input("Enter priority: "))
todo_list.add_task(description, priority)
print("Task added successfully.")
elif choice == 2:
index = int(input("Enter number of task to
remove: "))
todo_list.remove_task(index - 1)
print("Task removed successfully.")
elif choice == 3:
print("List of tasks:")
todo_list.display_tasks()
elif choice == 4:
todo_list.sort_tasks()
print("Tasks sorted by priority.")
elif choice == 0:
print("Exiting...")
break
else:
print("Invalid choice. Please try again.")
except ValueError:
print("Invalid input. Please enter a number.")
The main function handles the user input and calls the respective
methods of the TodoList object.
Here is the definition of the Task class that makes the objects of the
linked list structure:
class Task:
def __init__(self, description=None, priority=0):
self.description = description
self.priority = priority
self.next = None
The linked list consists of Task nodes that get linked one to the
other via the next reference. The head variable points to the first
element in the list:
from task import Task
class TodoList:
def __init__(self):
self.head = None
self.size = 0
...
previous = None
current = self.head
i=0
#Go to the selected index
while current is not None and i < index:
previous = current
current = current.next
i += 1
if current is None:
print("Index out of bounds.")
return
previous.next = current.next
current = None
self.size -= 1
The argument to the method is the index of the entry inside the
linked list, as it is presented during task listing in the console. As
we will see in the next snippet, we start listing the tasks from
number 1, which is something that we take into account in the
calculations above.
Here is the code for the task listing:
def display_tasks(self):
temp = self.head
i=1
while temp is not None:
print(f"{i}) Description: {temp.description}, Priority:
{temp.priority}")
temp = temp.next
i += 1
swapped = True
while swapped:
swapped = False
ptr1 = self.head
#Sort by artist
insertion_sort(songs, compare_by_artist)
print("Sorted by Artist:")
for song in songs:
print(f"{song.title} from {song.artist}")
print()
#Sort by album
insertion_sort(songs, compare_by_album)
print("Sorted by Album:")
for song in songs:
print(f"{song.title} from {song.album}")
print()
#Sort by release date
insertion_sort(songs, compare_by_release_date)
print("Sorted by Release Date:")
for song in songs:
print(f"{song.title} released in {song.release_year}")
if __name__ == "__main__":
main()
class Task:
def __init__(self, description: str, duration: int):
self.description = description
self.duration = duration
def main(self):
num_workers = int(input("Enter the number of workers: "))
while True:
print("\nMenu:")
print("1. Add Task")
print("2. Display Tasks")
print("3. Print Workers Queue")
print("4. Exit")
choice = int(input("Enter your choice: "))
if choice == 1:
self.add_task()
elif choice == 2:
self.display_tasks()
elif choice == 3:
self.print_workers_queue()
elif choice == 4:
print("Exiting program...")
break
else:
print("Invalid choice! Please try again.")
...
The main() method displays the menu and gets the user’s
selections. It also initializes a list that will store the tasks, and a
priority queue that will contain the workers.
heapq is a module in Python that provides an implementation of the
heap queue algorithm, also known as the priority queue algorithm.
Note that self.worker_queue is a simple Python list. We use the
heapq module to use it as a priority queue.
Also note that when the program starts, the users should select the
total number of workers; the workers will be subsequently referred
to by their ID.
Next, we implement the method that adds a new task to the system:
#Method to add a task and allocate it to a worker
def add_task(self):
description = input("Enter task description: ")
duration = int(input("Enter task duration (in minutes): "))
if len(self.worker_queue) == 0:
print("No workers available! Task cannot be assigned.")
return
In order to find the worker that has the lowest workload, we store
workers in the priority queue. Note that we are using the workload
property as the parameter that will be used to sort the priority
queue. This is specified in the definition of the less-than ( __lt__ )
method in the Worker class:
def __lt__(self, other):
#Used to compare (less-than <) Worker objects,
return self.workload < other.workload
When we dequeue an item from the queue, then we will get the
item with lowest workload.
In add_task() , we get the worker with the lowest workload (the one
that is positioned at the front of the queue) and we add the task’s
workload to the worker’s own workload. Then, we store the task
into the tasks list. Finally, the worker is inserted into the queue
again; now the worker will be positioned according to the newly
updated workload.
Finally, we present the code for two other operations, displaying
the tasks and the workers information respectively:
#Method to display all tasks
def display_tasks(self):
print("Task List:")
for task in self.tasks:
print(f"Task description: {task.description}, "
"Duration: {task.duration} minutes")
#Method to print the workers queue
def print_workers_queue(self):
print("Workers Queue:")
#heapq does not support direct iteration, we need to copy the heap
to display
temp_queue = list(self.worker_queue)
heapq.heapify(temp_queue)
while temp_queue:
worker = heapq.heappop(temp_queue)
print(f"Worker ID: {worker.id}, Workload: {worker.workload}
minutes")
if __name__ == "__main__":
TaskAllocation().main()
def clean_word(word):
#Remove non-letter characters and convert to lowercase
return ''.join([char.lower() for char in word if char.isalpha()])
def main():
word_frequency = defaultdict(int) #Dictionary to store word
frequencies
if __name__ == "__main__":
main()
Initially, we are reading the input file, line by line. Then, we split
each line, based on spaces and we process each word to make it
lowercase and remove non-letter characters. This processing is
performed by the clean_word() method, that takes a string, breaks it
into characters, then filters the characters to keep only letters and
numbers. Finally, it maps each character into lowercase ones and
creates a new string based on them.
Then, we add the word in the dictionary. If the word does not
already exist in the defaultdict , then a new entry will be created
with a default frequency of zero. Otherwise, the frequency for the
existing entry will be incremented.
Finally, we employ a loop to print all the words and their
frequencies to the console.
...
Next, we add the code for stack push and pop, as well as a method
to check if the stack is empty:
def push(self, c):
self.items.append(c) #Append the character to the stack
def pop(self):
if self.is_empty():
print("Stack is empty")
exit(1) #Exit if the stack is empty
return self.items.pop() #Pop and return the top item
def is_empty(self):
return len(self.items) == 0 #Return True if the stack is empty
Listing 9-2: syntax_checker.py
The most interesting part of the code is the algorithm that checks
whether the file is balanced or not:
class SyntaxChecker:
@staticmethod
def check_balanced(filename):
try:
with open(filename, 'r') as file:
stack = Stack()
while True:
#Read one character at a time
c = file.read(1)
if not c: # End of file
break
if c in '([{':
#If the character is an opening bracket
stack.push(c)
elif c in ')]}':
#If the character is a closing bracket
if stack.is_empty():
return 0
opening_char = stack.pop()
@staticmethod
def main():
try:
filename = input("Path to the source file: ")
if SyntaxChecker.check_balanced(filename) == 1:
print("The input file is balanced.")
else:
print("The input file is not balanced.")
except IOError as e:
print(e)
if __name__ == "__main__":
SyntaxChecker.main()
We open and parse the source code file, and we push the bracket
opening characters into the stack. When we encounter a closing
character, then we pop the first available opening character from
the stack.
If there is a mismatch between those two characters, we conclude
that the file is not balanced. At the end, we also check that the stack
is emptied; if not, then the file is still unbalanced.
Note that this is a trivial version of the algorithm. There will be
times that a source file that compiles will be found to be
unbalanced. An example for this would be the following code (in
Java) where we use single characters (opening or closing) in our
code during checking:
if (c == '(' || c == '[' || c == '{')
The entrance of the maze is at the top left corner, and the exit at the
bottom right corner.
We will use a stack structure to solve this maze. As we move
through the maze, we store the entered point coordinates in the
stack. When we reach a dead end, then we will have to backtrack,
and we will do this by popping one point from the stack. This
algorithm is called Depth-first search (DFS), as it goes inside the
maze as deep as possible, only to go back and try another direction
when no way is found.
Here is the code for the coordinate points:
class Point:
def __init__(self, row=0, col=0):
self.row = row
self.col = col
Note that we keep the stack simple and don’t include bounds
checking, as we will instantiate a stack with the maximum capacity
needed to solve the maze (ROWS*COLS).
Now, it is time to introduce a class that will handle the maze:
class Maze:
ROWS = 15
COLS = 15
def __init__(self):
#Create a stack with a size equal to total cells
self.stack = Stack(self.ROWS * self.COLS)
self.matrix = [
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0],
[0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0],
[0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 1, 0],
[0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0],
[0, 1, 0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 1, 0],
[0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0],
[0, 1, 0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1, 0],
[0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0],
[0, 1, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 0],
[0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0],
[0, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 0],
[0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0],
[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0]
]
...
The cell must be within the maze bounds and should be part of a
corridor.
We also provide a method to print the maze:
def print_maze(self):
#Print the maze
for row in self.matrix:
print(" ".join(str(cell) for cell in
row))
if self.can_move(row, col):
self.stack.push(Point(row, col))
self.matrix[row][col] = 2 #Mark as visited
#Move right
if self.solve(row, col + 1) == 1:
return 1
#Move down
if self.solve(row + 1, col) == 1:
return 1
#Move left
if self.solve(row, col - 1) == 1:
return 1
#Move up
if self.solve(row - 1, col) == 1:
return 1
if maze.solve(0, 0) == 1:
print("\n\nThis is the path found:")
maze.print_path()
if __name__ == "__main__":
main()
We first print the initial maze, then we solve the maze. Next, we
print the solution path. Finally, we display the map once more; all
the points that we crossed during our search will be marked with
‘2’.
class FileIndexer:
class Node:
def __init__(self, name, path):
self.fileName = name
self.filePath = path
self.left = None
self.right = None
def __init__(self):
self.root = None
...
Each node of the tree contains two strings, the file name and the
file location. It also contains references to the two children nodes.
Next, we define a method to insert a new node into the tree:
def insert_node(self, file_name, file_path):
#Insert node into the tree
if self.root is None:
self.root = self.Node(file_name,
file_path)
return
current = self.root
while True:
if file_name < current.fileName:
if current.left is None:
current.left = self.Node(file_name,
file_path)
return
current = current.left
else:
if current.right is None:
current.right =
self.Node(file_name, file_path)
return
current = current.right
Starting from the root of the tree, we move downwards to the left
or to the right depending on the inserted value.
Next, we define the method that will recursively index all files into
the tree:
def index_directory_helper(self, dir_path):
#If it's not a directory, return
if not os.path.isdir(dir_path):
return
We perform this by setting the root of the subtree to null, so that the
subtree will be removed by the garbage collector.
Then, we have the directory traversal method (also recursive):
def traverse(self, root):
if root is not None:
self.traverse(root.left)
print(f"{root.fileName}:
{root.filePath}")
self.traverse(root.right)
def print_files(self):
print("Indexed files:")
self.traverse(self.root)
After the tree has been set up, we can call method
search_file_location() to get the location of a file:
def search_file_location(self, filename):
#Search for a file in the BST
current = self.root
while current is not None:
if filename == current.fileName:
return current.filePath #File found
elif filename < current.fileName:
current = current.left #Search in the left
subtree
else:
current = current.right #Search in the
right subtree
return "" #File not found
indexer = FileIndexer()
indexer.index_directory(path)
indexer.print_files()
location = indexer.search_file_location(filename_to_search)
if location:
print(f"File {filename_to_search} found. Location: {location}")
else:
print(f"File {filename_to_search} not found.")
Users can index the contents of a folder and then they can search
for a specific filename.
You can find this project in GitHub:
https://github.com/htset/python_exercises_dsa/tree/master/FileInde
xer
12. Inventory with AVL Tree
In this exercise, we will create an inventory program, that will store
information about the company’s products in an AVL tree structure.
Proposed Solution
An AVL (Adelson-Velsky and Landis) tree is a self-balancing binary
search tree structure. With the term balanced, we mean that both
branches of the tree have the same depth or differ by one level at
the most. To achieve this, a process called rebalancing is
occasionally performed, that changes the tree structure in way that
the tree is closer to be balanced.
The AVL tree has almost the same structure as a simple binary
search tree (BST); the difference lies in the rebalancing algorithm.
Let’s see the structure:
import random
class Product:
def __init__(self, id, name, price, quantity):
self.id = id
self.name = name
self.price = price
self.quantity = quantity
class InventoryNode:
def __init__(self, product):
self.product = product
self.left = None
self.right = None
self.height = 1
The former gives us the height of the tree, while the latter checks
whether the tree is balanced or not.
Afterwards, we add code for the creation of a new node in the tree:
#Create a new node with the given product
def new_node(self, product):
return InventoryNode(product)
#Perform rotation
x.right = y
y.left = T2
#Update heights
y.height = max(self.get_height(y.left), self.get_height(y.right)) + 1
x.height = max(self.get_height(x.left), self.get_height(x.right)) + 1
#Perform rotation
y.left = x
x.right = T2
#Update heights
x.height = max(self.get_height(x.left), self.get_height(x.right)) + 1
y.height = max(self.get_height(y.left), self.get_height(y.right)) + 1
return node
The idea here is to check for the tree balance after inserting a new
node to it. If the tree becomes unbalanced, then we will have to
rotate it either to the left or to the right.
Next, we present the methods to traverse the tree while printing its
contents, as well as the code to search for a specific product in the
tree:
def traverse_tree(self, node):
if node is not None:
self.traverse_tree(node.left)
print(f"ID: {node.product.id}, Name:
{node.product.name}, "
"Price: {node.product.price}, "
"Quantity: {node.product.quantity}")
self.traverse_tree(node.right)
if id < node.product.id:
return self.search_product(node.left, id)
else:
return self.search_product(node.right, id)
Traversing the tree means visiting each node in the tree, and this is
performed recursively, first for the left branch and then for the right
branch.
Searching for a product in the tree works in similar fashion: we
visit a node, and we check the product’s ID. If it matches the search
ID, then we print the product details, and the method returns.
Otherwise, we visit the left or the right branch of the tree
recursively, depending on the search ID.
All the methods we have defined so far are not to be used directly.
We define three methods that will call them:
def insert_product_public(self, product):
self.root = self.insert_product(self.root, product)
def traverse_tree_public(self):
self.traverse_tree(self.root)
class User:
def __init__(self, name):
self.name = name
self.friends = None
The User class contains the name of the user as well as a linked
list of the user’s friends.
Next, we define a queue class that will be used by the friend
recommendation algorithm. For learning purposes, we will make
our own queue instead of using the one from Python:
class MyQueue:
class QueueNode:
def __init__(self, user_index):
self.user_index = user_index
self.next = None
def __init__(self):
self.front = self.rear = None
def is_empty(self):
return self.front is None
def dequeue(self):
if self.is_empty():
print("Queue is empty!")
return -1
user_index = self.front.user_index
self.front = self.front.next
if self.front is None:
self.rear = None
return user_index
The queue contains the indexes of the users, as they will appear
inside the users’ list (see below in the Graph class). We define the
class for the queue nodes, and methods to enqueue and dequeue
user indexes inside the queue, as well as to check whether it is
empty or not.
Now, let’s see how we will insert users into the graph and how we
will define the connections with their friends. We define the Graph
class, that essentially contains a list of all the users of the social
network:
class Graph:
MAX_USERS = 100
def __init__(self):
self.users = [None] * self.MAX_USERS
self.num_users = 0
...
self.users[self.num_users] = User(name)
self.num_users += 1
new_friend_dest = FriendNode(self.users[src].name)
new_friend_dest.next = self.users[dest].friends
self.users[dest].friends = new_friend_dest
visited[user_index] = True
#Enqueue the starting user
queue.enqueue(user_index)
graph.add_connection(0, 1)
graph.add_connection(1, 2)
graph.add_connection(2, 3)
graph.add_connection(4, 5)
graph.add_connection(5, 7)
graph.add_connection(3, 6)
graph.recommend_friends(0)
graph.recommend_friends(1)
graph.recommend_friends(7)
class City:
def __init__(self, name):
self.name = name
self.flights = {}
...
This class contains all the cities objects in a map along with their
names. We can add cities and flights to our graph with the
following methods:
#Add a city to the graph
def add_city(self, name):
self.cities[name] = City(name)
Note that we assume that flights are bidirectional, and that they
have the same price in both directions.
Next, we calculate the cheapest route between two cities using
Dijkstra’s algorithm:
#Function to find the cheapest route between two cities
# using Dijkstra's algorithm
def find_cheapest_route(self, src, dest):
dist = {city: float('inf') for city in self.cities}
prev = {city: None for city in self.cities}
pq = []
dist[src] = 0
heapq.heappush(pq, (0, src))
while pq:
u_dist, u = heapq.heappop(pq)
visited = set()
path = [src]
self.dfs(src, dest, visited, path)
if src == dest:
self.print_path(path)
else:
for flight in self.cities[src].flights:
if flight not in visited:
path.append(flight)
self.dfs(flight, dest, visited, path)
path.pop()
visited.remove(src)
Finally in the main code, we add cities and flights to the graph, and
we ask the user to select a pair of cities to calculate the best
(cheapest) combination of flights:
if __name__ == "__main__":
graph = FlightGraph()
graph.add_city("London")
graph.add_city("Paris")
graph.add_city("Berlin")
graph.add_city("Rome")
graph.add_city("Madrid")
graph.add_city("Amsterdam")
Source: Wikipedia
class Image:
def __init__(self, data, id):
self.data = data[:]
self.id = id
def print_image(self):
for i in range(len(self.data)):
if self.data[i] == 0:
print(" ", end="")
else:
print("*", end="")
if (i + 1) % 28 == 0:
print()
try:
with open("input.dat", "rb") as file:
#Skip the metadata at the beginning of the file
file.seek(16)
count = 0
while True:
pixels = file.read(784)
if len(pixels) == 0:
break
images.append(Image(list(pixels), count))
count += 1
except IOError as e:
print(e)
closest_image = None
min_distance = float('inf')
min_index = 0
for i in range(len(images)):
distance = random_image.euclidean_distance(images[i])
if distance != 0 and distance < min_distance:
min_distance = distance
min_index = i
closest_image = images[i]
class LRUCache:
CACHE_SIZE = 3
class Node:
def __init__(self, url, content):
self.url = url
self.content = content
self.prev = None
self.next = None
def __init__(self):
#Initialize the cache
self.head = None
self.tail = None
self.size = 0
...
if __name__ == "__main__":
main()
def __init__(self):
self.cache = LRUCache()
def start(self):
#Start the HTTP server
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as
server:
server.bind(('localhost', self.PORT))
server.listen()
print("Server started on port", self.PORT)
while True:
client, _ = server.accept()
self.handle_request(client)
client.close()
parts = request.split()
if len(parts) < 2 or parts[0] != "GET":
print("Invalid request format.")
return
url = parts[1]
content = self.cache.get_content(url)
if not content:
try:
with open(url[1:], 'r', encoding='utf-8') as file:
content = file.read()
print("Got content from file:", content)
self.cache.put_content(url, content)
except FileNotFoundError:
print("File not found:", url[1:])
content = "HTTP/1.1 404 Not Found\r\n\r\n"
except IOError:
print("File not found:", url[1:])
content = "HTTP/1.1 404 Not Found\r\n\r\n"
else:
print("Serving content from cache.")
content_type = "text/plain"
if url.endswith((".html", ".htm")):
content_type = "text/html"
client.sendall(response.encode('utf-8'))
If the URL is found in the cache, then we get the content from there
and we send it with the response.
Let’s see how we do this, in method get_content() from the
LRUCache class:
def get_content(self, url):
#Get content associated with a URL from the cache
current = self.head
while current:
if current.url == url:
#Move the accessed node to the head
self.move_to_head(current)
print("Got content from cache:",
current.content)
return current.content
current = current.next
return ""
We start from the head of the list, and we search for the URL in the
cache’s nodes. If we find the URL, then we move the node to the
head of the cache (the most recently used entry) and we return the
stored HTML content. The method returns an empty string if the
URL is not found in the cache.
When a page is read from its file, then we store its content into the
cache, with put_content() :
def put_content(self, url, content):
#Put a URL-content pair into the cache
if self.size == self.CACHE_SIZE:
#If cache is full, remove the least recently
used node
self.delete_node(self.tail)
self.size -= 1
new_node = self.create_node(url, content)
self.insert_at_head(new_node)
self.size += 1
Next, we see how to insert a new node at the head of the cache:
def insert_at_head(self, node):
#Insert a new node at the head of the cache
node.next = self.head
node.prev = None
if self.head:
self.head.prev = node
self.head = node
if not self.tail:
self.tail = node
print("Node inserted at head:", node.content)
...
Here we define the Client class that stores information about the
connected client (its ID and the corresponding socket, writer and
reader objects).
The writer is responsible for sending data from the server to the
client. In Python, this is typically done using a file-like object
created from the socket, which allows the server to write strings to
the socket as if it were writing to a file. The same holds for the
reader object, but this time for reading from the client.
Next, we have the definition of the server’s global variables:
#Constants
PORT = 8080 #Port number for the server
MAX_CLIENTS = 5 #Maximum number of clients
#Auction details
best_bid = 0 #Highest bid received
winning_client = 0 #Client ID of the highest bidder
try:
server = socket.socket(socket.AF_INET,
socket.SOCK_STREAM)
server.bind(('localhost', PORT))
server.listen(5)
print("Server started. Waiting for connections...")
try:
client = clients[client_id]
bid_amount = int(bid)
print("Received bid", bid_amount, "from client", client.id)
time.sleep(duration)
if not event.is_set():
print("Auction finished. Winning bid:", best_bid
, ", winner: client no.", winning_client)
After informing all clients about the winning bid, we exit the
program. The client sockets will be closed by the respective clients
when they receive the message of the auction completion.
Now for the auction client, we first implement the main() method:
import socket
import threading
#Constants
PORT = 8080 # Port number to connect to the server
SERVER_IP = "127.0.0.1" # Server IP address
client = None
is_running = True
receive_thread = None
try:
client.sendall((bid + '\n').encode('utf-8'))
except Exception as ex:
print("Send failed:", ex)
return
except Exception as e:
print("Error:", e)
finally:
#Ensure client is closed
if client:
client.close()
print("\nServer:", message)
if message.startswith("Auction"):
print("Auction ended. Exiting
program.")
is_running = False
break
except Exception as ex:
if is_running:
print("Receive failed:", ex)
if __name__ == "__main__":
main()
This thread receives messages from the auction server and prints
them in the console. When the final message, starting with
“Auction” arrives, then it closes down the resources and exits the
program.