diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 241b25ed..61670702 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,12 +1,12 @@ -name: test +name: Build/test code on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - - uses: actions/checkout@master - - uses: ruby/setup-ruby@master + - uses: actions/checkout@v4 + - uses: ruby/setup-ruby@v1 with: - ruby-version: '3.0' + ruby-version: '3.4' - name: Run tests run: rake test diff --git a/.github/workflows/update_directory_md.yml b/.github/workflows/update_directory_md.yml index e0b19811..83732bd9 100644 --- a/.github/workflows/update_directory_md.yml +++ b/.github/workflows/update_directory_md.yml @@ -13,17 +13,17 @@ jobs: from typing import Iterator URL_BASE = "https://github.com/TheAlgorithms/Ruby/blob/master" g_output = [] - + def good_filepaths(top_dir: str = ".") -> Iterator[str]: for dirpath, dirnames, filenames in os.walk(top_dir): dirnames[:] = [d for d in dirnames if d[0] not in "._"] for filename in filenames: if os.path.splitext(filename)[1].lower() == ".rb": yield os.path.join(dirpath, filename).lstrip("./") - + def md_prefix(i): return f"{i * ' '}*" if i else "\n##" - + def print_path(old_path: str, new_path: str) -> str: global g_output old_parts = old_path.split(os.sep) @@ -32,7 +32,7 @@ jobs: if new_part: g_output.append(f"{md_prefix(i)} {new_part.replace('_', ' ').title()}") return new_path - + def build_directory_md(top_dir: str = ".") -> str: global g_output old_path = "" @@ -47,12 +47,12 @@ jobs: return "\n".join(g_output) with open("DIRECTORY.md", "w") as out_file: out_file.write(build_directory_md(".") + "\n") - + - name: Update DIRECTORY.md run: | cat DIRECTORY.md - git config --global user.name github-actions - git config --global user.email '${GITHUB_ACTOR}@users.noreply.github.com' + git config --global user.name "$GITHUB_ACTOR" + git config --global user.email "$GITHUB_ACTOR@users.noreply.github.com" git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/$GITHUB_REPOSITORY git add DIRECTORY.md git commit -am "updating DIRECTORY.md" || true diff --git a/360 b/360 deleted file mode 100644 index e69de29b..00000000 diff --git a/DIRECTORY.md b/DIRECTORY.md index f2a6c7a3..020f9507 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -52,10 +52,25 @@ * [Two Sum](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/arrays/two_sum.rb) * [Two Sum Ii](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/arrays/two_sum_ii.rb) * Binary Trees + * [Avl Tree](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/binary_trees/avl_tree.rb) + * [Avl Tree Test](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/binary_trees/avl_tree_test.rb) + * [Bst](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/binary_trees/bst.rb) + * [Bst Test](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/binary_trees/bst_test.rb) * [Inorder Traversal](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/binary_trees/inorder_traversal.rb) * [Invert](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/binary_trees/invert.rb) * [Postorder Traversal](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/binary_trees/postorder_traversal.rb) * [Preorder Traversal](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/binary_trees/preorder_traversal.rb) + * Disjoint Sets + * [Disjoint Sets](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/disjoint_sets/disjoint_sets.rb) + * Graphs + * [Bfs](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/graphs/bfs.rb) + * [Bfs Test](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/graphs/bfs_test.rb) + * [Topological Sort](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/graphs/topological_sort.rb) + * [Topological Sort Test](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/graphs/topological_sort_test.rb) + * [Unweighted Graph](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/graphs/unweighted_graph.rb) + * [Unweighted Graph Test](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/graphs/unweighted_graph_test.rb) + * [Weighted Graph](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/graphs/weighted_graph.rb) + * [Weighted Graph Test](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/graphs/weighted_graph_test.rb) * Hash Table * [Anagram Checker](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/hash_table/anagram_checker.rb) * [Arrays Intersection](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/hash_table/arrays_intersection.rb) @@ -67,6 +82,9 @@ * [Richest Customer Wealth](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/hash_table/richest_customer_wealth.rb) * [Two Sum](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/hash_table/two_sum.rb) * [Uncommon Words](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/hash_table/uncommon_words.rb) + * Heaps + * [Max Heap](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/heaps/max_heap.rb) + * [Max Heap Test](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/heaps/max_heap_test.rb) * Linked Lists * [Circular Linked List](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/linked_lists/circular_linked_list.rb) * [Doubly Linked List](https://github.com/TheAlgorithms/Ruby/blob/master/data_structures/linked_lists/doubly_linked_list.rb) @@ -88,8 +106,10 @@ * [Climbing Stairs](https://github.com/TheAlgorithms/Ruby/blob/master/dynamic_programming/climbing_stairs.rb) * [Coin Change](https://github.com/TheAlgorithms/Ruby/blob/master/dynamic_programming/coin_change.rb) * [Count Sorted Vowel Strings](https://github.com/TheAlgorithms/Ruby/blob/master/dynamic_programming/count_sorted_vowel_strings.rb) + * [Editdistance](https://github.com/TheAlgorithms/Ruby/blob/master/dynamic_programming/editdistance.rb) * [Fibonacci](https://github.com/TheAlgorithms/Ruby/blob/master/dynamic_programming/fibonacci.rb) * [House Robber](https://github.com/TheAlgorithms/Ruby/blob/master/dynamic_programming/house_robber.rb) + * [Knapsack](https://github.com/TheAlgorithms/Ruby/blob/master/dynamic_programming/knapsack.rb) * [Ones And Zeros](https://github.com/TheAlgorithms/Ruby/blob/master/dynamic_programming/ones_and_zeros.rb) * [Pascal Triangle Ii](https://github.com/TheAlgorithms/Ruby/blob/master/dynamic_programming/pascal_triangle_ii.rb) @@ -134,29 +154,40 @@ * [Number Of Days](https://github.com/TheAlgorithms/Ruby/blob/master/other/number_of_days.rb) ## Project Euler - * Problem 1 - * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_1/sol1.rb) - * Problem 2 - * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_2/sol1.rb) - * Problem 20 - * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_20/sol1.rb) - * Problem 21 - * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_21/sol1.rb) - * Problem 22 - * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_22/sol1.rb) - * Problem 3 - * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_3/sol1.rb) - * [Sol2](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_3/sol2.rb) - * Problem 4 - * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_4/sol1.rb) - * [Sol2](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_4/sol2.rb) - * Problem 5 - * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_5/sol1.rb) + * Problem 001 + * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_001/sol1.rb) + * Problem 002 + * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_002/sol1.rb) + * Problem 003 + * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_003/sol1.rb) + * [Sol2](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_003/sol2.rb) + * Problem 004 + * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_004/sol1.rb) + * [Sol2](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_004/sol2.rb) + * Problem 005 + * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_005/sol1.rb) + * Problem 006 + * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_006/sol1.rb) + * Problem 007 + * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_007/sol1.rb) + * Problem 010 + * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_010/sol1.rb) + * Problem 014 + * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_014/sol1.rb) + * Problem 020 + * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_020/sol1.rb) + * Problem 021 + * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_021/sol1.rb) + * Problem 022 + * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_022/sol1.rb) + * Problem 025 + * [Sol1](https://github.com/TheAlgorithms/Ruby/blob/master/project_euler/problem_025/sol1.rb) ## Searches * [Binary Search](https://github.com/TheAlgorithms/Ruby/blob/master/searches/binary_search.rb) * [Depth First Search](https://github.com/TheAlgorithms/Ruby/blob/master/searches/depth_first_search.rb) * [Double Linear Search](https://github.com/TheAlgorithms/Ruby/blob/master/searches/double_linear_search.rb) + * [Fibonacci Search](https://github.com/TheAlgorithms/Ruby/blob/master/searches/fibonacci_search.rb) * [Jump Search](https://github.com/TheAlgorithms/Ruby/blob/master/searches/jump_search.rb) * [Linear Search](https://github.com/TheAlgorithms/Ruby/blob/master/searches/linear_search.rb) * [Number Of Islands](https://github.com/TheAlgorithms/Ruby/blob/master/searches/number_of_islands.rb) @@ -167,6 +198,8 @@ ## Sorting * [Bead Sort](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/bead_sort.rb) * [Bead Sort Test](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/bead_sort_test.rb) + * [Binary Insertion Sort](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/binary_insertion_sort.rb) + * [Binary Insertion Sort Test](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/binary_insertion_sort_test.rb) * [Bogo Sort](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/bogo_sort.rb) * [Bogo Sort Test](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/bogo_sort_test.rb) * [Bubble Sort](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/bubble_sort.rb) @@ -177,6 +210,10 @@ * [Cocktail Sort Test](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/cocktail_sort_test.rb) * [Comb Sort](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/comb_sort.rb) * [Comb Sort Test](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/comb_sort_test.rb) + * [Counting Sort](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/counting_sort.rb) + * [Counting Sort Test](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/counting_sort_test.rb) + * [Gnome Sort](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/gnome_sort.rb) + * [Gnome Sort Test](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/gnome_sort_test.rb) * [Heap Sort](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/heap_sort.rb) * [Heap Sort Test](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/heap_sort_test.rb) * [Insertion Sort](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/insertion_sort.rb) @@ -195,3 +232,10 @@ * [Shell Sort Test](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/shell_sort_test.rb) * [Sort Color](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/sort_color.rb) * [Sort Tests](https://github.com/TheAlgorithms/Ruby/blob/master/sorting/sort_tests.rb) + +## Strings + * [Boyer Moore Horspool Search](https://github.com/TheAlgorithms/Ruby/blob/master/strings/boyer_moore_horspool_search.rb) + * [Boyer Moore Horspool Search Test](https://github.com/TheAlgorithms/Ruby/blob/master/strings/boyer_moore_horspool_search_test.rb) + * [Hamming Distance](https://github.com/TheAlgorithms/Ruby/blob/master/strings/hamming_distance.rb) + * [Max K Most Frequent Words](https://github.com/TheAlgorithms/Ruby/blob/master/strings/max_k_most_frequent_words.rb) + * [Max K Most Frequent Words Test](https://github.com/TheAlgorithms/Ruby/blob/master/strings/max_k_most_frequent_words_test.rb) diff --git a/LICENSE.md b/LICENSE.md index f6bcf04e..eb813ea4 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 The Algorithms +Copyright (c) 2024 The Algorithms Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index a66a07ac..a8b8cde4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # The Algorithms - Ruby -[![Gitter chat](https://img.shields.io/badge/Chat-Gitter-ff69b4.svg?label=Chat&logo=gitter&style=flat-square)](https://gitter.im/TheAlgorithms)  +[![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/#TheAlgorithms_community:gitter.im)  [![contributions welcome](https://img.shields.io/static/v1.svg?label=Contributions&message=Welcome&color=0059b3&style=flat-square)](https://github.com/TheAlgorithms/Ruby/blob/master/CONTRIBUTING.md)  ![](https://img.shields.io/github/repo-size/TheAlgorithms/Ruby.svg?label=Repo%20size&style=flat-square)  [![Discord chat](https://img.shields.io/discord/808045925556682782.svg?logo=discord&colorB=7289DA&style=flat-square)](https://discord.gg/c7MnfGFGa6)  diff --git a/ciphers/caesar.rb b/ciphers/caesar.rb index c76ea743..8a15d9f0 100644 --- a/ciphers/caesar.rb +++ b/ciphers/caesar.rb @@ -30,4 +30,3 @@ def self.decrypt(ciphertext, shift) end.join end end - diff --git a/ciphers/caesar_test.rb b/ciphers/caesar_test.rb index 090945d8..be5a23e8 100644 --- a/ciphers/caesar_test.rb +++ b/ciphers/caesar_test.rb @@ -19,4 +19,3 @@ def run_tests(plaintext, expected_cipher, shift) assert_equal decrypted, plaintext end end - diff --git a/ciphers/rsa.rb b/ciphers/rsa.rb index 0b9520e5..c4aa097a 100644 --- a/ciphers/rsa.rb +++ b/ciphers/rsa.rb @@ -1,4 +1,4 @@ -require "prime" +require 'prime' def initialize(keys = {}) @e ||= keys[:e] @@ -7,16 +7,16 @@ def initialize(keys = {}) def cipher(message) message.bytes.map do |byte| - cbyte = ((byte.to_i ** e) % n).to_s + cbyte = ((byte.to_i**e) % n).to_s missing_chars = n.to_s.size - cbyte.size - "0" * missing_chars + cbyte + '0' * missing_chars + cbyte end.join end def decipher(ciphed_message) ciphed_message.chars.each_slice(n.to_s.size).map do |arr| - (arr.join.to_i ** d) % n - end.pack("c*") + (arr.join.to_i**d) % n + end.pack('c*') end def public_keys @@ -52,43 +52,47 @@ def d end def extended_gcd(a, b) - last_remainder, remainder = a.abs, b.abs - x, last_x, y, last_y = 0, 1, 1, 0 + last_remainder = a.abs + remainder = b.abs + x = 0 + last_x = 1 + y = 1 + last_y = 0 while remainder != 0 - last_remainder, (quotient, remainder) = remainder, last_remainder.divmod(remainder) + (quotient, remainder) = last_remainder.divmod(remainder) + last_remainder = remainder x, last_x = last_x - quotient * x, x y, last_y = last_y - quotient * y, y end - return last_remainder, last_x * (a < 0 ? -1 : 1) + [last_remainder, last_x * (a < 0 ? -1 : 1)] end def invmod(e, et) g, x = extended_gcd(e, et) - raise "The maths are broken!" if g != 1 + raise 'The maths are broken!' if g != 1 + x % et end def random_prime_number number = Random.rand(1..1000) - until Prime.prime?(number) || number == p || number == q - number = Random.rand(1..1000) - end + number = Random.rand(1..1000) until Prime.prime?(number) || number == p || number == q number end -def main() - puts "Enter the message you want to encrypt and decrypt with RSA algorithm: " - message = gets.chomp().to_s - puts "Encoded Text:" +def main + puts 'Enter the message you want to encrypt and decrypt with RSA algorithm: ' + message = gets.chomp.to_s + puts 'Encoded Text:' puts cipher(message) - puts "Decoded Text:" + puts 'Decoded Text:' puts decipher(cipher(message)) - puts "p: #{p()}" - puts "q: #{q()}" - puts "e: #{e()}" - puts "d: #{d()}" - puts "totient: #{totient()}" + puts "p: #{p}" + puts "q: #{q}" + puts "e: #{e}" + puts "d: #{d}" + puts "totient: #{totient}" end -main() +main diff --git a/data_structures/binary_trees/avl_tree.rb b/data_structures/binary_trees/avl_tree.rb new file mode 100644 index 00000000..2774e811 --- /dev/null +++ b/data_structures/binary_trees/avl_tree.rb @@ -0,0 +1,281 @@ +class AvlTreeNode + + attr_reader :key + attr_accessor :parent + attr_accessor :left + attr_accessor :right + attr_accessor :height + + def initialize(key, parent=nil) + @key = key + @parent = parent + @height = 1 + end +end + +## +# This class represents an AVL tree (a self-balancing binary search tree) with distinct node keys. +# Starting from the root, every node has up to two children (one left and one right child node). +# +# For the BST property: +# - the keys of nodes in the left subtree of a node are strictly less than the key of the node; +# - the keys of nodes in the right subtree of a node are strictly greater than the key of the node. +# +# Due to self-balancing upon key insertion and deletion, the main operations of this data structure +# (insertion, deletion, membership) run - in worst case - in O(log(n)), where n is the number of nodes in the tree. + +class AvlTree + + attr_reader :size + attr_accessor :root + + def initialize(keys=[]) + @size = 0 + keys.each {|key| insert_key(key) } + end + + def empty? + size == 0 + end + + def insert_key(key) + @size += 1 + if root.nil? + @root = AvlTreeNode.new(key) + return + end + parent = root + while (key < parent.key && !parent.left.nil? && parent.left.key != key) || + (key > parent.key && !parent.right.nil? && parent.right.key != key) + parent = key < parent.key ? parent.left : parent.right + end + if key < parent.key + raise ArgumentError.new("Key #{key} is already present in the AvlTree") unless parent.left.nil? + parent.left = AvlTreeNode.new(key, parent) + else + raise ArgumentError.new("Key #{key} is already present in the AvlTree") unless parent.right.nil? + parent.right = AvlTreeNode.new(key, parent) + end + balance(parent) + end + + def min_key(node=root) + return nil if node.nil? + min_key_node(node).key + end + + def max_key(node=root) + return nil if node.nil? + max_key_node(node).key + end + + def contains_key?(key) + !find_node_with_key(key).nil? + end + + def delete_key(key) + parent = find_parent_of_node_with_key(key) + if parent.nil? + return if root.nil? || root.key != key + @size -= 1 + @root = adjusted_subtree_after_deletion(root.left, root.right) + root.parent = nil + balance(root.right.nil? ? root : root.right) + return + end + if key < parent.key + node = parent.left + parent.left = adjusted_subtree_after_deletion(node.left, node.right) + unless parent.left.nil? + parent.left.parent = parent + balance(parent.left.right.nil? ? parent.left : parent.left.right) + end + else + node = parent.right + parent.right = adjusted_subtree_after_deletion(node.left, node.right) + unless parent.right.nil? + parent.right.parent = parent + balance(parent.right.right.nil? ? parent.right : parent.right.right) + end + end + @size -= 1 + end + + def traverse_preorder(key_consumer, node=root) + return if node.nil? + key_consumer.call(node.key) + traverse_preorder(key_consumer, node.left) unless node.left.nil? + traverse_preorder(key_consumer, node.right) unless node.right.nil? + end + + def traverse_inorder(key_consumer, node=root) + return if node.nil? + traverse_inorder(key_consumer, node.left) unless node.left.nil? + key_consumer.call(node.key) + traverse_inorder(key_consumer, node.right) unless node.right.nil? + end + + def traverse_postorder(key_consumer, node=root) + return if node.nil? + traverse_postorder(key_consumer, node.left) unless node.left.nil? + traverse_postorder(key_consumer, node.right) unless node.right.nil? + key_consumer.call(node.key) + end + + def to_array(visit_traversal=:traverse_preorder) + visited = [] + method(visit_traversal).call(->(key) { visited.append(key) }) + visited + end + + private + def min_key_node(node=root) + return nil if node.nil? + until node.left.nil? + node = node.left + end + node + end + + def max_key_node(node=root) + return nil if node.nil? + until node.right.nil? + node = node.right + end + node + end + + def find_node_with_key(key) + node = root + until node.nil? || node.key == key + node = key < node.key ? node.left : node.right + end + node + end + + def find_parent_of_node_with_key(key) + return nil if root.nil? || root.key == key + parent = root + until parent.nil? + if key < parent.key + return nil if parent.left.nil? + return parent if parent.left.key == key + parent = parent.left + else + return nil if parent.right.nil? + return parent if parent.right.key == key + parent = parent.right + end + end + nil + end + + def adjusted_subtree_after_deletion(left, right) + return right if left.nil? + return left if right.nil? + if right.left.nil? + right.left = left + left.parent = right + return right + end + successor_parent = right + until successor_parent.left.left.nil? + successor_parent = successor_parent.left + end + successor = successor_parent.left + successor_parent.left = successor.right + successor.right.parent = successor_parent unless successor.right.nil? + successor.right = right + right.parent = successor + successor.left = left + left.parent = successor + successor + end + + def balance(node) + return if node.nil? + left_height = node.left&.height || 0 + right_height = node.right&.height || 0 + # Assumption: the subtrees rooted at `node.left` and `node.right` are balanced + adjust_height(node) + if right_height - left_height > 1 + # `node` is right-heavy + if !node.right.left.nil? && (node.right.left.height || 0) > (node.right.right&.height || 0) + rotate_right_left(node) + else + rotate_left(node) + end + elsif left_height - right_height > 1 + # `node` is left-heavy + if !node.left.right.nil? && (node.left.right.height || 0) > (node.left.left&.height || 0) + rotate_left_right(node) + else + rotate_right(node) + end + end + + balance(node.parent) + end + + def rotate_left(node) + new_root = node.right + if node == root + @root = new_root + elsif node.parent.left == node + node.parent.left = new_root + else + node.parent.right = new_root + end + new_root.parent = node.parent + if new_root.left.nil? + node.right = nil + new_root.left = node + node.parent = new_root + else + node.right = new_root.left + new_root.left.parent = node + new_root.left = node + node.parent = new_root + end + adjust_height(node) + adjust_height(new_root) + end + + def rotate_right(node) + new_root = node.left + if node == root + @root = new_root + elsif node.parent.left == node + node.parent.left = new_root + else + node.parent.right = new_root + end + new_root.parent = node.parent + if new_root.right.nil? + node.left = nil + new_root.right = node + node.parent = new_root + else + node.left = new_root.right + new_root.right.parent = node + new_root.right = node + node.parent = new_root + end + adjust_height(node) + adjust_height(new_root) + end + + def rotate_right_left(node) + rotate_right(node.right) + rotate_left(node) + end + + def rotate_left_right(node) + rotate_left(node.left) + rotate_right(node) + end + + def adjust_height(node) + node.height = 1 + [node.left&.height || 0, node.right&.height || 0].max + end +end diff --git a/data_structures/binary_trees/avl_tree_test.rb b/data_structures/binary_trees/avl_tree_test.rb new file mode 100644 index 00000000..df577f3b --- /dev/null +++ b/data_structures/binary_trees/avl_tree_test.rb @@ -0,0 +1,132 @@ +require 'minitest/autorun' +require_relative 'avl_tree' + +class TestAvlTree < Minitest::Test + def test_default_constructor_creates_empty_tree + tree = AvlTree.new + assert tree.to_array.empty? + end + + def test_default_constructor_creates_tree_with_given_keys + tree = AvlTree.new([1, 2, 3, 4, 5, 6, 7]) + assert tree.to_array == [4, 2, 1, 3, 6, 5, 7] + end + + def test_exception_when_inserting_key_already_present + tree = AvlTree.new([4, 2, 6, 3, 1]) + assert_raises ArgumentError do + tree.insert_key(6) + end + end + + def test_size_returns_zero_given_empty_tree + tree = AvlTree.new + assert tree.size == 0 + end + + def test_empty_returns_number_of_nodes_in_tree + tree = AvlTree.new([4, 2, 6, 3, 1]) + assert tree.size == 5 + end + + def test_empty_returns_true_given_empty_tree + tree = AvlTree.new + assert tree.empty? + end + + def test_empty_returns_false_given_non_empty_tree + tree = AvlTree.new([1]) + assert !tree.empty? + end + + def test_min_key_returns_minimum_key + tree = AvlTree.new([4, 2, 6, 3, 1]) + assert tree.min_key == 1 + end + + def test_max_key_returns_maximum_key + tree = AvlTree.new([4, 2, 6, 3, 1]) + assert tree.max_key == 6 + end + + def test_contains_key_returns_true_if_key_in_tree + tree = AvlTree.new([4, 2, 6, 3, 1]) + assert tree.contains_key?(3) + end + + def test_contains_key_returns_false_if_key_not_in_tree + tree = AvlTree.new([4, 2, 6, 3, 1]) + assert !tree.contains_key?(7) + end + + def test_delete_key_does_nothing_if_key_not_in_tree + tree = AvlTree.new([4, 2, 6, 3, 1]) + tree.delete_key(7) + assert tree.to_array == [4, 2, 1, 3, 6] + end + + def test_delete_key_keeps_avl_property_if_leaf_node + tree = AvlTree.new([1, 2, 3, 4, 5, 6, 7]) + tree.delete_key(3) + assert tree.to_array == [4, 2, 1, 6, 5, 7] + end + + def test_delete_key_keeps_avl_property_if_node_with_left_child + tree = AvlTree.new([4, 2, 5, 1]) + tree.delete_key(2) + assert tree.to_array == [4, 1, 5] + end + + def test_delete_key_keeps_avl_property_if_node_with_right_child + tree = AvlTree.new([4, 2, 5, 6]) + tree.delete_key(5) + assert tree.to_array == [4, 2, 6] + end + + def test_delete_key_keeps_avl_property_if_node_with_both_children + tree = AvlTree.new([1, 2, 3, 4, 5, 6, 7, 8, 9]) + tree.delete_key(4) + assert tree.to_array == [5, 2, 1, 3, 8, 6, 7, 9] + end + + def test_preorder_traversal_uses_expected_order + tree = AvlTree.new([1, 2, 3, 4, 5, 6, 7]) + visited = [] + tree.traverse_preorder(->(key) { visited.append(key) }) + assert visited == [4, 2, 1, 3, 6, 5, 7] + end + + def test_inorder_traversal_uses_expected_order + tree = AvlTree.new([1, 2, 3, 4, 5, 6, 7]) + visited = [] + tree.traverse_inorder(->(key) { visited.append(key) }) + assert visited == [1, 2, 3, 4, 5, 6, 7] + end + + def test_postorder_traversal_uses_expected_order + tree = AvlTree.new([1, 2, 3, 4, 5, 6, 7]) + visited = [] + tree.traverse_postorder(->(key) { visited.append(key) }) + assert visited == [1, 3, 2, 5, 7, 6, 4] + end + + def test_left_rotation + tree = AvlTree.new([1, 2, 3]) + assert tree.to_array == [2, 1, 3] + end + + def test_right_rotation + tree = AvlTree.new([3, 2, 1]) + assert tree.to_array == [2, 1, 3] + end + + def test_right_left_rotation + tree = AvlTree.new([1, 3, 2]) + assert tree.to_array == [2, 1, 3] + end + + def test_left_right_rotation + tree = AvlTree.new([3, 1, 2]) + assert tree.to_array == [2, 1, 3] + end +end diff --git a/data_structures/binary_trees/bst.rb b/data_structures/binary_trees/bst.rb new file mode 100644 index 00000000..3ec5aab0 --- /dev/null +++ b/data_structures/binary_trees/bst.rb @@ -0,0 +1,176 @@ +class BinarySearchTreeNode + + attr_reader :key + attr_accessor :left + attr_accessor :right + + def initialize(key) + @key = key + end +end + +## +# This class represents a binary search tree (not implementing self-balancing) with distinct node keys. +# Starting from the root, every node has up to two children (one left and one right child node). +# +# For the BST property: +# - the keys of nodes in the left subtree of a node are strictly less than the key of the node; +# - the keys of nodes in the right subtree of a node are strictly greater than the key of the node. +# +# The main operations of this data structure (insertion, deletion, membership) run - in worst case - in O(n), +# where n is the number of nodes in the tree. +# The average case for those operations is O(log(n)) due to the structure of the tree. + +class BinarySearchTree + + attr_reader :size + attr_accessor :root + + def initialize(keys=[]) + @size = 0 + keys.each {|key| insert_key(key) } + end + + def empty? + size == 0 + end + + def insert_key(key) + @size += 1 + if root.nil? + @root = BinarySearchTreeNode.new(key) + return + end + parent = root + while (key < parent.key && !parent.left.nil? && parent.left.key != key) || + (key > parent.key && !parent.right.nil? && parent.right.key != key) + parent = key < parent.key ? parent.left : parent.right + end + if key < parent.key + raise ArgumentError.new("Key #{key} is already present in the BinarySearchTree") unless parent.left.nil? + parent.left = BinarySearchTreeNode.new(key) + else + raise ArgumentError.new("Key #{key} is already present in the BinarySearchTree") unless parent.right.nil? + parent.right = BinarySearchTreeNode.new(key) + end + end + + def min_key(node=root) + return nil if node.nil? + min_key_node(node).key + end + + def max_key(node=root) + return nil if node.nil? + max_key_node(node).key + end + + def contains_key?(key) + !find_node_with_key(key).nil? + end + + def delete_key(key) + parent = find_parent_of_node_with_key(key) + if parent.nil? + return if root.nil? || root.key != key + @size -= 1 + @root = adjusted_subtree_after_deletion(root.left, root.right) + return + end + if key < parent.key + node = parent.left + parent.left = adjusted_subtree_after_deletion(node.left, node.right) + else + node = parent.right + parent.right = adjusted_subtree_after_deletion(node.left, node.right) + end + @size -= 1 + end + + def traverse_preorder(key_consumer, node=root) + return if node.nil? + key_consumer.call(node.key) + traverse_preorder(key_consumer, node.left) unless node.left.nil? + traverse_preorder(key_consumer, node.right) unless node.right.nil? + end + + def traverse_inorder(key_consumer, node=root) + return if node.nil? + traverse_inorder(key_consumer, node.left) unless node.left.nil? + key_consumer.call(node.key) + traverse_inorder(key_consumer, node.right) unless node.right.nil? + end + + def traverse_postorder(key_consumer, node=root) + return if node.nil? + traverse_postorder(key_consumer, node.left) unless node.left.nil? + traverse_postorder(key_consumer, node.right) unless node.right.nil? + key_consumer.call(node.key) + end + + def to_array(visit_traversal=:traverse_preorder) + visited = [] + method(visit_traversal).call(->(key) { visited.append(key) }) + visited + end + + private + def min_key_node(node=root) + return nil if node.nil? + until node.left.nil? + node = node.left + end + node + end + + def max_key_node(node=root) + return nil if node.nil? + until node.right.nil? + node = node.right + end + node + end + + def find_node_with_key(key) + node = root + until node.nil? || node.key == key + node = key < node.key ? node.left : node.right + end + node + end + + def find_parent_of_node_with_key(key) + return nil if root.nil? || root.key == key + parent = root + until parent.nil? + if key < parent.key + return nil if parent.left.nil? + return parent if parent.left.key == key + parent = parent.left + else + return nil if parent.right.nil? + return parent if parent.right.key == key + parent = parent.right + end + end + nil + end + + def adjusted_subtree_after_deletion(left, right) + return right if left.nil? + return left if right.nil? + if right.left.nil? + right.left = left + return right + end + successor_parent = right + until successor_parent.left.left.nil? + successor_parent = successor_parent.left + end + successor = successor_parent.left + successor_parent.left = successor.right + successor.right = right + successor.left = left + successor + end +end diff --git a/data_structures/binary_trees/bst_test.rb b/data_structures/binary_trees/bst_test.rb new file mode 100644 index 00000000..5a761dde --- /dev/null +++ b/data_structures/binary_trees/bst_test.rb @@ -0,0 +1,112 @@ +require 'minitest/autorun' +require_relative 'bst' + +class TestBinarySearchTree < Minitest::Test + def test_default_constructor_creates_empty_tree + bst = BinarySearchTree.new + assert bst.to_array.empty? + end + + def test_default_constructor_creates_tree_with_given_keys + bst = BinarySearchTree.new([4, 2, 6, 3, 1]) + assert bst.to_array == [4, 2, 1, 3, 6] + end + + def test_exception_when_inserting_key_already_present + bst = BinarySearchTree.new([4, 2, 6, 3, 1]) + assert_raises ArgumentError do + bst.insert_key(6) + end + end + + def test_size_returns_zero_given_empty_tree + bst = BinarySearchTree.new + assert bst.size == 0 + end + + def test_empty_returns_number_of_nodes_in_tree + bst = BinarySearchTree.new([4, 2, 6, 3, 1]) + assert bst.size == 5 + end + + def test_empty_returns_true_given_empty_tree + bst = BinarySearchTree.new + assert bst.empty? + end + + def test_empty_returns_false_given_non_empty_tree + bst = BinarySearchTree.new([1]) + assert !bst.empty? + end + + def test_min_key_returns_minimum_key + bst = BinarySearchTree.new([4, 2, 6, 3, 1]) + assert bst.min_key == 1 + end + + def test_max_key_returns_maximum_key + bst = BinarySearchTree.new([4, 2, 6, 3, 1]) + assert bst.max_key == 6 + end + + def test_contains_key_returns_true_if_key_in_tree + bst = BinarySearchTree.new([4, 2, 6, 3, 1]) + assert bst.contains_key?(3) + end + + def test_contains_key_returns_false_if_key_not_in_tree + bst = BinarySearchTree.new([4, 2, 6, 3, 1]) + assert !bst.contains_key?(7) + end + + def test_delete_key_does_nothing_if_key_not_in_tree + bst = BinarySearchTree.new([4, 2, 6, 3, 1]) + bst.delete_key(7) + assert bst.to_array == [4, 2, 1, 3, 6] + end + + def test_delete_key_keeps_bst_property_if_leaf_node + bst = BinarySearchTree.new([4, 2, 6, 3, 1]) + bst.delete_key(1) + assert bst.to_array == [4, 2, 3, 6] + end + + def test_delete_key_keeps_bst_property_if_node_with_left_child + bst = BinarySearchTree.new([4, 2, 3, 1]) + bst.delete_key(4) + assert bst.to_array == [2, 1, 3] + end + + def test_delete_key_keeps_bst_property_if_node_with_right_child + bst = BinarySearchTree.new([4, 2, 6, 3]) + bst.delete_key(2) + assert bst.to_array == [4, 3, 6] + end + + def test_delete_key_keeps_bst_property_if_node_with_both_children + bst = BinarySearchTree.new([4, 2, 7, 3, 1, 5, 10, 6]) + bst.delete_key(4) + assert bst.to_array == [5, 2, 1, 3, 7, 6, 10] + end + + def test_preorder_traversal_uses_expected_order + bst = BinarySearchTree.new([4, 2, 6, 3, 1]) + visited = [] + bst.traverse_preorder(->(key) { visited.append(key) }) + assert visited == [4, 2, 1, 3, 6] + end + + def test_inorder_traversal_uses_expected_order + bst = BinarySearchTree.new([4, 2, 6, 3, 1]) + visited = [] + bst.traverse_inorder(->(key) { visited.append(key) }) + assert visited == [1, 2, 3, 4, 6] + end + + def test_postorder_traversal_uses_expected_order + bst = BinarySearchTree.new([4, 2, 6, 3, 1]) + visited = [] + bst.traverse_postorder(->(key) { visited.append(key) }) + assert visited == [1, 3, 2, 6, 4] + end +end diff --git a/data_structures/disjoint_sets/disjoint_sets.rb b/data_structures/disjoint_sets/disjoint_sets.rb new file mode 100644 index 00000000..f89dc63b --- /dev/null +++ b/data_structures/disjoint_sets/disjoint_sets.rb @@ -0,0 +1,47 @@ +class Node + attr_accessor :data, :parent, :rank, :parent, :rank + + def initialize(data) + @data = data + @parent = self + @rank = 0 + end +end + +class DisjointSets + def make_set(d) + Node.new(d) + end + + def find_set(x) + raise ArgumentError unless x.class <= Node + + x.parent = (find_set(x.parent)) unless x.parent == x + x.parent + end + + def union_set(x, y) + px = find_set(x) + py = find_set(y) + return if px == py + + if px.rank > py.rank + py.parent = px + elsif py.rank > px.rank + px.parent = py + else + px.parent = py + py.rank += 1 + end + end +end + +ds = DisjointSets.new +one = ds.make_set(1) +two = ds.make_set(2) +three = ds.make_set(3) +ds.union_set(one, two) +puts ds.find_set(one) == ds.find_set(two) # should be true +ds.union_set(one, three) +puts ds.find_set(two) == ds.find_set(three) # should be true +puts one.rank + two.rank + three.rank == 1 # should be true diff --git a/data_structures/graphs/bfs.rb b/data_structures/graphs/bfs.rb new file mode 100644 index 00000000..9d9facfa --- /dev/null +++ b/data_structures/graphs/bfs.rb @@ -0,0 +1,65 @@ +require 'set' + +## +# This class represents the result of a breadth-first search performed on an unweighted graph. +# +# It exposes: +# - the set of visited nodes +# - a hash of distances by node from the search root node +# (only for visited nodes, 0 for the search root node); +# - a hash of parent nodes by node +# (only for visited nodes, nil for the search root node). + +class GraphBfsResult + attr_reader :visited + attr_reader :parents + attr_reader :distances + + def initialize(visited, parents, distances) + @visited = visited + @parents = parents + @distances = distances + end +end + +## +# Performs a breadth-first search for the provided graph, starting at the given node. +# Returns the search result (see GraphBfsResult). +# Nodes are consumed using the provided consumers upon being first seen, or being completely visited +# (nothing, by default). +# +# The algorithm has a time complexity of O(|V| + |E|), where: +# - |V| is the number of nodes in the graph; +# - |E| is the number of edges in the graph. + +def bfs(graph, start_node, seen_node_consumer: method(:do_nothing_on_node), visited_node_consumer: method(:do_nothing_on_node)) + seen = Set[] + visited = Set[] + parents = { start_node => nil } + distances = { start_node => 0 } + + seen.add(start_node) + seen_node_consumer.call(start_node) + q = Queue.new + q.push(start_node) + until q.empty? + node = q.pop + for neighbor in graph.neighbors(node) + unless seen.include?(neighbor) + seen.add(neighbor) + distances[neighbor] = distances[node] + 1 + parents[neighbor] = node + seen_node_consumer.call(neighbor) + q.push(neighbor) + end + end + visited.add(node) + visited_node_consumer.call(node) + end + + GraphBfsResult.new(visited, parents, distances) +end + +private +def do_nothing_on_node(node) +end diff --git a/data_structures/graphs/bfs_test.rb b/data_structures/graphs/bfs_test.rb new file mode 100644 index 00000000..ab3f9d14 --- /dev/null +++ b/data_structures/graphs/bfs_test.rb @@ -0,0 +1,89 @@ +require 'minitest/autorun' +require_relative 'bfs' +require_relative 'unweighted_graph' + +class TestBfs < Minitest::Test + def test_bfs_visits_single_graph_node + graph = UnweightedGraph.new(nodes: [:u, :v, :w], directed: false) + graph.add_edge(:u, :v) + + bfs_result = bfs(graph, :w) + + assert bfs_result.visited.to_set == [:w].to_set + assert bfs_result.parents == { + :w => nil + } + assert bfs_result.distances == { + :w => 0 + } + end + + def test_bfs_visits_graph_fully + graph = UnweightedGraph.new(nodes: [:u, :v, :w, :x], directed: false) + graph.add_edge(:u, :v) + graph.add_edge(:u, :w) + graph.add_edge(:w, :x) + + bfs_result = bfs(graph, :u) + + assert bfs_result.visited.to_set == [:u, :v, :w, :x].to_set + assert bfs_result.parents == { + :u => nil, + :v => :u, + :w => :u, + :x => :w + } + assert bfs_result.distances == { + :u => 0, + :v => 1, + :w => 1, + :x => 2 + } + end + + def test_bfs_visits_graph_partially + graph = UnweightedGraph.new(nodes: [:u, :v, :w, :x, :y, :z], directed: false) + graph.add_edge(:u, :v) + graph.add_edge(:w, :x) + graph.add_edge(:x, :y) + graph.add_edge(:y, :z) + + bfs_result = bfs(graph, :x) + + assert bfs_result.visited.to_set == [:w, :x, :y, :z].to_set + assert bfs_result.parents == { + :w => :x, + :x => nil, + :y => :x, + :z => :y + } + assert bfs_result.distances == { + :w => 1, + :x => 0, + :y => 1, + :z => 2 + } + end + + def test_bfs_visits_with_seen_node_consumer + graph = UnweightedGraph.new(nodes: [:u, :v, :w], directed: false) + graph.add_edge(:u, :v) + graph.add_edge(:u, :w) + + seen_order = [] + bfs(graph, :w, seen_node_consumer: ->(node) { seen_order.append(node) }) + + assert seen_order == [:w, :u, :v] + end + + def test_bfs_visits_with_visited_node_consumer + graph = UnweightedGraph.new(nodes: [:u, :v, :w], directed: false) + graph.add_edge(:u, :v) + graph.add_edge(:u, :w) + + visited_order = [] + bfs(graph, :w, visited_node_consumer: ->(node) { visited_order.append(node) }) + + assert visited_order == [:w, :u, :v] + end +end diff --git a/data_structures/graphs/topological_sort.rb b/data_structures/graphs/topological_sort.rb new file mode 100644 index 00000000..88955e48 --- /dev/null +++ b/data_structures/graphs/topological_sort.rb @@ -0,0 +1,37 @@ +require 'set' + +## +# This class aims to provide topological sorting capabilities for directed acyclic graphs. +# +# Topological sorting runs in O(|V|), where |V| is the number of graph nodes. + +class TopologicalSorter + attr_reader :graph + + def initialize(graph) + raise ArgumentError, "Topological sort is only applicable to directed graphs!" unless graph.directed + @graph = graph + end + + def topological_sort + @sorted_nodes = [] + @seen = Set[] + @visited = Set[] + for node in graph.nodes + dfs_visit(node) + end + @sorted_nodes + end + + private + def dfs_visit(node) + return if @visited.include?(node) + raise ArgumentError, "Cycle in graph detected on node #{node}!" if @seen.include?(node) + @seen.add(node) + for neighbor in graph.neighbors(node) + dfs_visit(neighbor) + end + @visited.add(node) + @sorted_nodes.unshift(node) + end +end diff --git a/data_structures/graphs/topological_sort_test.rb b/data_structures/graphs/topological_sort_test.rb new file mode 100644 index 00000000..01472dad --- /dev/null +++ b/data_structures/graphs/topological_sort_test.rb @@ -0,0 +1,52 @@ +require 'minitest/autorun' +require_relative 'topological_sort' +require_relative 'unweighted_graph' + +class TestTopologicalSort < Minitest::Test + def test_topological_sort_returns_valid_order_for_acyclic_graph + wardrobe_items = [:underwear, :trousers, :belt, :shirt, :tie, :jacket, :socks, :shoes, :watch] + wardrobe_graph = UnweightedGraph.new(nodes: wardrobe_items, directed: true) + wardrobe_graph.add_edge(:underwear, :trousers) + wardrobe_graph.add_edge(:underwear, :shoes) + wardrobe_graph.add_edge(:socks, :shoes) + wardrobe_graph.add_edge(:trousers, :shoes) + wardrobe_graph.add_edge(:trousers, :belt) + wardrobe_graph.add_edge(:shirt, :belt) + wardrobe_graph.add_edge(:belt, :jacket) + wardrobe_graph.add_edge(:shirt, :tie) + wardrobe_graph.add_edge(:tie, :jacket) + + sorted_items = TopologicalSorter.new(wardrobe_graph).topological_sort + + assert sorted_items.index(:underwear) < sorted_items.index(:trousers) + assert sorted_items.index(:underwear) < sorted_items.index(:shoes) + assert sorted_items.index(:socks) < sorted_items.index(:shoes) + assert sorted_items.index(:trousers) < sorted_items.index(:shoes) + assert sorted_items.index(:trousers) < sorted_items.index(:belt) + assert sorted_items.index(:shirt) < sorted_items.index(:belt) + assert sorted_items.index(:belt) < sorted_items.index(:jacket) + assert sorted_items.index(:shirt) < sorted_items.index(:tie) + assert sorted_items.index(:tie) < sorted_items.index(:jacket) + end + + def test_topological_sort_raises_exception_for_undirected_graph + nodes = [:u, :v] + graph = UnweightedGraph.new(nodes: nodes, directed: false) + graph.add_edge(:u, :v) + + assert_raises ArgumentError do + TopologicalSorter.new(graph).topological_sort + end + end + + def test_topological_sort_raises_exception_for_cyclic_graph + nodes = [:u, :v] + graph = UnweightedGraph.new(nodes: nodes, directed: true) + graph.add_edge(:u, :v) + graph.add_edge(:v, :u) + + assert_raises ArgumentError do + TopologicalSorter.new(graph).topological_sort + end + end +end diff --git a/data_structures/graphs/unweighted_graph.rb b/data_structures/graphs/unweighted_graph.rb new file mode 100644 index 00000000..8da857ac --- /dev/null +++ b/data_structures/graphs/unweighted_graph.rb @@ -0,0 +1,62 @@ +require 'set' + +## +# This class aims to represent unweighted graphs +# (i.e. graphs for which edges between nodes have no specific weight associated to them). +# +# Both directed (i.e. an edge between node U and node V does not imply an edge in the opposite direction) +# and undirected graphs are supported, depending on the constructor invocation. + +class UnweightedGraph + attr_reader :nodes + attr_reader :directed + + def initialize(nodes: [], neighbors: {}, directed: true) + @nodes = Set[] + @neighbors = {} + @directed = directed + for node in nodes + add_node(node) + end + neighbors.each do |node, neighbors| + for neighbor in neighbors + add_edge(node, neighbor) + end + end + end + + def add_node(node) + if include?(node) + raise ArgumentError, "node #{node} already exists in this graph!" + end + @nodes.add(node) + @neighbors[node] = Set[] + end + + def add_edge(start_node, end_node) + if has_neighbor?(start_node, end_node) + raise ArgumentError, "node #{start_node} already has an edge to #{end_node} in this graph!" + end + @neighbors[start_node].add(end_node) + @neighbors[end_node].add(start_node) unless directed + end + + def neighbors(node) + unless include?(node) + raise ArgumentError, "node #{node} does not exist in this graph!" + end + @neighbors[node] + end + + def empty? + nodes.empty? + end + + def include?(node) + nodes.include?(node) + end + + def has_neighbor?(start_node, end_node) + neighbors(start_node).include?(end_node) + end +end diff --git a/data_structures/graphs/unweighted_graph_test.rb b/data_structures/graphs/unweighted_graph_test.rb new file mode 100644 index 00000000..734cc748 --- /dev/null +++ b/data_structures/graphs/unweighted_graph_test.rb @@ -0,0 +1,82 @@ +require 'minitest/autorun' +require 'set' +require_relative 'unweighted_graph' + +class TestUnweightedGraph < Minitest::Test + def test_directed_unweighted_graph_creation + graph = UnweightedGraph.new(nodes: [:u, :v, :w], neighbors: {:u => [:v]}, directed: true) + + assert graph.nodes.to_set == Set[:u, :v, :w] + assert graph.neighbors(:u).to_set == Set[:v] + assert graph.neighbors(:v).empty? + assert graph.neighbors(:w).empty? + end + + def test_undirected_unweighted_graph_creation + graph = UnweightedGraph.new(nodes: [:u, :v, :w], neighbors: {:u => [:v]}, directed: false) + + assert graph.nodes.to_set == Set[:u, :v, :w] + assert graph.neighbors(:u).to_set == Set[:v] + assert graph.neighbors(:v).to_set == Set[:u] + assert graph.neighbors(:w).empty? + end + + def test_empty_returns_true_for_empty_graph + graph = UnweightedGraph.new + + assert graph.empty? + end + + def test_empty_returns_false_for_non_empty_graph + graph = UnweightedGraph.new(nodes: [:u]) + + assert !graph.empty? + end + + def test_include_returns_true_for_graph_nodes + graph = UnweightedGraph.new(nodes: [:u]) + + assert graph.include?(:u) + end + + def test_include_returns_false_for_non_graph_nodes + graph = UnweightedGraph.new + + assert !graph.include?(:u) + end + + def test_has_neighbor_returns_true_for_graph_node_neighbors + graph = UnweightedGraph.new(nodes: [:u, :v], neighbors: {:u => [:v]}) + + assert graph.has_neighbor?(:u, :v) + end + + def test_has_neighbor_returns_false_for_non_graph_node_neighbors + graph = UnweightedGraph.new(nodes: [:u, :v]) + + assert !graph.has_neighbor?(:u, :v) + end + + def test_add_node_adds_node_to_graph + graph = UnweightedGraph.new + graph.add_node(:u) + + assert graph.nodes.to_set == Set[:u] + end + + def test_add_edge_adds_edge_to_directed_unweighted_graph + graph = UnweightedGraph.new(nodes: [:u, :v], directed: true) + graph.add_edge(:u, :v) + + assert graph.neighbors(:u).to_set == Set[:v] + assert graph.neighbors(:v).empty? + end + + def test_add_edge_adds_edge_to_undirected_unweighted_graph + graph = UnweightedGraph.new(nodes: [:u, :v], directed: false) + graph.add_edge(:u, :v) + + assert graph.neighbors(:u).to_set == Set[:v] + assert graph.neighbors(:v).to_set == Set[:u] + end +end diff --git a/data_structures/graphs/weighted_graph.rb b/data_structures/graphs/weighted_graph.rb new file mode 100644 index 00000000..86e6a175 --- /dev/null +++ b/data_structures/graphs/weighted_graph.rb @@ -0,0 +1,66 @@ +require 'set' + +## +# This class aims to represent weighted graphs +# (i.e. graphs for which edges between nodes have specific weights associated to them). +# +# Both directed (i.e. an edge between node U and node V does not imply an edge in the opposite direction) +# and undirected graphs are supported, depending on the constructor invocation. + +class WeightedGraph + attr_reader :nodes + attr_reader :directed + + def initialize(nodes: [], edges: {}, directed: true) + @nodes = Set[] + @edges = {} + @directed = directed + for node in nodes + add_node(node) + end + edges.each do |node, edges| + for neighbor, weight in edges + add_edge(node, neighbor, weight) + end + end + end + + def add_node(node) + if include?(node) + raise ArgumentError, "node #{node} already exists in this graph!" + end + @nodes.add(node) + @edges[node] = {} + end + + def add_edge(start_node, end_node, weight) + if has_neighbor?(start_node, end_node) + raise ArgumentError, "node #{start_node} already has an edge to #{end_node} in this graph!" + end + @edges[start_node][end_node] = weight + @edges[end_node][start_node] = weight unless directed + end + + def edges(node) + unless include?(node) + raise ArgumentError, "node #{node} does not exist in this graph!" + end + @edges[node] + end + + def empty? + nodes.empty? + end + + def include?(node) + nodes.include?(node) + end + + def has_neighbor?(start_node, end_node) + edges(start_node).key?(end_node) + end + + def edge_weight(start_node, end_node) + edges(start_node)[end_node] + end +end diff --git a/data_structures/graphs/weighted_graph_test.rb b/data_structures/graphs/weighted_graph_test.rb new file mode 100644 index 00000000..d30023a1 --- /dev/null +++ b/data_structures/graphs/weighted_graph_test.rb @@ -0,0 +1,88 @@ +require 'minitest/autorun' +require 'set' +require_relative 'weighted_graph' + +class TestWeightedGraph < Minitest::Test + def test_directed_weighted_graph_creation + graph = WeightedGraph.new(nodes: [:u, :v, :w], edges: {:u => [[:v, 1]]}, directed: true) + + assert graph.nodes.to_set == Set[:u, :v, :w] + assert graph.edges(:u) == {:v => 1} + assert graph.edges(:v).empty? + assert graph.edges(:w).empty? + end + + def test_undirected_weighted_graph_creation + graph = WeightedGraph.new(nodes: [:u, :v, :w], edges: {:u => [[:v, 1]]}, directed: false) + + assert graph.nodes.to_set == Set[:u, :v, :w] + assert graph.edges(:u) == {:v => 1} + assert graph.edges(:v) == {:u => 1} + assert graph.edges(:w).empty? + end + + def test_empty_returns_true_for_empty_graph + graph = WeightedGraph.new + + assert graph.empty? + end + + def test_empty_returns_false_for_non_empty_graph + graph = WeightedGraph.new(nodes: [:u]) + + assert !graph.empty? + end + + def test_include_returns_true_for_graph_nodes + graph = WeightedGraph.new(nodes: [:u]) + + assert graph.include?(:u) + end + + def test_include_returns_false_for_non_graph_nodes + graph = WeightedGraph.new + + assert !graph.include?(:u) + end + + def test_has_neighbor_returns_true_for_graph_node_neighbors + graph = WeightedGraph.new(nodes: [:u, :v], edges: {:u => [[:v, 1]]}) + + assert graph.has_neighbor?(:u, :v) + end + + def test_has_neighbor_returns_false_for_non_graph_node_neighbors + graph = WeightedGraph.new(nodes: [:u, :v]) + + assert !graph.has_neighbor?(:u, :v) + end + + def test_edge_weight_returns_neighbor_edge_weight + graph = WeightedGraph.new(nodes: [:u, :v], edges: {:u => [[:v, 4]]}) + + assert graph.edge_weight(:u, :v) == 4 + end + + def test_add_node_adds_node_to_graph + graph = WeightedGraph.new + graph.add_node(:u) + + assert graph.nodes.to_set == Set[:u] + end + + def test_add_edge_adds_edge_to_directed_weighted_graph + graph = WeightedGraph.new(nodes: [:u, :v], directed: true) + graph.add_edge(:u, :v, 2) + + assert graph.edges(:u) == {:v => 2} + assert graph.edges(:v).empty? + end + + def test_add_edge_adds_edge_to_undirected_weighted_graph + graph = WeightedGraph.new(nodes: [:u, :v], directed: false) + graph.add_edge(:u, :v, 2) + + assert graph.edges(:u) == {:v => 2} + assert graph.edges(:v) == {:u => 2} + end +end diff --git a/data_structures/heaps/max_heap.rb b/data_structures/heaps/max_heap.rb new file mode 100644 index 00000000..7996f7c4 --- /dev/null +++ b/data_structures/heaps/max_heap.rb @@ -0,0 +1,87 @@ +## +# This class represents an array-backed max-heap. + +class MaxHeap + + attr_reader :arr + attr_accessor :heap_size + + ## + # Creates a new max-heap using the provided collection of initial values, if provided (empty by default). + # Note: a clone of the input collection is created to avoid alterations to the input. + + def initialize(elements = []) + @arr = [0] + elements.map(&:clone) + @heap_size = arr.size - 1 + for i in ((arr.size / 2).floor).downto 1 + max_heapify(i) + end + end + + def to_array + return arr[1..heap_size].map(&:clone) + end + + def empty? + return heap_size == 0 + end + + def max + return nil if empty? + return @arr[1] + end + + def extract_max + return nil if empty? + m = max + @arr[1] = @arr[heap_size] + @heap_size -= 1 + max_heapify(1) + return m + end + + def insert(k) + @heap_size += 1 + @arr[heap_size] = -Float::INFINITY + increase_to(heap_size, k) + end + + private + def max_heapify(i) + l = left(i) + r = right(i) + m = i + if l <= heap_size && arr[l] > arr[i] + m = l + end + if r <= heap_size && arr[r] > arr[m] + m = r + end + if m != i + arr[i], arr[m] = arr[m], arr[i] + max_heapify(m) + end + end + + def increase_to(i, k) + raise ArgumentError.new('MaxHeap#increase_to does not support lower values for the key') if arr[i] > k + @arr[i] = k + j = i + while parent(j) > 0 && arr[parent(j)] < arr[j] + arr[j], arr[parent(j)] = arr[parent(j)], arr[j] + j = parent(j) + end + end + + def parent(i) + return (i / 2).floor + end + + def left(i) + return 2*i + end + + def right(i) + return 2*i + 1 + end +end \ No newline at end of file diff --git a/data_structures/heaps/max_heap_test.rb b/data_structures/heaps/max_heap_test.rb new file mode 100644 index 00000000..ef0bbf76 --- /dev/null +++ b/data_structures/heaps/max_heap_test.rb @@ -0,0 +1,46 @@ +require 'minitest/autorun' +require_relative 'max_heap' + +class TestMaxHeap < Minitest::Test + def test_to_array_returns_array_representation + heap = MaxHeap.new([4, 1, 3, 3, 16, 9, 10, 14, 8, 7]) + assert heap.to_array == [16, 14, 10, 8, 7, 9, 3, 3, 4, 1] + end + + def test_empty_returns_true_for_empty_heap + heap = MaxHeap.new + assert heap.empty? + end + + def test_empty_returns_false_for_non_empty_heap + heap = MaxHeap.new([1]) + assert !heap.empty? + end + + def test_max_returns_maximum_heap_element + heap = MaxHeap.new([4, 1, 3]) + assert heap.max == 4 + end + + def test_max_returns_nil_if_empty_heap + heap = MaxHeap.new + assert heap.max.nil? + end + + def test_extract_max_returns_and_removes_maximum_heap_element + heap = MaxHeap.new([4, 1, 3]) + assert heap.extract_max == 4 + assert heap.to_array == [3, 1] + end + + def test_extract_max_returns_nil_if_empty_heap + heap = MaxHeap.new + assert heap.extract_max.nil? + end + + def test_insert_adds_element_to_appropriate_position + heap = MaxHeap.new([4, 1, 3]) + heap.insert(2) + assert heap.to_array == [4, 2, 3, 1] + end +end diff --git a/dynamic_programming/editdistance.rb b/dynamic_programming/editdistance.rb new file mode 100644 index 00000000..c4744cd5 --- /dev/null +++ b/dynamic_programming/editdistance.rb @@ -0,0 +1,74 @@ +require "test/unit" + +def editDistDP(str1, str2, m, n) + rows, cols = m+1,n+1 + + # Create a 2D array to store results of subproblems + dp = Array.new(rows) { Array.new(cols) } + + #using bottom up approach + for i in (0..m + 1-1) do + for j in (0..n + 1-1) do + + #If the first string is empty, insert all the characters of the second string + if i == 0 + dp[i][j] = j + + #If the second string is empty, insert all the characters of the first string + elsif j == 0 + dp[i][j] = i + + #If the last character in both the strings are same, we can ignore the character and move to the next character in both the strings + elsif str1[i-1] == str2[j-1] + dp[i][j] = dp[i-1][j-1] + + #If the last character of both the strings are different, find out the minimum value of the three operations(insert, delete, replace) + else + dp[i][j] = 1 +[dp[i][j-1],dp[i-1][j],dp[i-1][j-1]].min() + + end + + end + + end + + return dp[m][n] +end + + + + +class Editdistancetest < Test::Unit::TestCase + + #Test1 + #Replace 'n' with 'r' + #insert 'a' + #insert 't' + #No of total operations : 3 + def test_distance1 + assert_equal 3, editDistDP( "sunday","saturday",6,8), "Should return 3" + end + + #Test2 + #Replace 'a' with 'u' + #No of total operations : 1 + def test_distance2 + assert_equal 1, editDistDP("cat","cut",3,3), "editDistDpShould return 1" + end + + #Test3 + #Insert 'a','p', 'p','l','e','p','i','e' into string 1 + #No of total operations : 8 + def test_distance3 + assert_equal 8, editDistDP("","applepie",0,8), "editDistDpShould return 1" + end + + #Test4 + #Both the strings are equal, thus no operation needed + #No of total operations : 0 + def test_distance4 + assert_equal 0, editDistDP("Hello","Hello",5,5), "editDistDpShould return 1" + end + + end + diff --git a/dynamic_programming/knapsack.rb b/dynamic_programming/knapsack.rb new file mode 100644 index 00000000..bcac5494 --- /dev/null +++ b/dynamic_programming/knapsack.rb @@ -0,0 +1,64 @@ +require "test/unit" + +# 0-1 Knapsack problem +# The function returns the maximum value that can be put in a knapsack of a given capacity + +def knapSack(weight, wt, val, n) + + rows, cols = n+1,weight+1 + # Create a 2D array to store results of subproblems + dp = Array.new(rows) { Array.new(cols) } + + for i in (0..n + 1-1) + for w in (0..weight + 1-1) + # if the weight is 0 or value is zero, the corresponding cell in the 2D array is set to 0 + if i == 0 || w == 0 + dp[i][w] = 0 + + #If the weight of an element is less than the capacity of the bag, the maximum value of the two cases is taken(Either the element is taken into consideration + #or is ignored) + elsif wt[i-1] <= w + dp[i][w] = [ val[i-1] + dp[i-1][w-wt[i-1]],dp[i-1][w]].max() + + #If the weight of the element is greater than the capacity of the bag, the cell is set to the value of the previous cell + else + dp[i][w] = dp[i-1][w] + end + end + end + + return dp[n][weight] +end + + + + + +class Knapsacktest < Test::Unit::TestCase + + #Test1 + def test_knapsack1 + assert_equal 220, knapSack(50,[10,20,30],[60,100,120],3), "Should return 220" + end + + + #Test2 + def test_knapsack2 + assert_equal 500, knapSack(50,[50, 20, 30],[100, 200, 300],3), "Should return 500" + end + + #Test3 + def test_knapsack3 + assert_equal 17, knapSack(10,[3,4,5, 2, 1],[10,2,3,4,0],5), "Should return 17" + end + + #Test4 + def test_knapsack4 + assert_equal 0, knapSack(0,[23, 17, 12, 8, 20],[199,200,30,41,10],5), "Should return 0" + end + + +end + + + \ No newline at end of file diff --git a/electronics/ohms_law.rb b/electronics/ohms_law.rb index bececb9e..4f7a4195 100644 --- a/electronics/ohms_law.rb +++ b/electronics/ohms_law.rb @@ -3,13 +3,13 @@ # Reference: https://en.wikipedia.org/wiki/Ohm's_law def ohms_law(i, r) - if(i > 0 && r > 0) + if i > 0 && r > 0 "The voltage for given #{i} ampheres current and #{r} ohms resistance is #{r * i} volts." else raise end - rescue - "Error: Please provide valid inputs only!" +rescue StandardError + 'Error: Please provide valid inputs only!' end # Valid inputs @@ -25,7 +25,7 @@ def ohms_law(i, r) # Error: Please provide valid inputs only! puts(ohms_law(-5, -10)) # Error: Please provide valid inputs only! -puts(ohms_law(5, "10")) +puts(ohms_law(5, '10')) # Error: Please provide valid inputs only! -puts(ohms_law("a", 10)) +puts(ohms_law('a', 10)) # Error: Please provide valid inputs only! diff --git a/project_euler/problem_1/sol1.rb b/project_euler/problem_001/sol1.rb similarity index 100% rename from project_euler/problem_1/sol1.rb rename to project_euler/problem_001/sol1.rb diff --git a/project_euler/problem_2/sol1.rb b/project_euler/problem_002/sol1.rb similarity index 100% rename from project_euler/problem_2/sol1.rb rename to project_euler/problem_002/sol1.rb diff --git a/project_euler/problem_3/sol1.rb b/project_euler/problem_003/sol1.rb similarity index 100% rename from project_euler/problem_3/sol1.rb rename to project_euler/problem_003/sol1.rb diff --git a/project_euler/problem_3/sol2.rb b/project_euler/problem_003/sol2.rb similarity index 100% rename from project_euler/problem_3/sol2.rb rename to project_euler/problem_003/sol2.rb diff --git a/project_euler/problem_4/sol1.rb b/project_euler/problem_004/sol1.rb similarity index 100% rename from project_euler/problem_4/sol1.rb rename to project_euler/problem_004/sol1.rb diff --git a/project_euler/problem_4/sol2.rb b/project_euler/problem_004/sol2.rb similarity index 100% rename from project_euler/problem_4/sol2.rb rename to project_euler/problem_004/sol2.rb diff --git a/project_euler/problem_5/sol1.rb b/project_euler/problem_005/sol1.rb similarity index 100% rename from project_euler/problem_5/sol1.rb rename to project_euler/problem_005/sol1.rb diff --git a/project_euler/problem_006/sol1.rb b/project_euler/problem_006/sol1.rb new file mode 100644 index 00000000..b00c9e78 --- /dev/null +++ b/project_euler/problem_006/sol1.rb @@ -0,0 +1,32 @@ +#Project Euler Problem 6: #https://projecteuler.net/problem=6 + +#Sum square difference + +#The sum of the squares of the first ten natural numbers #is, +# 1^2 + 2^2 + ... + 10^2 = 385 +#The square of the sum of the first ten natural numbers #is, +# (1 + 2 + ... + 10)^2 = 55^2 = 3025 +#Hence the difference between the sum of the squares of #the first ten +#natural numbers and the square of the sum is 3025 - 385 = 2640. +#Find the difference between the sum of the squares of the first one +#hundred natural numbers and the square of the sum. + +def solution(num=10) + x = 1 + y = 1 + result = 1 + gap = 3 + while y < num + x += gap + gap += 2 + y += 1 + result += x + end + r_n_pow2_plus_n_pow2 = result + r_sum_n_pow2 = (((num / 2) + 0.5) * num) ** 2 + + r_sum_n_pow2 - r_n_pow2_plus_n_pow2 +end + +answer = solution() +p answer diff --git a/project_euler/problem_007/sol1.rb b/project_euler/problem_007/sol1.rb new file mode 100644 index 00000000..1f79cfbc --- /dev/null +++ b/project_euler/problem_007/sol1.rb @@ -0,0 +1,40 @@ +#Project Euler Problem 7: https://projecteuler.net/problem=7 +#10001st prime +#By listing the first six prime numbers: 2, 3, 5, 7, 11, and 13, we +#can see that the 6th prime is 13. +#What is the 10001st prime number? +#References: https://en.wikipedia.org/wiki/Prime_number + +def is_prime?(number) + value = true + if number > 1 and number < 4 + # 2 and 3 are primes + value = true + elsif number < 2 or number % 2 == 0 or number % 3 == 0 + # Negatives, 0, 1, all even numbers, all multiples of 3 are not primes + value = false + end + end_range = (Math.sqrt(number) + 1).to_i + # All primes number are in format of 6k +/- 1 + for i in (5..end_range).step(6) + if number % i == 0 or number % (i + 2) == 0 + value = false + end + end + result = value +end + +def solution(nth = 10001) + primes = Array.new() + num = 2 + while primes.length < nth + if is_prime?(num) + primes.append(num) + end + num += 1 + end + primes[primes.length - 1] +end + +answer = solution() +p answer \ No newline at end of file diff --git a/project_euler/problem_010/sol1.rb b/project_euler/problem_010/sol1.rb new file mode 100644 index 00000000..053fb624 --- /dev/null +++ b/project_euler/problem_010/sol1.rb @@ -0,0 +1,42 @@ +#Project Euler Problem 10: https://projecteuler.net/problem=10 +#Summation of primes +#The sum of the primes below 10 is 2 + 3 + 5 + 7 = 17. +#Find the sum of all the primes below two million. +#References: https://en.wikipedia.org/wiki/Prime_number +def is_prime?(number) + value = true + if number > 1 and number < 4 + # 2 and 3 are primes + value = true + elsif number < 2 or number % 2 == 0 or number % 3 == 0 + # Negatives, 0, 1, all even numbers, all multiples of 3 are not primes + value = false + end + end_range = (Math.sqrt(number) + 1).to_i + # All primes number are in format of 6k +/- 1 + for i in (5..end_range).step(6) + if number % i == 0 or number % (i + 2) == 0 + value = false + end + end + result = value +end + +def solution(max_total = 2000000) + sum = 1 + num = 2 + value = 1 + while num < max_total and value < max_total + if is_prime?(num) + value += num + if value < max_total + sum = value + end + end + num += 1 + end + result = sum +end + +answer = solution() +p answer \ No newline at end of file diff --git a/project_euler/problem_014/sol1.rb b/project_euler/problem_014/sol1.rb new file mode 100644 index 00000000..6a671dc8 --- /dev/null +++ b/project_euler/problem_014/sol1.rb @@ -0,0 +1,41 @@ +#Problem 14: https://projecteuler.net/problem=14 + +#Problem Statement: +#The following iterative sequence is defined for the set of positive integers: +# +# n → n/2 (n is even) +# n → 3n + 1 (n is odd) +# +#Using the rule above and starting with 13, we generate the following sequence: +# +# 13 → 40 → 20 → 10 → 5 → 16 → 8 → 4 → 2 → 1 +# +#It can be seen that this sequence (starting at 13 and finishing at 1) contains +#10 terms. Although it has not been proved yet (Collatz Problem), it is thought +#that all starting numbers finish at 1. + +#Which starting number, under one million, produces the longest chain? + +def solution() + index_best_result = 0 + for num in 2..1000000 + index_candidate = 0 + n = num + while n > 1 + if n%2 == 0 + n = n / 2 + else + n = (3*n) + 1 + end + index_candidate +=1 + end + if index_best_result < index_candidate + index_best_result = index_candidate + value = num + end + end + result = value +end + +answer = solution() +p answer \ No newline at end of file diff --git a/project_euler/problem_20/sol1.rb b/project_euler/problem_020/sol1.rb similarity index 100% rename from project_euler/problem_20/sol1.rb rename to project_euler/problem_020/sol1.rb diff --git a/project_euler/problem_21/sol1.rb b/project_euler/problem_021/sol1.rb similarity index 100% rename from project_euler/problem_21/sol1.rb rename to project_euler/problem_021/sol1.rb diff --git a/project_euler/problem_22/p022_names.txt b/project_euler/problem_022/p022_names.txt similarity index 100% rename from project_euler/problem_22/p022_names.txt rename to project_euler/problem_022/p022_names.txt diff --git a/project_euler/problem_22/sol1.rb b/project_euler/problem_022/sol1.rb similarity index 100% rename from project_euler/problem_22/sol1.rb rename to project_euler/problem_022/sol1.rb diff --git a/project_euler/problem_025/sol1.rb b/project_euler/problem_025/sol1.rb new file mode 100644 index 00000000..7a9c7c6a --- /dev/null +++ b/project_euler/problem_025/sol1.rb @@ -0,0 +1,44 @@ +#The Fibonacci sequence is defined by the recurrence relation: +# Fn = Fn−1 + Fn−2, where F1 = 1 and F2 = 1. +#Hence the first 12 terms will be: +# +# F1 = 1 +# F2 = 1 +# F3 = 2 +# F4 = 3 +# F5 = 5 +# F7 = 13 +# F8 = 21 +# F6 = 8 +# F9 = 34 +# F10 = 55 +# F11 = 89 +# F12 = 144 +# +#The 12th term, F12, is the first term to contain three digits. +#What is the index of the first term in the Fibonacci sequence to contain 1000 digits? + +def solution(num_digits = 1000) + #Fn = Fn−1 + Fn−2, where F1 = 1 and F2 = 1. + resultn1 = 1 + resultn2 = 1 + result = 2 + index = 3 + value = true + while value + resultn2 = resultn1 + resultn1 = result + if (resultn1 + resultn2).abs.digits.length < num_digits + value = true + else + value = false + end + result = resultn1 + resultn2 + index += 1 + end + res = index +end + +answer = solution() +p answer + \ No newline at end of file diff --git a/searches/fibonacci_search.rb b/searches/fibonacci_search.rb new file mode 100644 index 00000000..439ac41a --- /dev/null +++ b/searches/fibonacci_search.rb @@ -0,0 +1,34 @@ +def fibonacci_search int arr, int element + n = n.size + f2 = 0 + f1 = 1 + f = f2 + f1 + offset = -1 + + while f < n do + f2 = f1; + f1 = f; + f = f2 + f1; + end + + while f > 1 do + i = [offset+f2, n-1].min + + if arr[i] < element + f = f1 + f1 = f2 + f2 = f - f1 + offset = i + elsif arr[i] > element + f = f2 + f1 = f1 - f2 + f2 = f - f1 + else + return i + end + end + + return offset + 1 if f1 && arr[offset + 1] == element + + -1 +end diff --git a/sorting/binary_insertion_sort.rb b/sorting/binary_insertion_sort.rb new file mode 100644 index 00000000..c487464b --- /dev/null +++ b/sorting/binary_insertion_sort.rb @@ -0,0 +1,46 @@ +# Ruby implementation of binary insertion sort algorithm + +def binary_search(arr, val, start, stop) + while start <= stop + + mid = (start + stop) / 2 + + if val == arr[mid] # val is in the middle + return mid + elsif val > arr[mid] # val is on the right side + start = mid + 1 + else + stop = mid - 1 # val is on the left side + end + end + + start +end + +def binary_insertion_sort(arr) + n = arr.size + + (0...n).each do |index| + j = index - 1 + selected = arr[index] + + # find location where selected value should be inserted + location = binary_search(arr, selected, 0, j) + + # move all elements after location to make space + while j >= location + arr[j + 1] = arr[j] + j -= 1 + arr[j + 1] = selected + end + end + + arr +end + +if $0 == __FILE__ + puts 'Enter a list of numbers separated by space' + + list = gets.split.map(&:to_i) + p binary_insertion_sort(list) +end \ No newline at end of file diff --git a/sorting/binary_insertion_sort_test.rb b/sorting/binary_insertion_sort_test.rb new file mode 100644 index 00000000..7b83d4be --- /dev/null +++ b/sorting/binary_insertion_sort_test.rb @@ -0,0 +1,11 @@ +require 'minitest/autorun' +require_relative './sort_tests' +require_relative './binary_insertion_sort' + +class TestBinaryInsertionSort < Minitest::Test + include SortTests + + def sort(input) + binary_insertion_sort(input) + end +end \ No newline at end of file diff --git a/sorting/bubble_sort.rb b/sorting/bubble_sort.rb index 9bef7f12..0c97c813 100644 --- a/sorting/bubble_sort.rb +++ b/sorting/bubble_sort.rb @@ -1,19 +1,19 @@ def bubble_sort(array) - n = array.length - loop do - swapped = false - - (n - 1).times do |i| - if array[i] > array[i + 1] - array[i], array[i + 1] = array[i + 1], array[i] - swapped = true + array_length = array.length + return array if array_length <= 1 + unsorted_until_index = array_length - 1 + sorted = false + until sorted + sorted = true + 0.upto(unsorted_until_index - 1) do |i| + if array[i] > array[i+1] + array[i], array[i+1] = array[i+1], array[i] + sorted = false end end - - break unless swapped + unsorted_until_index -= 1 end - - array + return array end if $0 == __FILE__ diff --git a/sorting/counting_sort.rb b/sorting/counting_sort.rb new file mode 100644 index 00000000..a50f6365 --- /dev/null +++ b/sorting/counting_sort.rb @@ -0,0 +1,26 @@ +## +# Given a non-negative integer value_upper_bound and an array of integers arr with values between 0 and value_upper_bound, +# returns a sorted copy of the input array. +# When value_upper_bound = O(arr.length), sorting runs in O(arr.length). + +def counting_sort(arr, value_upper_bound) + if !value_upper_bound.integer? || value_upper_bound < 0 + raise ArgumentError.new("counting_sort must be invoked with integer value_upper_bound >= 0") + end + if !arr.all? { |elem| elem.integer? && elem.between?(0, value_upper_bound) } + raise ArgumentError.new("counting_sort must be invoked with integer array elements in (0..value_upper_bound)") + end + sorted_arr = Array.new(arr.length) { 0 } + tmp_arr = Array.new(value_upper_bound+1) { 0 } + for elem in arr + tmp_arr[elem] += 1 + end + for i in 1..value_upper_bound + tmp_arr[i] += tmp_arr[i-1] + end + arr.reverse_each do |elem| + sorted_arr[tmp_arr[elem]-1] = elem + tmp_arr[elem] -= 1 + end + sorted_arr +end diff --git a/sorting/counting_sort_test.rb b/sorting/counting_sort_test.rb new file mode 100644 index 00000000..8cabfba4 --- /dev/null +++ b/sorting/counting_sort_test.rb @@ -0,0 +1,42 @@ +require 'minitest/autorun' +require_relative 'counting_sort' + +class TestCountingSort < Minitest::Test + def test_empty_array_given_empty_array + assert counting_sort([], 1).empty? + end + + def test_array_sorted_correctly + assert counting_sort([1, 5, 3, 0, 4, 2, 4], 5) == [0, 1, 2, 3, 4, 4, 5] + end + + def test_exception_given_non_integer_upper_bound + assert_raises ArgumentError do + counting_sort([1, 3, 2], 5.5) + end + end + + def test_exception_given_negative_upper_bound + assert_raises ArgumentError do + counting_sort([1, 3, 2], -1) + end + end + + def test_exception_given_non_integer_elements + assert_raises ArgumentError do + counting_sort([1, 3, 2.5], 5) + end + end + + def test_exception_given_negative_elements + assert_raises ArgumentError do + counting_sort([1, 3, -2], 5) + end + end + + def test_exception_given_elements_above_upper_bound + assert_raises ArgumentError do + counting_sort([1, 3, 6], 5) + end + end +end diff --git a/sorting/gnome_sort.rb b/sorting/gnome_sort.rb new file mode 100644 index 00000000..0e6768eb --- /dev/null +++ b/sorting/gnome_sort.rb @@ -0,0 +1,12 @@ +def gnome_sort(arr) + i = 0 + while i < arr.length + if i == 0 || arr[i] >= arr[i - 1] + i += 1 + else + arr[i], arr[i - 1] = arr[i - 1], arr[i] + i -= 1 + end + end + arr +end diff --git a/sorting/gnome_sort_test.rb b/sorting/gnome_sort_test.rb new file mode 100644 index 00000000..992ee2ad --- /dev/null +++ b/sorting/gnome_sort_test.rb @@ -0,0 +1,11 @@ +require 'minitest/autorun' +require_relative './sort_tests' +require_relative './gnome_sort' + +class TestGnomeSort < Minitest::Test + include SortTests + + def sort(input) + gnome_sort(input) + end +end diff --git a/strings/boyer_moore_horspool_search.rb b/strings/boyer_moore_horspool_search.rb new file mode 100644 index 00000000..78eb7afd --- /dev/null +++ b/strings/boyer_moore_horspool_search.rb @@ -0,0 +1,60 @@ +## +# This class represents a table of {bad_match_character => slide_offset} +# to be used in Boyer-Moore-Horspool substring finding algorithm. + +class BadMatchTable + + attr_reader :pattern + attr_reader :table + + def initialize(pattern) + @pattern = pattern + @table = {} + for i in 0...pattern.size + @table[pattern[i]] = pattern.size - 1 - i + end + end + + ## + # Given a mismatch character belonging to the search string, returns + # the offset to be used when sliding the pattern towards the right. + + def slide_offset(mismatch_char) + table.fetch(mismatch_char, pattern.size) + end +end + +## +# Returns the first starting index of the given pattern's occurrence (as a substring) +# in the provided search string if a match is found, -1 otherwise. + +def first_match_index(search_string, pattern) + matches = matches_indices(search_string, pattern, true) + matches.empty? ? -1 : matches[0] +end + +## +# Returns the list of starting indices of the given pattern's occurrences (as a substring) +# in the provided search string. +# If no match is found, an empty list is returned. +# If `stop_at_first_match` is provided as `true`, the returned list will contain at most one element, +# being the leftmost encountered match in the search string. + +def matches_indices(search_string, pattern, stop_at_first_match=false) + table = BadMatchTable.new(pattern) + i = pattern.size - 1 + indices = [] + while i < search_string.size + for j in 0...pattern.size + if search_string[i-j] != pattern[pattern.size-1-j] + i += table.slide_offset(search_string[i-j]) + break + elsif j == pattern.size-1 + indices.append(i-j) + return indices if stop_at_first_match + i += 1 + end + end + end + indices +end diff --git a/strings/boyer_moore_horspool_search_test.rb b/strings/boyer_moore_horspool_search_test.rb new file mode 100644 index 00000000..990e4f57 --- /dev/null +++ b/strings/boyer_moore_horspool_search_test.rb @@ -0,0 +1,20 @@ +require 'minitest/autorun' +require_relative 'boyer_moore_horspool_search' + +class TestBoyerMooreHorspoolSearch < Minitest::Test + def test_first_match_returns_negative_index_if_no_match + assert first_match_index('abcdefghijk', 'defz') < 0 + end + + def test_first_match_returns_first_match_index + assert first_match_index('abcdefghijkghilmno', 'ghi') == 6 + end + + def test_match_indices_returns_empty_list_if_no_match + assert matches_indices('abcdefghijk', 'defz').empty? + end + + def test_match_indices_returns_list_of_match_indices + assert matches_indices('abcdefghijkghilmno', 'ghi') == [6, 11] + end +end diff --git a/strings/hamming_distance.rb b/strings/hamming_distance.rb new file mode 100644 index 00000000..faa738e7 --- /dev/null +++ b/strings/hamming_distance.rb @@ -0,0 +1,23 @@ +# https://en.wikipedia.org/wiki/Hamming_distance + +def hamming_distance(str1, str2) + abort 'Strings must be of the same length' unless str1.length == str2.length + + str1.chars.zip(str2.chars).sum { |chr1, chr2| chr1 == chr2 ? 0 : 1 } +end + +if $0 == __FILE__ + # Valid inputs + puts hamming_distance 'ruby', 'rust' + # => 2 + puts hamming_distance 'karolin', 'kathrin' + # => 3 + puts hamming_distance 'kathrin', 'kerstin' + # => 4 + puts hamming_distance '0000', '1111' + # => 4 + + # Invalid inputs + puts hamming_distance 'ruby', 'foobar' + # => Strings must be of the same length +end diff --git a/strings/max_k_most_frequent_words.rb b/strings/max_k_most_frequent_words.rb new file mode 100644 index 00000000..210fb46f --- /dev/null +++ b/strings/max_k_most_frequent_words.rb @@ -0,0 +1,36 @@ +require_relative '../data_structures/heaps/max_heap' + +## +# This class represents a word count information +# (i.e. how many occurrences for a word). + +class WordCount + include Comparable + + attr_reader :word + attr_reader :occurrences + + def <=>(other) + occurrences <=> other.occurrences + end + + def initialize(word, occurrences) + @word = word + @occurrences = occurrences + end +end + +## +# Returns the `k` most frequently occurring words, in non-increasing order of occurrence. +# In this context, a word is defined as an element in the provided list. +# +# In case `k` is greater than the number of distinct words, a value of `k` equal +# to the number of distinct words will be considered, instead. + +def max_k_most_frequent_words(words, k) + count_by_word = words.tally + heap = MaxHeap.new(count_by_word.map { |w, c| WordCount.new(w, c) }) + most_frequent_words = [] + [k, count_by_word.size].min.times { most_frequent_words.append(heap.extract_max.word) } + most_frequent_words +end \ No newline at end of file diff --git a/strings/max_k_most_frequent_words_test.rb b/strings/max_k_most_frequent_words_test.rb new file mode 100644 index 00000000..a6397fc3 --- /dev/null +++ b/strings/max_k_most_frequent_words_test.rb @@ -0,0 +1,28 @@ +require 'minitest/autorun' +require_relative 'max_k_most_frequent_words' + +class TestMaxKMostFrequentWords < Minitest::Test + def test_top_3_frequent_words + assert max_k_most_frequent_words(['a', 'b', 'c', 'a', 'c', 'c'], 3) == ['c', 'a', 'b'] + end + + def test_top_2_frequent_words + assert max_k_most_frequent_words(['a', 'b', 'c', 'a', 'c', 'c'], 2) == ['c', 'a'] + end + + def test_top_frequent_word + assert max_k_most_frequent_words(['a', 'b', 'c', 'a', 'c', 'c'], 1) == ['c'] + end + + def test_no_frequent_word_given_zero_k + assert max_k_most_frequent_words(['a', 'b', 'c', 'a', 'c', 'c'], 0) == [] + end + + def test_no_frequent_word_given_empty_word_list + assert max_k_most_frequent_words([], 1) == [] + end + + def test_all_frequent_words_given_k_too_large + assert max_k_most_frequent_words(['a', 'a'], 2) == ['a'] + end +end