Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions Python/algorithms/graph_algorithms/floyd_warshall.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# 📌 Floyd-Warshall Algorithm
# Language: Python
# Category: Graph Algorithms
# Time Complexity: O(V^3)
# Space Complexity: O(V^2)

"""
Floyd-Warshall Algorithm is an algorithm for finding shortest paths in a directed weighted graph
with positive or negative edge weights (but with no negative cycles).

It computes shortest paths between all pairs of nodes in a single pass.
"""

import math

def floyd_warshall(graph):
"""
Computes the shortest paths between all pairs of nodes in a graph using the Floyd-Warshall algorithm.

:param graph: A 2D list (matrix) representing the adjacency matrix of the graph.
graph[i][j] is the weight of the edge from node i to node j.
If there is no edge, the value should be math.inf.
Self-loops (i to i) should be 0.
:return: A 2D list (matrix) where dist[i][j] is the shortest distance from node i to node j.
"""
num_vertices = len(graph)
dist = list(map(lambda i: list(map(lambda j: j, i)), graph)) # Initialize dist with the graph itself

# Add all vertices one by one to the set of intermediate vertices.
# Before the start of an iteration, shortest distances between all pairs
# of vertices is calculated using vertices from 0 to k-1 as intermediate vertices.
# After the end of a iteration, vertex k is added to the set of intermediate vertices
# and the set becomes {0, 1, .. k}.
for k in range(num_vertices):
# Pick all vertices as source one by one
for i in range(num_vertices):
# Pick all vertices as destination for the above picked source
for j in range(num_vertices):
# If vertex k is on the shortest path from i to j, then update the value of dist[i][j]
dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])

return dist

# 🧪 Example usage
if __name__ == "__main__":
# Example 1: Graph with 4 vertices
# Representing infinity with math.inf
INF = math.inf
graph1 = [
[0, 5, INF, 10],
[INF, 0, 3, INF],
[INF, INF, 0, 1],
[INF, INF, INF, 0]
]
print("Graph 1:")
for row in graph1:
print(row)

shortest_paths1 = floyd_warshall(graph1)
print("\nShortest paths for Graph 1:")
for row in shortest_paths1:
print([round(x, 2) if x != INF else 'INF' for x in row])

# Expected output for Graph 1:
# [0, 5, 8, 9]
# [INF, 0, 3, 4]
# [INF, INF, 0, 1]
# [INF, INF, INF, 0]

# Example 2: Graph with negative weights (no negative cycles)
graph2 = [
[0, 1, INF, INF],
[INF, 0, -1, INF],
[INF, INF, 0, -1],
[-1, INF, INF, 0]
]
print("\nGraph 2:")
for row in graph2:
print(row)

shortest_paths2 = floyd_warshall(graph2)
print("\nShortest paths for Graph 2:")
for row in shortest_paths2:
print([round(x, 2) if x != INF else 'INF' for x in row])

# Expected output for Graph 2:
# [0, 1, 0, -1]
# [-1, 0, -1, -2]
# [-2, -1, 0, -1]
# [-1, 0, -1, 0]

# Example 3: Graph with 2 vertices
graph3 = [
[0, 10],
[5, 0]
]
print("\nGraph 3:")
for row in graph3:
print(row)

shortest_paths3 = floyd_warshall(graph3)
print("\nShortest paths for Graph 3:")
for row in shortest_paths3:
print([round(x, 2) if x != INF else 'INF' for x in row])

# Expected output for Graph 3:
# [0, 10]
# [5, 0]
101 changes: 101 additions & 0 deletions Python/algorithms/mathematical/euclidean_algorithm.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# 📌 Euclidean Algorithm for Greatest Common Divisor (GCD)

"""
Algorithm: Euclidean Algorithm
Description: Computes the greatest common divisor (GCD) of two non-negative integers.
Time Complexity: O(log(min(a, b))) - The number of steps is proportional to the number of digits in the smaller number.
Space Complexity: O(log(min(a, b))) for recursive calls, O(1) for iterative.
Author: Gemini
"""

def gcd_recursive(a: int, b: int) -> int:
"""
Computes the greatest common divisor (GCD) of two non-negative integers using the recursive Euclidean algorithm.

Args:
a (int): The first non-negative integer.
b (int): The second non-negative integer.

Returns:
int: The greatest common divisor of a and b.

Raises:
ValueError: If either a or b is negative.
"""
if a < 0 or b < 0:
raise ValueError("Both numbers must be non-negative.")
if b == 0:
return a
return gcd_recursive(b, a % b)

def gcd_iterative(a: int, b: int) -> int:
"""
Computes the greatest common divisor (GCD) of two non-negative integers using the iterative Euclidean algorithm.

Args:
a (int): The first non-negative integer.
b (int): The second non-negative integer.

Returns:
int: The greatest common divisor of a and b.

Raises:
ValueError: If either a or b is negative.
"""
if a < 0 or b < 0:
raise ValueError("Both numbers must be non-negative.")
while b:
a, b = b, a % b
return a

def main():
"""Test the Euclidean algorithm with example cases."""
test_cases = [
(48, 18, 6),
(101, 103, 1),
(0, 5, 5),
(7, 0, 7),
(10, 10, 10),
(1, 1, 1),
(12, 15, 3),
(270, 192, 6)
]

print("--- Recursive GCD ---")
for a, b, expected in test_cases:
try:
result = gcd_recursive(a, b)
print(f"GCD_recursive({a}, {b}) = {result} (Expected: {expected}) {'✅' if result == expected else '❌'}")
except ValueError as e:
print(f"GCD_recursive({a}, {b}) raised error: {e} (Expected: {expected}) ❌")

print("\n--- Iterative GCD ---")
for a, b, expected in test_cases:
try:
result = gcd_iterative(a, b)
print(f"GCD_iterative({a}, {b}) = {result} (Expected: {expected}) {'✅' if result == expected else '❌'}")
except ValueError as e:
print(f"GCD_iterative({a}, {b}) raised error: {e} (Expected: {expected}) ❌")

# Test edge cases with negative numbers
print("\n--- Negative Number Test Cases ---")
negative_test_cases = [
(-5, 10),
(10, -5),
(-5, -10)
]
for a, b in negative_test_cases:
try:
gcd_recursive(a, b)
print(f"GCD_recursive({a}, {b}) - Expected ValueError, but no error. ❌")
except ValueError as e:
print(f"GCD_recursive({a}, {b}) - Caught expected error: {e} ✅")
try:
gcd_iterative(a, b)
print(f"GCD_iterative({a}, {b}) - Expected ValueError, but no error. ❌")
except ValueError as e:
print(f"GCD_iterative({a}, {b}) - Caught expected error: {e} ✅")


if __name__ == "__main__":
main()
120 changes: 120 additions & 0 deletions Python/algorithms/sorting/tim_sort.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# 📌 Tim Sort Algorithm
# Language: Python
# Category: Sorting
# Time Complexity: O(n log n)
# Space Complexity: O(n)

"""
Tim Sort is a hybrid stable sorting algorithm, derived from merge sort and insertion sort,
designed to perform well on many kinds of real-world data. It is the default sorting
algorithm used in Python's list.sort() and sorted() functions.

It works by dividing the array into blocks called "runs". These runs are then sorted
using insertion sort, and then merged using a modified merge sort.
"""

MIN_MERGE = 32

def calc_min_run(n):
"""Returns the minimum length of a run for Tim Sort."""
r = 0
while n >= MIN_MERGE:
r |= n & 1
n >>= 1
return n + r

def insertion_sort(arr, left, right):
"""Sorts a subarray using insertion sort."""
for i in range(left + 1, right + 1):
j = i
while j > left and arr[j] < arr[j - 1]:
arr[j], arr[j - 1] = arr[j - 1], arr[j]
j -= 1

def merge(arr, l, m, r):
"""Merges two sorted subarrays arr[l..m] and arr[m+1..r]."""
len1, len2 = m - l + 1, r - m
left, right = [], []
for i in range(0, len1):
left.append(arr[l + i])
for i in range(0, len2):
right.append(arr[m + 1 + i])

i, j, k = 0, 0, l

while i < len1 and j < len2:
if left[i] <= right[j]:
arr[k] = left[i]
i += 1
else:
arr[k] = right[j]
j += 1
k += 1

while i < len1:
arr[k] = left[i]
k += 1
i += 1

while j < len2:
arr[k] = right[j]
k += 1
j += 1

def tim_sort(arr):
"""
Sorts the input list 'arr' using the Tim Sort algorithm.
:param arr: list of elements (int/float) to be sorted
:return: sorted list
"""
n = len(arr)

if n == 0: # Handle empty array
return arr

min_run = calc_min_run(n)

# Sort individual runs of size MIN_MERGE or less using insertion sort
for i in range(0, n, min_run):
insertion_sort(arr, i, min((i + min_run - 1), n - 1))

# Start merging from size MIN_MERGE. It will merge
# to form size 2*MIN_MERGE, then 4*MIN_MERGE, and so on.
size = min_run
while size < n:
for left in range(0, n, 2 * size):
mid = min((left + size - 1), (n - 1))
right = min((left + 2 * size - 1), (n - 1))

if mid < right:
merge(arr, left, mid, right)
size *= 2

return arr

# 🧪 Example usage
if __name__ == "__main__":
sample_array = [38, 27, 43, 3, 9, 82, 10]
print("Original array:", sample_array)
sorted_array = tim_sort(sample_array)
print("Sorted array:", sorted_array)

sample_array_2 = [5, 4, 3, 2, 1]
print("Original array 2:", sample_array_2)
sorted_array_2 = tim_sort(sample_array_2)
print("Sorted array 2:", sorted_array_2)

sample_array_3 = []
print("Original array 3:", sample_array_3)
sorted_array_3 = tim_sort(sample_array_3)
print("Sorted array 3:", sorted_array_3)

sample_array_4 = [1]
print("Original array 4:", sample_array_4)
sorted_array_4 = tim_sort(sample_array_4)
print("Sorted array 4:", sorted_array_4)

sample_array_5 = [1, 2, 3, 4, 5]
print("Original array 5:", sample_array_5)
sorted_array_5 = tim_sort(sample_array_5)
print("Sorted array 5:", sorted_array_5)
Loading
Loading