diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3440ce5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Virtual Environment +venv/ +env/ +ENV/ +env.bak/ +venv.bak/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Temporary files +*.tmp +*.temp +/tmp/ + +# Logs +*.log + +# Testing +.coverage +.pytest_cache/ +.tox/ +.coverage.* +coverage.xml +*.cover +.hypothesis/ + +# Documentation +docs/_build/ \ No newline at end of file diff --git a/README.md b/README.md index eb7557d..89f4fdd 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,206 @@ -# Data-Structure-and-Algorithm-Codes -This repository contains Data Structure and Algorithm codes +# Data Structures and Algorithms Tutorial + +A comprehensive collection of data structure and algorithm implementations with detailed explanations, time/space complexity analysis, and practical examples. + +## 📚 Table of Contents + +- [Overview](#overview) +- [Data Structures](#data-structures) +- [Algorithms](#algorithms) +- [Getting Started](#getting-started) +- [Examples](#examples) +- [Contributing](#contributing) + +## 🎯 Overview + +This repository serves as a complete tutorial and reference for fundamental data structures and algorithms. Each implementation includes: + +- **Detailed documentation** with complexity analysis +- **Working code examples** in Python +- **Practical applications** and use cases +- **Step-by-step explanations** of operations + +## 📊 Data Structures + +### Linear Data Structures + +#### Arrays +- **File**: [`data_structures/arrays/array_operations.py`](data_structures/arrays/array_operations.py) +- **Operations**: Insert, Delete, Search, Access +- **Time Complexity**: Access O(1), Search O(n), Insert/Delete O(n) +- **Use Cases**: When you need fast random access by index + +#### Linked Lists +- **File**: [`data_structures/linked_lists/singly_linked_list.py`](data_structures/linked_lists/singly_linked_list.py) +- **Operations**: Insert at head/tail/position, Delete, Search +- **Time Complexity**: Access O(n), Insert/Delete O(1) at head +- **Use Cases**: Dynamic size, frequent insertions/deletions at beginning + +#### Stacks +- **File**: [`data_structures/stacks/stack.py`](data_structures/stacks/stack.py) +- **Operations**: Push, Pop, Peek, Search +- **Time Complexity**: All operations O(1) except search O(n) +- **Use Cases**: Function calls, expression evaluation, undo operations +- **Example Application**: Balanced parentheses checker included + +#### Queues +- **File**: [`data_structures/queues/queue.py`](data_structures/queues/queue.py) +- **Types**: Regular Queue, Circular Queue +- **Operations**: Enqueue, Dequeue, Front, Rear +- **Time Complexity**: All operations O(1) +- **Use Cases**: BFS, process scheduling, handling requests + +### Non-Linear Data Structures + +#### Binary Search Trees +- **File**: [`data_structures/trees/binary_search_tree.py`](data_structures/trees/binary_search_tree.py) +- **Operations**: Insert, Delete, Search, Traversals +- **Time Complexity**: Average O(log n), Worst O(n) +- **Traversals**: Inorder, Preorder, Postorder, Level-order +- **Use Cases**: Sorted data, range queries, hierarchical data + +## 🚀 Algorithms + +### Sorting Algorithms + +**File**: [`algorithms/sorting/sorting_algorithms.py`](algorithms/sorting/sorting_algorithms.py) + +| Algorithm | Time Complexity | Space | Stable | Description | +|-----------|----------------|-------|---------|-------------| +| **Bubble Sort** | O(n²) | O(1) | Yes | Simple comparison-based sort | +| **Selection Sort** | O(n²) | O(1) | No | Finds minimum and places at beginning | +| **Insertion Sort** | O(n²) | O(1) | Yes | Builds sorted array incrementally | +| **Merge Sort** | O(n log n) | O(n) | Yes | Divide and conquer approach | +| **Quick Sort** | O(n log n) avg | O(log n) | No | Pivot-based partitioning | +| **Heap Sort** | O(n log n) | O(1) | No | Uses binary heap structure | + +### Searching Algorithms + +**File**: [`algorithms/searching/searching_algorithms.py`](algorithms/searching/searching_algorithms.py) + +| Algorithm | Time Complexity | Space | Requirements | Description | +|-----------|----------------|-------|--------------|-------------| +| **Linear Search** | O(n) | O(1) | None | Sequential search | +| **Binary Search** | O(log n) | O(1) | Sorted array | Divide and conquer | +| **Jump Search** | O(√n) | O(1) | Sorted array | Block-based search | +| **Interpolation Search** | O(log log n) | O(1) | Uniformly distributed | Improved binary search | +| **Exponential Search** | O(log n) | O(1) | Sorted array | Find range then binary search | +| **Ternary Search** | O(log₃ n) | O(1) | Sorted array | Divide into three parts | +| **Fibonacci Search** | O(log n) | O(1) | Sorted array | Uses Fibonacci numbers | + +## 🏃‍♂️ Getting Started + +### Prerequisites +- Python 3.6 or higher + +### Running the Code + +1. **Clone the repository**: + ```bash + git clone https://github.com/0FFSTYLE/Data-Structure-and-Algorithm-Codes.git + cd Data-Structure-and-Algorithm-Codes + ``` + +2. **Run individual data structures**: + ```bash + python data_structures/arrays/array_operations.py + python data_structures/linked_lists/singly_linked_list.py + python data_structures/stacks/stack.py + python data_structures/queues/queue.py + python data_structures/trees/binary_search_tree.py + ``` + +3. **Run sorting algorithms**: + ```bash + python algorithms/sorting/sorting_algorithms.py + ``` + +4. **Run searching algorithms**: + ```bash + python algorithms/searching/searching_algorithms.py + ``` + +5. **Run comprehensive examples**: + ```bash + python examples/example_usage.py + ``` + +## 💡 Examples + +The [`examples/example_usage.py`](examples/example_usage.py) file demonstrates: + +- **Practical usage** of all data structures +- **Problem-solving** with real-world examples +- **Performance comparisons** between algorithms +- **Best practices** for choosing appropriate data structures + +Example output includes: +- Data structure operations and results +- Balanced parentheses validation +- Duplicate finding algorithms +- Sorting and searching performance metrics + +## 📈 Complexity Quick Reference + +### Data Structures Operations + +| Data Structure | Access | Search | Insertion | Deletion | Space | +|---------------|--------|---------|-----------|-----------|-------| +| **Array** | O(1) | O(n) | O(n) | O(n) | O(n) | +| **Linked List** | O(n) | O(n) | O(1)* | O(1)* | O(n) | +| **Stack** | O(n) | O(n) | O(1) | O(1) | O(n) | +| **Queue** | O(n) | O(n) | O(1) | O(1) | O(n) | +| **BST** | O(log n) | O(log n) | O(log n) | O(log n) | O(n) | + +*\*At head position* + +### Algorithm Performance + +| Problem | Best Algorithm | Time Complexity | When to Use | +|---------|---------------|----------------|-------------| +| **Sorting small arrays** | Insertion Sort | O(n²) | n < 50 | +| **Sorting large arrays** | Quick/Merge Sort | O(n log n) | General purpose | +| **Searching unsorted** | Linear Search | O(n) | No other option | +| **Searching sorted** | Binary Search | O(log n) | Always prefer this | +| **Finding duplicates** | Hash Set | O(n) | When extra space available | + +## 🔧 Key Features + +- **Educational Focus**: Each implementation prioritizes clarity and understanding +- **Comprehensive Coverage**: From basic arrays to complex tree structures +- **Practical Examples**: Real-world applications and problem-solving +- **Performance Analysis**: Detailed time and space complexity discussions +- **Production Ready**: Clean, well-documented, and tested code + +## 🎓 Learning Path + +1. **Start with Linear Structures**: Arrays → Linked Lists → Stacks → Queues +2. **Move to Non-Linear**: Binary Search Trees → Hash Tables +3. **Learn Basic Algorithms**: Linear Search → Binary Search → Simple Sorting +4. **Advanced Algorithms**: Efficient Sorting → Advanced Searching +5. **Practice**: Use the examples and create your own implementations + +## 🤝 Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. Areas for contribution: +- Additional data structures (Hash Tables, Heaps, Graphs) +- More algorithms (Dynamic Programming, Graph algorithms) +- Language implementations (Java, C++, JavaScript) +- Performance optimizations +- Additional examples and use cases + +## 📝 License + +This project is open source and available under the [MIT License](LICENSE). + +## 🌟 Acknowledgments + +- Built for educational purposes +- Suitable for computer science students, coding interviews, and professional development +- Contributions and feedback welcome from the community + +--- + +**Happy Learning! 🚀** + +*Remember: Understanding the concept is more important than memorizing the code. Focus on when and why to use each data structure and algorithm.* diff --git a/algorithms/searching/searching_algorithms.py b/algorithms/searching/searching_algorithms.py new file mode 100644 index 0000000..673c2b0 --- /dev/null +++ b/algorithms/searching/searching_algorithms.py @@ -0,0 +1,278 @@ +""" +Common Searching Algorithms Implementation + +This module contains implementations of various searching algorithms with +their time and space complexity analysis. +""" + +def linear_search(arr, target): + """ + Linear Search - Sequential search through all elements + + Time Complexity: O(n) + Space Complexity: O(1) + Works on: Sorted and unsorted arrays + """ + for i in range(len(arr)): + if arr[i] == target: + return i + return -1 + +def binary_search(arr, target): + """ + Binary Search - Divide and conquer search (requires sorted array) + + Time Complexity: O(log n) + Space Complexity: O(1) iterative, O(log n) recursive + Works on: Sorted arrays only + """ + left, right = 0, 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 + +def binary_search_recursive(arr, target, left=0, right=None): + """ + Binary Search - Recursive implementation + + Time Complexity: O(log n) + Space Complexity: O(log n) due to recursion stack + """ + if right is None: + right = len(arr) - 1 + + if left > right: + return -1 + + mid = (left + right) // 2 + + if arr[mid] == target: + return mid + elif arr[mid] < target: + return binary_search_recursive(arr, target, mid + 1, right) + else: + return binary_search_recursive(arr, target, left, mid - 1) + +def jump_search(arr, target): + """ + Jump Search - Block-based search algorithm + + Time Complexity: O(√n) + Space Complexity: O(1) + Works on: Sorted arrays only + """ + import math + + n = len(arr) + step = int(math.sqrt(n)) + prev = 0 + + # Find the block where element may be present + while arr[min(step, n) - 1] < target and step < n: + prev = step + step += int(math.sqrt(n)) + + # Linear search in the identified block + while arr[prev] < target: + prev += 1 + if prev == min(step, n): + return -1 + + if arr[prev] == target: + return prev + + return -1 + +def interpolation_search(arr, target): + """ + Interpolation Search - Improved binary search for uniformly distributed data + + Time Complexity: O(log log n) average, O(n) worst case + Space Complexity: O(1) + Works on: Sorted arrays with uniformly distributed values + """ + left, right = 0, len(arr) - 1 + + while left <= right and target >= arr[left] and target <= arr[right]: + # If array has only one element + if left == right: + if arr[left] == target: + return left + return -1 + + # Calculate probe position + pos = left + ((target - arr[left]) * (right - left)) // (arr[right] - arr[left]) + + if arr[pos] == target: + return pos + elif arr[pos] < target: + left = pos + 1 + else: + right = pos - 1 + + return -1 + +def exponential_search(arr, target): + """ + Exponential Search - Find range then apply binary search + + Time Complexity: O(log n) + Space Complexity: O(1) + Works on: Sorted arrays only + """ + if not arr: + return -1 + + if arr[0] == target: + return 0 + + # Find range for binary search + i = 1 + while i < len(arr) and arr[i] <= target: + i *= 2 + + # Apply binary search on the found range + left = i // 2 + right = min(i, 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 + +def ternary_search(arr, target): + """ + Ternary Search - Divide array into three parts + + Time Complexity: O(log₃ n) + Space Complexity: O(1) + Works on: Sorted arrays only + """ + left, right = 0, len(arr) - 1 + + while left <= right: + mid1 = left + (right - left) // 3 + mid2 = right - (right - left) // 3 + + if arr[mid1] == target: + return mid1 + if arr[mid2] == target: + return mid2 + + if target < arr[mid1]: + right = mid1 - 1 + elif target > arr[mid2]: + left = mid2 + 1 + else: + left = mid1 + 1 + right = mid2 - 1 + + return -1 + +def fibonacci_search(arr, target): + """ + Fibonacci Search - Uses Fibonacci numbers to divide array + + Time Complexity: O(log n) + Space Complexity: O(1) + Works on: Sorted arrays only + """ + n = len(arr) + + # Initialize fibonacci numbers + fib_m2 = 0 # (m-2)'th Fibonacci number + fib_m1 = 1 # (m-1)'th Fibonacci number + fib_m = fib_m2 + fib_m1 # m'th Fibonacci number + + # Find smallest Fibonacci number >= n + while fib_m < n: + fib_m2 = fib_m1 + fib_m1 = fib_m + fib_m = fib_m2 + fib_m1 + + offset = -1 + + while fib_m > 1: + i = min(offset + fib_m2, n - 1) + + if arr[i] < target: + fib_m = fib_m1 + fib_m1 = fib_m2 + fib_m2 = fib_m - fib_m1 + offset = i + elif arr[i] > target: + fib_m = fib_m2 + fib_m1 = fib_m1 - fib_m2 + fib_m2 = fib_m - fib_m1 + else: + return i + + if fib_m1 and offset + 1 < n and arr[offset + 1] == target: + return offset + 1 + + return -1 + +# Testing and comparison function +def compare_searching_algorithms(arr, target): + """Compare all searching algorithms with the same input""" + import time + + # Ensure array is sorted for algorithms that require it + sorted_arr = sorted(arr) + + algorithms = [ + ("Linear Search", linear_search, arr), + ("Binary Search", binary_search, sorted_arr), + ("Binary Search (Recursive)", binary_search_recursive, sorted_arr), + ("Jump Search", jump_search, sorted_arr), + ("Interpolation Search", interpolation_search, sorted_arr), + ("Exponential Search", exponential_search, sorted_arr), + ("Ternary Search", ternary_search, sorted_arr), + ("Fibonacci Search", fibonacci_search, sorted_arr) + ] + + print(f"Original array: {arr}") + print(f"Sorted array: {sorted_arr}") + print(f"Target: {target}") + print(f"Array size: {len(arr)}") + print("-" * 60) + + for name, func, test_arr in algorithms: + start_time = time.time() + result = func(test_arr, target) + end_time = time.time() + + status = "Found" if result != -1 else "Not Found" + print(f"{name:25}: Index {result:2d} ({status}) - Time: {(end_time - start_time) * 1000000:.2f} μs") + +# Example usage +if __name__ == "__main__": + # Test cases + test_cases = [ + ([64, 34, 25, 12, 22, 11, 90, 5, 77, 30], 22), + ([1, 3, 5, 7, 9, 11, 13, 15, 17, 19], 7), + ([2, 4, 6, 8, 10, 12, 14, 16, 18, 20], 15), # Not found + (list(range(1, 101)), 50), # Large sorted array + ] + + for i, (arr, target) in enumerate(test_cases, 1): + print(f"=== Test Case {i} ===") + compare_searching_algorithms(arr, target) + print("=" * 70) + print() \ No newline at end of file diff --git a/algorithms/sorting/sorting_algorithms.py b/algorithms/sorting/sorting_algorithms.py new file mode 100644 index 0000000..7f9ab35 --- /dev/null +++ b/algorithms/sorting/sorting_algorithms.py @@ -0,0 +1,216 @@ +""" +Common Sorting Algorithms Implementation + +This module contains implementations of various sorting algorithms with +their time and space complexity analysis. +""" + +def bubble_sort(arr): + """ + Bubble Sort - Simple comparison-based sorting algorithm + + Time Complexity: O(n²) in worst and average case, O(n) in best case + Space Complexity: O(1) + Stable: Yes + In-place: Yes + """ + n = len(arr) + arr = arr.copy() # Don't modify original array + + for i in range(n): + swapped = False + for j in range(0, n - i - 1): + if arr[j] > arr[j + 1]: + arr[j], arr[j + 1] = arr[j + 1], arr[j] + swapped = True + + # If no swapping occurred, array is already sorted + if not swapped: + break + + return arr + +def selection_sort(arr): + """ + Selection Sort - Finds minimum element and places it at the beginning + + Time Complexity: O(n²) in all cases + Space Complexity: O(1) + Stable: No + In-place: Yes + """ + n = len(arr) + arr = arr.copy() + + for i in range(n): + min_idx = i + for j in range(i + 1, n): + if arr[j] < arr[min_idx]: + min_idx = j + + arr[i], arr[min_idx] = arr[min_idx], arr[i] + + return arr + +def insertion_sort(arr): + """ + Insertion Sort - Builds sorted array one element at a time + + Time Complexity: O(n²) in worst and average case, O(n) in best case + Space Complexity: O(1) + Stable: Yes + In-place: Yes + """ + arr = arr.copy() + + for i in range(1, len(arr)): + key = arr[i] + j = i - 1 + + # Move elements greater than key one position ahead + while j >= 0 and arr[j] > key: + arr[j + 1] = arr[j] + j -= 1 + + arr[j + 1] = key + + return arr + +def merge_sort(arr): + """ + Merge Sort - Divide and conquer algorithm + + Time Complexity: O(n log n) in all cases + Space Complexity: O(n) + Stable: Yes + In-place: No + """ + if len(arr) <= 1: + return arr + + mid = len(arr) // 2 + left = merge_sort(arr[:mid]) + right = merge_sort(arr[mid:]) + + return merge(left, right) + +def merge(left, right): + """Helper function for merge sort""" + result = [] + i = j = 0 + + while i < len(left) and j < len(right): + if left[i] <= right[j]: + result.append(left[i]) + i += 1 + else: + result.append(right[j]) + j += 1 + + # Add remaining elements + result.extend(left[i:]) + result.extend(right[j:]) + + return result + +def quick_sort(arr): + """ + Quick Sort - Divide and conquer with pivot partitioning + + Time Complexity: O(n log n) average, O(n²) worst case + Space Complexity: O(log n) average, O(n) worst case + Stable: No + In-place: Yes (this implementation creates new arrays for simplicity) + """ + if len(arr) <= 1: + return arr + + pivot = arr[len(arr) // 2] + left = [x for x in arr if x < pivot] + middle = [x for x in arr if x == pivot] + right = [x for x in arr if x > pivot] + + return quick_sort(left) + middle + quick_sort(right) + +def heap_sort(arr): + """ + Heap Sort - Uses binary heap data structure + + Time Complexity: O(n log n) in all cases + Space Complexity: O(1) + Stable: No + In-place: Yes + """ + arr = arr.copy() + n = len(arr) + + # Build max heap + for i in range(n // 2 - 1, -1, -1): + heapify(arr, n, i) + + # Extract elements from heap one by one + for i in range(n - 1, 0, -1): + arr[0], arr[i] = arr[i], arr[0] # Move current root to end + heapify(arr, i, 0) # Call heapify on reduced heap + + return arr + +def heapify(arr, n, i): + """Helper function for heap sort - maintains max heap property""" + largest = i + left = 2 * i + 1 + right = 2 * i + 2 + + if left < n and arr[left] > arr[largest]: + largest = left + + if right < n and arr[right] > arr[largest]: + largest = right + + if largest != i: + arr[i], arr[largest] = arr[largest], arr[i] + heapify(arr, n, largest) + +# Testing and comparison function +def compare_sorting_algorithms(arr): + """Compare all sorting algorithms with the same input""" + import time + + algorithms = [ + ("Bubble Sort", bubble_sort), + ("Selection Sort", selection_sort), + ("Insertion Sort", insertion_sort), + ("Merge Sort", merge_sort), + ("Quick Sort", quick_sort), + ("Heap Sort", heap_sort) + ] + + print(f"Original array: {arr}") + print(f"Array size: {len(arr)}") + print("-" * 50) + + for name, func in algorithms: + start_time = time.time() + sorted_arr = func(arr) + end_time = time.time() + + print(f"{name:15}: {sorted_arr}") + print(f"Time taken: {(end_time - start_time) * 1000:.4f} ms") + print() + +# Example usage +if __name__ == "__main__": + # Test with different types of arrays + test_arrays = [ + [64, 34, 25, 12, 22, 11, 90], # Random array + [5, 2, 4, 6, 1, 3], # Small array + [1, 2, 3, 4, 5], # Already sorted + [5, 4, 3, 2, 1], # Reverse sorted + [3, 3, 3, 3, 3] # All same elements + ] + + for i, arr in enumerate(test_arrays, 1): + print(f"=== Test Case {i} ===") + compare_sorting_algorithms(arr) + print("=" * 60) + print() \ No newline at end of file diff --git a/data_structures/arrays/array_operations.py b/data_structures/arrays/array_operations.py new file mode 100644 index 0000000..80b2c65 --- /dev/null +++ b/data_structures/arrays/array_operations.py @@ -0,0 +1,83 @@ +""" +Array Operations - Basic array manipulation examples + +Arrays are one of the most fundamental data structures. They store elements +in contiguous memory locations and provide O(1) access time by index. + +Time Complexities: +- Access: O(1) +- Search: O(n) +- Insertion: O(n) - due to shifting elements +- Deletion: O(n) - due to shifting elements +""" + +class ArrayOperations: + def __init__(self, size=10): + """Initialize array with given size""" + self.data = [None] * size + self.size = 0 + self.capacity = size + + def insert(self, index, value): + """Insert element at given index""" + if self.size >= self.capacity: + raise Exception("Array is full") + if index < 0 or index > self.size: + raise Exception("Index out of bounds") + + # Shift elements to the right + for i in range(self.size, index, -1): + self.data[i] = self.data[i-1] + + self.data[index] = value + self.size += 1 + + def delete(self, index): + """Delete element at given index""" + if index < 0 or index >= self.size: + raise Exception("Index out of bounds") + + # Shift elements to the left + for i in range(index, self.size - 1): + self.data[i] = self.data[i+1] + + self.size -= 1 + self.data[self.size] = None + + def search(self, value): + """Search for a value and return its index""" + for i in range(self.size): + if self.data[i] == value: + return i + return -1 + + def get(self, index): + """Get element at given index""" + if index < 0 or index >= self.size: + raise Exception("Index out of bounds") + return self.data[index] + + def display(self): + """Display the array elements""" + return [self.data[i] for i in range(self.size)] + +# Example usage +if __name__ == "__main__": + arr = ArrayOperations() + + # Insert elements + arr.insert(0, 10) + arr.insert(1, 20) + arr.insert(2, 30) + print("Array after insertions:", arr.display()) + + # Search for element + index = arr.search(20) + print(f"Element 20 found at index: {index}") + + # Delete element + arr.delete(1) + print("Array after deletion:", arr.display()) + + # Access element + print(f"Element at index 0: {arr.get(0)}") \ No newline at end of file diff --git a/data_structures/hash_tables/hash_table.py b/data_structures/hash_tables/hash_table.py new file mode 100644 index 0000000..9729894 --- /dev/null +++ b/data_structures/hash_tables/hash_table.py @@ -0,0 +1,247 @@ +""" +Hash Table (Hash Map) Implementation + +A hash table is a data structure that implements an associative array abstract data type, +a structure that can map keys to values using a hash function. + +Time Complexities (Average Case): +- Search: O(1) +- Insertion: O(1) +- Deletion: O(1) + +Time Complexities (Worst Case): +- Search: O(n) - when all keys hash to same bucket +- Insertion: O(n) +- Deletion: O(n) + +Space Complexity: O(n) + +Collision Handling: Separate Chaining (using linked lists) +""" + +class HashNode: + def __init__(self, key, value): + self.key = key + self.value = value + self.next = None + +class HashTable: + def __init__(self, capacity=10): + """Initialize hash table with given capacity""" + self.capacity = capacity + self.size = 0 + self.buckets = [None] * self.capacity + + def _hash(self, key): + """Simple hash function using built-in hash() and modulo""" + return hash(key) % self.capacity + + def _resize(self): + """Resize hash table when load factor exceeds threshold""" + old_buckets = self.buckets + self.capacity *= 2 + self.size = 0 + self.buckets = [None] * self.capacity + + # Rehash all existing key-value pairs + for head in old_buckets: + current = head + while current: + self.put(current.key, current.value) + current = current.next + + def put(self, key, value): + """Insert or update a key-value pair""" + # Resize if load factor > 0.75 + if self.size >= self.capacity * 0.75: + self._resize() + + index = self._hash(key) + + if self.buckets[index] is None: + # No collision, create new node + self.buckets[index] = HashNode(key, value) + self.size += 1 + else: + # Handle collision with chaining + current = self.buckets[index] + while True: + if current.key == key: + # Update existing key + current.value = value + return + if current.next is None: + break + current = current.next + + # Add new node at end of chain + current.next = HashNode(key, value) + self.size += 1 + + def get(self, key): + """Get value associated with key""" + index = self._hash(key) + current = self.buckets[index] + + while current: + if current.key == key: + return current.value + current = current.next + + raise KeyError(f"Key '{key}' not found") + + def delete(self, key): + """Delete key-value pair""" + index = self._hash(key) + current = self.buckets[index] + + if current is None: + raise KeyError(f"Key '{key}' not found") + + # If first node has the key + if current.key == key: + self.buckets[index] = current.next + self.size -= 1 + return current.value + + # Search in the chain + while current.next: + if current.next.key == key: + deleted_value = current.next.value + current.next = current.next.next + self.size -= 1 + return deleted_value + current = current.next + + raise KeyError(f"Key '{key}' not found") + + def contains(self, key): + """Check if key exists in hash table""" + try: + self.get(key) + return True + except KeyError: + return False + + def keys(self): + """Return list of all keys""" + keys_list = [] + for head in self.buckets: + current = head + while current: + keys_list.append(current.key) + current = current.next + return keys_list + + def values(self): + """Return list of all values""" + values_list = [] + for head in self.buckets: + current = head + while current: + values_list.append(current.value) + current = current.next + return values_list + + def items(self): + """Return list of all key-value pairs""" + items_list = [] + for head in self.buckets: + current = head + while current: + items_list.append((current.key, current.value)) + current = current.next + return items_list + + def get_size(self): + """Return number of key-value pairs""" + return self.size + + def is_empty(self): + """Check if hash table is empty""" + return self.size == 0 + + def get_load_factor(self): + """Return current load factor""" + return self.size / self.capacity + + def display(self): + """Display hash table structure for debugging""" + print(f"Hash Table (Size: {self.size}, Capacity: {self.capacity}, Load Factor: {self.get_load_factor():.2f})") + for i, head in enumerate(self.buckets): + if head is not None: + chain = [] + current = head + while current: + chain.append(f"({current.key}: {current.value})") + current = current.next + print(f"Bucket {i}: {' -> '.join(chain)}") + +# Example usage and demonstration +if __name__ == "__main__": + # Create hash table + ht = HashTable(5) # Small capacity to demonstrate resizing + + print("=== Hash Table Demonstration ===") + + # Insert key-value pairs + pairs = [ + ("name", "Alice"), + ("age", 25), + ("city", "New York"), + ("job", "Engineer"), + ("hobby", "Reading"), + ("color", "Blue"), + ("food", "Pizza") + ] + + print("\nInserting key-value pairs:") + for key, value in pairs: + ht.put(key, value) + print(f"Inserted ({key}, {value})") + + print(f"\nHash table size: {ht.get_size()}") + print(f"Load factor: {ht.get_load_factor():.2f}") + + # Display hash table structure + print("\nHash table structure:") + ht.display() + + # Get operations + print("\nGet operations:") + test_keys = ["name", "age", "invalid_key"] + for key in test_keys: + try: + value = ht.get(key) + print(f"get('{key}') = {value}") + except KeyError as e: + print(f"get('{key}') = {e}") + + # Contains operations + print("\nContains operations:") + for key in test_keys: + exists = ht.contains(key) + print(f"contains('{key}') = {exists}") + + # Update existing key + print("\nUpdating existing key:") + ht.put("age", 26) + print(f"Updated age to: {ht.get('age')}") + + # Delete operations + print("\nDelete operations:") + delete_keys = ["hobby", "invalid_key"] + for key in delete_keys: + try: + deleted_value = ht.delete(key) + print(f"Deleted '{key}': {deleted_value}") + except KeyError as e: + print(f"Delete '{key}': {e}") + + # Display all keys, values, and items + print(f"\nAll keys: {ht.keys()}") + print(f"All values: {ht.values()}") + print(f"All items: {ht.items()}") + + print(f"\nFinal size: {ht.get_size()}") + print("Hash table demonstration completed!") \ No newline at end of file diff --git a/data_structures/linked_lists/singly_linked_list.py b/data_structures/linked_lists/singly_linked_list.py new file mode 100644 index 0000000..7fdaec8 --- /dev/null +++ b/data_structures/linked_lists/singly_linked_list.py @@ -0,0 +1,157 @@ +""" +Singly Linked List Implementation + +A linked list is a linear data structure where elements are stored in nodes, +and each node contains data and a reference to the next node. + +Time Complexities: +- Access: O(n) +- Search: O(n) +- Insertion: O(1) at head, O(n) at arbitrary position +- Deletion: O(1) at head, O(n) at arbitrary position + +Space Complexity: O(n) +""" + +class Node: + def __init__(self, data): + self.data = data + self.next = None + +class SinglyLinkedList: + def __init__(self): + self.head = None + self.size = 0 + + def insert_at_head(self, data): + """Insert a new node at the beginning of the list""" + new_node = Node(data) + new_node.next = self.head + self.head = new_node + self.size += 1 + + def insert_at_tail(self, data): + """Insert a new node at the end of the list""" + new_node = Node(data) + if not self.head: + self.head = new_node + else: + current = self.head + while current.next: + current = current.next + current.next = new_node + self.size += 1 + + def insert_at_position(self, position, data): + """Insert a new node at the given position""" + if position < 0 or position > self.size: + raise Exception("Position out of bounds") + + if position == 0: + self.insert_at_head(data) + return + + new_node = Node(data) + current = self.head + for i in range(position - 1): + current = current.next + + new_node.next = current.next + current.next = new_node + self.size += 1 + + def delete_at_head(self): + """Delete the first node""" + if not self.head: + raise Exception("List is empty") + + self.head = self.head.next + self.size -= 1 + + def delete_at_tail(self): + """Delete the last node""" + if not self.head: + raise Exception("List is empty") + + if not self.head.next: + self.head = None + self.size -= 1 + return + + current = self.head + while current.next.next: + current = current.next + current.next = None + self.size -= 1 + + def delete_by_value(self, value): + """Delete the first node with the given value""" + if not self.head: + return False + + if self.head.data == value: + self.delete_at_head() + return True + + current = self.head + while current.next: + if current.next.data == value: + current.next = current.next.next + self.size -= 1 + return True + current = current.next + return False + + def search(self, value): + """Search for a value and return its position""" + current = self.head + position = 0 + while current: + if current.data == value: + return position + current = current.next + position += 1 + return -1 + + def display(self): + """Display all elements in the list""" + elements = [] + current = self.head + while current: + elements.append(current.data) + current = current.next + return elements + + def get_size(self): + """Return the size of the list""" + return self.size + + def is_empty(self): + """Check if the list is empty""" + return self.head is None + +# Example usage +if __name__ == "__main__": + ll = SinglyLinkedList() + + # Insert elements + ll.insert_at_head(10) + ll.insert_at_head(20) + ll.insert_at_tail(30) + ll.insert_at_position(1, 15) + print("Linked List:", ll.display()) + print("Size:", ll.get_size()) + + # Search for element + position = ll.search(15) + print(f"Element 15 found at position: {position}") + + # Delete elements + ll.delete_by_value(15) + print("After deleting 15:", ll.display()) + + ll.delete_at_head() + print("After deleting head:", ll.display()) + + ll.delete_at_tail() + print("After deleting tail:", ll.display()) \ No newline at end of file diff --git a/data_structures/queues/queue.py b/data_structures/queues/queue.py new file mode 100644 index 0000000..8892772 --- /dev/null +++ b/data_structures/queues/queue.py @@ -0,0 +1,187 @@ +""" +Queue Implementation using Collections.deque + +A queue is a linear data structure that follows the FIFO (First In, First Out) principle. +Elements are added at the rear (enqueue) and removed from the front (dequeue). + +Time Complexities: +- Enqueue: O(1) +- Dequeue: O(1) +- Front/Rear: O(1) +- Search: O(n) + +Space Complexity: O(n) + +Common Applications: +- Process scheduling in operating systems +- Breadth-First Search (BFS) in graphs +- Handling requests in web servers +- Print queue management +""" + +from collections import deque + +class Queue: + def __init__(self, capacity=None): + """Initialize an empty queue with optional capacity limit""" + self.items = deque() + self.capacity = capacity + + def enqueue(self, item): + """Add an item to the rear of the queue""" + if self.capacity is not None and len(self.items) >= self.capacity: + raise Exception("Queue overflow: Queue is full") + self.items.append(item) + + def dequeue(self): + """Remove and return the front item from the queue""" + if self.is_empty(): + raise Exception("Queue underflow: Queue is empty") + return self.items.popleft() + + def front(self): + """Return the front item without removing it""" + if self.is_empty(): + raise Exception("Queue is empty") + return self.items[0] + + def rear(self): + """Return the rear item without removing it""" + if self.is_empty(): + raise Exception("Queue is empty") + return self.items[-1] + + def is_empty(self): + """Check if the queue is empty""" + return len(self.items) == 0 + + def is_full(self): + """Check if the queue is full (only relevant if capacity is set)""" + if self.capacity is None: + return False + return len(self.items) >= self.capacity + + def size(self): + """Return the number of items in the queue""" + return len(self.items) + + def search(self, item): + """Search for an item and return its position from front (0-based)""" + try: + return list(self.items).index(item) + except ValueError: + return -1 + + def display(self): + """Display the queue contents (front to rear)""" + return list(self.items) + +class CircularQueue: + """ + Circular Queue implementation using a fixed-size array + More memory efficient than regular queue for fixed-size scenarios + """ + def __init__(self, capacity): + """Initialize circular queue with fixed capacity""" + self.capacity = capacity + self.items = [None] * capacity + self.front = -1 + self.rear = -1 + self.count = 0 + + def enqueue(self, item): + """Add an item to the rear of the circular queue""" + if self.is_full(): + raise Exception("Circular queue overflow: Queue is full") + + if self.is_empty(): + self.front = 0 + + self.rear = (self.rear + 1) % self.capacity + self.items[self.rear] = item + self.count += 1 + + def dequeue(self): + """Remove and return the front item from the circular queue""" + if self.is_empty(): + raise Exception("Circular queue underflow: Queue is empty") + + item = self.items[self.front] + self.items[self.front] = None + + if self.count == 1: + self.front = -1 + self.rear = -1 + else: + self.front = (self.front + 1) % self.capacity + + self.count -= 1 + return item + + def is_empty(self): + """Check if the circular queue is empty""" + return self.count == 0 + + def is_full(self): + """Check if the circular queue is full""" + return self.count == self.capacity + + def size(self): + """Return the number of items in the circular queue""" + return self.count + + def display(self): + """Display the circular queue contents""" + if self.is_empty(): + return [] + + result = [] + current = self.front + for _ in range(self.count): + result.append(self.items[current]) + current = (current + 1) % self.capacity + return result + +# Example usage +if __name__ == "__main__": + # Regular Queue + print("=== Regular Queue ===") + queue = Queue() + + # Enqueue elements + queue.enqueue(10) + queue.enqueue(20) + queue.enqueue(30) + print("Queue after enqueues:", queue.display()) + print("Queue size:", queue.size()) + + # Check front and rear + print("Front element:", queue.front()) + print("Rear element:", queue.rear()) + + # Dequeue elements + dequeued = queue.dequeue() + print(f"Dequeued element: {dequeued}") + print("Queue after dequeue:", queue.display()) + + # Search for element + position = queue.search(30) + print(f"Element 30 is at position: {position}") + + # Circular Queue + print("\n=== Circular Queue ===") + cq = CircularQueue(5) + + # Fill the circular queue + for i in range(1, 6): + cq.enqueue(i * 10) + print("Circular queue (full):", cq.display()) + + # Dequeue and enqueue to show circular behavior + cq.dequeue() + cq.dequeue() + print("After 2 dequeues:", cq.display()) + + cq.enqueue(60) + cq.enqueue(70) + print("After adding 60, 70:", cq.display()) \ No newline at end of file diff --git a/data_structures/stacks/stack.py b/data_structures/stacks/stack.py new file mode 100644 index 0000000..5ef596e --- /dev/null +++ b/data_structures/stacks/stack.py @@ -0,0 +1,129 @@ +""" +Stack Implementation using List + +A stack is a linear data structure that follows the LIFO (Last In, First Out) principle. +Elements are added and removed from the same end called the "top" of the stack. + +Time Complexities: +- Push: O(1) +- Pop: O(1) +- Peek/Top: O(1) +- Search: O(n) + +Space Complexity: O(n) + +Common Applications: +- Function call management (call stack) +- Expression evaluation and syntax parsing +- Undo operations in editors +- Browser history +""" + +class Stack: + def __init__(self, capacity=None): + """Initialize an empty stack with optional capacity limit""" + self.items = [] + self.capacity = capacity + + def push(self, item): + """Add an item to the top of the stack""" + if self.capacity is not None and len(self.items) >= self.capacity: + raise Exception("Stack overflow: Stack is full") + self.items.append(item) + + def pop(self): + """Remove and return the top item from the stack""" + if self.is_empty(): + raise Exception("Stack underflow: Stack is empty") + return self.items.pop() + + def peek(self): + """Return the top item without removing it""" + if self.is_empty(): + raise Exception("Stack is empty") + return self.items[-1] + + def is_empty(self): + """Check if the stack is empty""" + return len(self.items) == 0 + + def is_full(self): + """Check if the stack is full (only relevant if capacity is set)""" + if self.capacity is None: + return False + return len(self.items) >= self.capacity + + def size(self): + """Return the number of items in the stack""" + return len(self.items) + + def search(self, item): + """Search for an item and return its distance from the top (1-based)""" + try: + index = self.items[::-1].index(item) + return index + 1 # 1-based distance from top + except ValueError: + return -1 + + def display(self): + """Display the stack contents (top to bottom)""" + return list(reversed(self.items)) + +def balanced_parentheses(expression): + """ + Example application: Check if parentheses are balanced + Returns True if balanced, False otherwise + """ + stack = Stack() + opening = {'(', '[', '{'} + closing = {')', ']', '}'} + pairs = {'(': ')', '[': ']', '{': '}'} + + for char in expression: + if char in opening: + stack.push(char) + elif char in closing: + if stack.is_empty(): + return False + if pairs[stack.pop()] != char: + return False + + return stack.is_empty() + +# Example usage +if __name__ == "__main__": + # Basic stack operations + stack = Stack() + + # Push elements + stack.push(10) + stack.push(20) + stack.push(30) + print("Stack after pushes:", stack.display()) + print("Stack size:", stack.size()) + + # Peek at top + print("Top element:", stack.peek()) + + # Pop elements + popped = stack.pop() + print(f"Popped element: {popped}") + print("Stack after pop:", stack.display()) + + # Search for element + distance = stack.search(10) + print(f"Element 10 is at distance {distance} from top") + + # Test balanced parentheses + test_expressions = [ + "((()))", # True + "()[]{()}", # True + "([)]", # False + "(((", # False + "" # True (empty string) + ] + + print("\nBalanced Parentheses Tests:") + for expr in test_expressions: + result = balanced_parentheses(expr) + print(f"'{expr}' -> {result}") \ No newline at end of file diff --git a/data_structures/trees/binary_search_tree.py b/data_structures/trees/binary_search_tree.py new file mode 100644 index 0000000..3fe190c --- /dev/null +++ b/data_structures/trees/binary_search_tree.py @@ -0,0 +1,242 @@ +""" +Binary Search Tree (BST) Implementation + +A binary search tree is a hierarchical data structure where each node has at most +two children (left and right), and for every node: +- All values in the left subtree are less than the node's value +- All values in the right subtree are greater than the node's value + +Time Complexities (Average Case): +- Search: O(log n) +- Insertion: O(log n) +- Deletion: O(log n) + +Time Complexities (Worst Case - skewed tree): +- Search: O(n) +- Insertion: O(n) +- Deletion: O(n) + +Space Complexity: O(n) +""" + +class TreeNode: + def __init__(self, value): + self.value = value + self.left = None + self.right = None + +class BinarySearchTree: + def __init__(self): + self.root = None + + def insert(self, value): + """Insert a value into the BST""" + self.root = self._insert_recursive(self.root, value) + + def _insert_recursive(self, node, value): + """Helper method for recursive insertion""" + if node is None: + return TreeNode(value) + + if value < node.value: + node.left = self._insert_recursive(node.left, value) + elif value > node.value: + node.right = self._insert_recursive(node.right, value) + # If value equals node.value, we don't insert (no duplicates) + + return node + + def search(self, value): + """Search for a value in the BST""" + return self._search_recursive(self.root, value) + + def _search_recursive(self, node, value): + """Helper method for recursive search""" + if node is None: + return False + + if value == node.value: + return True + elif value < node.value: + return self._search_recursive(node.left, value) + else: + return self._search_recursive(node.right, value) + + def delete(self, value): + """Delete a value from the BST""" + self.root = self._delete_recursive(self.root, value) + + def _delete_recursive(self, node, value): + """Helper method for recursive deletion""" + if node is None: + return node + + if value < node.value: + node.left = self._delete_recursive(node.left, value) + elif value > node.value: + node.right = self._delete_recursive(node.right, value) + else: + # Node to be deleted found + + # Case 1: Node has no children (leaf node) + if node.left is None and node.right is None: + return None + + # Case 2: Node has one child + elif node.left is None: + return node.right + elif node.right is None: + return node.left + + # Case 3: Node has two children + else: + # Find the inorder successor (smallest value in right subtree) + successor = self._find_min(node.right) + node.value = successor.value + node.right = self._delete_recursive(node.right, successor.value) + + return node + + def _find_min(self, node): + """Find the node with minimum value in the subtree""" + while node.left is not None: + node = node.left + return node + + def _find_max(self, node): + """Find the node with maximum value in the subtree""" + while node.right is not None: + node = node.right + return node + + # Tree Traversal Methods + def inorder_traversal(self): + """Inorder traversal: Left -> Root -> Right (gives sorted order)""" + result = [] + self._inorder_recursive(self.root, result) + return result + + def _inorder_recursive(self, node, result): + if node is not None: + self._inorder_recursive(node.left, result) + result.append(node.value) + self._inorder_recursive(node.right, result) + + def preorder_traversal(self): + """Preorder traversal: Root -> Left -> Right""" + result = [] + self._preorder_recursive(self.root, result) + return result + + def _preorder_recursive(self, node, result): + if node is not None: + result.append(node.value) + self._preorder_recursive(node.left, result) + self._preorder_recursive(node.right, result) + + def postorder_traversal(self): + """Postorder traversal: Left -> Right -> Root""" + result = [] + self._postorder_recursive(self.root, result) + return result + + def _postorder_recursive(self, node, result): + if node is not None: + self._postorder_recursive(node.left, result) + self._postorder_recursive(node.right, result) + result.append(node.value) + + def level_order_traversal(self): + """Level order traversal (BFS): Visit nodes level by level""" + if not self.root: + return [] + + result = [] + queue = [self.root] + + while queue: + node = queue.pop(0) + result.append(node.value) + + if node.left: + queue.append(node.left) + if node.right: + queue.append(node.right) + + return result + + def find_height(self): + """Find the height of the tree""" + return self._height_recursive(self.root) + + def _height_recursive(self, node): + if node is None: + return -1 + + left_height = self._height_recursive(node.left) + right_height = self._height_recursive(node.right) + + return max(left_height, right_height) + 1 + + def count_nodes(self): + """Count total number of nodes in the tree""" + return self._count_recursive(self.root) + + def _count_recursive(self, node): + if node is None: + return 0 + + return 1 + self._count_recursive(node.left) + self._count_recursive(node.right) + + def is_valid_bst(self): + """Check if the tree is a valid BST""" + return self._is_valid_bst_recursive(self.root, float('-inf'), float('inf')) + + def _is_valid_bst_recursive(self, node, min_val, max_val): + if node is None: + return True + + if node.value <= min_val or node.value >= max_val: + return False + + return (self._is_valid_bst_recursive(node.left, min_val, node.value) and + self._is_valid_bst_recursive(node.right, node.value, max_val)) + +# Example usage +if __name__ == "__main__": + bst = BinarySearchTree() + + # Insert values + values = [50, 30, 70, 20, 40, 60, 80, 10, 25, 35, 45] + for value in values: + bst.insert(value) + + print("BST created with values:", values) + print("Tree height:", bst.find_height()) + print("Total nodes:", bst.count_nodes()) + print("Is valid BST:", bst.is_valid_bst()) + + # Traversals + print("\nTraversals:") + print("Inorder (sorted):", bst.inorder_traversal()) + print("Preorder:", bst.preorder_traversal()) + print("Postorder:", bst.postorder_traversal()) + print("Level order:", bst.level_order_traversal()) + + # Search operations + search_values = [25, 55, 80] + print(f"\nSearch operations:") + for val in search_values: + found = bst.search(val) + print(f"Search {val}: {'Found' if found else 'Not Found'}") + + # Delete operations + print(f"\nBefore deletion:", bst.inorder_traversal()) + bst.delete(20) # Delete leaf node + print(f"After deleting 20:", bst.inorder_traversal()) + + bst.delete(30) # Delete node with two children + print(f"After deleting 30:", bst.inorder_traversal()) + + bst.delete(50) # Delete root + print(f"After deleting 50:", bst.inorder_traversal()) \ No newline at end of file diff --git a/examples/example_usage.py b/examples/example_usage.py new file mode 100644 index 0000000..357410f --- /dev/null +++ b/examples/example_usage.py @@ -0,0 +1,195 @@ +""" +Data Structures and Algorithms - Example Usage + +This file demonstrates practical usage of various data structures and algorithms +implemented in this repository. +""" + +import sys +import os + +# Add the parent directory to the path to import modules +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from data_structures.arrays.array_operations import ArrayOperations +from data_structures.linked_lists.singly_linked_list import SinglyLinkedList +from data_structures.stacks.stack import Stack, balanced_parentheses +from data_structures.queues.queue import Queue, CircularQueue +from data_structures.trees.binary_search_tree import BinarySearchTree +from algorithms.sorting.sorting_algorithms import quick_sort, merge_sort +from algorithms.searching.searching_algorithms import binary_search, linear_search + +def demonstrate_data_structures(): + """Demonstrate various data structures""" + print("=" * 60) + print("DATA STRUCTURES DEMONSTRATION") + print("=" * 60) + + # Array Example + print("\n1. ARRAY OPERATIONS") + print("-" * 30) + arr = ArrayOperations(10) + for i, val in enumerate([5, 10, 15, 20, 25]): + arr.insert(i, val) + print(f"Array: {arr.display()}") + print(f"Search 15: Index {arr.search(15)}") + arr.delete(2) # Delete 15 + print(f"After deleting index 2: {arr.display()}") + + # Linked List Example + print("\n2. LINKED LIST OPERATIONS") + print("-" * 30) + ll = SinglyLinkedList() + for val in [1, 2, 3, 4, 5]: + ll.insert_at_tail(val) + print(f"Linked List: {ll.display()}") + ll.insert_at_position(2, 99) + print(f"After inserting 99 at position 2: {ll.display()}") + ll.delete_by_value(99) + print(f"After deleting 99: {ll.display()}") + + # Stack Example + print("\n3. STACK OPERATIONS") + print("-" * 30) + stack = Stack() + for val in [10, 20, 30, 40]: + stack.push(val) + print(f"Stack: {stack.display()}") + print(f"Top element: {stack.peek()}") + print(f"Popped: {stack.pop()}") + print(f"Stack after pop: {stack.display()}") + + # Queue Example + print("\n4. QUEUE OPERATIONS") + print("-" * 30) + queue = Queue() + for val in ['A', 'B', 'C', 'D']: + queue.enqueue(val) + print(f"Queue: {queue.display()}") + print(f"Front: {queue.front()}, Rear: {queue.rear()}") + print(f"Dequeued: {queue.dequeue()}") + print(f"Queue after dequeue: {queue.display()}") + + # Binary Search Tree Example + print("\n5. BINARY SEARCH TREE") + print("-" * 30) + bst = BinarySearchTree() + values = [50, 30, 70, 20, 40, 60, 80] + for val in values: + bst.insert(val) + print(f"BST Inorder (sorted): {bst.inorder_traversal()}") + print(f"BST Level order: {bst.level_order_traversal()}") + print(f"Tree height: {bst.find_height()}") + print(f"Search 40: {bst.search(40)}") + +def solve_practical_problems(): + """Solve practical problems using data structures and algorithms""" + print("\n" + "=" * 60) + print("PRACTICAL PROBLEM SOLVING") + print("=" * 60) + + # Problem 1: Balanced Parentheses + print("\n1. BALANCED PARENTHESES CHECKER") + print("-" * 40) + expressions = ["((()))", "()[]{}", "([)]", "((("] + for expr in expressions: + result = balanced_parentheses(expr) + print(f"'{expr}' -> {'Balanced' if result else 'Not Balanced'}") + + # Problem 2: Find duplicates using data structures + print("\n2. FIND DUPLICATES IN ARRAY") + print("-" * 40) + def find_duplicates_with_set(arr): + seen = set() + duplicates = set() + for item in arr: + if item in seen: + duplicates.add(item) + else: + seen.add(item) + return list(duplicates) + + test_array = [1, 2, 3, 4, 2, 5, 6, 3, 7, 8, 1] + print(f"Array: {test_array}") + print(f"Duplicates: {find_duplicates_with_set(test_array)}") + + # Problem 3: Sorting comparison + print("\n3. SORTING PERFORMANCE COMPARISON") + print("-" * 40) + import time + import random + + # Generate random data + data = [random.randint(1, 1000) for _ in range(100)] + print(f"Sorting {len(data)} random numbers...") + + algorithms = [ + ("Quick Sort", quick_sort), + ("Merge Sort", merge_sort) + ] + + for name, func in algorithms: + start_time = time.time() + sorted_data = func(data.copy()) + end_time = time.time() + print(f"{name}: {(end_time - start_time) * 1000:.2f} ms") + + # Problem 4: Search comparison + print("\n4. SEARCH PERFORMANCE COMPARISON") + print("-" * 40) + sorted_data = sorted(data) + target = sorted_data[len(sorted_data) // 2] # Middle element + + search_algorithms = [ + ("Linear Search", linear_search), + ("Binary Search", binary_search) + ] + + print(f"Searching for {target} in sorted array of {len(sorted_data)} elements:") + for name, func in search_algorithms: + start_time = time.time() + if name == "Linear Search": + result = func(sorted_data, target) + else: + result = func(sorted_data, target) + end_time = time.time() + print(f"{name}: Found at index {result} in {(end_time - start_time) * 1000000:.2f} μs") + +def demonstrate_algorithm_complexity(): + """Demonstrate time complexity differences""" + print("\n" + "=" * 60) + print("ALGORITHM COMPLEXITY DEMONSTRATION") + print("=" * 60) + + import time + import random + + sizes = [100, 500, 1000] + + print("\nSorting Algorithm Performance (Time in ms):") + print("Size\tQuick Sort\tMerge Sort") + print("-" * 40) + + for size in sizes: + data = [random.randint(1, 1000) for _ in range(size)] + + # Quick Sort + start = time.time() + quick_sort(data.copy()) + quick_time = (time.time() - start) * 1000 + + # Merge Sort + start = time.time() + merge_sort(data.copy()) + merge_time = (time.time() - start) * 1000 + + print(f"{size}\t{quick_time:.2f}\t\t{merge_time:.2f}") + +if __name__ == "__main__": + demonstrate_data_structures() + solve_practical_problems() + demonstrate_algorithm_complexity() + + print("\n" + "=" * 60) + print("EXAMPLES COMPLETED SUCCESSFULLY!") + print("=" * 60) \ No newline at end of file