diff --git a/digital_image_processing/change_brightness.py b/digital_image_processing/change_brightness.py index 97493f1a399e..6f14c57905ba 100644 --- a/digital_image_processing/change_brightness.py +++ b/digital_image_processing/change_brightness.py @@ -1,26 +1,54 @@ +""" +Module for adjusting the brightness of images using PIL. +""" + from PIL import Image -def change_brightness(img: Image, level: float) -> Image: - """ - Change the brightness of a PIL Image to a given level. - """ +def change_brightness(img: Image.Image, level: float) -> Image.Image: + """Change the brightness of a PIL Image by the given level. - def brightness(c: int) -> float: - """ - Fundamental Transformation/Operation that'll be performed on - every bit. - """ - return 128 + level + (c - 128) + Args: + img: The input PIL Image to modify. + level: Brightness adjustment level between -255.0 (darker) and + 255.0 (brighter). 0 means no change. + Returns: + A new Image with adjusted brightness. + + Raises: + ValueError: If level is outside the valid range [-255.0, 255.0]. + + Examples: + >>> from PIL import Image + >>> img = Image.new("RGB", (10, 10), (128, 128, 128)) + >>> bright_img = change_brightness(img, 50) + >>> bright_img.getpixel((5, 5)) # doctest: +ELLIPSIS + (178, 178, 178) + """ if not -255.0 <= level <= 255.0: raise ValueError("level must be between -255.0 (black) and 255.0 (white)") - return img.point(brightness) + + def adjust_pixel(c: int) -> int: + """Adjust a single pixel channel value by the brightness level.""" + return max(0, min(255, c + int(level))) + + return img.point(adjust_pixel) if __name__ == "__main__": - # Load image - with Image.open("image_data/lena.jpg") as img: - # Change brightness to 100 - brigt_img = change_brightness(img, 100) - brigt_img.save("image_data/lena_brightness.png", format="png") + import sys + + if len(sys.argv) != 4: + print("Usage: python change_brightness.py ") + print(" level: brightness adjustment from -255 to 255") + sys.exit(1) + + input_path = sys.argv[1] + output_path = sys.argv[2] + level = float(sys.argv[3]) + + with Image.open(input_path) as img: + bright_img = change_brightness(img, level) + bright_img.save(output_path, format="png") + print(f"Brightness changed by {level} and saved to {output_path}") diff --git a/dynamic_programming/abbreviation.py b/dynamic_programming/abbreviation.py index 5175aa9ed92f..28c996b4d9ee 100644 --- a/dynamic_programming/abbreviation.py +++ b/dynamic_programming/abbreviation.py @@ -1,35 +1,69 @@ """ -https://www.hackerrank.com/challenges/abbr/problem -You can perform the following operation on some string, : +String abbreviation problem solved using dynamic programming. -1. Capitalize zero or more of 's lowercase letters at some index i - (i.e., make them uppercase). -2. Delete all of the remaining lowercase letters in . +Given two strings 'a' and 'b', determine if 'a' can be transformed to 'b' by: +1. Capitalizing zero or more of 'a's lowercase letters at any index. +2. Deleting all remaining lowercase letters. + +Reference: https://www.hackerrank.com/challenges/Abbr/problem Example: -a=daBcd and b="ABC" -daBcd -> capitalize a and c(dABCd) -> remove d (ABC) + a = "daBcd" and b = "ABC" + "daBcd" -> capitalize 'a' and 'c' to get "dABCd" -> delete 'd' to get "ABC" """ -def abbr(a: str, b: str) -> bool: - """ - >>> abbr("daBcd", "ABC") +def abbreviation(a: str, b: str) -> bool: + """Check if string 'a' can be transformed to 'b'. + + Uses dynamic programming where dp[i][j] indicates whether the first i + characters of 'a' can be transformed to the first j characters of 'b'. + + Args: + a: The source string containing lowercase and uppercase letters. + b: The target string containing only uppercase letters. + + Returns: + True if 'a' can be transformed to 'b', False otherwise. + + Examples: + >>> abbreviation("daBcd", "ABC") + True + >>> abbreviation("dBcd", "ABC") + False + >>> abbreviation("ABC", "ABC") True - >>> abbr("dBcd", "ABC") + >>> abbreviation("abc", "ABC") False + >>> abbreviation("abCD", "ABCD") + True """ n = len(a) m = len(b) - dp = [[False for _ in range(m + 1)] for _ in range(n + 1)] + + # Early exit: if 'b' has lowercase letters, it can never match + if b != b.upper(): + return False + + # Early exit: if 'a' has more uppercase letters than the length of 'b' + a_upper_count = sum(1 for c in a if c.isupper()) + if a_upper_count > m: + return False + + dp = [[False] * (m + 1) for _ in range(n + 1)] dp[0][0] = True + for i in range(n): for j in range(m + 1): - if dp[i][j]: - if j < m and a[i].upper() == b[j]: - dp[i + 1][j + 1] = True - if a[i].islower(): - dp[i + 1][j] = True + if not dp[i][j]: + continue + + if j < m and a[i].upper() == b[j]: + dp[i + 1][j + 1] = True + + if a[i].islower(): + dp[i + 1][j] = True + return dp[n][m] diff --git a/graphs/g_topological_sort.py b/graphs/g_topological_sort.py index 77543d51f61d..0fe1ff25a36c 100644 --- a/graphs/g_topological_sort.py +++ b/graphs/g_topological_sort.py @@ -1,47 +1,131 @@ -# Author: Phyllipe Bezerra (https://github.com/pmba) +""" +Graph topological sort implementation using Depth-First Search (DFS). -clothes = { - 0: "underwear", - 1: "pants", - 2: "belt", - 3: "suit", - 4: "shoe", - 5: "socks", - 6: "shirt", - 7: "tie", - 8: "watch", -} +A topological sort or topological ordering of a directed graph is a linear +ordering of its vertices in which each vertex comes before all vertices +to which it has outgoing edges. -graph = [[1, 4], [2, 4], [3], [], [], [4], [2, 7], [3], []] +For example, the graph representing clothing dependencies: +- underwear -> pants -> belt -> suit +- shirt -> tie -> suit +- socks -> shoes -visited = [0 for x in range(len(graph))] -stack = [] +Author: Phyllipe Bezerra (https://github.com/pmba) +""" +from collections.abc import Sequence -def print_stack(stack, clothes): - order = 1 - while stack: - current_clothing = stack.pop() - print(order, clothes[current_clothing]) - order += 1 +class TopologicalSort: + """Topological sort implementation using DFS.""" -def depth_first_search(u, visited, graph): - visited[u] = 1 - for v in graph[u]: - if not visited[v]: - depth_first_search(v, visited, graph) + def __init__( + self, graph: list[list[int]], labels: dict[int, str] | None = None + ) -> None: + """Initialize the topological sorter. + + Args: + graph: Adjacency list representation where graph[u] contains + all vertices v such that there is an edge from u to v. + labels: Optional mapping from vertex indices to label strings + for pretty printing results. + """ + self.graph = graph + self.labels = labels or {} + self.n = len(graph) + self.visited: list[int] = [0] * self.n + self.stack: list[int] = [] + + def _depth_first_search(self, u: int) -> None: + """Perform DFS from vertex u, adding to stack after exploring all neighbors.""" + self.visited[u] = 1 + for v in self.graph[u]: + if not self.visited[v]: + self._depth_first_search(v) + self.stack.append(u) + + def sort(self) -> list[int]: + """Compute and return the topological ordering of vertices. + + Returns: + A list of vertices in topological order (each vertex appears before + all vertices reachable from it). + """ + self.stack = [] + self.visited = [0] * self.n + + for v in range(self.n): + if not self.visited[v]: + self._depth_first_search(v) + + return self.stack[::-1] + + def print_sorted(self, order: list[int]) -> None: + """Print the vertices in topological order with their labels. + + Args: + order: A list of vertices in topological order. + """ + for i, vertex in enumerate(order, 1): + label = self.labels.get(vertex, str(vertex)) + print(f"{i}. {label}") - stack.append(u) +def topological_sort(graph: list[list[int]], visited: list[int]) -> list[int]: + """Topological sort of a directed acyclic graph using DFS. + + Args: + graph: Adjacency list representation of the graph. + visited: List to track visited vertices (modified in place). + + Returns: + Vertices in topological order. + + Examples: + >>> graph = [[1, 2], [3], [3], []] + >>> visited = [0] * len(graph) + >>> topological_sort(graph, visited) + [0, 2, 1, 3] + """ + stack = [] + + def dfs(u: int) -> None: + visited[u] = 1 + for v in graph[u]: + if not visited[v]: + dfs(v) + stack.append(u) -def topological_sort(graph, visited): for v in range(len(graph)): if not visited[v]: - depth_first_search(v, visited, graph) + dfs(v) + + return stack[::-1] if __name__ == "__main__": - topological_sort(graph, visited) - print(stack) - print_stack(stack, clothes) + # Example: Clothing dependencies + clothes = { + 0: "underwear", + 1: "pants", + 2: "belt", + 3: "suit", + 4: "shoe", + 5: "socks", + 6: "shirt", + 7: "tie", + 8: "watch", + } + + graph = [[1, 4], [2, 4], [3], [], [], [4], [2, 7], [3], []] + + # Using the class-based interface + sorter = TopologicalSort(graph, clothes) + order = sorter.sort() + sorter.print_sorted(order) + + # Also demonstrate the functional interface + print("\n--- Using functional interface ---") + visited = [0] * len(graph) + result = topological_sort(graph, visited) + print(result) diff --git a/searches/sentinel_linear_search.py b/searches/sentinel_linear_search.py index 69c1cf9f351a..0a05ea91bee0 100644 --- a/searches/sentinel_linear_search.py +++ b/searches/sentinel_linear_search.py @@ -1,5 +1,8 @@ """ -This is pure Python implementation of sentinel linear search algorithm +This is pure Python implementation of sentinel linear search algorithm. + +The sentinel linear search improves on linear search by adding a sentinel +(target value) at the end of the list to avoid bounds checking on each iteration. For doctests run following command: python -m doctest -v sentinel_linear_search.py @@ -10,26 +13,30 @@ python sentinel_linear_search.py """ +from collections.abc import Sequence +from typing import Any + -def sentinel_linear_search(sequence, target): - """Pure implementation of sentinel linear search algorithm in Python +def sentinel_linear_search(sequence: list, target: Any) -> int | None: + """Pure implementation of sentinel linear search algorithm in Python. - :param sequence: some sequence with comparable items - :param target: item value to search - :return: index of found item or None if item is not found + Args: + sequence: A list of comparable items. Note: this list will be modified + temporarily by appending the target value (restored before return). + target: The item value to search for. + + Returns: + The index of the found item, or None if the item is not found. Examples: >>> sentinel_linear_search([0, 5, 7, 10, 15], 0) 0 - >>> sentinel_linear_search([0, 5, 7, 10, 15], 15) 4 - >>> sentinel_linear_search([0, 5, 7, 10, 15], 5) 1 - - >>> sentinel_linear_search([0, 5, 7, 10, 15], 6) - + >>> sentinel_linear_search([0, 5, 7, 10, 15], 6) is None + True """ sequence.append(target) @@ -39,7 +46,7 @@ def sentinel_linear_search(sequence, target): sequence.pop() - if index == len(sequence): + if index >= len(sequence): return None return index @@ -53,6 +60,6 @@ def sentinel_linear_search(sequence, target): target = int(target_input) result = sentinel_linear_search(sequence, target) if result is not None: - print(f"{target} found at positions: {result}") + print(f"{target} found at position: {result}") else: print("Not found") diff --git a/sorts/pancake_sort.py b/sorts/pancake_sort.py index e5d600738435..ab3c1c326953 100644 --- a/sorts/pancake_sort.py +++ b/sorts/pancake_sort.py @@ -1,18 +1,32 @@ """ -This is a pure Python implementation of the pancake sort algorithm +This is a pure Python implementation of the pancake sort algorithm. + +Pancake sort works by repeatedly finding the maximum element in the unsorted +portion and flipping the subarray from the beginning to that position, then +flipping the entire unsorted portion to move the maximum to its correct position. + For doctests run following command: python3 -m doctest -v pancake_sort.py or python -m doctest -v pancake_sort.py + For manual testing run: python pancake_sort.py """ +from collections.abc import Collection +from typing import Any + + +def pancake_sort(arr: list) -> list: + """Sort a list using Pancake Sort. + + Args: + arr: A list of comparable items. + + Returns: + A new list with items ordered in ascending order. -def pancake_sort(arr): - """Sort Array with Pancake Sort. - :param arr: Collection containing comparable items - :return: Collection ordered in ascending order of items Examples: >>> pancake_sort([0, 5, 3, 2, 2]) [0, 2, 2, 3, 5] @@ -21,16 +35,25 @@ def pancake_sort(arr): >>> pancake_sort([-2, -5, -45]) [-45, -5, -2] """ - cur = len(arr) + if not arr: + return [] + + result = arr.copy() + cur = len(result) + while cur > 1: - # Find the maximum number in arr - mi = arr.index(max(arr[0:cur])) - # Reverse from 0 to mi - arr = arr[mi::-1] + arr[mi + 1 : len(arr)] - # Reverse whole list - arr = arr[cur - 1 :: -1] + arr[cur : len(arr)] + # Find the index of the maximum number in the unsorted portion + max_index = result[:cur].index(max(result[:cur])) + + # Flip from 0 to max_index to move max to the front + result[: max_index + 1] = result[: max_index + 1][::-1] + + # Flip from 0 to cur-1 to move max to its correct position + result[:cur] = result[:cur][::-1] + cur -= 1 - return arr + + return result if __name__ == "__main__":