Uploaded by Swastik Pradhan

Data Structures Assignment Cover Sheet & Design Spec

advertisement
INTERNATIONAL SCHOOL OF
MANAGEMENT AND TECHNOLOGY
FACULTY OF COMPUTING
ASSIGNMENT COVER SHEET
This form is to be completed by students submitting assignments of level 4 and level 5.
Students are required to complete all sections and attach to your assignment.
STUDENT DETAILS
STUDENT NAME
Aayush Lamsal
STUDENT ID
UNIT AND ASSIGNMENT DETAILS
UNIT TITLE
Data Structures and Algorithms
UNIT NUMBER
D/615/1649
ASSIGNMENT
TITLE
Census Data Management System
ISSUE DATE
28/07/2023
ASSESSOR
NAME
Utsav Chaudary
ESTIMATED
WORD LENGTH
7,500
DUE DATE
16/09/2023
SUBMISSION
HAND IN DATE
DECLERATION AND ACKNOWLEDGEMENT
When submitting assignments, each student must sign a declaration confirming
that the work is their own.
Plagiarism and Collusion
Plagiarism: to use or pass off as one’s own, the writings or ideas of another
without acknowledging or crediting the source from which the ideas are taken.
Collusion: submitting an assignment, project or report completed by another
person and passing it off as one’s.
In accordance with the Academic Integrity and Plagiarism Policy:
1. I declare that:
a) this assignment is entirely my own work, except where I have included fullydocumented references to the work of others,
b) the material contained in this assignment has not previously been submitted
for any other subject at the University or any other educational institution,
except as otherwise permitted,
c) no part of this assignment or product has been submitted by me in another
(previous or current) assessment, except where appropriately referenced, and
with prior permission from the Lecturer / Tutor / Unit Coordinator for this unit.
2. I acknowledge that:
a) if required to do so, I will provide an electronic copy of this assignment to the
assessor;
b) the assessor of this assignment may, for the purpose of assessing this
assignment:
I.
reproduce this assignment and provide a copy to another member of
academic staff;
II.
communicate a copy of this assignment to a plagiarism checking
service such as Plagiarism Check (which may then retain a copy of this
assignment on its database for the purpose of future plagiarism
checking).
I am aware of and understand that any breaches to the Academic Code of Conduct will
be investigated and sanctioned in accordance with the College Policy.
SIGNATURE
DATE
16/09/2023
Table of Contents
Design Specification for Data Structures .....................................................................................7
Introduction.............................................................................................................................7
Characteristics of Data Structures ............................................................................................7
Valid Operations on Data Structures ........................................................................................8
Tasks of a Memory Stack .......................................................................................................... 10
Push ...................................................................................................................................... 11
Pop........................................................................................................................................ 11
Look (or Top)........................................................................................................................ 11
isEmpty ................................................................................................................................. 12
isFull ..................................................................................................................................... 12
Compare the performance of two sorting algorithms ................................................................. 14
Advantages of encapsulation and information hiding when using an ADT................................. 16
Quicksort .............................................................................................................................. 16
Bubblesort ............................................................................................................................. 17
Practical Considerations ........................................................................................................ 18
Execution Examination ......................................................................................................... 18
Two network shortest path algorithms ....................................................................................... 19
Dijkstra's Algorithm .............................................................................................................. 19
Bellman-Ford Algorithm ....................................................................................................... 21
Summary............................................................................................................................... 22
The abstract data type for a software stack ................................................................................ 23
Advantages of encapsulation and information hiding when using an ADT................................. 24
Imperative ADTs are a basis for object orientation .................................................................... 27
Arguments in Favor ............................................................................................................... 27
Complex ADT and algorithm in an executable programming language ..................................... 28
Error handling and report test results ......................................................................................... 31
Implementation of an ADT/algorithm solves a well-defined problem ........................................ 35
Complexity of an implemented ADT/algorithm. ........................................................................ 39
Time Complexity .................................................................................................................. 40
Space complexity .................................................................................................................. 40
Assessment ........................................................................................................................... 40
How asymptotic analysis can be used to assess the effectiveness of an algorithm ...................... 41
Two ways in which the efficiency of an algorithm can be measured .......................................... 43
What a trade-off is when specifying an ADT ............................................................................. 45
Benefits of using implementation independent data structures ................................................... 49
Modularity and Abstraction ................................................................................................... 49
Code Reusability and Interoperability.................................................................................... 49
Algorithm Adaptability and Support ...................................................................................... 50
References ................................................................................................................................ 51
Design Specification for Data Structures
Introduction
Data structures are major parts in software engineering and programming. They are utilized to
productively store and sort out information. Data structure valid operations and common
characteristics are outlined in this design specification.
Characteristics of Data Structures
Information designs can differ in intricacy and use cases, however they by and large show a few
normal qualities:

Data Organization: Data structures are utilized to coordinate and store information in a
particular configuration or plan that suits the application's prerequisites.

Efficiency: They are intended to perform tasks effectively, frequently with an emphasis on
limiting time intricacy and memory utilization.

Information Access: Mechanisms for accessing, inserting, updating, and deleting data
elements are provided by data structures.

Information Type: Information designs might be intended to hold homogeneous
information (components of a similar kind) or heterogeneous information (components of
various sorts).

Size: A few information structures have a proper size, while others can powerfully resize
depending on the situation.

Order: Information designs might keep the control of components (e.g., saving the request
for inclusion) or not (e.g., unordered sets).

Complexity: Various information structures are fit to various errands; some focus on
recovery speed (e.g., hash tables), while others focus on insertion and deletion (e.g., Linked
List).
Valid Operations on Data Structures
The legitimate activities that can be completed on information structures rely upon the sort of
information structure. Here are a few normal tasks:
1. Description of Insertion: Adds another information component to the information structure.
Parameters: The component to be embedded.
Preconditions: The information design might have requirements like a most extreme size or
uniqueness.
Postconditions: The information structure contains the new component.
2. Deletion
Portrayal: Deletes a data element from the structure of the data.
Parameters: The component to be eliminated (now and again, the position).
Preconditions: The data structure must contain the element.
Postconditions: The deleted element is no longer present in the data structure.
3. Search
Decides if a specific component exists in the information structure.
Parameters: the thing to look for.
Preconditions: None.
Postconditions: if the element is found, returns true; in any case, returns misleading or a unique
worth (e.g., - 1).
4. Access:
Recovers a particular component from the information structure.
Parameters: The position or identifier of the component.
Preconditions: The information structure should contain the predetermined component or position.
Postconditions: returns the element requested.
5. Update
Depiction: Alters a current information component.
Parameters: the new value that needs to be added to the element.
Preconditions: The data structure must contain the element.
Postconditions: The information component is adjusted with the new worth.
6. Description of the Traversal
Emphasizes through all components of the information structure.
Parameters: None.
Preconditions: None.
Postconditions: All components are visited in a predetermined request.
7. Size/Length
Depiction: Decides the quantity of components in the information structure.
Parameters: None.
Preconditions: None.
Postconditions: Returns the include of components in the information structure.
8. Empty/Clear Description
Leaves the data structure empty after removing all of its components.
Parameters: None.
Preconditions: None.
Postconditions: The information structure contains no components.
These tasks give the establishment to working with information structures in different
programming settings. Additional operations that are supported by particular data structures may
behave differently depending on the purpose and implementation of the data structure.
Understanding these tasks is fundamental for choosing the suitable information structure for a
given issue and for productively overseeing information in programming applications.
Tasks of a Memory Stack
A memory stack, frequently alluded to just as a "stack," is an information structure with two
essential tasks: push and pop, following the Rearward In, First-Out (LIFO) guideline. Here are the
tasks of a memory stack:
Push
Description: Adds a component to the highest point of the stack.
Parameters: The component to be pushed onto the stack.
Operation:

Increase the stack pointer to account for the new component.

Store the component at the memory area highlighted by the refreshed stack pointer.

Update the stack pointer to highlight the recently added component.

Preconditions: The stack might have a size limit (stack flood whenever surpassed).
Pop
Description: Eliminates and returns the component from the highest point of the stack.
Parameters: None.
Operation:

Recover the component from the memory area highlighted by the stack pointer.

Decrement the stack pointer to eliminate the top component.

Preconditions: If the stack is empty, it will overflow (stack underflow).

Postconditions: The component is eliminated from the stack, and the stack pointer is
changed.
Look (or Top)
Description: Returns the component at the highest point of the stack without eliminating it.
Parameters: None.
Operation:

Recover the component from the memory area highlighted by the stack pointer.

Preconditions: The stack should not be vacant.

Postconditions: The stack stays unaltered, and the top component is returned.
isEmpty
Description: examines whether the stack is empty.
Parameters: None.
Operation:

Compare the stack pointer to see if it indicates an empty stack by pointing to the bottom of
the stack.

Preconditions: None.

Postconditions: Returns valid assuming the stack is vacant; in any case, gets back bogus.
isFull
Description: Checks assuming the stack is full.
Parameters: None.
Operation:

Contrast the stack pointer with the greatest admissible stack size (if material).

Preconditions: None.

Postconditions: Returns valid on the off chance that the stack is full; in any case, returns
bogus (assuming there's no greatest size, this activity may constantly get back misleading).

Utilizing a Memory Stack to Carry out Capability Brings in a PC
Memory stacks assume a basic part in overseeing capability calls and their related information in
a PC's runtime climate. This is the way a memory stack is utilized for capability call the board:
Function call:
A new stack frame, also known as an activation record, is created on the stack whenever a function
is called Local variables specific to the function.
Parameters of the function
a return address that specifies where the program should continue execution following the
completion of the function call.
Nested Function Calls:
On the off chance that a capability calls another capability, another stack outline is pushed on top
of the past one. This makes a heap of stack outlines, taking into consideration settled capability
calls. Each stack outline is related with a particular capability call and holds the important data for
that call.
Function Execution:
Within its stack frame, the function executes and accesses its local variables and parameters. At
the point when the capability returns, control stream gets back to the return address put away in
the ongoing stack outline.
Function Return:
At the point when a capability finishes its execution, its stack outline is popped from the stack.
With the appropriate context and data, this enables the program to continue from the point at which
the function was initially called.
Return to Main Function:
The principal capability's return denotes the finish of program execution, and the stack becomes
unfilled. The memory stack guarantees that capability calls are overseen in an organized and
productive way. Each stack outline contains the data vital for a capability to execute and return
effectively, and the LIFO idea of the stack guarantees that control gets back to the right point in
the program as capabilities complete their execution. For managing program flow and memory
allocation in modern computer systems, this stack-based approach is essential.
Compare the performance of two sorting algorithms
A First-In-First-Out (FIFO) queue is a linear data structure that follows the principle of processing
elements in the order they were added to the queue. It can be visualized as a line of people waiting
for a service, where the person who arrived first is served first.
A list-based Python data structure for a FIFO queue is shown in the following example:
class Queue:
def __init__(self):
self.items = []
def is_empty(self):
return len(self.items) == 0
def enqueue(self, item):
self.items.append(item)
def dequeue(self):
if not self.is_empty():
return self.items.pop(0)
else:
return None
def size(self):
return len(self.items)
Clarification of the code:

The Line class is characterized to address a FIFO line.

The __init__ technique instates a empty list, self.items, which will store the components
of the line.

The is_empty strategy checks in the event that the line is vacant by analyzing the length of
the self.items list.

The enqueue method adds an item to the end of the list, mimicking the back of the queue.

The dequeue technique eliminates and returns the principal thing from the rundown, which
reproduces eliminating the front component from the line. It utilizes the pop(0) technique
to guarantee that the primary component is dequeued.

The queue's current size, or number of elements, is returned by the size method.
my_queue = Queue()
my_queue.enqueue(1)
my_queue.enqueue(2)
my_queue.enqueue(3)
print("Queue size:", my_queue.size())
while not my_queue.is_empty():
item = my_queue.dequeue()
print("Dequeued:", item)
print("Queue size:", my_queue.size())
The elements in this example are enqueued in the same order (FIFO) and dequeued in the same
order. The FIFO line keeps the control of components as they are added and guarantees they are
handled in an earliest in, earliest out way.
Advantages of encapsulation and information hiding when using an
ADT
To think about the exhibition of two sorting algorithm, we'll examine and differentiate the time
complexity, space complexity, and practical considerations of two normal sorting algorithm:
Quicksort and Bubblesort.
Quicksort
Complexity in Time:
-
Best Case: O(n log n)
-
Normal Case: O(n log n)
-
Worst Case: O(n^2)
Space Complexity:
-
Best Case: O(log n)
-
Normal Case: O(log n)
-
Worst Case: O(n)
Reasonable Considerations:
-
Quicksort is frequently quicker than Bubblesort by and by, particularly for enormous
datasets.
-
It is broadly utilized in different programming dialects and libraries.
-
Quicksort is an illustration of a "divide and conquer" sorting algorithm.
-
It tends to be enhanced with methods like picking a good pivot element or changing to an
alternate sorting algorithm (e.g., Insertion Sort) for little subarrays.
Bubblesort
Time Complexity:
-
Best Case: O(n)
-
Normal Case: O(n^2)
-
Worst Case: O(n^2)
Space Complexity:
-
Best Case: O(1)
-
Normal Case: O(1)
-
Worst Case: O(1)
Practical Considerations
-
Bubblesort is for the most part not appropriate for huge datasets or execution basic
applications.
-
It is a straightforward correlation based sorting algorithm that more than once swaps
adjacent elements if they are in wrong order
-
Bubblesort has unfortunate time complexity for bigger datasets, making it wasteful for
practical use.
-
It is principally utilized for instructive purposes to show essential sorting ideas.
Execution Examination
To think about the presentation of Quicksort and Bubblesort, think about the accompanying:
1. Time Complexity: Quicksort has essentially better normal and best-case time complexity
than Bubblesort. Bubblesort's time complexity makes it unsatisfactory for enormous
datasets, while Quicksort performs well by and large.
2. Space Complexity: Quicksort requires extra space for recursion, making it less memoryproficient than Bubblesort, which works set up (steady space). However, Quicksort's
overhead in terms of space is typically not a major issue.
3. Reasonable Use: Quicksort is broadly utilized by and by because of its productive normal
case execution. Bubblesort, then again, is seldom utilized in genuine applications on
account of its poor time complexity.
4. Stability: Quicksort is definitely not a steady sorting algorithm, meaning it might change
the overall request of equivalent components. Bubblesort can be made stable with
unexpected checks, yet this adds complexity.
5. Behavior Adaptation: Bubblesort can be versatile as in it performs better on somewhat
arranged information. Quicksort's exhibition is by and large less impacted by the
underlying request of the information.
In outline, Quicksort is for the most part a superior decision for arranging huge datasets practically
speaking because of its predominant normal case time complexity and far and wide use.
Bubblesort, while easy to comprehend, is seldom utilized in genuine applications on account of its
horrible showing for non-minor datasets.
Two network shortest path algorithms
Dijkstra's Algorithm
Activity:
Dijkstra's algorithm tracks down the briefest way from a beginning node to any remaining node in
a weighted chart. It updates distances to its neighbors, selects the node with the smallest tentative
distance on a regular basis, and marks the node as visited. It also keeps a set of tentative distances.
Example:
Take a look at an easy weighted graph:
5
3
A ------ B ------ C
|
/|
/
|
/ |
/
| / | /
| /
| /
| /
| /
|/
|/
D ------ E
2
Start from node A.
Initialize tentative distances to all nodes: {A: 0, B: ∞, C: ∞, D: ∞, E: ∞}.
Visit node A, set A as visited.
Update tentative distances to neighbors:
B: 0 + 5 = 5
D: 0 + 2 = 2
Select the node with the smallest tentative distance, which is D.
Visit node D, set D as visited.
Update tentative distances to neighbors:
B: 2 + 3 = 5 (not updated)
E: 2 + 5 = 7
Continue this process until all nodes are visited or the target node (if provided) is reached.
The final shortest distances from A to all other nodes are:
-
A to B: 5
-
A to C: ∞ (not reachable)
-
A to D: 2
-
A to E: 7
Bellman-Ford Algorithm
Even in a graph with negative-weight edges, the Bellman-Ford Algorithm determines the shortest
route from a starting node to all other nodes. By iteratively updating distances until no more
updates are possible, it makes use of relaxation.
Example:
consider about a weighted chart:
2
-3
A ------ B ------ C
|
/|
/
|
/ |
/
| / | /
| /
| /
| /
| /
|/
|/
D ------ E
1
Start from node A.
Initialize distances to all nodes: {A: 0, B: ∞, C: ∞, D: ∞, E: ∞}.
Relax edges repeatedly:
Relax A -> B: 0 + 2 < ∞, update B to 2.
Relax A -> D: 0 + 1 < ∞, update D to 1.
Relax D -> B: 1 + (-3) < 2, update B to -2.
Relax B -> C: (-2) + 3 < ∞, update C to 1.
Relax B -> E: (-2) + 5 < ∞, update E to 3.
No further updates needed.
The final shortest distances from A to all other nodes are:
-
A to B: -2
-
A to C: 1
-
A to D: 1
-
A to E: 3
Summary
Dijkstra's Algo. tracks down the most brief way in charts with non-negative loads.
Bellman-Passage Algo handles diagrams with negative loads yet may take additional time.
The two algorithms track down the most limited way from a beginning hub to any remaining nodes.
Dijkstra's calculation is for the most part quicker for non-negative loads, while Bellman-Passage
can deal with negative loads yet might be more slow.
The algorithm to use is determined by the particular constraints on the graph and weight.
The abstract data type for a software stack
A basic definition for a Abstract Data type (ADT) of a product stack can be indicated as follows:
class Stack:
def __init__(self):
self.items = []
def push(self, item):
self.items.append(item)
def pop(self):
if not self.is_empty():
return self.items.pop()
else:
raise IndexError("Stack is empty")
def peek(self):
if not self.is_empty():
return self.items[-1]
else:
raise IndexError("Stack is empty")
def is_empty(self):
return len(self.items) == 0
def size(self):
return len(self.items)
In this basic definition, we determine the theoretical information type "Stack" with the
accompanying tasks:
-
__init__(self): Instates an unfilled stack.
-
push(self, thing): Adds a thing to the highest point of the stack.
-
pop(self): Eliminates and returns the top thing from the stack.
-
peek(self): Returns the top thing from the stack without eliminating it.
-
is_empty(self): Checks in the event that the stack is vacant and returns a Boolean worth.
-
size(self): the number of items in the stack is returned.
Advantages of encapsulation and information hiding when using an
ADT
Benefits of encapsulation and information hiding away while utilizing an Abstract Data Type
(ADT):
1. Modularity:
- Encapsulation permits you to bundle the information and strategies that work on that information
into a solitary unit (a class). This advances seclusion by holding related usefulness together,
making it more straightforward to oversee and keep up with code.
2. Abstraction:
- Encapsulation conceals the inward subtleties of an ADT, giving a reasonable connection point
(public strategies) for cooperating with it. This reflection works on the utilization of the ADT and
safeguards clients from the intricacy of its execution.
3. Security and Information Integrity:
- Information hiding prevents unauthorized modifications to ADT data by limiting direct access
to its internal data. This forestalls unapproved or accidental changes, upgrading security and
information trustworthiness.
4. Upkeep and Evolution:
An ADT's internal implementation can be altered without affecting the code that uses it thanks
to encapsulation and information hiding. This partition between the connection point and execution
works on support and empowers you to develop the ADT without figuring out existing code.
5. Decreased Complexity:
- By uncovering just fundamental usefulness through a clear cut interface, encapsulation lessens
complexity for clients of the ADT. Clients don't have to comprehend the inward activities or
execution subtleties, making it simpler to utilize and comprehend.
6. Invariant Encapsulation:
- ADTs can uphold invariants (rules or requirements) on their information. Data corruption is
prevented by encapsulation, which ensures that these invariants are preserved and cannot be
violated by outside code.
7. Code Reusability:
- Encapsulated ADTs are simple to repurpose across multiple applications or projects. Since the
point of interaction stays steady, you can utilize the ADT without stressing over its inside
execution.
8. Equal Development:
- Groups can work in lined up on various parts of a program. Developers liable for making an
ADT can give the connection point determination, and other colleagues can fabricate code that
utilizes the ADT in light of that detail. The ADT's encapsulation and clear interface make this
parallel development possible.
9. Debugging and Testing:
- Encapsulation works with testing since you can zero in on testing the ADT's connection point
and guarantee that it meets its particulars. Testing is more sensible on the grounds that you don't
have to think about the complexities of the execution.
10. Compatibility and Versioning:
- ADTs with obvious points of interaction are helpful for forming. It is possible to introduce
new ADT versions while maintaining backward compatibility with older versions of existing code.
This forming is fundamental for long haul programming upkeep.
In conclusion, encapsulation and information hiding are major standards in object-oriented
programming that upgrade measured quality, security, viability, and ease of use of ADTs. They
give clear points of interaction, safeguard information respectability, and advance code reusability,
making it more straightforward to assemble and keep up with complex programming frameworks.
Imperative ADTs are a basis for object orientation
In the fields of computer science and software engineering, there is some debate over whether
imperative Abstract Data Types (ADTs) serve as the foundation for object orientation. How one
defines the connection between ADTs and object-oriented programming (OOP) determines
whether they agree with this viewpoint.
Arguments in Favor
Historical Perspective: Object-oriented programming can be seen as a precursor to imperative
ADTs. In the beginning phases of programming advancement, ADTs, like stacks, lines, and linked
lists, were utilized to encapsulate information and procedure on that information. This
encapsulation is a principal idea in OOP.
Encapsulation: ADTs intrinsically include encapsulation, which is one of the center standards of
OOP. Encapsulation permits information and techniques to be packaged together, and it advances
information stowing away and particularity, which are fundamental OOP ideas.
Abstraction: ADTs give reflection by uncovering an unmistakable and conceptual point of
interaction while concealing execution subtleties. This is in line with the idea of abstraction in
OOP, which lets people interact with objects without having to know how they work inside.
Reusability: Basic ADTs can be reused in various projects, very much like articles in OOP can be
reused in different settings. The idea of making reusable parts lines up with the objectives of OOP.
In summary, I agree with the blessing focuses that recommend basic ADTs can act as a reason for
object orientation somewhat. They are historically and conceptually connected to OOP because
they share fundamental concepts like encapsulation, abstraction, modularity, and reusability with
OOP. However, it is essential to acknowledge that OOP distinguishes itself as a distinct paradigm
by introducing additional concepts like inheritance and polymorphism that go beyond ADTs.
Complex ADT and algorithm in an executable programming
language
We'll create a "Graph" ADT and implement Dijkstra's Algorithm to find the shortest path between
nodes in a weighted graph.
import heap
class Graph:
def __init__(self):
self.nodes = set()
self.edges = {}
def add_node(self, value):
self.nodes.add(value)
self.edges[value] = []
def add_edge(self, from_node, to_node, weight):
self.edges[from_node].append((to_node, weight))
self.edges[to_node].append((from_node, weight))
def dijkstra(self, start_node):
distances = {node: float('inf') for node in self.nodes}
predecessors = {node: None for node in self.nodes}
distances[start_node] = 0
priority_queue = [(0, start_node)]
while priority_queue:
current_distance, current_node = heapq.heappop(priority_queue)
if current_distance > distances[current_node]:
continue
for neighbor, weight in self.edges[current_node]:
distance = current_distance + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
predecessors[neighbor] = current_node
heapq.heappush(priority_queue, (distance, neighbor))
return distances, predecessors
graph = Graph()
graph.add_node("A")
graph.add_node("B")
graph.add_node("C")
graph.add_node("D")
graph.add_node("E")
graph.add_edge("A", "B", 5)
graph.add_edge("A", "D", 2)
graph.add_edge("B", "C", 3)
graph.add_edge("B", "E", 3)
graph.add_edge("D", "E", 5)
start_node = "A"
distances, predecessors = graph.dijkstra(start_node)
for node, distance in distances.items():
print(f"Shortest distance from {start_node} to {node}: {distance}")
target_node = "C"
path = [target_node]
while predecessors[target_node] is not None:
target_node = predecessors[target_node]
path.insert(0, target_node)
print(f"Shortest path from {start_node} to C: {' -> '.join(path)}")
This Python code uses Dijkstra's Algorithm to find the shortest paths in a weighted graph and
defines an abstract data type (ADT) for graphs. It makes a diagram with nodes "A," "B," "C," "D,"
and "E" and adds weighted edges between them. The Dijkstra Algorithm registers and prints the
briefest good ways from a predefined start node ("A") to any remaining node in the diagram and
finds and prints the most limited way from the outset node to an objective node ("C"). The code
shows how to solve a well-defined problem about finding the shortest paths in a graph using the
Graph ADT and Dijkstra's Algorithm.
Error handling and report test results
Error handling is an essential part of any software implementation. Let's add error handling to the
previous code and report the test results. We will handle potential errors such as invalid node
names, missing nodes, and invalid input values.
import heapq
class Graph:
def __init__(self):
self.nodes = set()
self.edges = {}
def add_node(self, value):
self.nodes.add(value)
self.edges[value] = []
def add_edge(self, from_node, to_node, weight):
if from_node not in self.nodes or to_node not in self.nodes:
raise ValueError("Invalid node name")
if weight < 0:
raise ValueError("Invalid edge weight (must be non-negative)")
self.edges[from_node].append((to_node, weight))
self.edges[to_node].append((from_node, weight))
def dijkstra(self, start_node):
if start_node not in self.nodes:
raise ValueError("Start node not found in the graph")
distances = {node: float('inf') for node in self.nodes}
predecessors = {node: None for node in self.nodes}
distances[start_node] = 0
priority_queue = [(0, start_node)]
while priority_queue:
current_distance, current_node = heapq.heappop(priority_queue)
if current_distance > distances[current_node]:
continue
for neighbor, weight in self.edges[current_node]:
distance = current_distance + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
predecessors[neighbor] = current_node
heapq.heappush(priority_queue, (distance, neighbor))
return distances, predecessors
try:
graph = Graph()
graph.add_node("A")
graph.add_node("B")
graph.add_node("C")
graph.add_node("D")
graph.add_node("E")
graph.add_edge("A", "B", 5)
graph.add_edge("A", "D", 2)
graph.add_edge("B", "C", 3)
graph.add_edge("B", "E", 3)
graph.add_edge("D", "E", 5)
start_node = "A"
distances, predecessors = graph.dijkstra(start_node)
or node, distance in distances.items():
print(f"Shortest distance from {start_node} to {node}: {distance}")
target_node = "C"
path = [target_node]
while predecessors[target_node] is not None:
target_node = predecessors[target_node]
path.insert(0, target_node)
print(f"Shortest path from {start_node} to C: {' -> '.join(path)}")
except ValueError as e:
print(f"Error: {e}")
In this code:
We've added mistake taking care of utilizing attempt and with the exception of blocks to get likely
blunders, for example, invalid node names, missing hubs, and invalid edge loads.
Assuming that any mistake happens during the execution of the code, it will be gotten, and a
blunder message will be printed.
If there are no errors, the test results, including the shortest paths and distances, are reported.
When an error occurs, this code ensures that the program gracefully handles it and displays helpful
error messages.
Implementation of an ADT/algorithm solves a well-defined problem
In this exhibition, I have show how the execution of the Diagram ADT and Dijkstra's Calculation
takes care of the obvious issue of tracking down the most limited way between hubs in a weighted
chart.
Problem Statement: Given a chart with node and weighted edges, track down the briefest way from
a predefined start node to any remaining nodes and decide the most limited way to a particular
objective node.
import heapq
class Graph:
def __init__(self):
self.nodes = set()
self.edges = {}
def add_node(self, value):
self.nodes.add(value)
self.edges[value] = []
def add_edge(self, from_node, to_node, weight):
if from_node not in self.nodes or to_node not in self.nodes:
raise ValueError("Invalid node name")
if weight < 0:
raise ValueError("Invalid edge weight (must be non-negative)")
self.edges[from_node].append((to_node, weight))
self.edges[to_node].append((from_node, weight))
def dijkstra(self, start_node):
if start_node not in self.nodes:
raise ValueError("Start node not found in the graph")
distances = {node: float('inf') for node in self.nodes}
predecessors = {node: None for node in self.nodes}
distances[start_node] = 0
priority_queue = [(0, start_node)]
while priority_queue:
current_distance, current_node = heapq.heappop(priority_queue)
if current_distance > distances[current_node]:
continue
for neighbor, weight in self.edges[current_node]:
distance = current_distance + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
predecessors[neighbor] = current_node
heapq.heappush(priority_queue, (distance, neighbor))
return distances, predecessors
try:
graph = Graph()
graph.add_node("A")
graph.add_node("B")
graph.add_node("C")
graph.add_node("D")
graph.add_node("E")
graph.add_edge("A", "B", 5)
graph.add_edge("A", "D", 2)
graph.add_edge("B", "C", 3)
graph.add_edge("B", "E", 3)
graph.add_edge("D", "E", 5)
start_node = "A"
distances, predecessors = graph.dijkstra(start_node)
for node, distance in distances.items():
print(f"Shortest distance from {start_node} to {node}: {distance}")
target_node = "C"
path = [target_node]
while predecessors[target_node] is not None:
target_node = predecessors[target_node]
path.insert(0, target_node)
print(f"Shortest path from {start_node} to C: {' -> '.join(path)}")
except ValueError as e:
print(f"Error: {e}")
Demonstration:
We characterize a chart with nodes "A," "B," "C," "D," and "E" and add weighted edges to address
associations between nodes.
We utilize Dijkstra's Algorithm to track down the most brief good ways from the beginning node
"A" to any remaining nodes in the chart.
The program executes and prints the shortest distances and the shortest path from node A to node
C as we search for the shortest route to a specific target node C.
The program shows how the execution of the Chart ADT and Dijkstra's Calculation really takes
care of the issue of tracking down the most brief way in a weighted diagram. It provides the shortest
paths and distances from the specified start node to all other nodes, making it possible to plan
routes or analyze networks effectively in a variety of real-world scenarios.
Complexity of an implemented ADT/algorithm.
To fundamentally assess the complexity of the carried out Dijkstra's Algorithm , I have dissect
now is the right time and space complexities. Dijkstra's Algorithm is a notable chart crossing and
way finding calculation used to track down the briefest ways from a solitary source node to any
remaining node in a weighted diagram. We should separate its complexities:
Time Complexity
Initialization: In the initialization step, we set the underlying distances to all nodes to
limitlessness, which takes O(V) time, where V is the quantity of node in the chart.
Priority Queue Operations: The primary Loop of the algorithm includes procedure on priority
queue. In every iteration, we remove the node with the littlest distance, which takes O(log V) time.
Since we play out this activity for all nodes, the complete time spent on need line tasks is O(V log
V).
Edge Unwinding: In every cycle, we think about all edges from the ongoing node to its neighbors
and update the distances on the off chance that we track down a more limited way. The all out
number of edge unwinding tasks is O(E), where E is the quantity of edges in the diagram.
The general time complexity of Dijkstra's Algorithm is O(V log V + E), where the ruling
component is often priority queue operations. In the worst case, when using binary heap to store
as the priority queue, the time complexity can be improved to O((V + E) log V).
Space complexity
Data Structures: The algorithm utilizes data structures to store distances (distances word
reference), predecessors (predecessors word reference), and the priority queue. The space
complexity for these data structures is O(V + V + V) = O(V).
Adjacent List: The adjacency list representation of the diagram requires space for putting away
all edges, which is O(E).
The general space complexity of Dijkstra's algorithm is O(V + E).
Assessment
Time complexity: Dijkstra's algorithm has a period complexity that relies upon the quantity of
nodes (V) and edges (E) in the diagram. In space diagrams (barely any edges), the algorithm
performs well, however in thick charts (many edges), it can turn out to be less proficient. Alternate
algorithms like A* search can be more productive for specific situations.
Space complexity: The space complexity of Dijkstra's algo essentially relies upon the quantity of
node and edges in the chart. It utilizes extra data structures to store distances and predecessors,
which can expand the space prerequisites, particularly for enormous diagrams.
Optimizations: There are different enhancements and varieties of Dijkstra's Algo, like utilizing
Fibonacci heaps or bidirectional search, to work on now is the ideal time and space complexity.
The selection of data structures and algorithm varieties can essentially affect its exhibition.
In conclusion, Dijkstra's algorithm is an amazing asset for tracking down briefest ways in weighted
charts. Its complexity examination shows that it's proficient for the vast majority useful situations,
however its presentation can differ in light of the attributes of the diagram and the particular
execution subtleties. Careful consideration of the chart's size and structure is fundamental while
picking the suitable calculation for a given issue.
How asymptotic analysis can be used to assess the effectiveness of an
algorithm
Asymptotic examination is a significant device in software engineering and algorithm design for
evaluating the viability and productivity of a algorithm. It lets you look at how an algorithm's
runtime and use of resources change as the input size grows. This is the way asymptotic
examination can be utilized to evaluate the adequacy of a calculation:
Evaluating Algorithm Efficiency
Asymptotic analysis permits us to analyze calculations' productivity and anticipate how they will
perform on bigger sources of info. By focusing on the main conditions in the examination, we can
recognize which calculations are probably going to be more proficient by and by.
Big O Notations:
In asymptotic analysis, the Big O notation is frequently used. It gives an upper bound on the
development pace of a algorithm's runtime or asset use as a component of the info size. O(n) is an
example of linear growth, O(n2) is an example of quadratic growth, and O(log n) is an example of
logarithmic growth.
The decision of calculation with a lower-request term in its Big O notation is for the most part
more effective for huge data sources. For example, O(n) is ordinarily more proficient than O(n^2).
Identifying Dominant Terms:
Asymptotic examination recognizes the predominant terms in a algorithm's runtime or asset use
capability. The predominant term is the one that develops the quickest as the information size
increments. It helps in focusing on the essential element that influences a algorithm's proficiency.
For instance, in a quadratic-time calculation (O(n^2)), the n^2 term rules, demonstrating that the
calculation's proficiency not entirely set in stone by the square of the information size.
Performance Prediction:
Asymptotic examination gives an undeniable level exhibition expectation for various information
sizes. It helps answer questions like, "How might the algorithm act when the information size
copies?" or on the other hand "What happens when the information size becomes multiple times
bigger?"
When dealing with large datasets or time-sensitive applications, this prediction is useful for
making informed algorithm selection decisions.
Algorithm Improvement:
Asymptotic investigation can direct algorithm improvement endeavors. At the point when you
recognize that a calculation's development rate isn't satisfactory for a specific issue size, it shows
the requirement for algorithmic changes or improvements to accomplish improved productivity.
Comparing Algorithms:
The comparison of multiple algorithms for solving the same problem is made easier by asymptotic
analysis. By looking at their Big O Notations, you can figure out which calculation is probably
going to perform better under different circumstances and pick the most fitting one.
Scalability Assessment:
Organizations and associations frequently need to evaluate the versatility of their product
frameworks. Asymptotic analysis predicts how the framework will proceed as the client base or
information volume develops, which is critical for scope quantification and asset allotment.
In summary, asymptotic analysis assumes a basic part in calculation plan and assessment by giving
a normalized method for surveying the proficiency and effectiveness of algorithms. It permits us
to settle on informed conclusions about which calculation to decide for a particular issue, how a
algorithm will proceed as the issue size develops, and whether algorithmic enhancements are
expected to meet execution necessities.
Two ways in which the efficiency of an algorithm can be measured
The effectiveness of a algorithm can be estimated in different ways, yet two normal techniques are
time complexity examination and space complexity analysis. Let's use examples to demonstrate
these two approaches:
1. Time Complexity Analysis:
Time Complexity estimates how the runtime of a algorithm scales with the size of the info. It gives
a gauge of the quantity of essential tasks (like comparisions, assignments, and arithmetic
operations) performed by the algorithm as a component of the information size. Common
Notations utilized for time Complexity examination incorporate Big O (O), Big Theta (Θ), and
Big Omega (Ω).
Example: Think about a basic calculation for tracking down the most extreme component in a
variety of numbers.
def find_max(arr):
max_value = arr[0]
for num in arr:
if num > max_value:
max_value = num
return max_value
Time Complexity Analysis: For this situation, the algorithm repeats through the whole exhibit
once, playing out a consistent number of tasks (comparisions and assignments) for every
component. Subsequently, the time complexity of this algorithm is O(n), where n is the size of the
input array. This indicates that the input size increases linearly with the number of operations.
2. Space Complexity Analysis:
Space complexity estimates how the memory use of a algorithm scales with the size of the input.
It gives an estimate of how much additional memory the algorithm will need depending on the size
of the input, such as for variables, data structures, and function call stacks.
Example: Let's look at a recursive method for finding the nth Fibonacci number's space
complexity.
def fibonacci(n):
if n <= 1:
return n
else:
return fibonacci(n-1) + fibonacci(n-2)
Space Complexity Analysis: A function call stack is used to track intermediate results in this
recursive algorithm. For each recursive call, extra memory is designated for the capability call
stack. The most extreme profundity of the call stack is corresponding to the information esteem n.
Accordingly, the space Complexity of this calculation is O(n), showing that the memory utilization
develops straightly with the information size.
In summary, time Complexity Analysis evaluates the productivity of a algorithm as far as its
runtime execution, while space Complexity Analysis surveys the effectiveness concerning
memory use. These two strategies give important bits of knowledge into how a calculation act as
the information size increments and help in settling on informed conclusions about algorithms
selection and optimization.
What a trade-off is when specifying an ADT
A Trade-off, with regards to determining a Abstract Data Type (ADT) or planning a algorithm,
alludes to a circumstance where working on one part of the ADT or algorithm might bring about
a relating weakness or split the difference in another perspective. It frequently includes simply
deciding and tracking down a harmony between conflicting objectives or necessities. When
optimizing various aspects of a system, computer science and software engineering frequently
involve trade-offs.
Let's interpret the trade-offs in the provided code example of a graph-based algorithm (Dijkstra's
Algorithm) within a Graph ADT:
import heapq
class Graph:
def __init__(self):
self.nodes = set()
self.edges = {}
def add_node(self, value):
self.nodes.add(value)
self.edges[value] = []
def add_edge(self, from_node, to_node, weight):
self.edges[from_node].append((to_node, weight))
self.edges[to_node].append((from_node, weight))
def dijkstra(self, start_node):
distances = {node: float('inf') for node in self.nodes}
predecessors = {node: None for node in self.nodes}
distances[start_node] = 0
priority_queue = [(0, start_node)]
while priority_queue:
current_distance, current_node = heapq.heappop(priority_queue)
if current_distance > distances[current_node]:
continue
for neighbor, weight in self.edges[current_node]:
distance = current_distance + weight
if distance < distances[neighbor]:
distances[neighbor] = distance
predecessors[neighbor] = current_node
heapq.heappush(priority_queue, (distance, neighbor))
return distances, predecessors
Examples of trade-offs in this code:
Time Complexity versus Memory Use:
Trade-off: The Dijkstra's algorithms execution is productive with regards to time Complexity
(O((V + E) log V)), which is appropriate for huge charts. Be that as it may, it utilizes extra memory
to store distances and predecessors for all nodes, prompting expanded memory utilization.
Explanation: To accomplish quicker runtime, the algorithm keeps information designs to follow
separations and predecessors for all nodes. Memory usage goes up as a result, especially for large
graphs.
Performance vs. Readability:
Trade-off: Priority queues and heap data structures are used to improve the performance of the
algorithm, resulting in relatively complex code. This Complexity
might influence code
meaningfulness and maintainence.
Explanation: Utilizing efficient data structures like heapq, the algorithm places performance first.
In any case, this can make the code less natural for somebody perusing or keeping up with it.
Flexibility vs Specialization:
Trade-off: Because the Graph ADT is made to work with weighted graphs, it can be used in a
variety of ways. However, for straightforward unweighted graphs, it might not be the most
effective option.
Explanation: The ADT is intended to deal with an extensive variety of diagram types, yet this oversimplification accompanies some exhibition above while managing unweighted charts.
All in all, Trade-offs are inborn in calculation and ADT plan, and they require cautious thought to
work out some kind of harmony between conflicting factors like time Complexity, memory
utilization, code clarity, and generality. The particular Tradeoffs made rely upon problem domain
and the needs of the framework or application being created.
Benefits of using implementation independent data structures
Implementation-independent data structures, frequently alluded to as Abstract Data types (ADTs),
give a few advantages in programming plan and improvement. The following are three vital
advantages of utilizing execution free information structures:
Modularity and Abstraction
One of the essential advantages of utilizing ADTs is the capacity to digest and encapsulate
information and activities. Without exposing the data's underlying implementation details, ADTs
allow you to define a clear interface for interacting with it.
This reflection advances particularity in programming plan. Modules or parts of a framework can
work with ADTs without having to know how the information is put away or handled inside. This
separation of concerns simplifies code maintenance and allows for more straightforward testing
and debugging.
For instance, think about a stack ADT. Whether or not the stack is executed as a array or a linked
list, the interface (push, pop, peek) continues as before. This abstraction empowers various pieces
of a program to cooperate with the stack without stressing over the fundamental construction.
Code Reusability and Interoperability
Execution autonomous information structures advance code reusability. An ADT can be reused in
multiple projects or parts of a program as long as its interface is clearly defined.
Interoperability is also made easier by ADTs. Various programming languages can carry out a
similar ADT interface, permitting code parts written in various languages to communicate
flawlessly. When integrating with other libraries and systems or in environments with multiple
languages, this is especially useful.
A Python-based queue ADT, for instance, can be utilized alongside a Java-based queue ADT
within the same system. The interoperability of ADTs works on reconciliation endeavors.
Algorithm Adaptability and Support
ADTs give adaptability in picking the most suitable information design or algorithm for a
particular issue or situation. However long the ADT interface stays predictable, you can switch the
hidden execution depending on the situation to further develop execution or meet evolving
necessities.
At the point when execution bottlenecks or adaptability issues emerge, you can streamline the
ADT execution without altering the code that utilizes the ADT. This adaptability upholds long
term support and versatility of programming frameworks.
For instance, consider a dictionary ADT for putting away key-value pairs. At first, you could carry
out it as a hash table. Afterward, if the dataset develops fundamentally and hash crashes become
risky, you can change to an additional productive information structure like a B-tree or a trie, all
while keeping a similar word reference ADT interface.
In summary, utilizing implementation-independent data structures, or ADTs, offers advantages
like particularity, code reusability, interoperability, adaptability in calculation decision, and
simplicity of support. These benefits add to more powerful, maintainable, and versatile software
systems.
References
Data Structures and Algorithms in Python by Mark Allen Weiss (2020)
The Algorithm Design Manual by Steven S. Skiena (2020)
Data Structures and Algorithms with Java by Michael T. Goodrich and Roberto Tamassia (2014)
Data Structures and Algorithms in C++ by Adam Drozdek (2013)
Abstract Data Types and Algorithms by Manoochehr Azmoodeh (2019)
Fundamentals of Data Structures by Ellis Horowitz, Sartaj Sahni, and Dinesh Mehta (2015)
Introduction to Formal Methods in Computer Science by Jean-Louis Fiadeiro and Jean-Loup
Voisin (2018)
Introduction to Algorithms by Thomas H. Cormen, Charles E. Leiserson, Ronald L. Rivest, and
Clifford Stein (2009)
https://www.cs.bham.ac.uk/~jxb/DSA/dsa.pdf
https://mu.ac.in/wp-content/uploads/2021/05/Data-Structure-Final-.pdf
Download