This activity is dedicated to the manipulation of data structures and to see empirically the impact of the choice of structure over another.
Important
The aim of this session is to help you master important notions in computer science.
An intelligent programming assistant such as GitHub Copilot, that you may have installed already, will be able to provide you with a solution to these exercises based only on a wisely chosen file name.
For the sake of training, we advise you to disable such tools first.
At the end of the practical activity, we suggest you to work on the exercise again with these tools activated.
Following these two steps will improve your skills both fundamentally and practically.
Also, we provide you the solutions to the exercises.
Make sure to check them only after you have a solution to the exercises, for comparison purpose!
Even if you are sure your solution is correct, please have a look at them, as they sometimes provide additional elements you may have missed.
Activity contents
1 — List-based implementation of a stack
In a file named list_stack.py, give an implementation of a stack using the built-in python list to store the data.
Here are the primitives that should be implemented.
initialize – Initializes the stack.
push – Adds an element to the top of the stack.
pop – Removes the top element from the stack.
is_empty – Returns true if the stack is empty, and false otherwise.
Then, in a second file named test_stack.py, perform some operations on a stack to check its primitives.
Correction
"""
This module provides an implementation of the primitives to handle a stack of values.
The stack is materialized by means of a list with a size limit to simulate an array.
"""# Needed importsfrom typing import List,Any
# define your own custom typeStack = List[Any]
definitialize () -> Stack:
"""
Initializes an empty stack.
In:
* None.
Out:
* An empty stack.
"""# We use lists to represent stacksreturn []
defis_empty (stack: Stack) -> bool:
"""
This function checks if the stack is empty.
In:
* stack: Stack to check.
Out:
* True if the stack is empty, False otherwise.
"""# Check list lengthreturn len(stack) ==0defpush (stack: Stack, item: Any) ->None:
"""
This function adds an element to the stack.
The element is added at the last position of the storage structure.
In:
* stack: Stack to modify.
* item: Element to add.
"""# Add the element to the end of the list stack.append(item)
defpop (stack: Stack) -> Any:
"""
This function removes the next element of the stack.
In:
* stack: Stack to modify.
Out:
* The next element of the stack.
"""# Extract and return the last element of the listreturn stack.pop()
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
publicclassListStack {
private List<Object> stack;
//The constructeur replace the initialise primitivepublicListStack() {
this.stack=new ArrayList<>();
}
publicbooleanisEmpty() {
/**
* This function checks if the stack is empty.
* @return True if the stack is empty, False otherwise.
*/returnthis.stack.isEmpty();
}
publicvoidpush(Object item) {
/**
* This function adds an element to the stack.
* The element is added at the end.
* @param item Element to add.
*/this.stack.add(item);
}
public Object pop() {
/**
* This function removes the last element of the stack.
* @return The last element of the stack.
*/if (this.stack.isEmpty()) {
thrownew IllegalStateException("Stack is empty");
}
returnthis.stack.remove(this.stack.size() - 1);
}
}
2 — Calculate a postfix arithmetic expression
In this exercise we will use a stack to calculate an arithmetic expression in postfix notation, i.e., where each operator is preceded by its operands, such as 3 12 3 - 3 / 1 - *.
The equivalent infix expression using parentheses is 3 * (((12 - 3) / 3) - 1).
In the file test_stack.py write a function evaluate_postfix that calculates the result of a postfix integer arithmetic expression.
The function takes as input a string containing an arithmetic expression and return its final result.
The result of the calculation will be a real number.
The input is a sequence of characters.
Each piece of data is separated from the others by a space.
The method consists of using a stack to store the numerical values as they are read, and performing a processing operation when an operator is read.
This processing operation removes the last two values in the stack, performs the operation, and then pushes the result.
The final result is stored in the stack.
Allowed operators are +, -, *, /.
It is for the moment considered that the expression is correctly written and only integers are used.
Information
You will need to use different programming tools to implement this function.
to split a string according to a separator "3 2 * 25 +".split(" ") will split the string using a space as a delimiter.
to check if a string can be translated into an int "3".isnumeric() returns true whereas "afea".isnumeric() returns false.
to translate a string into an integer int("34") returns 34.
Modify the entry point of your test_stack.py file to ask for the input of an expression and to then display the result of its evaluation.
Correction
defevaluate_postfix (expression: str) -> float:
"""
This function uses a stack to evaluate a postfix expression.
The function takes a string as input, which is a postfix expression.
The function returns the result of the expression.
In:
* expression: Expression to be evaluated.
Out:
* Evaluated of the evaluated expression.
"""# Create an empty stack stack : Stack = initialize()
# Iterate over the expression split_expression = expression.split(" ")
for i in split_expression:
# If the character is a number, push it to the stackif i.isnumeric():
push(stack, int(i))
# If the character is an operator, apply itelse:
# Pop the last two elements from the stack a = pop(stack)
b = pop(stack)
# Apply the operatorif i =='+':
push(stack, b + a)
elif i =='-':
push(stack, b - a)
elif i =='*':
push(stack, b * a)
elif i =='/':
push(stack, b / a)
# Return the resultreturn pop(stack)
/**
* To run this code, you need to have Java installed on your computer, then:
* - Create a file named `Main.java` in a directory of your choice.
* - Copy this code in the file.
* - Open a terminal in the directory where the file is located.
* - Run the command `javac Main.java` to compile the code.
* - Run the command `java -ea Main` to execute the compiled code.
* Note: '-ea' is an option to enable assertions in Java.
*/// Needed importsimport java.util.Stack;
publicclassMain {
/**
* This function uses a stack to evaluate a postfix expression.
* The function takes a string as input, which is a postfix expression.
* The function returns the result of the expression.
*
* @param expression Expression to be evaluated.
* @return Evaluated result of the postfix expression.
*/publicstaticdoubleevaluatePostfix(String expression) {
// Create an empty stack Stack<Double> stack =new Stack<>();
// Iterate over the expression String[] splitExpression = expression.split(" ");
for (String i : splitExpression) {
// If the character is a number, push it to the stackif (i.matches("-?\\d+(\\.\\d+)?")) {
stack.push(Double.parseDouble(i));
}
// If the character is an operator, apply itelse {
// Pop the last two elements from the stackdouble a = stack.pop();
double b = stack.pop();
// Apply the operatorswitch (i) {
case"+":
stack.push(b + a);
break;
case"-":
stack.push(b - a);
break;
case"*":
stack.push(b * a);
break;
case"/":
stack.push(b / a);
break;
default:
thrownew IllegalArgumentException("Invalid operator: "+ i);
}
}
}
// Return the resultreturn stack.pop();
}
}
3 — Compare different implementations of queues
3.1 — Using a built-in list
As you defined an implementation of a stack using the built-in list type (file list_stack.py), create a file name list_queue.py that implements the primitives of a queue using a list.
Recall of the primitives:
initialize – Initializes the queue.
push – Adds an element to the end of the queue.
pop – Removes the first element from the queue.
is_empty – Returns true if the queue is empty, and false otherwise.
Correction
"""
This module provides an implementation of the primitives to handle a queue of values.
The queue is materialized by means of a list with a size limit to simulate an array.
"""# Needed importsfrom typing import List,Any
# Define your own custom typeQueue = List[Any]
definitialize() -> Queue:
"""
Initializes an empty queue.
In:
* None.
Out:
* An empty queue.
"""# We use lists to represent queuesreturn []
defis_empty (queue: Queue) -> bool:
"""
This function checks if the queue is empty.
In:
* queue: Queue to check.
Out:
* True if the queue is empty, False otherwise.
"""# Check list lengthreturn len(queue) ==0defpush (queue: Queue, item: Any) ->None:
"""
This function adds an element to the queue.
The element is added at the last position of the storage structure.
In:
* queue: Queue to modify.
* item: Element to add.
"""# Add the element to the end of the list queue.append(item)
defpop (queue: Queue) -> Any:
"""
This function removes the next element of the queue.
In:
* queue: Queue to modify.
Out:
* The next element of the queue.
"""# Extract and return the first element of the listreturn queue.pop(0)
import java.util.ArrayList;
import java.util.List;
publicclassListQueue {
private List<Object> queue;
publicListQueue() {
this.queue=new ArrayList<>();
}
publicbooleanisEmpty() {
/**
* This function checks if the queue is empty.
* @return True if the queue is empty, False otherwise.
*/returnthis.queue.isEmpty();
}
publicvoidpush(Object item) {
/**
* This function adds an element to the queue.
* The element is added at the end.
* @param item Element to add.
*/this.queue.add(item);
}
public Object pop() {
/**
* This function removes the last element of the queue.
* @return The removed element.
*/if (this.queue.isEmpty()) {
thrownew IllegalStateException("Queue is empty");
}
returnthis.queue.remove(0);
}
}
3.2 — Using doubly-linked list
The doubly-linked list is a data structure giving an access to the first and last element of a list.
Write an implementation of the primitives to manage a doubly-linked list in a file dll_queue.py.
Recall of the primitives necessary for this practical session (there can be others):
initialize – Initializes an empty list.
insert_at_end – Adds an element to the end of the list.
pop_from_beginning – Returns and removes the head element of the list.
is_empty – Returns true if the list does not contain any data.
Then test your implementation for example by enqueueing a few elements and dequeueing them.
Correction
"""
This module provides functions to manipulate a doubly-linked list.
The list is represented as a dictionary with 'head' and 'tail' keys pointing to the first and last elements, respectively.
Each element is also represented as a dictionary with 'data', 'prev', and 'next' keys.
"""# Needed importsfrom typing import Any, Dict, Optional
# Create your own custom typeElement = Dict[str, Any]
defcreate_element (data: Any) -> Element:
"""
Creates a new element with the given data.
In:
* data: The data to store in the element.
Out:
* A dictionary representing the new element.
"""# Return the new elementreturn {'data': data, 'prev': None, 'next': None}
definitialize () -> Dict[str, Optional[Element]]:
"""
Initializes an empty doubly-linked list.
In:
* None.
Out:
* A dictionary representing the doubly-linked list, with 'head' and 'tail' keys.
"""# Initialize the default pointersreturn {'head': None, 'tail': None}
defis_empty (dll: Dict[str, Optional[Element]]) -> bool:
"""
Checks if the doubly-linked list is empty.
In:
* dll: The doubly-linked list.
Out:
* True if the list is empty, False otherwise.
"""# Check if the head pointer is uninitializedreturn dll['head'] isNonedefinsert_at_beginning (dll: Dict[str, Optional[Element]], data: Any) ->None:
"""
Insert a new element with the given data at the beginning of the list.
In:
* dll: The doubly-linked list.
* data: The data to store in the new element.
Out:
* None.
"""# Create the element to store new_element = create_element(data)
# Adapt the pointersif dll['head'] isNone:
dll['head'] = new_element
dll['tail'] = new_element
else:
new_element['next'] = dll['head']
dll['head']['prev'] = new_element
dll['head'] = new_element
definsert_at_end (dll: Dict[str, Optional[Element]], data: Any) ->None:
"""
Insert a new element with the given data at the end of the list.
In:
* dll: The doubly-linked list.
* data: The data to store in the new element.
Out:
* None.
"""# Create the element to store new_element = create_element(data)
# Adapt the pointersif dll['tail'] isNone:
dll['head'] = new_element
dll['tail'] = new_element
else:
new_element['prev'] = dll['tail']
dll['tail']['next'] = new_element
dll['tail'] = new_element
defpop_from_beginning (dll: Dict[str, Optional[Element]]) -> Any:
"""
Removes and returns the data of the element at the beginning of the list.
In:
* dll: The doubly-linked list.
Out:
* The data of the element removed from the beginning of the list.
"""# Verify it is not emptyif is_empty(dll):
returnNone# Extract the data data = dll['head']['data']
# Adapt the pointersif dll['head'] == dll['tail']:
dll['head'] =None dll['tail'] =Noneelse:
dll['head'] = dll['head']['next']
dll['head']['prev'] =None# Return the datareturn data
defpop_from_end (dll: Dict[str, Optional[Element]]) -> Any:
"""
Removes and returns the data of the element at the end of the list.
In:
* dll: The doubly-linked list.
Out:
* The data of the element removed from the end of the list.
"""# Verify it is not emptyif is_empty(dll):
returnNone# Extract the data data = dll['tail']['data']
# Adapt the pointersif dll['head'] == dll['tail']:
dll['head'] =None dll['tail'] =Noneelse:
dll['tail'] = dll['tail']['prev']
dll['tail']['next'] =None# Return the datareturn data
/**
* This class provides methods to manipulate a doubly-linked list.
* The list is represented with a head and tail pointing to the first and last nodes, respectively.
* Each node is represented with data, prev, and next fields.
*/import java.util.Optional;
classDoublyLinkedList {
// Define the Node structurestaticclassNode {
int data;
Node prev;
Node next;
Node(int data) {
this.data= data;
this.prev=null;
this.next=null;
}
}
private Node head;
private Node tail;
// Initialize the listpublicDoublyLinkedList() {
this.head=null;
this.tail=null;
}
/**
* Check if the list is empty.
*
* @return true if the list is empty, false otherwise.
*/publicbooleanisEmpty() {
return head ==null;
}
/**
* Insert a new node with the given data at the beginning of the list.
*
* @param data The data to store in the new node.
*/publicvoidinsertAtBeginning(int data) {
Node newNode =new Node(data);
if (head ==null) {
head = tail = newNode;
} else {
newNode.next= head;
head.prev= newNode;
head = newNode;
}
}
/**
* Insert a new node with the given data at the end of the list.
*
* @param data The data to store in the new node.
*/publicvoidinsertAtEnd(int data) {
Node newNode =new Node(data);
if (tail ==null) {
head = tail = newNode;
} else {
newNode.prev= tail;
tail.next= newNode;
tail = newNode;
}
}
/**
* Delete the first node with the given data from the list.
*
* @param data The data of the node to delete.
*/publicvoiddeleteNode(int data) {
Node current = head;
while (current !=null) {
if (current.data== data) {
if (current.prev!=null) {
current.prev.next= current.next;
} else {
head = current.next;
}
if (current.next!=null) {
current.next.prev= current.prev;
} else {
tail = current.prev;
}
return;
}
current = current.next;
}
}
/**
* Remove and return the data from the node at the head of the list.
*
* @return The data from the node at the head of the list, or -1 if the list is empty.
*/publicintpopFromHead() {
if (head ==null) {
thrownew RuntimeException("Empty list");//notion of Exception seen in prog session }
int data = head.data;
head = head.next;
if (head !=null) {
head.prev=null;
} else {
tail =null;
}
return data;
}
/**
* Remove and return the data from the node at the tail of the list.
*
* @return The data from the node at the tail of the list, or -1 if the list is empty.
*/publicintpopFromTail() {
if (tail ==null) {
thrownew RuntimeException("Empty list");
}
int data = tail.data;
tail = tail.prev;
if (tail !=null) {
tail.next=null;
} else {
head =null;
}
return data;
}
/**
* Search for a node with the given data.
*
* @param data The data to search for.
* @return An Optional containing the found node, or an empty Optional if no node was found.
*/public Optional<Node>searchNode(int data) {
Node current = head;
while (current !=null) {
if (current.data== data) {
return Optional.of(current);
}
current = current.next;
}
return Optional.empty();
}
/**
* Display the data of all nodes in the list.
*/publicvoiddisplayList() {
Node current = head;
while (current !=null) {
System.out.print(current.data+" ");
current = current.next;
}
System.out.println();
}
}
3.3 — Comparing implementations
At this point of the practical activity, you should have available two possible implementations of a queue:
Using the built-in list type (in list_queue.py),
Using the home-made, though perfectible, doubly-linked list type (in dll_queue.py).
The idea now is to see the impact of choosing an implementation over another.
In addition to the two previously mentioned implementation, a third implementation using the deque python type will be used.
The deque type is similar to the doubly-linked list but optimized compared to our home-made version.
The following code aims at comparing three implementations of a queue focusing on the queueing primitive.
10,000 values are added to each implementation of the queue and the average time needed to perform this queueing operation is calculated and displayed.
Copy the following code in a file named test_perf_queueing.py and to run this code.
Also, check that the files named list_queue.py and dll_queue.py are located in the same directory as test_perf_queueing.py.
Output
"""
compares the time needed to add elements to the front of a list and deque.
"""# Needed importsfrom collections import deque
from time import perf_counter
from typing import List, Deque, Callable, Any
import list_queue as lq
import dll_queue as dllq
# Number of insertions to performNB_OPS =10_000# Initialize the various structuresqueue_using_list = lq.initialize()
queue_using_dll = dllq.initialize()
queue_using_deque = deque()
defaverage_time (func: Callable[[Any], None], times: int) -> float:
"""
This function computes the average time needed to execute a function.
In:
* func: Function to execute.
* times: Number of times to execute the function.
Out:
* Average time needed to execute the function.
"""# Run the function multiple times total =0.0for i in range(times):
start = perf_counter()
func(i)
total += (perf_counter() - start) *1e9# Return the average timereturn total / times
# Compute the average time needed to insert an element in the list-based queuelist_time = average_time(lambda i: lq.push(queue_using_list, i), NB_OPS)
# Compute the average time needed to insert an element in the doubly linked list-based queuedll_time = average_time(lambda i: dllq.insert_at_end(queue_using_dll, i), NB_OPS)
# Compute the average time needed to insert an element in the deque-based queuedeque_time = average_time(lambda i: queue_using_deque.appendleft(i), NB_OPS)
# Evaluate the gaingain = list_time / deque_time
gain2 = list_time / dll_time
gain3 = dll_time / deque_time
# Print resultsprint("----\n INSERTION \n---")
print(f"Average time list queue: {list_time:.6} ns")
print(f"Average time doubly-linked list queue: {dll_time:.6} ns")
print(f"Average time deque: {deque_time:.6} ns")
print(f"deque over list: {gain:.6}x faster")
print(f"doubly-linked list over list: {gain2:.6}x faster")
print(f"deque over doubly-linked list: {gain3:.6}x faster")
# Compute the average time needed to remove an element in the list-based queuelist_time = average_time(lambda i: lq.pop(queue_using_list), NB_OPS)
# Compute the average time needed to remove an element in the doubly linked list-based queuedll_time = average_time(lambda i: dllq.pop_from_beginning(queue_using_dll), NB_OPS)
# Compute the average time needed to remove an element in the deque-based queuedeque_time = average_time(lambda i: queue_using_deque.pop(), NB_OPS)
# Evaluate the gaingain = list_time / deque_time
gain2 = list_time / dll_time
gain3 = dll_time / deque_time
# Print resultsprint("----\n DELETION \n---")
print(f"Average time list queue: {list_time:.6} ns")
print(f"Average time doubly-linked list queue: {dll_time:.6} ns")
print(f"Average time deque: {deque_time:.6} ns")
print(f"deque over list: {gain:.6}x faster")
print(f"doubly-linked list over list: {gain2:.6}x faster")
print(f"deque over doubly-linked list: {gain3:.6}x faster")
What do you observe?
Correction
Here are the kind of results you should obtain:
Output
----
INSERTION
---
Average time list queue: 133.448 ns
Average time doubly-linked list queue: 429.624 ns
Average time deque: 97.645 ns
deque over list: 1.36666x faster
doubly-linked list over list: 0.310616x faster
deque over doubly-linked list: 4.39986x faster
----
DELETION
---
Average time list queue: 763.207 ns
Average time doubly-linked list queue: 375.372 ns
Average time deque: 91.0943 ns
deque over list: 8.3782x faster
doubly-linked list over list: 2.0332x faster
deque over doubly-linked list: 4.1207x faster
As it can be seen, the type of data structure used to implement a queue may strongly affects the efficiency of some primitives.
Here, deleting an element at the first position of a list, even using the optimized built-in list type, is costly as it requires to physically shift, in memory, all the elements in the list.
So a dequeue operation leads to N shift operations when the list contains N elements.
This explains the significative difference in terms of efficiency compared to the doubly-linked list version, which only updates pointers.
Also, comparing the home-made version of doubly-linked list and the deque shows you should always favor official implementation when they are available.
4 — Removing elements in a doubly-linked list
Complete the home-made implementation of the doubly-linked list type with a function that removes the first element of the list having a given value (passed as a parameter of the function).
Correction
defdelete_element (dll: Dict[str, Optional[Element]], data: Any) ->None:
"""
Deletes the first element with the given data from the list.
In:
* dll: The doubly-linked list.
* data: The data of the element to delete.
Out:
* None.
"""# Iterate over the list from the head to the tail until data is found current = dll['head']
while current isnotNone:
if current['data'] == data:
# Adapt the pointersif current['prev'] isnotNone:
current['prev']['next'] = current['next']
else:
dll['head'] = current['next']
if current['next'] isnotNone:
current['next']['prev'] = current['prev']
else:
dll['tail'] = current['prev']
# Stop herereturn# Next element current = current['next']
/**
* Delete the first node with the given data from the list.
*
* @param data The data of the node to delete.
*/publicvoiddeleteNode(int data) {
Node current = head;
while (current !=null) {
if (current.data== data) {
if (current.prev!=null) {
current.prev.next= current.next;
} else {
head = current.next;
}
if (current.next!=null) {
current.next.prev= current.prev;
} else {
tail = current.prev;
}
return;
}
current = current.next;
}
}
5 — Sort a (doubly-linked) list
Apply the basic selection sort algorithm to turn any doubly-linked list into a sorted list (in an increasing order).
Correction
defselection_sort (dll: Dict[str, Optional[Element]]) ->None:
"""
Sorts the list using the basic selection sort algorithm.
Here, sorting is done in place.
In:
* dll: The doubly linked list to be sorted.
Out:
* None.
"""# Nothing to do if empty structureif dll['head'] isNone:
return# Iterate over the structure current = dll['head']
while current isnotNone:
# Find the smallest value in the remaining part of the list (i.e., from current) min_node = current
next_node = current['next']
while next_node isnotNone:
if next_node['data'] < min_node['data']:
min_node = next_node
next_node = next_node['next']
# Swap the dataif min_node != current:
current['data'], min_node['data'] = min_node['data'], current['data']
# Next element current = current['next']
/**
* Apply the selection sort algorithm on the list
*/publicvoidselectionSort() {
if (head !=null) {
Node current = head;
while (current !=null) {
Node minNode = current;
Node nextNode = current.next;
while (nextNode !=null) {
if (nextNode.data< minNode.data) {
minNode = nextNode;
}
nextNode = nextNode.next;
}
// Swap the data of the current node and the minNodeif (minNode != current) {
int temp = current.data;
current.data= minNode.data;
minNode.data= temp;
}
current = current.next;
}
}
}
6 — Sorted doubly-linked list
In some application cases, where the stored values are ordinal, it may be interesting (to possibly speed up value retrieval) to maintain an order of the values within the list.
Given that integer values are stored in the list, suggest an implementation of an insert_element function that keeps the values in descending (or ascending) order.
Correction
definsert_order (dll: Dict[str, Optional[Element]], data: Any) ->None:
"""
Insert a new element with the given data into the list, maintaining the elements in increasing order.
In:
* dll: The doubly-linked list.
* data: The data to store in the new Element.
Out:
* None.
"""# Create a new Element new_element = create_element(data)
# If the list is empty, initialize itif dll['head'] isNone:
dll['head'] = dll['tail'] = new_element
# Otherwise, find where to insert the dataelse:
# Iterate over elements current = dll['head']
while current isnotNoneand current['data'] < data:
current = current['next']
# If it is the largest data seen, add to the endif current isNone:
insert_at_end(dll, data)
# If it is the largest data seen, add to the beginningelif current['prev'] isNone:
insert_at_beginning(dll, data)
# otherwise, adapt the pointerselse:
new_element['next'] = current
new_element['prev'] = current['prev']
current['prev']['next'] = new_element
current['prev'] = new_element
7 — Optimize your solutions
What you can do now is to use AI tools such as GitHub Copilot or ChatGPT, either to generate the solution, or to improve the first solution you came up with!
Try to do this for all exercises above, to see the differences with your solutions.
To go further
8 — Convert infix expression to postfix expression
Write a function infix_to_prefix that converts an infix expression with integers (in brackets) to postfix notation.
You will need to use both a stack and a queue.
For instance, the expression 3*(((12-3)/3)-1) will be converted to: 3 12 3 - 3 / 1 - *
The valid operators are +, -, * and /.
The algorithm reads an arithmetic infix expression and stores the result of the conversion in a queue, which is displayed.
The algorithm will use a stack to manage the operators, and a queue to construct the postfix expression.
The numeric values must be directly stored in the queue, while the operators must wait before adding them to the queue after their operands.
The treatment of an operator, poping then enqueuing, must occur when a closing parenthesis is encountered.
Also, beware of operators priority!
Correction
# Needed importsimport re
definfix_to_postfix (infix_expression: str) -> str:
"""
This function uses a stack and a queue to transform an infix expression to a postfix expression.
All expressions are supposed to be well parenthesized.
In:
* infix_expression: Expression to be transformed to postfix.
Out:
* Postfix expression equivalent to the input infix expression.
"""# Define the precedence of the operators precedence = {"*": 2, "/": 2, "+": 1, "-": 1, "(": 0, ")": 0}
# Initialize the stack and the queue stack = []
queue = []
# Split the infix expression into tokens tokens = [t for t in re.split(r"(\+|\-|\*|\/|\(|\))", infix_expression) if t.strip() !=""]
# Iterate over tokensfor token in tokens:
# If the token is an operand, add it to the queueif token.isnumeric():
queue.append(token)
# If the token is a left parenthesis, add it to the stackelif token =="(":
stack.append(token)
# If the token is a right parenthesis, pop all the operators from the stack and add them to the queue until a left parenthesis is foundelif token ==")":
while stack and stack[-1] !="(":
queue.append(stack.pop())
stack.pop()
# If the token is an operator, pop all the operators from the stack with higher precedence and add them to the queue# Then, add the operator to the stackelse:
while stack and precedence[stack[-1]] >= precedence[token]:
queue.append(stack.pop())
stack.append(token)
# Add the remaining operators to the queuewhile stack:
queue.append(stack.pop())
# Return the postfix expressionreturn" ".join(queue)
# Test the functionprint("3*(((12-3)/3)-1) transformed into", infix_to_postfix("3*(((12-3)/3)-1)"))
print("1+(2*3) transformed into", infix_to_postfix("1+(2*3)"))
print("(1+2)*3 transformed into", infix_to_postfix("(1+2)*3"))
print("3*(2+1) transformed into", infix_to_postfix("3*(2+1)"))
/**
* To run this code, you need to have Java installed on your computer, then:
* - Create a file named `Main.java` in a directory of your choice.
* - Copy this code in the file.
* - Open a terminal in the directory where the file is located.
* - Run the command `javac Main.java` to compile the code.
* - Run the command `java -ea Main` to execute the compiled code.
* Note: '-ea' is an option to enable assertions in Java.
*/// Needed importsimport java.util.Stack;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
publicclassMain {
/**
* This function uses a stack and a queue to transform an infix expression to a postfix expression.
* All expressions are supposed to be well parenthesized.
*
* @param infixExpression Expression to be transformed to postfix.
* @return Postfix expression equivalent to the input infix expression.
*/publicstatic String infixToPostfix(String infixExpression) {
// Define the precedence of the operatorsvar precedence =new java.util.HashMap<String, Integer>();
precedence.put("*", 2);
precedence.put("/", 2);
precedence.put("+", 1);
precedence.put("-", 1);
precedence.put("(", 0);
precedence.put(")", 0);
// Initialize the stack and the queue Stack<String> stack =new Stack<>();
List<String> queue =new ArrayList<>();
// Split the infix expression into tokens Pattern pattern = Pattern.compile("(\\+|\\-|\\*|\\/|\\(|\\))");
Matcher matcher = pattern.matcher(infixExpression);
List<String> tokens =new ArrayList<>();
int lastIndex = 0;
while (matcher.find()) {
if (matcher.start() > lastIndex) {
tokens.add(infixExpression.substring(lastIndex, matcher.start()).trim());
}
tokens.add(matcher.group());
lastIndex = matcher.end();
}
if (lastIndex < infixExpression.length()) {
tokens.add(infixExpression.substring(lastIndex).trim());
}
// Iterate over tokensfor (String token : tokens) {
// If the token is an operand, add it to the queueif (token.matches("\\d+")) {
queue.add(token);
}
// If the token is a left parenthesis, add it to the stackelseif (token.equals("(")) {
stack.push(token);
}
// If the token is a right parenthesis, pop all the operators from the stack and add them to the queue until a left parenthesis is foundelseif (token.equals(")")) {
while (!stack.isEmpty() &&!stack.peek().equals("(")) {
queue.add(stack.pop());
}
stack.pop();
}
// If the token is an operator, pop all the operators from the stack with higher precedence and add them to the queue// Then, add the operator to the stackelse {
while (!stack.isEmpty() && precedence.get(stack.peek()) >= precedence.get(token)) {
queue.add(stack.pop());
}
stack.push(token);
}
}
// Add the remaining operators to the queuewhile (!stack.isEmpty()) {
queue.add(stack.pop());
}
// Return the postfix expressionreturn String.join(" ", queue);
}
/**
* This is the entry point of your program.
* It contains the first codes that are going to be executed.
*
* @param args Command line arguments received.
*/publicstaticvoidmain(String[] args) {
// Test the function System.out.println("3*(((12-3)/3)-1) transformed into "+ infixToPostfix("3*(((12-3)/3)-1)"));
System.out.println("1+(2*3) transformed into "+ infixToPostfix("1+(2*3)"));
System.out.println("(1+2)*3 transformed into "+ infixToPostfix("(1+2)*3"));
System.out.println("3*(2+1) transformed into "+ infixToPostfix("3*(2+1)"));
}
}
9 — Compute the Graham convex hull
The Graham scan algorithm is a computational geometry method used to find the convex hull of a set of points in a 2D plane, with complexity $O(n \cdot log(n))$.
The convex hull is the smallest convex polygon that can enclose all the given points.
It uses a stack to detect and remove concavities in the boundary efficiently.
Write the graham algorithm which takes:
As input – A set of n points in the plane, each represented by its coordinates (abscissa and ordinate).
As output – A list of points that are the vertices of the polygon surrounding the convex envelope, in the order in which they appear on this polygon.
You must follow the steps of the Graham algorithm:
Find the point with the lowest y-coordinate.
If there are multiple points with the same lowest y-coordinate, choose the one with the lowest x-coordinate.
This point is called the pivot point.
Sort the set of points in increasing order of the angle they and the pivot point make with the x-axis.
Any general-purpose sorting algorithm is appropriate for this.
Iterate through the sorted list of points to construct the convex hull:
We start with the pivot.
Then the sorted points are examined.
For each point, we first determine whether travelling from the two points immediately preceding this point is a left turn (counter-clockwise orientation) or a right turn (clockwise orientation).
In the case of a right turn, this means that the point that ended the polygon being created (the point in red) is to the left of the segment formed by the penultimate point of the polygon being created and the point under consideration (the point in black).
In this case, the last point (the one in red) is not part of the convex envelope and must therefore be rejected.
These rejections are repeated as long as there is a “right turn”.
If the algorithm encounters a “left turn”, it moves on to the next point in the sorted array.
Determining whether three points constitute a “left turn” or a “right turn” does not require computing the actual angle between the two line segments, and can be achieved with simple arithmetic only.
For three points $(x_1, y_1)$, $(x_2, y_2)$ and $(x_3, y_3)$, simply calculate the direction of the vector product of the two vectors $[(x_1, y_1), (x_2, y_2)]$ and $[(x_1, y_1), (x_3, y_3)]$, given by the sign of the expression $(x_2 - x_1)(y_3 - y_1) - (y_2 - y_1)(x_3 - x_1)$:
If the result is zero, the points are aligned.
If it is positive, the three points form a “left turn” or counter-clockwise orientation.
If the result is negative, it is a “right turn” or clockwise orientation.
Correction
# Needed importsimport math
import matplotlib.pyplot as plt
defmin_y (points: list[tuple[int, int]]) -> tuple[int, int]:
"""
Returns the points having the smallest y coordinate.
In case of tie, the point with the smallest x coordinate is returned.
In:
* points: List of points.
Out:
* The point with the smallest y coordinate.
"""# Check all the points min_idx =0for i in range(1, len(points)):
if points[i][1] < points[min_idx][1]:
min_idx = i
elif points[i][1] == points[min_idx][1] and points[i][0] < points[min_idx][0]:
min_idx = i
# Return the found onereturn points[min_idx]
defright_turn (p1: tuple[int, int], p2: tuple[int, int], p3: tuple[int, int]) ->None:
"""
Determines if the points p1, p2, p3 make a right turn.
In:
* p1: Point 1.
* p2: Point 2.
* p3: Point 3.
Out:
* None.
"""# Check value of Euclidean distance d = (p2[0] - p1[0]) * (p3[1] - p1[1]) - (p2[1] - p1[1]) * (p3[0] - p1[0])
return d <0defgraham_scan (points: list[tuple[int, int]]) -> list[tuple[int, int]]:
"""
Determines the convex hull of a set of points using the Graham scan algorithm.
In:
* points: List of points.
Out:
* Points forming the convex hull.
"""# Get the pivot pivot = min_y(points)
# Sort the points by the angle they make with the pivot and the x-axis sorted_points = sorted(points, key=lambda x: math.atan2(x[1] - pivot[1], x[0] - pivot[0]))
hull = []
for point in sorted_points:
# Remove the point from the stack if one finds a right turnwhile len(hull) >1and right_turn(hull[-2], hull[-1], point):
hull.pop()
# Add the point to the stack and move to the next one hull.append(point)
# Return the hullreturn hull
# Make some testsif __name__ =="__main__":
# Test on a few poins points = [(0, 0), (1, 1), (1.5, 2.5), (3.5, 1), (2, 2), (3, 0), (3.5, 2), (0, 3), (3, 3), (2, 3.5)]
hull = graham_scan(points)
print("Convex hull:", hull)
# Plot the hull plt.plot([x for x, y in points], [y for x, y in points], 'ro')
plt.plot([x for x, y in hull] + [hull[0][0]], [y for x, y in hull] + [hull[0][1]], 'bo-')
plt.show()
import java.util.*;
publicclassConvexHull {
/**
* This function calculates the Euclidean distance between two points.
* @param p1 point 1 defined by its two coordinates - array of 2 integers
* @param p2 point 2 defined by its two coordinates - array of 2 integers
* @return the Euclidean distance.
*/publicstaticdoubleeuclideanDistance(int[] p1, int[] p2) {
return Math.pow(p2[0]- p1[0], 2) + Math.pow(p2[1]- p1[1], 2);
}
/**
* This function calculates the direction of the vector product of the two vectors defined by the points p, q and p, r
* @param p point 1 defined by its two coordinates - array of 2 integers
* @param q point 2 defined by its two coordinates - array of 2 integers
* @param r point 3 defined by its two coordinates - array of 2 integers
* @return true if the vector product is negative, false otherwise
*/publicstaticbooleanrightTurn(int[] p, int[] q, int[] r) {
int direction = (q[0]- p[0]) * (r[1]- p[1]) - (q[1]- p[1]) * (r[0]- p[0]);
return direction < 0;
}
/**
* This function implements the Graham algorithm to compute the convex hull of point in a 2D plane.
* @param points list of points in the plane, each represented by its coordinates (abscissa, ordinate) - a tuple with two integer elements.
* @return list of integer arrays representing the points forming the convex envelope.
*/publicstatic List<int[]>grahamScan(List<int[]> points) {
// Find the point with the lowest y-coordinate, if there are multiple points with the same y-coordinate, pick the one with the smallest x-coordinateint[] minY = Collections.min(points, Comparator.comparingInt((int[] p) -> p[1]).thenComparingInt(p -> p[0]));
// Sort the set of points in increasing order of the angle they and the pivot point make with the x-axis. points.sort((p1, p2) -> {
double angle1 = Math.atan2(p1[1]- minY[1], p1[0]- minY[0]);
double angle2 = Math.atan2(p2[1]- minY[1], p2[0]- minY[0]);
if (angle1 != angle2) {
return Double.compare(angle1, angle2);
} else {
return Double.compare(euclideanDistance(minY, p1), euclideanDistance(minY, p2));
}
});
// Create the convexe hull Stack<int[]> hull =new Stack<>();
for (int[] point : points) {
while (hull.size() > 1 && rightTurn(hull.get(hull.size() - 2), hull.peek(), point)) {
hull.pop(); // remove the point from the stack }
hull.push(point); // add the point to the stack and move to the next one }
returnnew ArrayList<>(hull);
}
publicstaticvoidmain(String[] args) {
// Example List<int[]> points = Arrays.asList(
newint[]{0, 0}, newint[]{1, 1}, newint[]{1, 2}, newint[]{2, 2},
newint[]{3, 0}, newint[]{3, 2}, newint[]{0, 3}, newint[]{3, 3},
newint[]{2, 4}
);
List<int[]> hull = grahamScan(points);
System.out.println("Convex hull:");
for (int[] point : hull) {
System.out.println(Arrays.toString(point));
}
}
}
To go beyond
10 — Use pointers in a language where they are explicit
Try the implementation of a list of integers in C following the tutorial available at the following url.
You may need an introductory tutorial on programming with C though :)