diff --git a/lib/red_black_tree.ex b/lib/red_black_tree.ex index 145a10c..1d9d26a 100644 --- a/lib/red_black_tree.ex +++ b/lib/red_black_tree.ex @@ -8,6 +8,7 @@ defmodule Dasie.RedBlackTree do - http://www.cs.ox.ac.uk/ralf.hinze/WG2.8/32/slides/red-black-pearl.pdf - https://functional.works-hub.com/learn/Persistent-Red-Black-Trees-in-Haskell + - http://inst.eecs.berkeley.edu/~cs61b/fa17/materials/demos/ll-red-black-demo.html """ defstruct data: nil, @@ -142,22 +143,22 @@ defmodule Dasie.RedBlackTree do end end - def delete_left(element, %__MODULE__{color: :red} = node, compare_fn) do - %__MODULE__{node | left: do_delete(element, node.left, compare_fn)} - end - - def delete_left(element, %__MODULE__{color: :black} = node, compare_fn) do + def delete_left(element, %__MODULE__{left: %__MODULE__{color: :black}} = node, compare_fn) do balance_left(%__MODULE__{node | left: do_delete(element, node.left, compare_fn)}) end - def delete_right(element, %__MODULE__{color: :red} = node, compare_fn) do - %__MODULE__{node | right: do_delete(element, node.right, compare_fn)} + def delete_left(element, %__MODULE__{} = node, compare_fn) do + %__MODULE__{node | color: :red, left: do_delete(element, node.left, compare_fn)} end - def delete_right(element, %__MODULE__{color: :black} = node, compare_fn) do + def delete_right(element, %__MODULE__{right: %__MODULE__{color: :black}} = node, compare_fn) do balance_right(%__MODULE__{node | right: do_delete(element, node.right, compare_fn)}) end + def delete_right(element, %__MODULE__{} = node, compare_fn) do + %__MODULE__{node | color: :red, right: do_delete(element, node.right, compare_fn)} + end + def fuse(nil, right) do right end @@ -188,20 +189,11 @@ defmodule Dasie.RedBlackTree do right: %__MODULE__{right | left: red.right} } - %__MODULE__{color: :black} = black -> - %__MODULE__{left | right: %__MODULE__{right | left: black}} + other -> + %__MODULE__{left | right: %__MODULE__{right | left: other}} end end - # This is probably the case i'm the least confident is doing the right thing - # at all times... maybe we can even get rid of it? - def fuse( - %__MODULE__{color: :black, right: nil} = left, - %__MODULE__{color: :black, left: nil} = right - ) do - balance_right(%__MODULE__{right | left: left}) - end - def fuse( %__MODULE__{color: :black} = left, %__MODULE__{color: :black} = right @@ -216,35 +208,20 @@ defmodule Dasie.RedBlackTree do right: %__MODULE__{right | left: red.right} } - %__MODULE__{color: :black} = black -> - balance_left(%__MODULE__{left | right: %__MODULE__{right | left: black}}) + other -> + balance_left(%__MODULE__{left | right: %__MODULE__{right | left: other}}) end end - def balance_left(%__MODULE__{color: :black, left: %__MODULE__{color: :red} = x} = y) do + def balance_left(%__MODULE__{left: %__MODULE__{color: :red} = x} = y) do %__MODULE__{y | color: :red, left: %__MODULE__{x | color: :black}} end - def balance_left( - %__MODULE__{ - color: :black, - right: %__MODULE__{color: :black} = z - } = y - ) do - balance(%__MODULE__{y | right: %__MODULE__{z | color: :red}}) + def balance_left(%__MODULE__{right: %__MODULE__{color: :black} = z} = y) do + balance(%__MODULE__{y | color: :black, right: %__MODULE__{z | color: :red}}) end - def balance_left( - %__MODULE__{ - color: :black, - right: - %__MODULE__{ - color: :red, - left: %__MODULE__{color: :black} = u, - right: %__MODULE__{color: :black} - } = z - } = y - ) do + def balance_left(%__MODULE__{right: %__MODULE__{color: :red, left: %__MODULE__{color: :black} = u} = z} = y) do %__MODULE__{ u | color: :red, @@ -259,32 +236,15 @@ defmodule Dasie.RedBlackTree do } end - def balance_left(node), do: node - - def balance_right(%__MODULE__{color: :black, right: %__MODULE__{color: :red} = x} = y) do + def balance_right(%__MODULE__{right: %__MODULE__{color: :red} = x} = y) do %__MODULE__{y | color: :red, right: %__MODULE__{x | color: :black}} end - def balance_right( - %__MODULE__{ - color: :black, - left: %__MODULE__{color: :black} = z - } = y - ) do - balance(%__MODULE__{y | left: %__MODULE__{z | color: :red}}) + def balance_right(%__MODULE__{left: %__MODULE__{color: :black} = z} = y) do + balance(%__MODULE__{y | color: :black, left: %__MODULE__{z | color: :red}}) end - def balance_right( - %__MODULE__{ - color: :black, - left: - %__MODULE__{ - color: :red, - left: %__MODULE__{color: :black}, - right: %__MODULE__{color: :black} = u - } = z - } = y - ) do + def balance_right(%__MODULE__{left: %__MODULE__{color: :red, right: %__MODULE__{color: :black} = u} = z} = y) do %__MODULE__{ u | color: :red, @@ -295,9 +255,7 @@ defmodule Dasie.RedBlackTree do left: %__MODULE__{z.left | color: :red}, right: u.left }), - right: %__MODULE__{y | left: u.right} + right: %__MODULE__{y | color: :black, left: u.right} } end - - def balance_right(node), do: node end diff --git a/test/red_black_tree_test.exs b/test/red_black_tree_test.exs index 0fdcaf8..6d3d6ed 100644 --- a/test/red_black_tree_test.exs +++ b/test/red_black_tree_test.exs @@ -1,5 +1,6 @@ defmodule Dasie.RedBlackTreeTest do use ExUnit.Case + use ExUnitProperties alias Dasie.RedBlackTree @@ -95,9 +96,7 @@ defmodule Dasie.RedBlackTreeTest do root | left: %RedBlackTree{ color: :red, - data: 2, - left: nil, - right: nil + data: 2 } } @@ -115,9 +114,7 @@ defmodule Dasie.RedBlackTreeTest do root | right: %RedBlackTree{ color: :red, - data: 6, - left: nil, - right: nil + data: 6 } } @@ -179,6 +176,18 @@ defmodule Dasie.RedBlackTreeTest do assert every_red_node_has_black_children?(result) end + + property "every red node has black children" do + check all tree <- red_black_tree_generator() do + assert every_red_node_has_black_children?(tree) + end + end + + property "all paths have the same number of black nodes" do + check all tree <- red_black_tree_generator() do + assert all_paths_have_same_black_nodes?(tree) + end + end end describe "balance_left/1" do @@ -305,9 +314,9 @@ defmodule Dasie.RedBlackTreeTest do assert RedBlackTree.delete_left(2, rbt, &RedBlackTree.default_compare_function/2) == %RedBlackTree{ - color: :red, + color: :black, data: 4, - right: %RedBlackTree{color: :black, data: 6} + right: %RedBlackTree{color: :red, data: 6} } end @@ -318,7 +327,7 @@ defmodule Dasie.RedBlackTreeTest do assert RedBlackTree.delete_left(2, rbt, &RedBlackTree.default_compare_function/2) == %RedBlackTree{ - color: :black, + color: :red, data: 4, right: %RedBlackTree{ color: :red, @@ -336,9 +345,9 @@ defmodule Dasie.RedBlackTreeTest do assert RedBlackTree.delete_right(6, rbt, &RedBlackTree.default_compare_function/2) == %RedBlackTree{ - color: :red, + color: :black, data: 4, - left: %RedBlackTree{color: :black, data: 2} + left: %RedBlackTree{color: :red, data: 2} } end @@ -349,7 +358,7 @@ defmodule Dasie.RedBlackTreeTest do assert RedBlackTree.delete_right(6, rbt, &RedBlackTree.default_compare_function/2) == %RedBlackTree{ - color: :black, + color: :red, data: 4, left: %RedBlackTree{ color: :red, @@ -489,6 +498,42 @@ defmodule Dasie.RedBlackTreeTest do } } end + + test "funky case" do + tree = %RedBlackTree{ + color: :black, + data: 124, + left: %RedBlackTree{ + color: :black, + data: 0, + right: %RedBlackTree{color: :red, data: 123} + }, + right: %RedBlackTree{ + color: :black, + data: 8505, + left: %RedBlackTree{color: :red, data: 7593} + } + } + + delete1 = 124 + + assert RedBlackTree.delete(tree, delete1) == %RedBlackTree{ + color: :black, + data: 123, + left: %RedBlackTree{ + color: :black, + data: 0 + }, + right: %RedBlackTree{ + color: :black, + data: 8505, + left: %RedBlackTree{ + color: :red, + data: 7593 + } + } + } + end end describe "delete/2" do @@ -504,6 +549,51 @@ defmodule Dasie.RedBlackTreeTest do assert RedBlackTree.delete(rbt, 6) == root end + + test "failing after deletions" do + tree = %RedBlackTree{ + color: :black, + data: 2, + left: %RedBlackTree{ + color: :black, + data: 1, + left: %RedBlackTree{color: :red, data: 0} + }, + right: %RedBlackTree{color: :black, data: 4169} + } + + delete1 = 2 + delete2 = 1 + delete3 = 4169 + + tree1 = RedBlackTree.delete(tree, delete1) + + assert tree1 == %RedBlackTree{ + color: :black, + data: 1, + left: %RedBlackTree{color: :black, data: 0}, + right: %RedBlackTree{color: :black, data: 4169} + } + + tree2 = RedBlackTree.delete(tree1, delete2) + + assert tree2 == %RedBlackTree{color: :black, data: 0, right: %RedBlackTree{color: :red, data: 4169}} + + tree3 = RedBlackTree.delete(tree2, delete3) + assert tree3 == %RedBlackTree{color: :black, data: 0} + end + + property "every red node has black children" do + check all tree <- red_black_tree_w_deletes_generator() do + assert every_red_node_has_black_children?(tree) + end + end + + property "all paths have the same number of black nodes" do + check all tree <- red_black_tree_w_deletes_generator() do + assert all_paths_have_same_black_nodes?(tree) + end + end end test "bigger example" do @@ -539,8 +629,6 @@ defmodule Dasie.RedBlackTreeTest do assert all_paths_have_same_black_nodes?(rbt3) end - # TODO Write some property based tests! - defp balanced_rbt() do bx = %RedBlackTree{RedBlackTree.new(7) | color: :black} bz = %RedBlackTree{RedBlackTree.new(9) | color: :black} @@ -606,4 +694,136 @@ defmodule Dasie.RedBlackTreeTest do def count_black_nodes_to_leaf(nil, acc) do acc end + + def red_black_tree_generator() do + gen all values <- StreamData.nonempty(StreamData.list_of(StreamData.integer())) do + [first | rest] = values + + Enum.reduce(rest, RedBlackTree.new(first), fn value, acc -> + RedBlackTree.insert(acc, value) + end) + end + end + + test "delete leaf case" do + tree = %RedBlackTree{ + color: :black, + data: 4621, + left: %RedBlackTree{ + color: :red, + data: 1608, + left: %RedBlackTree{color: :black, data: 1141}, + right: %RedBlackTree{color: :black, data: 1707} + }, + right: %RedBlackTree{ + color: :black, + data: 9983, + left: %RedBlackTree{color: :red, data: 7429} + } + } + + delete1 = 1707 + new_tree = RedBlackTree.delete(tree, delete1) + + assert all_paths_have_same_black_nodes?(new_tree) + assert every_red_node_has_black_children?(new_tree) + + assert new_tree == %RedBlackTree{ + color: :black, + data: 4621, + left: %RedBlackTree{ + color: :black, + data: 1608, + left: %RedBlackTree{color: :red, data: 1141} + }, + right: %RedBlackTree{ + color: :black, + data: 9983, + left: %RedBlackTree{color: :red, data: 7429} + } + } + end + + test "delete leaf case 2" do + # values: [6503, 1690, 4067, 9009, 4004] + tree = %RedBlackTree{ + color: :black, + data: 4067, + left: %RedBlackTree{ + color: :black, + data: 1690, + right: %RedBlackTree{color: :red, data: 4004} + }, + right: %RedBlackTree{ + color: :black, + data: 6503, + right: %RedBlackTree{color: :red, data: 9009} + } + } + + # delete items: [nr_items: 5, delete1: 9009, delete2: 6503, delete3: 4004] + new_tree = RedBlackTree.delete(tree, 9009) + assert all_paths_have_same_black_nodes?(new_tree) + + assert new_tree == %RedBlackTree{ + color: :black, + data: 4067, + left: %RedBlackTree{ + color: :black, + data: 1690, + right: %RedBlackTree{ + color: :red, + data: 4004 + } + }, + right: %RedBlackTree{ + color: :black, + data: 6503 + } + } + end + + def red_black_tree_w_deletes_generator() do + gen all values <- StreamData.nonempty(StreamData.uniq_list_of(StreamData.integer(-10_000..10_000), length: 4..100)) do + [first | rest] = values + # |> IO.inspect(label: "values") + + tree = + Enum.reduce(rest, RedBlackTree.new(first), fn value, acc -> + RedBlackTree.insert(acc, value) + end) + + # |> IO.inspect(label: "tree") + + all_paths_have_same_black_nodes?(tree) + + delete1 = Enum.at(values, :rand.uniform(length(values) - 2)) + delete2 = List.first(values) + delete3 = List.last(values) + + # IO.inspect([nr_items: length(values), delete1: delete1, delete2: delete2, delete3: delete3], label: "delete items") + + # tree1 = RedBlackTree.delete(tree, delete1) + # |> IO.inspect(label: "after delete1") + + # all_paths_have_same_black_nodes?(tree1) + + # tree2 = RedBlackTree.delete(tree1, delete2) + # |> IO.inspect(label: "after delete2") + + # all_paths_have_same_black_nodes?(tree2) + + # tree3 = RedBlackTree.delete(tree2, delete3) + # |> IO.inspect(label: "after delete3") + + # all_paths_have_same_black_nodes?(tree3) + + # tree3 + + tree + |> RedBlackTree.delete(delete1) + |> RedBlackTree.delete(delete2) + |> RedBlackTree.delete(delete3) + end + end end