Skip to content
Open
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
57 changes: 57 additions & 0 deletions DSA/activity_selection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
Activity Selection Problem - Greedy Algorithm Implementation

The activity selection problem is a classic example of a greedy algorithm.
Given a set of activities with start and finish times, find the maximum
number of activities that can be performed by a single person, assuming
that a person can only work on a single activity at a time.
"""

def activity_selection(start_times, finish_times):
"""
Find the maximum number of activities that can be performed.

Args:
start_times: List of start times for each activity
finish_times: List of finish times for each activity

Returns:
List of indices of selected activities
"""
# Create a list of activities with their start and finish times
activities = list(zip(range(len(start_times)), start_times, finish_times))

# Sort activities by finish time
activities.sort(key=lambda x: x[2])

# Select the first activity
selected = [activities[0][0]]
last_finish_time = activities[0][2]

# Consider the rest of the activities
for i in range(1, len(activities)):
# If the start time of this activity is greater than or equal to
# the finish time of the previously selected activity, select it
if activities[i][1] >= last_finish_time:
selected.append(activities[i][0])
last_finish_time = activities[i][2]

return selected

# Test cases
if __name__ == "__main__":
# Example 1
start_times1 = [1, 3, 0, 5, 8, 5]
finish_times1 = [2, 4, 6, 7, 9, 9]

selected1 = activity_selection(start_times1, finish_times1)
print("Selected activities (indices):", selected1)
print("Number of activities selected:", len(selected1))

# Example 2
start_times2 = [10, 12, 20]
finish_times2 = [20, 25, 30]

selected2 = activity_selection(start_times2, finish_times2)
print("\nSelected activities (indices):", selected2)
print("Number of activities selected:", len(selected2))
46 changes: 46 additions & 0 deletions DSA/binary_search.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""
Binary Search Algorithm

Implementation of Binary Search algorithm to find an element in a sorted array.
Time complexity: O(log n)
"""

def binary_search(arr, target):
"""
Search for target in a sorted array using binary search.

Args:
arr: Sorted list of elements
target: Element to search for

Returns:
Index of target if found, -1 otherwise
"""
left = 0
right = len(arr) - 1

while left <= right:
mid = (left + right) // 2

if arr[mid] == target:
return mid
elif arr[mid] < target:
left = mid + 1
else:
right = mid - 1

return -1

# Test cases
if __name__ == "__main__":
# Test case 1
arr1 = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
target1 = 7
print(f"Array: {arr1}, Target: {target1}")
print(f"Found at index: {binary_search(arr1, target1)}")

# Test case 2
arr2 = [1, 3, 5, 7, 9]
target2 = 4
print(f"Array: {arr2}, Target: {target2}")
print(f"Found at index: {binary_search(arr2, target2)}")
106 changes: 106 additions & 0 deletions DSA/fibonacci.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
"""
Fibonacci Sequence - Dynamic Programming Implementation

This file demonstrates different approaches to calculate Fibonacci numbers:
1. Naive recursive approach (exponential time)
2. Memoization approach (top-down dynamic programming)
3. Tabulation approach (bottom-up dynamic programming)
"""

def fibonacci_recursive(n):
"""
Calculate the nth Fibonacci number using naive recursion.
Time Complexity: O(2^n)
Space Complexity: O(n) for recursion stack

Args:
n: Position in Fibonacci sequence (0-indexed)

Returns:
The nth Fibonacci number
"""
if n <= 1:
return n
return fibonacci_recursive(n-1) + fibonacci_recursive(n-2)

def fibonacci_memoization(n, memo=None):
"""
Calculate the nth Fibonacci number using memoization (top-down DP).
Time Complexity: O(n)
Space Complexity: O(n)

Args:
n: Position in Fibonacci sequence (0-indexed)
memo: Dictionary to store already computed values

Returns:
The nth Fibonacci number
"""
if memo is None:
memo = {}

if n in memo:
return memo[n]

if n <= 1:
return n

memo[n] = fibonacci_memoization(n-1, memo) + fibonacci_memoization(n-2, memo)
return memo[n]

def fibonacci_tabulation(n):
"""
Calculate the nth Fibonacci number using tabulation (bottom-up DP).
Time Complexity: O(n)
Space Complexity: O(n)

Args:
n: Position in Fibonacci sequence (0-indexed)

Returns:
The nth Fibonacci number
"""
if n <= 1:
return n

# Initialize table
dp = [0] * (n + 1)
dp[1] = 1

# Fill the table
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]

return dp[n]

def fibonacci_optimized(n):
"""
Calculate the nth Fibonacci number using optimized space.
Time Complexity: O(n)
Space Complexity: O(1)

Args:
n: Position in Fibonacci sequence (0-indexed)

Returns:
The nth Fibonacci number
"""
if n <= 1:
return n

a, b = 0, 1
for _ in range(2, n + 1):
a, b = b, a + b

return b

# Test cases
if __name__ == "__main__":
n = 10 # 10th Fibonacci number

print(f"Fibonacci({n}) using recursive approach:", fibonacci_recursive(n))
print(f"Fibonacci({n}) using memoization:", fibonacci_memoization(n))
print(f"Fibonacci({n}) using tabulation:", fibonacci_tabulation(n))
print(f"Fibonacci({n}) using optimized approach:", fibonacci_optimized(n))

# Expected output for n=10: 55
103 changes: 103 additions & 0 deletions DSA/graph_bfs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""
Graph Breadth-First Search (BFS)

Implementation of Breadth-First Search algorithm for graph traversal.
"""

from collections import deque

def bfs(graph, start):
"""
Perform breadth-first search on a graph.

Args:
graph: Dictionary representing adjacency list of the graph
start: Starting vertex

Returns:
List of vertices in BFS order
"""
# Initialize visited set and result list
visited = set([start])
result = [start]

# Initialize queue with start vertex
queue = deque([start])

# Process vertices in queue
while queue:
# Dequeue a vertex
vertex = queue.popleft()

# Process all neighbors
for neighbor in graph[vertex]:
if neighbor not in visited:
visited.add(neighbor)
result.append(neighbor)
queue.append(neighbor)

return result

def shortest_path_bfs(graph, start, end):
"""
Find shortest path between start and end vertices using BFS.

Args:
graph: Dictionary representing adjacency list of the graph
start: Starting vertex
end: Ending vertex

Returns:
Shortest path as a list of vertices, or None if no path exists
"""
# Check for edge cases
if start == end:
return [start]

# Keep track of visited vertices and their parents
visited = {start: None}
queue = deque([start])

# BFS traversal
while queue:
vertex = queue.popleft()

# Check all neighbors
for neighbor in graph[vertex]:
if neighbor not in visited:
visited[neighbor] = vertex
queue.append(neighbor)

# If we found the end vertex, reconstruct the path
if neighbor == end:
path = [end]
while path[-1] != start:
path.append(visited[path[-1]])
return path[::-1] # Reverse to get path from start to end

# No path found
return None

# Test cases
if __name__ == "__main__":
# Example graph represented as an adjacency list
graph = {
'A': ['B', 'C'],
'B': ['A', 'D', 'E'],
'C': ['A', 'F'],
'D': ['B'],
'E': ['B', 'F'],
'F': ['C', 'E']
}

print("BFS starting from vertex 'A':", bfs(graph, 'A'))

# Test shortest path
start_vertex = 'A'
end_vertex = 'F'
path = shortest_path_bfs(graph, start_vertex, end_vertex)

if path:
print(f"Shortest path from {start_vertex} to {end_vertex}:", ' -> '.join(path))
else:
print(f"No path exists from {start_vertex} to {end_vertex}")
1 change: 1 addition & 0 deletions DSA/temp.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@