diff --git a/README.md b/README.md index 5296d3e4..803fdb64 100644 --- a/README.md +++ b/README.md @@ -1776,8 +1776,8 @@ In order to achieve greater coverage and encourage more people to contribute to - - + + diff --git a/src/ruby/binary_search_tree.rb b/src/ruby/binary_search_tree.rb new file mode 100644 index 00000000..f51e6ad7 --- /dev/null +++ b/src/ruby/binary_search_tree.rb @@ -0,0 +1,215 @@ +# frozen_string_literal: true + +class BinarySearchTree + class Node + attr_accessor :left, :right, :value + + def initialize(value) + @left = nil + @right = nil + @value = value + end + end + + def initialize + @root = nil + end + + attr_reader :root + + def insert(value) + new_node = Node.new(value) + + if @root.nil? + @root = new_node + return self + end + + current_node = @root + + loop do + if value < current_node.value + if current_node.left.nil? + current_node.left = new_node + return self + end + current_node = current_node.left + else + if current_node.right.nil? + current_node.right = new_node + return self + end + current_node = current_node.right + end + end + + self + end + + def lookup(value) + return nil if @root.nil? + + current_node = @root + + until current_node.nil? + if value < current_node.value + current_node = current_node.left + elsif value > current_node.value + current_node = current_node.right + else + return current_node + end + end + + nil + end + + def remove(value) + return false if @root.nil? + + current_node = @root + parent_node = nil + + while current_node + if value < current_node.value + parent_node = current_node + current_node = current_node.left + elsif value > current_node.value + parent_node = current_node + current_node = current_node.right + elsif current_node.value == value + # We have a match, get to work! + + if current_node.right.nil? + update_parent_link(parent_node, current_node, current_node.left) + elsif current_node.right.left.nil? + current_node.right.left = current_node.left + update_parent_link(parent_node, current_node, current_node.right) + else + leftmost, leftmost_parent = find_leftmost(current_node.right) + leftmost_parent.left = leftmost.right + leftmost.left = current_node.left + leftmost.right = current_node.right + update_parent_link(parent_node, current_node, leftmost) + end + + return true + end + end + + false + end + + def traverse(node = @root) + return nil if node.nil? + + tree = { value: node.value } + tree[:left] = traverse(node.left) + tree[:right] = traverse(node.right) + + tree + end + + private + + def update_parent_link(parent_node, current_node, new_node) + if parent_node.nil? + @root = new_node + elsif current_node.value < parent_node.value + parent_node.left = new_node + else + parent_node.right = new_node + end + end + + def find_leftmost(node) + leftmost = node.left + leftmost_parent = node + + while leftmost.left + leftmost_parent = leftmost + leftmost = leftmost.left + end + + [leftmost, leftmost_parent] + end +end + +RSpec.describe BinarySearchTree do + let(:bst) { BinarySearchTree.new } + + before(:each) do + # Construct the following tree: + # + # 9 + # 4 20 + # 1 6 15 170 + # + bst.insert(9) + bst.insert(4) + bst.insert(6) + bst.insert(20) + bst.insert(170) + bst.insert(15) + bst.insert(1) + end + + describe "#insert" do + it "adds a value to the tree" do + expect(bst.traverse).to eq({ value: 9, + left: { value: 4, + left: { value: 1, left: nil, right: nil }, + right: { value: 6, left: nil, right: nil } }, + right: { value: 20, left: { value: 15, left: nil, right: nil }, + right: { value: 170, left: nil, right: nil } } }) + end + end + + describe "#lookup" do + it "finds a value in the tree" do + node = bst.lookup(15) + expect(node.value).to eq(15) + end + + it "returns nil for a value not in the tree" do + node = bst.lookup(100) + expect(node).to be_nil + end + end + + describe "#remove" do + it "removes a leaf node" do + expect(bst.remove(1)).to be true + + expect(bst.traverse).to eq({ value: 9, + left: { value: 4, left: nil, + right: { value: 6, left: nil, right: nil } }, + right: { value: 20, left: { value: 15, left: nil, right: nil }, + right: { value: 170, left: nil, right: nil } } }) + end + + it "removes a node with one child" do + expect(bst.remove(20)).to be true + + expect(bst.traverse).to eq({ value: 9, + left: { value: 4, left: { value: 1, left: nil, right: nil }, + right: { value: 6, left: nil, right: nil } }, + right: { value: 170, left: { value: 15, left: nil, right: nil }, + right: nil } }) + end + + it "removes a node with two children" do + expect(bst.remove(4)).to be true + + expect(bst.traverse).to eq({ value: 9, + left: { value: 6, left: { value: 1, left: nil, right: nil }, + right: nil }, + right: { value: 20, left: { value: 15, left: nil, right: nil }, + right: { value: 170, left: nil, right: nil } } }) + end + + it "returns false for a value not in the tree" do + expect(bst.remove(100)).to be false + end + end +end