Skip to content
Merged
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v4
- uses: erlef/setup-beam@v1
with:
otp-version: '25'
elixir-version: '1.14'
otp-version: '28'
elixir-version: '1.19'
- run: mix deps.get
- run: mix test
67 changes: 67 additions & 0 deletions lib/ex_algo/tree/binary_search_tree.ex
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,71 @@ defmodule ExAlgo.Tree.BinarySearchTree do
_ -> find(left, key, key_fn)
end
end

@doc """
Delete an item from the tree.

## Example

iex> tree = BST.from([10, 5, 15, 3, 7, 12, 20])
iex> tree = BST.delete(tree, 15)
iex> Traversal.inorder(tree)
[3, 5, 7, 10, 12, 20]

iex> tree = BST.from([10, 5, 15])
iex> tree = BST.delete(tree, 10)
iex> Traversal.inorder(tree)
[5, 15]

iex> tree = BST.from([10, 5, 15])
iex> tree = BST.delete(tree, 99)
iex> Traversal.inorder(tree)
[5, 10, 15]

"""
@spec delete(t() | nil, key_type()) :: t() | nil
def delete(tree, key, key_fn \\ @identity)
def delete(nil, _, _), do: nil

def delete(%__MODULE__{data: data, left: left, right: right} = tree, key, key_fn) do
current_key = key_fn.(data)

cond do
current_key > key ->
%{tree | left: delete(left, key, key_fn)}

current_key < key ->
%{tree | right: delete(right, key, key_fn)}

true ->
case {left, right} do
{nil, nil} ->
nil

{nil, right} ->
right

{left, nil} ->
left

{_left, _right} ->
successor_data = find_min(right)
%{tree | data: successor_data, right: delete(right, key_fn.(successor_data), key_fn)}
end
end
end

@doc """
Find the minimum value in a tree (leftmost node).

## Example

iex> tree = BST.from([10, 5, 15, 3, 7])
iex> BST.find_min(tree)
3

"""
@spec find_min(t()) :: value_type()
def find_min(%__MODULE__{data: data, left: nil}), do: data
def find_min(%__MODULE__{left: left}), do: find_min(left)
end
136 changes: 136 additions & 0 deletions test/ex_algo/tree/binary_search_tree_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,140 @@ defmodule ExAlgo.Tree.BinarySearchTreeTest do
assert inorder_traversal == Enum.sort(list)
end
end

describe "delete/2" do
test "deleting from empty tree returns nil" do
assert BST.delete(nil, 5) == nil
end

test "deleting a leaf node" do
tree = BST.from([10, 5, 15, 3, 7, 12, 20])
tree = BST.delete(tree, 3)

assert Traversal.inorder(tree) == [5, 7, 10, 12, 15, 20]
end

test "deleting a node with only left child" do
tree = BST.from([10, 5, 3])
tree = BST.delete(tree, 5)

assert Traversal.inorder(tree) == [3, 10]
end

test "deleting a node with only right child" do
tree = BST.from([10, 5, 7])
tree = BST.delete(tree, 5)

assert Traversal.inorder(tree) == [7, 10]
end

test "deleting a node with two children" do
tree = BST.from([10, 5, 15, 3, 7, 12, 20])
tree = BST.delete(tree, 15)

assert Traversal.inorder(tree) == [3, 5, 7, 10, 12, 20]
end

test "deleting the root node with two children" do
tree = BST.from([10, 5, 15, 3, 7, 12, 20])
tree = BST.delete(tree, 10)

# Should be replaced by inorder successor (12)
assert Traversal.inorder(tree) == [3, 5, 7, 12, 15, 20]
assert tree.data == 12
end

test "deleting the root node with one child" do
tree = BST.from([10, 5])
tree = BST.delete(tree, 10)

assert Traversal.inorder(tree) == [5]
assert tree.data == 5
end

test "deleting the only node in tree" do
tree = BST.new(10)
tree = BST.delete(tree, 10)

assert tree == nil
end

test "deleting non-existent value does not change tree" do
tree = BST.from([10, 5, 15])
tree_after = BST.delete(tree, 99)

assert Traversal.inorder(tree) == Traversal.inorder(tree_after)
end

test "deleting all nodes one by one" do
values = [10, 5, 15, 3, 7, 12, 20]
tree = BST.from(values)

final_tree =
Enum.reduce(values, tree, fn value, acc ->
BST.delete(acc, value)
end)

assert final_tree == nil
end
end

describe "find_min/1" do
test "finds minimum in a tree" do
tree = BST.from([10, 5, 15, 3, 7, 12, 20])
assert BST.find_min(tree) == 3
end

test "finds minimum when root is minimum" do
tree = BST.from([10, 15, 20])
assert BST.find_min(tree) == 10
end

test "finds minimum in single node tree" do
tree = BST.new(42)
assert BST.find_min(tree) == 42
end
end

property "Deleting a value and then searching for it returns nil" do
check all list <- nonempty(list_of(integer())),
list = Enum.uniq(list),
value <- member_of(list) do
tree = BST.from(list)
tree_after_delete = BST.delete(tree, value)

assert BST.find(tree_after_delete, value) == nil
end
end

property "Deleting a value maintains BST property (inorder is sorted)" do
check all list <- nonempty(list_of(integer())),
value <- member_of(list) do
tree = BST.from(list)
tree_after_delete = BST.delete(tree, value)

if tree_after_delete do
inorder = Traversal.inorder(tree_after_delete)
assert inorder == Enum.sort(inorder)
else
# Tree became empty after deletion
assert list == [value] or Enum.all?(list, &(&1 == value))
end
end
end

property "Deleting all unique values from tree results in nil" do
check all list <- nonempty(list_of(integer())),
unique_list = Enum.uniq(list),
unique_list != [] do
tree = BST.from(unique_list)

final_tree =
Enum.reduce(unique_list, tree, fn value, acc ->
BST.delete(acc, value)
end)

assert final_tree == nil
end
end
end