diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..991b2c2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,62 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +bin/ +build/ +develop-eggs/ +dist/ +eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.cache +nosetests.xml +coverage.xml + +# Translations +*.mo + +# Mr Developer +.mr.developer.cfg +.project +.pydevproject + +# Rope +.ropeproject + +# Django stuff: +*.log +*.pot + +# Sphinx documentation +docs/_build/ + +# Pants workspace files +/.pants.d/ +/dist/ +/.pids +/.pants.workdir.file_lock* + +# Others +*.bak diff --git a/BUILD b/BUILD new file mode 100644 index 0000000..feacd05 --- /dev/null +++ b/BUILD @@ -0,0 +1,17 @@ +python_requirements() + +python_distribution( + name="python-algorithms", + dependencies=[ + ], + provides=setup_py( + name="python-algorithms", + version="1.0", + description="Python algorithms.", + author="Laurent Luce", + classifiers=[ + "Programming Language :: Python :: 3.6", + ], + ), + setup_py_commands=["sdist", "bdist_wheel", "--python-tag", "py36.py37"] +) diff --git a/README.md b/README.md index b081e2d..85c29c2 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,6 @@ The purpose of this library is to help you with common algorithms like: A* path finding. -Binary tree - - lookup. - - insert. - - delete. - - compare 2 trees. - - print tree. - - tree inorder generator. - String Matching - Naive. - Rabin-Karp. @@ -28,15 +20,16 @@ Generators - Permutations. Lists - - Subset with highest sum. - - Find integer in sorted list. + - Find integer using binary search. + - Find subset with max sum. - Merge sort. - Quicksort. -### Installation -Get the source and run +Binary tree + +### Tests - $ python setup.py install + $ ./pants test :: ### License The Python Algorithms Library is distributed under the MIT License diff --git a/algorithms/BUILD b/algorithms/BUILD new file mode 100644 index 0000000..d06a175 --- /dev/null +++ b/algorithms/BUILD @@ -0,0 +1,7 @@ +python_library( + name="algorithms", + sources=["*.py", "!*_test.py"], + interpreter_constraints=["==2.7.*", ">=3.6"], +) + + diff --git a/algorithms/a_star_path_finding.py b/algorithms/a_star_path_finding.py index e239247..1448cb7 100644 --- a/algorithms/a_star_path_finding.py +++ b/algorithms/a_star_path_finding.py @@ -21,6 +21,9 @@ def __init__(self, x, y, reachable): self.h = 0 self.f = 0 + def __lt__(self, other): + return self.f < other.f + class AStar(object): def __init__(self): diff --git a/algorithms/list.py b/algorithms/list.py index 86fb40f..1546904 100644 --- a/algorithms/list.py +++ b/algorithms/list.py @@ -1,45 +1,45 @@ -def find_int(i, l): +def find_int(i, lst): """Find integer in a sorted list. Example: 4 in [1, 3, 4, 6, 7, 9] -> 2 @param i integer to find. - @param l sorted list. + @param lst sorted list. @returns index if found, None if not. """ - if l: - p_idx = len(l) / 2 - p = l[p_idx] + if lst: + p_idx = len(lst) / 2 + p = lst[p_idx] if i == p: return p_idx - elif len(l) == 1: + elif len(lst) == 1: return elif i < p: - res = find_int(i, l[:p_idx]) + res = find_int(i, lst[:p_idx]) if res: return res elif i > p: - res = find_int(i, l[p_idx:]) + res = find_int(i, lst[p_idx:]) if res: return res + p_idx -def find_max_sub(l): - """Find subset with higest sum. +def find_max_sub(lst): + """Find subset with highest sum. Example: [-2, 3, -4, 5, 1, -5] -> (3,4), 6 - @param l list + @param lst list @returns subset bounds and highest sum """ # max sum - max = l[0] + max = lst[0] # current sum m = 0 # max sum subset bounds bounds = (0, 0) # current subset start s = 0 - for i in range(len(l)): - m += l[i] + for i in range(len(lst)): + m += lst[i] if m > max: max = m bounds = (s, i) @@ -49,7 +49,7 @@ def find_max_sub(l): return bounds, max -def merge_sort(l): +def merge_sort(lst): """Sort list using merge sort. Complexity: O(n log n) @@ -86,32 +86,32 @@ def merge(l1, l2): return res - length = len(l) + length = len(lst) if length <= 1: - return l + return lst mid = length / 2 - h1 = merge_sort(l[:mid]) - h2 = merge_sort(l[mid:]) + h1 = merge_sort(lst[:mid]) + h2 = merge_sort(lst[mid:]) return merge(h1, h2) -def quicksort(l): +def quicksort(lst): """Sort list using quick sort. Complexity: O(n log n). Worst: O(n2) - @param l list to sort. + @param lst list to sort. @returns sorted list. """ - if len(l) <= 1: - return l + if len(lst) <= 1: + return lst - pivot = l[0] + pivot = lst[0] less = [] equal = [] greater = [] - for e in l: + for e in lst: if e < pivot: less.append(e) elif e == pivot: diff --git a/algorithms/permutations.py b/algorithms/permutations.py index 0c789b1..f013d2d 100644 --- a/algorithms/permutations.py +++ b/algorithms/permutations.py @@ -1,11 +1,11 @@ -def permutations(l): +def permutations(lst): """Generator for list permutations. - @param l list to generate permutations for + @param lst list to generate permutations for @result yield each permutation Example: - l = [1,2,3] + lst = [1,2,3] a = [1] permutations([2,3]) = [[2,3], [3,2]] [2,3] @@ -17,10 +17,10 @@ def permutations(l): yield [3,1,2] yield [3,2,1] """ - if len(l) <= 1: - yield l + if len(lst) <= 1: + yield lst else: - a = [l.pop(0)] - for p in permutations(l): + a = [lst.pop(0)] + for p in permutations(lst): for i in range(len(p)+1): yield p[:i] + a + p[i:] diff --git a/algorithms/string.py b/algorithms/string.py index 8f6d134..a809fab 100644 --- a/algorithms/string.py +++ b/algorithms/string.py @@ -1,7 +1,7 @@ def string_matching_naive(text='', pattern=''): """Returns positions where pattern is found in text. - We slide the string to match 'pattern' over the text + Sliding window. O((n-m)m) Example: text = 'ababbababa', pattern = 'aba' @@ -24,6 +24,11 @@ def string_matching_naive(text='', pattern=''): def string_matching_rabin_karp(text='', pattern='', hash_base=256): """Returns positions where pattern is found in text. + Similar to the naive approach but matches the hash value of the pattern + with the hash value of current substring of text. Needs to match + individual characters once a match is found because of potential + hash collisions. + worst case: O(nm) O(n+m) if the number of valid matches is small and the pattern is large. @@ -75,6 +80,8 @@ def hash_value(s, base): def string_matching_knuth_morris_pratt(text='', pattern=''): """Returns positions where pattern is found in text. + https://en.wikipedia.org/wiki/Knuth%E2%80%93Morris%E2%80%93Pratt_algorithm + O(m+n) Example: text = 'ababbababa', pattern = 'aba' string_matching_knuth_morris_pratt(text, pattern) returns [0, 5, 7] @@ -115,6 +122,8 @@ def compute_prefix_function(p): def string_matching_boyer_moore_horspool(text='', pattern=''): """Returns positions where pattern is found in text. + https://en.wikipedia.org/wiki/Boyer%E2%80%93Moore_string-search_algorithm + O(n) Performance: ord() is slow so we shouldn't use it here @@ -187,11 +196,11 @@ def reverse_string_words(s): @param s string words to reverse. @returns reversed string words. """ - def reverse(l, i, j): + def reverse(lst, i, j): # 'word1' -> '1drow' # Complexity: O(n/2) while i != j: - l[i], l[j] = l[j], l[i] + lst[i], lst[j] = lst[j], lst[i] i += 1 j -= 1 diff --git a/algorithms/tests/BUILD b/algorithms/tests/BUILD new file mode 100644 index 0000000..e9edb5d --- /dev/null +++ b/algorithms/tests/BUILD @@ -0,0 +1,6 @@ +# `sources` defaults to ['*_test.py', 'test_*.py', 'conftest.py']. +# `dependencies` are inferred. +python_tests( + name = 'tests', + interpreter_constraints=["==2.7.*", ">=3.6"], +) diff --git a/algorithms/tests/all_tests.py b/algorithms/tests/all_tests.py deleted file mode 100644 index ba9bc3b..0000000 --- a/algorithms/tests/all_tests.py +++ /dev/null @@ -1,20 +0,0 @@ -"""Run all of the tests.""" -import sys -import unittest2 as unittest - - -def main(args=None): - unittest_dir = '.' - unittest_suite = unittest.defaultTestLoader.discover(unittest_dir) - - kwargs = {} - if args and '-v' in args: - kwargs['verbosity'] = 2 - runner = unittest.TextTestRunner(sys.stdout, "Unittests", - **kwargs) - results = runner.run(unittest_suite) - return results.wasSuccessful() - -if __name__ == '__main__': - status = main(sys.argv[1:]) - sys.exit(int(not status)) diff --git a/algorithms/tests/coverage.sh b/algorithms/tests/coverage.sh deleted file mode 100644 index a69b3be..0000000 --- a/algorithms/tests/coverage.sh +++ /dev/null @@ -1,2 +0,0 @@ -coverage run --source=$PYTHONPATH/algorithms --omit=$PYTHONPATH/algorithms/tests/* all_tests.py -coverage report --omit=$PYTHONPATH/algorithms/tests/* -m diff --git a/algorithms/tests/test_a_star_path_finding.py b/algorithms/tests/test_a_star_path_finding.py index e95b7eb..396388d 100644 --- a/algorithms/tests/test_a_star_path_finding.py +++ b/algorithms/tests/test_a_star_path_finding.py @@ -14,7 +14,7 @@ def test_maze(self): (3, 1), (3, 2), (3, 5), (4, 1), (4, 4), (5, 1)) a.init_grid(6, 6, walls, (0, 0), (5, 5)) path = a.solve() - self.assertEqual(path, [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (1, 4), + self.assertEqual(path, [(0, 0), (0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (2, 4), (3, 4), (3, 3), (4, 3), (5, 3), (5, 4), (5, 5)]) @@ -32,5 +32,6 @@ def test_maze_no_solution(self): a.init_grid(6, 6, walls, (0, 0), (5, 5)) self.assertIsNone(a.solve()) + if __name__ == '__main__': unittest.main() diff --git a/algorithms/tests/test_binary_tree.py b/algorithms/tests/test_binary_tree.py index 0ec3397..ca0eede 100644 --- a/algorithms/tests/test_binary_tree.py +++ b/algorithms/tests/test_binary_tree.py @@ -161,5 +161,6 @@ def test_tree_data(self): self.assertEqual([e for e in self.root.tree_data()], [3, 5, 7, 10, 11, 12, 15, 20]) + if __name__ == '__main__': unittest.main() diff --git a/build-support/.flake8 b/build-support/.flake8 new file mode 100644 index 0000000..ef09bcb --- /dev/null +++ b/build-support/.flake8 @@ -0,0 +1 @@ +[flake8] diff --git a/algorithms/maze.py b/constraints.txt similarity index 100% rename from algorithms/maze.py rename to constraints.txt diff --git a/pants b/pants new file mode 100755 index 0000000..e9b5768 --- /dev/null +++ b/pants @@ -0,0 +1,294 @@ +#!/usr/bin/env bash +# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +# =============================== NOTE =============================== +# This ./pants bootstrap script comes from the pantsbuild/setup +# project. It is intended to be checked into your code repository so +# that other developers have the same setup. +# +# Learn more here: https://www.pantsbuild.org/docs/installation +# ==================================================================== + +set -eou pipefail + +# NOTE: To use an unreleased version of Pants from the pantsbuild/pants master branch, +# locate the master branch SHA, set PANTS_SHA= in the environment, and run this script as usual. +# +# E.g., PANTS_SHA=725fdaf504237190f6787dda3d72c39010a4c574 ./pants --version + +PYTHON_BIN_NAME="${PYTHON:-unspecified}" + +# Set this to specify a non-standard location for this script to read the Pants version from. +# NB: This will *not* cause Pants itself to use this location as a config file. +# You can use PANTS_CONFIG_FILES or --pants-config-files to do so. +PANTS_TOML=${PANTS_TOML:-pants.toml} + +PANTS_BIN_NAME="${PANTS_BIN_NAME:-$0}" + +PANTS_SETUP_CACHE="${PANTS_SETUP_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/pants/setup}" +# If given a relative path, we fix it to be absolute. +if [[ "$PANTS_SETUP_CACHE" != /* ]]; then + PANTS_SETUP_CACHE="${PWD}/${PANTS_SETUP_CACHE}" +fi + +PANTS_BOOTSTRAP="${PANTS_SETUP_CACHE}/bootstrap-$(uname -s)-$(uname -m)" + +VENV_VERSION=${VENV_VERSION:-20.2.2} + +VENV_PACKAGE=virtualenv-${VENV_VERSION} +VENV_TARBALL=${VENV_PACKAGE}.tar.gz + +COLOR_RED="\x1b[31m" +COLOR_GREEN="\x1b[32m" +COLOR_RESET="\x1b[0m" + +function log() { + echo -e "$@" 1>&2 +} + +function die() { + (($# > 0)) && log "${COLOR_RED}$*${COLOR_RESET}" + exit 1 +} + +function green() { + (($# > 0)) && log "${COLOR_GREEN}$*${COLOR_RESET}" +} + +function tempdir { + mktemp -d "$1"/pants.XXXXXX +} + +function get_exe_path_or_die { + local exe="$1" + if ! command -v "${exe}"; then + die "Could not find ${exe}. Please ensure ${exe} is on your PATH." + fi +} + +function get_pants_config_value { + local config_key="$1" + local optional_space="[[:space:]]*" + local prefix="^${config_key}${optional_space}=${optional_space}" + local raw_value + raw_value="$(sed -ne "/${prefix}/ s#${prefix}##p" "${PANTS_TOML}")" + echo "${raw_value}" | tr -d \"\' && return 0 + return 0 +} + +function get_python_major_minor_version { + local python_exe="$1" + "$python_exe" <&1 > /dev/null)" == "pyenv: python${version}"* ]]; then + continue + fi + echo "${interpreter_path}" && return 0 + done +} + +function determine_python_exe { + local pants_version="$1" + set_supported_python_versions "${pants_version}" + local requirement_str="For \`pants_version = \"${pants_version}\"\`, Pants requires Python ${supported_message} to run." + + local python_bin_name + if [[ "${PYTHON_BIN_NAME}" != 'unspecified' ]]; then + python_bin_name="${PYTHON_BIN_NAME}" + else + python_bin_name="$(determine_default_python_exe)" + if [[ -z "${python_bin_name}" ]]; then + die "No valid Python interpreter found. ${requirement_str} Please check that a valid interpreter is installed and on your \$PATH." + fi + fi + local python_exe + python_exe="$(get_exe_path_or_die "${python_bin_name}")" + local major_minor_version + major_minor_version="$(get_python_major_minor_version "${python_exe}")" + for valid_version in "${supported_python_versions_int[@]}"; do + if [[ "${major_minor_version}" == "${valid_version}" ]]; then + echo "${python_exe}" && return 0 + fi + done + die "Invalid Python interpreter version for ${python_exe}. ${requirement_str}" +} + +# TODO(John Sirois): GC race loser tmp dirs leftover from bootstrap_XXX +# functions. Any tmp dir w/o a symlink pointing to it can go. + +function bootstrap_venv { + if [[ ! -d "${PANTS_BOOTSTRAP}/${VENV_PACKAGE}" ]]; then + ( + mkdir -p "${PANTS_BOOTSTRAP}" + local staging_dir + staging_dir=$(tempdir "${PANTS_BOOTSTRAP}") + cd "${staging_dir}" + curl -LO "https://pypi.io/packages/source/v/virtualenv/${VENV_TARBALL}" + tar -xzf "${VENV_TARBALL}" + ln -s "${staging_dir}/${VENV_PACKAGE}" "${staging_dir}/latest" + mv "${staging_dir}/latest" "${PANTS_BOOTSTRAP}/${VENV_PACKAGE}" + ) 1>&2 + fi + + local venv_path="${PANTS_BOOTSTRAP}/${VENV_PACKAGE}" + local venv_entry_point + + # shellcheck disable=SC2086 + if [[ -f "${venv_path}/virtualenv.py" ]]; then + venv_entry_point="${venv_path}/virtualenv.py" + elif [[ -f "${venv_path}/src/virtualenv/__main__.py" ]]; then + venv_entry_point="${venv_path}/src/virtualenv/__main__.py" + else + die "Could not find virtualenv entry point for version $VENV_VERSION" + fi + + echo "${venv_entry_point}" +} + +function find_links_url { + local pants_version="$1" + local pants_sha="$2" + echo -n "https://binaries.pantsbuild.org/wheels/pantsbuild.pants/${pants_sha}/${pants_version/+/%2B}/index.html" +} + +function get_version_for_sha { + local sha="$1" + + # Retrieve the Pants version associated with this commit. + local pants_version + pants_version="$(curl --fail -sL "https://raw.githubusercontent.com/pantsbuild/pants/${sha}/src/python/pants/VERSION")" + + # Construct the version as the release version from src/python/pants/VERSION, plus the string `+gitXXXXXXXX`, + # where the XXXXXXXX is the first 8 characters of the SHA. + echo "${pants_version}+git${sha:0:8}" +} + +function bootstrap_pants { + local pants_version="$1" + local python="$2" + local pants_sha="${3:-}" + + local pants_requirement="pantsbuild.pants==${pants_version}" + local maybe_find_links + if [[ -z "${pants_sha}" ]]; then + maybe_find_links="" + else + maybe_find_links="--find-links=$(find_links_url "${pants_version}" "${pants_sha}")" + fi + local python_major_minor_version + python_major_minor_version="$(get_python_major_minor_version "${python}")" + local target_folder_name + target_folder_name="${pants_version}_py${python_major_minor_version}" + + if [[ ! -d "${PANTS_BOOTSTRAP}/${target_folder_name}" ]]; then + ( + local venv_entry_point="$(bootstrap_venv)" + local staging_dir + staging_dir=$(tempdir "${PANTS_BOOTSTRAP}") + "${python}" "${venv_entry_point}" --no-download "${staging_dir}/install" && \ + "${staging_dir}/install/bin/pip" install -U pip && \ + "${staging_dir}/install/bin/pip" install ${maybe_find_links} --progress-bar off "${pants_requirement}" && \ + ln -s "${staging_dir}/install" "${staging_dir}/${target_folder_name}" && \ + mv "${staging_dir}/${target_folder_name}" "${PANTS_BOOTSTRAP}/${target_folder_name}" && \ + green "New virtual environment successfully created at ${PANTS_BOOTSTRAP}/${target_folder_name}." + ) 1>&2 + fi + echo "${PANTS_BOOTSTRAP}/${target_folder_name}" +} + +# Ensure we operate from the context of the ./pants buildroot. +cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +pants_version="$(determine_pants_version)" +python="$(determine_python_exe "${pants_version}")" +pants_dir="$(bootstrap_pants "${pants_version}" "${python}" "${PANTS_SHA:-}")" +pants_python="${pants_dir}/bin/python" +pants_binary="${pants_dir}/bin/pants" +pants_extra_args="" +if [[ -n "${PANTS_SHA:-}" ]]; then + pants_extra_args="${pants_extra_args} --python-repos-repos=$(find_links_url "$pants_version" "$PANTS_SHA")" +fi + +# We set the env var no_proxy to '*', to work around an issue with urllib using non +# async-signal-safe syscalls after we fork a process that has already spawned threads. +# +# See https://blog.phusion.nl/2017/10/13/why-ruby-app-servers-break-on-macos-high-sierra-and-what-can-be-done-about-it/ +export no_proxy='*' + +# shellcheck disable=SC2086 +exec "${pants_python}" "${pants_binary}" ${pants_extra_args} \ + --pants-bin-name="${PANTS_BIN_NAME}" --pants-version=${pants_version} "$@" diff --git a/pants.toml b/pants.toml new file mode 100644 index 0000000..0c3de5a --- /dev/null +++ b/pants.toml @@ -0,0 +1,30 @@ +[GLOBAL] +pants_version = "2.1.0" +backend_packages = [ + "pants.backend.python", + "pants.backend.python.lint.flake8" +] + +[source] +# The Python source root is the repo root. See https://www.pantsbuild.org/docs/source-roots. +root_patterns = ["/"] + +[python-setup] +# The default interpreter compatibility for code in this repo. Individual targets can override +# this with the `interpreter_constraints` field. See +# https://www.pantsbuild.org/docs/python-interpreter-compatibility. +interpreter_constraints = ["==2.7.*", ">=3.6"] +# Use a constraints file. See https://www.pantsbuild.org/docs/python-third-party-dependencies. +requirement_constraints = "constraints.txt" +# We search for interpreters on both on the $PATH and in the `$(pyenv root)/versions` folder. +# If you're using macOS, you may want to leave off the entry to avoid using the +# problematic system Pythons. See +# https://www.pantsbuild.org/docs/python-interpreter-compatibility#changing-the-interpreter-search-path. +interpreter_search_paths = ["", ""] + +[flake8] +config = "build-support/.flake8" + +[pytest] +version = "pytest>=4.0,<6.1" +pytest_plugins = ["zipp>=1.2"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e69de29 diff --git a/setup.py b/setup.py deleted file mode 100644 index 0137842..0000000 --- a/setup.py +++ /dev/null @@ -1,29 +0,0 @@ -from distutils.core import setup -setup( - name = "algorithms", - packages = ['algorithms'], - version = "0.1", - description = "Algorithms implemented in Python", - author = "Laurent Luce", - author_email = "laurentluce49@yahoo.com", - url = "http://github.com/laurentluce/python-algorithms", - download_url = "http://github.com/laurentluce/python-algorithms", - keywords = ["algorithms"], - classifiers = [ - "Programming Language :: Python", - "Operating System :: OS Independent", - "License :: OSI Approved :: MIT License", - "Intended Audience :: Developers", - "Development Status :: 5 - Production/Stable", - "Topic :: Software Development :: Libraries :: Python Modules" - ], - long_description = """\ - Python Algorithms Library - ---------------------------- - - DESCRIPTION - The purpose of this library is to help you with basic and more advanced - algorithms - - LICENSE The Python Algorithms Library is distributed under the MIT - License """ )