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
2 changes: 1 addition & 1 deletion data_structures/hashing/hash_table_with_linked_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def _set_value(self, key, data):
self.values[key] = deque([]) if self.values[key] is None else self.values[key]
self.values[key] = deque() if self.values[key] is None else self.values[key]
self.values[key].appendleft(data)
self._keys[key] = self.values[key]

Expand Down
29 changes: 13 additions & 16 deletions graphs/check_bipatrite.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
from collections import defaultdict, deque
from collections.abc import Hashable, Iterable, Mapping


def is_bipartite_dfs(graph: dict[int, list[int]]) -> bool:
def is_bipartite_dfs(graph: Mapping[Hashable, Iterable[Hashable]]) -> bool:
"""
Check if a graph is bipartite using depth-first search (DFS).

Args:
`graph`: Adjacency list representing the graph.
`graph`: Mapping of nodes to their neighbors. Nodes must be hashable.

Returns:
``True`` if bipartite, ``False`` otherwise.

Checks if the graph can be divided into two sets of vertices, such that no two
vertices within the same set are connected by an edge.
vertices within the same set are connected by an edge. Neighbor nodes that do
not appear as keys are treated as isolated nodes with no outgoing edges.

Examples:

Expand All @@ -33,7 +35,6 @@ def is_bipartite_dfs(graph: dict[int, list[int]]) -> bool:
>>> is_bipartite_dfs({7: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2], 4: [0]})
False

>>> # FIXME: This test should fails with KeyError: 4.
>>> is_bipartite_dfs({0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2], 9: [0]})
False
>>> is_bipartite_dfs({0: [-1, 3], 1: [0, -2]})
Expand All @@ -43,8 +44,6 @@ def is_bipartite_dfs(graph: dict[int, list[int]]) -> bool:
>>> is_bipartite_dfs({0.9: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2]})
True

>>> # FIXME: This test should fails with
>>> # TypeError: list indices must be integers or...
>>> is_bipartite_dfs({0: [1.0, 3.0], 1.0: [0, 2.0], 2.0: [1.0, 3.0], 3.0: [0, 2.0]})
True
>>> is_bipartite_dfs({"a": [1, 3], "b": [0, 2], "c": [1, 3], "d": [0, 2]})
Expand All @@ -53,7 +52,7 @@ def is_bipartite_dfs(graph: dict[int, list[int]]) -> bool:
True
"""

def depth_first_search(node: int, color: int) -> bool:
def depth_first_search(node: Hashable, color: int) -> bool:
"""
Perform Depth-First Search (DFS) on the graph starting from a node.

Expand All @@ -74,25 +73,26 @@ def depth_first_search(node: int, color: int) -> bool:
return False
return visited[node] == color

visited: defaultdict[int, int] = defaultdict(lambda: -1)
visited: defaultdict[Hashable, int] = defaultdict(lambda: -1)
for node in graph:
if visited[node] == -1 and not depth_first_search(node, 0):
return False
return True


def is_bipartite_bfs(graph: dict[int, list[int]]) -> bool:
def is_bipartite_bfs(graph: Mapping[Hashable, Iterable[Hashable]]) -> bool:
"""
Check if a graph is bipartite using a breadth-first search (BFS).

Args:
`graph`: Adjacency list representing the graph.
`graph`: Mapping of nodes to their neighbors. Nodes must be hashable.

Returns:
``True`` if bipartite, ``False`` otherwise.

Check if the graph can be divided into two sets of vertices, such that no two
vertices within the same set are connected by an edge.
vertices within the same set are connected by an edge. Neighbor nodes that do
not appear as keys are treated as isolated nodes with no outgoing edges.

Examples:

Expand All @@ -113,7 +113,6 @@ def is_bipartite_bfs(graph: dict[int, list[int]]) -> bool:
>>> is_bipartite_bfs({7: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2], 4: [0]})
False

>>> # FIXME: This test should fails with KeyError: 4.
>>> is_bipartite_bfs({0: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2], 9: [0]})
False
>>> is_bipartite_bfs({0: [-1, 3], 1: [0, -2]})
Expand All @@ -123,19 +122,17 @@ def is_bipartite_bfs(graph: dict[int, list[int]]) -> bool:
>>> is_bipartite_bfs({0.9: [1, 3], 1: [0, 2], 2: [1, 3], 3: [0, 2]})
True

>>> # FIXME: This test should fails with
>>> # TypeError: list indices must be integers or...
>>> is_bipartite_bfs({0: [1.0, 3.0], 1.0: [0, 2.0], 2.0: [1.0, 3.0], 3.0: [0, 2.0]})
True
>>> is_bipartite_bfs({"a": [1, 3], "b": [0, 2], "c": [1, 3], "d": [0, 2]})
True
>>> is_bipartite_bfs({0: ["b", "d"], 1: ["a", "c"], 2: ["b", "d"], 3: ["a", "c"]})
True
"""
visited: defaultdict[int, int] = defaultdict(lambda: -1)
visited: defaultdict[Hashable, int] = defaultdict(lambda: -1)
for node in graph:
if visited[node] == -1:
queue: deque[int] = deque()
queue: deque[Hashable] = deque()
queue.append(node)
visited[node] = 0
while queue:
Expand Down
12 changes: 4 additions & 8 deletions machine_learning/linear_discriminant_analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@
from math import log
from os import name, system
from random import gauss, seed
from typing import TypeVar


# Make a training dataset drawn from a gaussian distribution
Expand Down Expand Up @@ -249,16 +248,13 @@ def accuracy(actual_y: list, predicted_y: list) -> float:
return (correct / len(actual_y)) * 100


num = TypeVar("num")


def valid_input(
input_type: Callable[[object], num], # Usually float or int
def valid_input[T](
input_type: Callable[[object], T], # Usually float or int
input_msg: str,
err_msg: str,
condition: Callable[[num], bool] = lambda _: True,
condition: Callable[[T], bool] = lambda _: True,
default: str | None = None,
) -> num:
) -> T:
"""
Ask for user value and validate that it fulfill a condition.

Expand Down
7 changes: 2 additions & 5 deletions searches/jump_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,14 @@

import math
from collections.abc import Sequence
from typing import Any, Protocol, TypeVar
from typing import Any, Protocol


class Comparable(Protocol):
def __lt__(self, other: Any, /) -> bool: ...


T = TypeVar("T", bound=Comparable)


def jump_search(arr: Sequence[T], item: T) -> int:
def jump_search[T: Comparable](arr: Sequence[T], item: T) -> int:
"""
Comment on lines +20 to 21
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR title/description focuses on doctests for sentinel_linear_search(), but this change set also modifies typing/generics in other modules (e.g., jump_search, LDA valid_input, bipartite checks, hash table). Please update the PR description to reflect these changes or split them into separate PRs so review/rollback scope is clear.

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Python implementation of the jump search algorithm.
Return the index if the `item` is found, otherwise return -1.
Expand Down
38 changes: 37 additions & 1 deletion searches/sentinel_linear_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,17 @@
def sentinel_linear_search(sequence, target):
"""Pure implementation of sentinel linear search algorithm in Python

:param sequence: some sequence with comparable items
:param sequence: a mutable list with comparable items. Note: this function
temporarily mutates the input list by appending and then removing the
target value, but the list is restored to its original state.
:param target: item value to search
:return: index of found item or None if item is not found

The sentinel linear search algorithm works by appending the target value to the
end of the list before searching. This eliminates the need for boundary checking
during the loop, as the search is guaranteed to find the target (at minimum, at
the end where the sentinel is placed). The input list is restored after the search.

Examples:
>>> sentinel_linear_search([0, 5, 7, 10, 15], 0)
0
Expand All @@ -30,6 +37,35 @@ def sentinel_linear_search(sequence, target):

>>> sentinel_linear_search([0, 5, 7, 10, 15], 6)

>>> sentinel_linear_search([1], 1)
0

>>> sentinel_linear_search([1], 99)

>>> sentinel_linear_search([], 5)

>>> sentinel_linear_search([3, 1, 4, 1, 5, 9, 2, 6], 9)
5

>>> sentinel_linear_search([3, 1, 4, 1, 5, 9, 2, 6], 1)
1

>>> sentinel_linear_search(['a', 'b', 'c', 'd'], 'c')
2

>>> sentinel_linear_search(['a', 'b', 'c', 'd'], 'z')

>>> sentinel_linear_search([1.5, 2.7, 3.14, 4.0], 3.14)
2

>>> sentinel_linear_search([1.5, 2.7, 3.14, 4.0], 2.5)

>>> sentinel_linear_search([-10, -5, 0, 5, 10], -5)
1

>>> sentinel_linear_search([-10, -5, 0, 5, 10], 0)
2

"""
sequence.append(target)

Expand Down