Skip to content

Commit b73540a

Browse files
authored
Merge pull request #8 from djeada/djeada-patch-1
Update all_permutations.py
2 parents 2a9cb48 + 5e2fd4e commit b73540a

File tree

4 files changed

+210
-56
lines changed

4 files changed

+210
-56
lines changed

run_tests.sh

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,25 @@ set -e
33

44
# Determine base branch by looking at the upstream. If no upstream is set, fall back to "master".
55
detect_base_branch() {
6-
# Try to get the upstream name, e.g. "origin/master" or "origin/main"
6+
# If there’s an upstream (e.g. origin/master or origin/main), use that.
77
if base="$(git rev-parse --abbrev-ref --symbolic-full-name @{u} 2>/dev/null)"; then
88
echo "$base"
99
else
1010
echo "master"
1111
fi
1212
}
1313

14-
# Find all directories under start_dir that contain a CMakeLists.txt
14+
# Find all directories (under start_dir) that contain a CMakeLists.txt
1515
find_cmake_subdirs() {
1616
local start_dir="$1"
1717
find "$start_dir" -type f -name 'CMakeLists.txt' -printf '%h\n' | sort -u
1818
}
1919

20-
# Find all directories under start_dir that contain a __init__.py
20+
# Find only those Python-package roots that actually have a tests/ subdirectory.
21+
# In other words, look for “*/tests” and return the parent directory.
2122
find_python_subdirs() {
2223
local start_dir="$1"
23-
find "$start_dir" -type f -name '__init__.py' -printf '%h\n' | sort -u
24+
find "$start_dir" -type d -name 'tests' -printf '%h\n' | sort -u
2425
}
2526

2627
# Get the list of files changed since the last commit on base branch
@@ -34,15 +35,15 @@ test_cpp_projects() {
3435
local current_dir
3536
current_dir=$(pwd)
3637

37-
# Find every directory that has a CMakeLists.txt
38+
# All C++ project directories (where CMakeLists.txt lives)
3839
local all_subdirs
3940
all_subdirs=$(find_cmake_subdirs .)
4041

41-
# Which files changed in this PR (relative to base)
42+
# Files modified in this PR relative to base
4243
local modified_files
4344
modified_files=$(get_modified_files)
4445

45-
# Filter to only those C++ subdirs where at least one file was modified
46+
# Filter to only those C++ subdirs in which at least one file was modified
4647
local modified_subdirs=""
4748
for subdir in $all_subdirs; do
4849
# strip leading "./" for comparison against git output
@@ -85,14 +86,20 @@ test_cpp_projects() {
8586
cleanup
8687
cd "$current_dir"
8788

89+
# Safely extract “<number> tests from” (if any) or default to 0
8890
local cpp_total_tests
8991
cpp_total_tests=$(grep -oP '\d+(?= tests from)' "$cpp_test_log" | tail -1 || echo 0)
92+
cpp_total_tests="${cpp_total_tests:-0}"
93+
94+
# Safely extract passed count (if any) or default to 0
9095
local cpp_passed_tests
9196
cpp_passed_tests=$(grep -oP '(?<=\[ *PASSED *\] )\d+' "$cpp_test_log" | tail -1 || echo 0)
92-
local cpp_failed_tests=$((cpp_total_tests - cpp_passed_tests))
97+
cpp_passed_tests="${cpp_passed_tests:-0}"
98+
99+
local cpp_failed_tests=$(( cpp_total_tests - cpp_passed_tests ))
93100

94-
total_passed_tests=$((total_passed_tests + cpp_passed_tests))
95-
total_failed_tests=$((total_failed_tests + cpp_failed_tests))
101+
total_passed_tests=$(( total_passed_tests + cpp_passed_tests ))
102+
total_failed_tests=$(( total_failed_tests + cpp_failed_tests ))
96103

97104
echo "C++ Tests summary for $subdir:"
98105
echo -e " Passed: \e[32m$cpp_passed_tests\e[0m, Failed: \e[31m$cpp_failed_tests\e[0m"
@@ -106,15 +113,15 @@ test_python_projects() {
106113
local current_dir
107114
current_dir=$(pwd)
108115

109-
# Find every directory that has an __init__.py
116+
# Only pick up directories that actually have a “tests/” folder
110117
local all_subdirs
111118
all_subdirs=$(find_python_subdirs .)
112119

113120
# Which files changed in this PR (relative to base)
114121
local modified_files
115122
modified_files=$(get_modified_files)
116123

117-
# Filter to only those Python subdirs where at least one file was modified
124+
# Filter to only those Python-root dirs where at least one file was modified
118125
local modified_subdirs=""
119126
for subdir in $all_subdirs; do
120127
local sub="${subdir#./}"
@@ -140,17 +147,25 @@ test_python_projects() {
140147
python_test_log="$current_dir/python_test_$(echo "$subdir" | tr '/' '_').log"
141148
: > "$python_test_log"
142149

150+
# Run unittest discovery; any output goes into the log
143151
python3 -m unittest discover -v 2>&1 | tee -a "$python_test_log"
144152
cd "$current_dir"
145153

154+
# Safely grab “Ran X tests” (default to 0 if none found)
146155
local python_total_tests
147156
python_total_tests=$(grep -oP '(?<=Ran )\d+' "$python_test_log" | head -1 || echo 0)
157+
python_total_tests="${python_total_tests:-0}"
158+
159+
# Count how many “... ok” lines (default to 0)
148160
local python_passed_tests
149161
python_passed_tests=$(grep -c '\.\.\. ok' "$python_test_log" || echo 0)
150-
local python_failed_tests=$((python_total_tests - python_passed_tests))
162+
python_passed_tests="${python_passed_tests:-0}"
163+
164+
# Compute failures
165+
local python_failed_tests=$(( python_total_tests - python_passed_tests ))
151166

152-
total_passed_tests=$((total_passed_tests + python_passed_tests))
153-
total_failed_tests=$((total_failed_tests + python_failed_tests))
167+
total_passed_tests=$(( total_passed_tests + python_passed_tests ))
168+
total_failed_tests=$(( total_failed_tests + python_failed_tests ))
154169

155170
echo "Python Tests summary for $subdir:"
156171
echo -e " Passed: \e[32m$python_passed_tests\e[0m, Failed: \e[31m$python_failed_tests\e[0m"
Lines changed: 97 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
## Generating all permutations of a list of elements
1+
## Generating All Permutations of a List
22

3-
Given a list of elements, the problem is to generate all possible permutations of these elements.
4-
5-
For example, if the input list is `[1, 2, 3]`, the possible permutations are:
3+
Given a list of *n* distinct elements, the goal is to produce every possible ordering (permutation) of those elements. For example, if the input is `[1, 2, 3]`, the six permutations are:
64

75
```
86
[1, 2, 3]
@@ -13,22 +11,105 @@ For example, if the input list is `[1, 2, 3]`, the possible permutations are:
1311
[3, 2, 1]
1412
```
1513

16-
## Approach
14+
Because there are *n!* permutations of *n* items, any algorithm will inevitably take at least *O(n!)* time just to enumerate them. In practice, two common ways to generate all permutations in Python are:
15+
16+
1. **Using a built-in library function (e.g., `itertools.permutations`)**
17+
2. **Writing a pure-Python backtracking routine**
18+
19+
Below is a conceptual overview of each approach, along with its time complexity and a brief discussion of advantages and trade-offs.
20+
21+
---
22+
23+
### 1. Built-in Permutations (Conceptual)
24+
25+
Most languages have a library routine that directly yields all permutations of a sequence in an efficient, low-level implementation. In Python, for instance, `itertools.permutations` returns every ordering as a tuple. Converting those tuples to lists (if needed) simply costs an extra *O(n)* per permutation.
26+
27+
* **Core idea**
28+
29+
* Defer the heavy lifting to a built-in routine that is usually implemented in C (or another compiled language).
30+
* Each call to the library function produces one permutation in constant (amortized) time.
31+
* If you need the result as lists instead of tuples, you convert each tuple to a list before collecting it.
32+
33+
* **Time Complexity**
34+
35+
* There are *n!* total permutations.
36+
* Generating each tuple is effectively *O(1)* (amortized), but converting to a list of length *n* costs *O(n)*.
37+
* Overall: **O(n! · n)**.
38+
39+
* **Pros**
40+
41+
* Very concise—just a single function call.
42+
* Relies on a battle-tested standard library implementation.
43+
* Highly optimized in C under the hood.
44+
45+
* **Cons**
46+
47+
* Still incurs the *O(n)* conversion for each permutation if you need lists.
48+
* Less educational (you don’t see how the algorithm actually works).
49+
50+
---
51+
52+
### 2. In-Place Backtracking (Conceptual)
53+
54+
A backtracking algorithm builds each permutation by swapping elements in the original list in place, one position at a time. Once every position is “fixed,” you record a copy of the list in that order. Then you swap back and proceed to the next possibility.
55+
56+
* **Core idea (pseudocode)**
57+
58+
1. Let the input list be `A` of length *n*.
59+
2. Define a recursive routine `backtrack(pos)` that means “choose which element goes into index `pos`.”
60+
3. If `pos == n`, then all indices are filled—append a copy of `A` to the results.
61+
4. Otherwise, for each index `i` from `pos` to `n−1`:
62+
63+
* Swap `A[pos]` and `A[i]`.
64+
* Recursively call `backtrack(pos + 1)`.
65+
* Swap back to restore the original order before trying the next `i`.
66+
67+
* **Time Complexity**
68+
69+
* Exactly *n!* permutations will be generated.
70+
* Each time you reach `pos == n`, you copy the current list (cost *O(n)*).
71+
* Swapping elements is *O(1)*, and there are lower-order operations for looping.
72+
* Overall: **O(n! · n)**.
73+
74+
* **Pros**
75+
76+
* Pure-Python and fairly straightforward to implement once you understand swapping + recursion.
77+
* Does not require constructing new intermediate lists at every recursive call—only one final copy per permutation.
78+
* In-place swapping keeps overhead minimal (aside from the final copy).
79+
80+
* **Cons**
81+
82+
* A bit more code and recursion overhead.
83+
* Uses recursion up to depth *n* (though that is usually acceptable unless *n* is quite large).
84+
* Easier to make an off-by-one mistake if you forget to swap back.
85+
86+
---
87+
88+
## Time Complexity (Both Approaches)
89+
90+
No matter which method you choose, you must generate all *n!* permutations and produce them as lists/arrays. Since each complete permutation takes at least *O(n)* time to output or copy, the combined runtime is always:
91+
92+
```
93+
O(n! · n)
94+
```
95+
96+
* *n!* choices of permutation
97+
* Copying or formatting each permutation (length n) costs an extra n
1798

18-
One approach to solve this problem is to use a backtracking algorithm.
99+
---
19100

20-
A backtracking algorithm works by starting with an empty list and adding elements to it one at a time, then exploring the resulting permutations, and backtracking (removing the last added element) when it is no longer possible to generate new permutations.
101+
## When to Use Each Approach
21102

22-
To implement the backtracking algorithm, we can use a recursive function that takes two arguments:
103+
* **Built-in library function**
23104

24-
* `input_list`: the list of elements from which the permutations are generated.
25-
* `output_list`: the current permutation being generated.
105+
* Ideal if you want minimal code and maximum reliability.
106+
* Use whenever your language provides a standard “permutations” routine.
107+
* Particularly helpful if you only need to iterate lazily over permutations (you can yield one tuple at a time).
26108

27-
The function can follow these steps:
109+
* **In-place backtracking**
28110

29-
* If the length of `output_list` is equal to the length of `input_list`, it means that a permutation of all the elements has been found. In this case, the function can append the permutation to a list of results and return.
30-
* If the length of `output_list` is not equal to the length of `input_list`, the function can enter a loop that enumerates the elements of the input list.
31-
* For each element, the function can check if it is present in the `output_list`. If the element is not present, the function can call itself recursively with `input_list and output_list + [element]` (to add the element to the permutation).
32-
* After the loop, the function can return the list of results.
111+
* Educational: you see how swapping and recursion produce every ordering.
112+
* Useful if you need to integrate custom pruning or constraints (e.g., skip certain permutations early).
113+
* Allows you to avoid tuple-to-list conversions if you directly output lists, although you still pay an *O(n)* copy cost per permutation.
33114

34-
The time complexity of this approach is $O(n! * n)$, where n is the length of the input list.
115+
In most practical scenarios—unless you have a specialized constraint or want to illustrate the algorithm—using the built-in routine is recommended for brevity and performance. However, if you need to customize the traversal (e.g., skip certain branches), an in-place backtracking solution makes it easy to check conditions before fully expanding each branch.
Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,45 @@
1-
def all_permutations(input_list):
2-
def _all_permutations(input_list, output_list):
3-
if len(output_list) == len(input_list):
4-
result.append(output_list)
1+
from typing import Any, List
2+
import itertools
3+
4+
5+
def all_permutations_itertools(input_list: List[Any]) -> List[List[Any]]:
6+
"""
7+
Return all permutations of input_list using itertools.permutations,
8+
converted into lists.
9+
10+
Time complexity:
11+
O(n! * n) where n = len(input_list). itertools.permutations generates each
12+
of the n! permutations in O(1) amortized time, but converting each tuple to a list
13+
costs O(n), so the overall complexity is O(n! * n).
14+
"""
15+
return [list(p) for p in itertools.permutations(input_list)]
16+
17+
18+
def all_permutations_backtracking(input_list: List[Any]) -> List[List[Any]]:
19+
"""
20+
Return all permutations of input_list using in-place backtracking.
21+
22+
Time complexity:
23+
O(n! * n) where n = len(input_list).
24+
We generate n! permutations, and each time we reach a full permutation,
25+
we append a copy of length-n list (O(n) copy cost). Swapping operations
26+
also contribute lower-order overhead but do not change the factorial growth.
27+
"""
28+
29+
def _backtrack(start_index: int):
30+
# If we have fixed positions up to the end, record a copy of the current list
31+
if start_index == n:
32+
result.append(input_list.copy())
533
return
634

7-
for element in input_list:
8-
if element not in output_list:
9-
_all_permutations(input_list, output_list + [element])
35+
for i in range(start_index, n):
36+
# Swap element i into the 'start_index' position
37+
input_list[start_index], input_list[i] = input_list[i], input_list[start_index]
38+
_backtrack(start_index + 1)
39+
# Swap back to restore original order before the next iteration
40+
input_list[start_index], input_list[i] = input_list[i], input_list[start_index]
1041

11-
result = []
12-
_all_permutations(input_list, [])
42+
n = len(input_list)
43+
result: List[List[Any]] = []
44+
_backtrack(0)
1345
return result

src/backtracking/python/all_permutations/tests/test_all_permutations.py

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,50 @@
1-
from src.all_permutations import all_permutations
1+
# tests/test_all_permutations.py
2+
23
import unittest
34

5+
from src.all_permutations import (
6+
all_permutations_itertools,
7+
all_permutations_backtracking
8+
)
9+
410

511
class TestAllPermutations(unittest.TestCase):
12+
def setUp(self):
13+
# We will test these two implementations side by side in every test.
14+
self.funcs_to_test = [
15+
all_permutations_itertools,
16+
all_permutations_backtracking,
17+
]
18+
619
def test_two_elements(self):
720
input_list = [1, 2]
8-
excepted = [[1, 2], [2, 1]]
9-
actual = all_permutations(input_list)
21+
expected = [[1, 2], [2, 1]]
1022

11-
self.assertListEqual(sorted(excepted), sorted(actual))
23+
for func in self.funcs_to_test:
24+
with self.subTest(func=func.__name__):
25+
actual = func(input_list[:]) # pass a fresh copy to avoid side‐effects
26+
# Sort both lists of lists before comparing
27+
self.assertListEqual(sorted(expected), sorted(actual))
1228

1329
def test_three_elements(self):
1430
input_list = [3, 1, 2]
15-
excepted = [[1, 2, 3], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], [3, 1, 2]]
16-
17-
actual = all_permutations(input_list)
31+
expected = [
32+
[1, 2, 3],
33+
[1, 3, 2],
34+
[2, 1, 3],
35+
[2, 3, 1],
36+
[3, 2, 1],
37+
[3, 1, 2],
38+
]
1839

19-
self.assertListEqual(sorted(excepted), sorted(actual))
40+
for func in self.funcs_to_test:
41+
with self.subTest(func=func.__name__):
42+
actual = func(input_list[:])
43+
self.assertListEqual(sorted(expected), sorted(actual))
2044

2145
def test_three_strings(self):
2246
input_list = ["A", "B", "C"]
23-
excepted = [
47+
expected = [
2448
["A", "B", "C"],
2549
["A", "C", "B"],
2650
["B", "A", "C"],
@@ -29,13 +53,14 @@ def test_three_strings(self):
2953
["C", "B", "A"],
3054
]
3155

32-
actual = all_permutations(input_list)
33-
34-
self.assertListEqual(sorted(excepted), sorted(actual))
56+
for func in self.funcs_to_test:
57+
with self.subTest(func=func.__name__):
58+
actual = func(input_list[:])
59+
self.assertListEqual(sorted(expected), sorted(actual))
3560

3661
def test_four_elements(self):
3762
input_list = [3, 1, 2, 4]
38-
excepted = [
63+
expected = [
3964
[3, 1, 2, 4],
4065
[3, 1, 4, 2],
4166
[3, 2, 1, 4],
@@ -62,9 +87,10 @@ def test_four_elements(self):
6287
[4, 2, 1, 3],
6388
]
6489

65-
actual = all_permutations(input_list)
66-
67-
self.assertListEqual(sorted(excepted), sorted(actual))
90+
for func in self.funcs_to_test:
91+
with self.subTest(func=func.__name__):
92+
actual = func(input_list[:])
93+
self.assertListEqual(sorted(expected), sorted(actual))
6894

6995

7096
if __name__ == "__main__":

0 commit comments

Comments
 (0)