diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4940d2a..7e60447 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 \ No newline at end of file diff --git a/lib/ex_algo/tree/binary_search_tree.ex b/lib/ex_algo/tree/binary_search_tree.ex index 9184edb..14a3230 100644 --- a/lib/ex_algo/tree/binary_search_tree.ex +++ b/lib/ex_algo/tree/binary_search_tree.ex @@ -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 diff --git a/test/ex_algo/tree/binary_search_tree_test.exs b/test/ex_algo/tree/binary_search_tree_test.exs index 5ccf6e7..8e1ded4 100644 --- a/test/ex_algo/tree/binary_search_tree_test.exs +++ b/test/ex_algo/tree/binary_search_tree_test.exs @@ -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