Data on stack; a limited set of instructions; the lowest possible number of operations. Fun with data sorting algorithms!
- About π
push_swap
Requirements Overview βpush_swap
Operations- Operations Example
- Complexity
push_swap
Implementation π- Data Structures
- Processing Input Arguments
- Error / Invalid Input Handling
- Creating the Stacks
- Sorting the Stacks
- Freeing the Stacks
- The Algorithm (Loosely Based on the
QuickSort Algorithm
) - Bonus: Checker Requirements Overview β
- Checker Implementation π
- Usage π
- Build
push_swap
- Build
checker
- Tests π§ͺ
- General Tests
- Memory Leak Tests
- Run with GDB
- Check Available (make) Commands
- License π
-
It takes a stack as an argument, formatted as a list of positive and/or negative integers.
-
The first element should be at the top of the stack.
-
The program prints to
stdout
the instructions to sortstack_a
separated by a\n
. -
The goal is to sort the stack with the lowest possible number of operations.
-
If no argument is given, the program gives the prompt back and does nothing.
-
In case of an error, it displays "Error" followed by a
\n
tostderr
.
Errors include:
- Some arguments are not integers.
- Some arguments are bigger than an integer.
- Some arguments are duplicates.
Important
The objective of push_swap
is to sort the values in stack_a
in ascending order using a set of push_swap
operations.
To get the stack sorted, we have the following operations at our disposal:
Code | Operation | Description |
---|---|---|
sa |
swap a | swaps the 2 top elements of stack_a |
sb |
swap b | swaps the 2 top elements of stack_b |
ss |
swap a & swap b | performs both sa and sb |
pa |
push a | moves the top element of stack b to the top of stack_a |
pb |
push b | moves the top element of stack a to the top of stack_b |
ra |
rotate a | shifts all elements of stack_a from bottom to top |
rb |
rotate b | shifts all elements of stack_b from bottom to top |
rr |
rotate a & rotate b | both ra and rb |
rra |
reverse rotate a | shifts all elements of stack_a from top to bottom |
rrb |
reverse rotate b | shifts all elements of stack_b from top to bottom |
rrr |
reverse rotate a & reverse rotate b | performs both rra and rrb |
To show these instructions in action, letβs sort a random list of integers. In this example, weβll consider that both stacks grow from the right.
Init a and b | sa |
pb |
pb |
pb |
ra |
rb |
rra |
rrb |
sa |
pa |
pa |
pa |
---|---|---|---|---|---|---|---|---|---|---|---|---|
2 | 1 |
1 |
||||||||||
1 | 2 |
2 | 2 |
2 | ||||||||
3 | 3 | 3 | 3 | 3 |
3 | 3 | ||||||
6 | 6 | 6 | 6 | 6 3 |
5 3 | 5 2 | 6 2 |
6 3 |
5 3 |
5 | 5 | 5 |
5 | 5 | 5 | 5 2 |
5 2 | 8 2 | 8 1 | 5 1 | 5 2 | 6 2 |
6 2 | 6 | 6 |
8 | 8 | 8 1 |
8 1 | 8 1 | 6 1 |
6 3 |
8 3 | 8 1 | 8 1 | 8 1 | 8 1 | 8 |
a b | a b | a b | a b | a b | a b | a b | a b | a b | a b | a b | a b | a b |
Also known as Time Complexity
(studied by Complexity Analysis
), is the computational complexity that describes the amount of computer time it takes to run an algorithm (usually measured by the number of needed elementary operations, assuming each operation takes a fixed amount of time to be performed).
Note
Algorithm
Algorithms are procedures or instructions (set of steps) that tell a computer what to do and how to do it.
Computational Complexity
The computational complexity of an algorithm is the amount of resources required to run it. Particular focus is given to:
- computation time;
- and memory storage requirements.
Since an algorithm's running time usually varies between different inputs of the same size, it is common to consider primarily the worst-case time complexity.
Time Complexity represents the number of times a statement is executed. It is generally expressed as a function of the input size (How quick the run time of an algorithm grows relative to input size).
Such a function is often very difficult to define, and since its running time is usually inconsequential (dependant on hardware, programming language, Operating System software, processing power, etc.), it is customary to focus on the asymptotic behaviour of the complexity.
Important
The asymptotic behaviour of a function is the behaviour of the function as the input size increases.
Taking this into consideration, Time Complexity is most commonly defined using Big O Notation, expressed as
It represents the upper bound of the running time of an algorithm. As mentioned earlier, it gives the worst-case time complexity of an algorithm: the maximum length of time required to run a given algorithm.
It represents the lower bound of the running time of an algorithm. It gives the best-case time complexity of an algorithm: the lower-bound of an algorithm's time complexity.
Represents the upper-bound and lower-bound of the running time of an algorithm, enclosing the function from above and below: it is used to analyze the average-case time complexity of an algorithm.
Is used as a tight upper bound on the growth of an algorithmβs effort, even though, as written, it can also be a loose upper bound that cannot be tight.
Describes the relationship between two functions when one grows strictly faster than the other: a relative growth rate.
Complexity | Time Complexity | Example |
---|---|---|
Constant | Finding the median value in a sorted array of numbers. | |
|
Logarithmic | Binary Search |
Linear | Finding the smallest/largest element in an array. | |
Log Linear | Fastest possible comparison sort (Fast Fourier Transform). | |
Quadratic | Bubble sort, Insertion Sort, Direct Convolution. | |
Cubic | Naive multiplication of |
|
... | ... | ... |
The sorting algorithm used in this implementation is a variation of the QuickSort Algorithm
adapted to sort stacks using only the aforementioned operations.
Before we get into the nitty and gritty details of the algorithm, let's take a look at how the program handles input arguments and sets up the stacks.
stack_a
andstack_b
, are arrays oft_elem
structs:
typedef struct s_elem {
int num;
int index;
int set;
} t_elem;
int main(int argc, char **argv)
{
(...)
++argv;
input_list = argv;
must_free = 0;
if (argc == 2)
input_list = ft_get_elems(&argc, argv, &must_free);
(...)
}
- First, it increments
argv
so to skip the program's name; - It then assigns
argv
toinput_list
. - Initializes
must_free
flag to 0 (This flag is used to indicate that the memory was allocated and therefore needs to be freed later); - If
argc
is 2 (meaning a single command line argument was provided)ft_get_elems()
is called:
static char **ft_get_elems(int *argc, char **argv, int *must_free)
{
char **split_list;
split_list = ft_split(argv[0], ' ');
*argc = ft_argv_count(split_list) + 1;
*must_free = 1;
return (split_list);
}
- Splits the single argument into multiple elements based on the specified separator (' ') storing the result in
split_list
. This is accomplished using the ft_split function defined in my custom libft helper library;- It counts the number of elements in the
split_list
using theft_argv_count()
and adds 1 to the count (to account for the program name skipped at the start of execution). This updated count is assigned toargc
.- It sets the
must_free
flag to 1 (indicatingsplit_list
must be freed).- It returns
split_list
containing the list of input elements.
int main(int argc, char **argv)
{
(...)
error = ft_errors(argc, input_list);
if (error <= 0)
{
ft_free(NULL, NULL, input_list, must_free);
return (0);
}
(...)
}
- Next, the program checks for invalid input in the
input_list
using theft_errors()
function:- If an error is found (indicated by a return value less than or equal to 0), it frees any previously allocated memory and exits the cleanly.
int ft_errors(int argc, char **input_list)
{
if (argc == 1)
return (0);
if (ft_are_args_nbr(argc, input_list) == -1)
{
ft_putstr_fd("Error\n", 2);
return (-1);
}
if (ft_is_duplicate(argc, input_list) == -1)
{
ft_putstr_fd("Error\n", 2);
return (-1);
}
return (1);
}
- In case
argc
is 1, meaning no arguments were provided, the function simply returns 0.- If the values in
input_list
are NOT numbers, the call toft_are_args_nbr()
returns -1. In this case, the function prints "Error\n" to thestderr
and returns -1.- Lastly if there are duplicate numbers in the
input_list
, the call toft_is_duplicate()
returns -1, in which case the function prints "Error\n" to thestderr
and returns -1.- If no errors are detected, the function returns 1.
int main(int argc, char **argv)
{
(...)
stack_a = ft_create_stack(argc, input_list, 1);
stack_b = ft_create_stack(argc, input_list, 0);
(...)
}
- If no errors were detected, the program creates two stacks (
stack_a
andstack_b
) from the input arguments using theft_create_stack()
function (the third argument indicates whether the stack should be filled with the input elements (stack_a
) or left empty (stack_b
).
t_elem *ft_create_stack(int argc, char **argv, int select)
{
t_elem *stack;
int i;
stack = malloc(sizeof(t_elem) * (argc + 1));
if (stack == NULL)
return (NULL);
i = 0;
while (i < (argc - 1))
{
if (select == 1)
{
stack[i].num = ft_atoi(argv[i]);
stack[i].set = 1;
}
else
{
stack[i].num = 0;
stack[i].set = 0;
}
stack[i].index = i;
++i;
}
stack[i].index = -1;
return (stack);
}
- It allocates memory for the stack using
malloc()
. The size of the allocated memory is the size oft_elem
multiplied by (argc
+ 1) (the extra element is used as a sentinel value signaling the end of the stack.
- If the memory allocation fails, it returns NULL.
- Initializes
i
to 0.- Then it enters a loop initializing each element of the stack, with
i
as the index.
- If
select
is 1, it initializesstack_a
:
- Sets the
stack[i].num
field of the current element to the integer value of the corresponding argument (converted using ft_atoi()).- Sets the
stack[i].set
field to 1 (used for some conditional checks later in the program).- If
select
is 0, it initializesstack_b
:
- Sets the
stack[i].num
field of the current element to 0.- Sets
stack[i].set
fields of the current element to 0.- At the end of each iteration, sets the
stack[i].index
field of the current element toi
(representing the position of the element in the stack).- After the loop is done, the
stack[i].index
field is set to -1, signaling the end of the stack (sentinel value).- It returns the stack.
After initialization, the array of t_elem
structs can be visualized as follows:
Take the following input example:
"7 5 3 4 9 6 8 2 1"
stack.num | stack.index | stack.set |
---|---|---|
7 | 0 | 1 |
5 | 1 | 1 |
3 | 2 | 1 |
4 | 3 | 1 |
9 | 4 | 1 |
6 | 5 | 1 |
8 | 6 | 1 |
2 | 7 | 1 |
1 | 8 | 1 |
0 | -1 | 0 |
int main(int argc, char **argv)
{
(...)
if (ft_is_sorted(stack_a) == -1)
ft_sort(stack_a, stack_b, argc);
(...)
}
If stack_a
is not already sorted (indicated by ft_is_sorted()
returning -1), it is sorted using ft_sort()
.
static void ft_sort(t_elem *stack_a, t_elem *stack_b, int argc)
{
if (argc == 3)
{
if (ft_is_sorted(stack_a) == -1)
ft_swap_elem(stack_a, "sa\n");
}
else if (argc == 4)
ft_sort_three(stack_a);
else
ft_sort_stack(stack_a, stack_b, argc);
}
- If
argc
is 3, it means there are two elements instack_a
(plus the program's name).
- In this case, it checks if
stack_a
is already sorted. If it is NOT sorted (indicated byft_is_sorted()
returning -1):
- Swaps the two elements using
ft_swap_elem()
.- If
argc
is 4, it means there are three elements instack_a
.
- In this case, it sorts the three elements using
ft_sort_three()
.- If
argc
is greater than 4, meaning there are more than three elements instack_a
:
- The stack is sorted using
ft_sort_stack()
.
Note
We'll get into more detail about how these subroutines work in the algorithm description section.
int main(int argc, char **argv)
{
(...)
ft_free(stack_a, stack_b, input_list, must_free);
return (0);
}
- Finally, it frees any allocated memory before exiting the program using ft_free().
The algorithm implemented in this project is inspired by the QuickSort Algorithm.
- It leverages a second "temporary" stack (
stack_b
) in order to sort the stack (stack_a
) in ascending order. - Calculates the
median
value (used as a pivot, the key concept of the standardQuickSort Algorithm
) in the input stack (stack_a
), then proceeds to push elements intostack_b
. - If the pushed element is larger than the
median
we rotate it to the bottom ofstack_b
. We do this until there are only 3 elements left instack_a
. We get those sorted. - Then the algorithm starts moving one element at a time back to
stack_a
after calculating the least costly element to move at each iteration, effectively sorting the unordered input stack into ascending order.
Following this overview is a description of each case handled by the algorithm implemented in this project.
- If the elements are already in order:
- No operations are necessary.
- If the elements are NOT in order:
- A single swap operation is performed to sort
stack_a
.
- A single swap operation is performed to sort
The logic in ft_sort_three.c is triggered. This subroutine works as follows:
-
It starts by getting the
start
andend
indices ofstack_a
. -
It checks if
stack_a
is already sorted, if the minimum value is at thestart
ofstack_a
and the maximum value is at theend
,- Returns without executing any operations.
-
If the minimum value is at the
start
and the maximum value is in the middle (second position):- It swaps the top two elements.
- Then rotates
stack_a
.
This effectively moves the maximum value to the top (
end
) ofstack_a
.
- If the maximum value is at the
start
ofstack_a
:- it rotates the
stack_a
.
- it rotates the
This moves the maximum value to the
end
.
-
If the element at the bottom (
start
) is greater than the middle element:- It swaps the first two elements.
-
If the minimum value is at the
end
ofstack_a
:- A reverse rotation is performed.
This effectively moves the minimum value to the bottom (
start
) ofstack_a
.
Note
See the ft_sort_three.c for a direct look into the implementation of this part of the algorithm.
The logic in ft_sort_stack.c is triggered. This is where we get into the meat of the algorithm:
-
It starts by setting
i
to the start ofstack_a
; -
It then calculates the median value of
stack_a
using ft_median(). -
Loops through
stack_a
:- Pushing elements to
stack_b
until there are only three elements left instack_a
. - If the current top element in
stack_b
is greater than themedian
and the number of elements instack_a
is greater than 3, it rotatesstack_b
.
- Pushing elements to
-
The three remaining elements in
stack_a
are sorted with ft_sort_three.c. -
i
is reset to the start ofstack_b
; -
It then loops through
stack_b
:- For each element, it calculates the index in
stack_a
that requires the minimum number of operations to move that element into the correct sorted position.
- For each element, it calculates the index in
-
It rotates
stack_a
until the value right above the median is at the top. -
It pushes the top element of
stack_b
tostack_a
. -
It rotates
stack_a
to move its new smallest element to the top.
This process is repeated until all the values in stack_a
are sorted.
ft_exec_move() executes rotation and push operations according to the return of ft_best_op_idx()
which calculates the optimal move to make when transferring elements from stack_b
back to stack_a
during the sorting process.
ft_best_op_idx()
loops through all the elements instack_a
callingft_get_align_ops()
to calculate the cost (number of operations) of moving that element to the top and aligning it with the corresponding element instack_b
.ft_get_align_ops()
subroutine calculates the total number of operations needed to move an element at a given index instack_a
to the top, and to align a corresponding element instack_b
with it.- First it calculates the cost to move the element at index
idx
instack_a
to the top. - Afterwards calculates the cost to move the corresponding element in
stack_b
to the top. - It returns the total cost of aligning the element in
stack_b
with the element instack_a
.
- First it calculates the cost to move the element at index
In short,
ft_best_op_idx()
calculates the index that has the minimum cost.
- Once the optimal index is found,
ft_exec_move()
is called (with the calculated index as an argument) to actually execute the moves:-
Gets the start index of
stack_a
storing it instart
. -
Calls
ft_check_order()
to check if any elements instack_b
are already in order withstack_a
. It saves the number of elements found to already be in order intoordered
. -
Then calls
ft_order()
to move elements fromstack_b
tostack_a
that are already in the correct order relative to each other. -
If necessary, handles the adjustment of the index
idx
afterordered
elements have been moved fromstack_b
tostack_a
. -
If
idx
is past the end ofstack_a
:- It is recalculated and looped back to the start of the stack.
-
Else, if
idx
is past the start ofstack_a
:- It is recalculated and looped back to the end of the stack.
-
Rotates
stack_a
to move the element atidx
to the top. -
Again, check if
idx
is past the end ofstack_a
:- If so, it is recalculated and looped back to the start of the stack.
-
Checks if the min or max value of
stack_b
is less/greater than the element now at the top ofstack_a
:- If so, it *rotates
stack_b
to move its min to the top.
- If so, it *rotates
-
Else,
- It rotates
stack_b
to move the smallest element greater thanstack_a
's top to the top ofstack_b
.
- It rotates
-
Pushes the top element of
stack_b
tostack_a
.
-
The checker
program checks whether the list of instructions generated by the push_swap
program actually sorts the stack correctly.
- Takes
stack_a
as an argument. - If no argument is given, it stops displaying nothing.
- Waits until instructions are read from
stdin
, each instruction separated by a\n
. - Once all instructions have been read, it executes them on the stack received as an argument.
- If after executing the instructions, the stack is sorted:
- Displays "OK" followed by a
\n
.
- Displays "OK" followed by a
- Else:
- Displays "KO" followed by a
\n
.
- Displays "KO" followed by a
- In case of an error, it displays "Error" followed by a
\n
tostderr
.
Errors include:
- Some arguments are not integers.
- Some arguments are bigger than an integer.
- Some arguments are duplicates.
- An instruction doesn't exist or is incorrectly formatted.
/* push_swap checker (main_checker.c) */
int main(int argc, char **argv)
{
char **input_list;
t_elem *stack_a;
t_elem *stack_b;
int must_free;
int error;
++argv;
input_list = argv;
must_free = 0;
if (argc == 2)
input_list = ft_get_elems(&argc, argv, &must_free);
error = ft_errors(argc, input_list);
if (error <= 0)
{
ft_free(input_list, must_free);
return (0);
}
stack_a = ft_create_stack(argc, input_list, 1);
stack_b = ft_create_stack(argc, input_list, 0);
ft_free(input_list, must_free);
ft_check_stack(stack_a, stack_b);
free(stack_a);
free(stack_b);
return (0);
}
The checker
program reads a set of instructions from stdin
and executes them on the stack received as an argument. It has a very similar structure and uses many of the same subroutines as this project's push_swap
implementation.
Important
The main difference between the checker
program and the push_swap
program is that other than sorting stack_a
it also checks if the instructions produced by push_swap
are valid.
void ft_check_stack(t_elem *stack_a, t_elem *stack_b)
{
int result;
int start;
char *line;
result = 1;
while (result)
{
line = get_next_line(0);
if (!line)
result = 0;
else
ft_check_op(stack_a, stack_b, line);
}
start = 0;
while ((stack_b[start].index != -1) && (stack_b[start].set != 1))
++start;
if ((ft_is_sorted(stack_a) == 1) && (stack_b[start].index == -1))
ft_putstr_fd("OK\n", 1);
else
ft_putstr_fd("KO\n", 1);
}
-
It takes in two stacks -
stack_a
andstack_b
. -
It reads input commands line by line using get_next_line().
- For each line, it calls
ft_check_op()
which will process and check if the line contains a valid instruction.
- For each line, it calls
-
After reading all the input, it checks if the stacks are in the expected sorted state:
-
stack_b
should be empty, so it loops through to find the first element with index = -1 (the sentinel value signifying the end of the stack). -
stack_a
should be sorted, so it callsft_is_sorted()
to check. -
If both conditions are met:
- It prints "OK\n" to
stdout
, meaning the instructions are valid. - else prints "KO\n" to
stdout
, meaning the instructions are invalid.
- It prints "OK\n" to
-
static void ft_check_op(t_elem *stack_a, t_elem *stack_b, char *op)
{
int result;
if ((op == NULL) || (ft_strlen(op) == 1))
result = -1;
else
result = ft_parse_op(op, stack_a, stack_b);
if (op != NULL)
free(op);
if (result == -1)
{
free(stack_a);
free(stack_b);
get_next_line(0);
ft_putstr_fd("Error\n", 2);
exit(EXIT_FAILURE);
}
}
-
It takes in
stack_a
andstack_b
, and the operation stringop
. -
It first checks if the
op
string is valid:- If it's NULL or only 1 character, set result = -1 to indicate invalid operation.
- Else it calls
ft_parse_op()
to execute the operation on the appropriate stack.
-
It frees the
op
string. -
It checks the return value of
ft_parse_op()
stored inresult
. If -1, it means an invalid operation was parsed.- It frees the stacks.
- It reads and discards the rest of input.
- It prints "Error\n" to
stderr
and exits.
static int ft_parse_op(char *op, t_elem *stack_a, t_elem *stack_b)
{
if (ft_strncmp_checker(op, "pa\n", 3) == 0)
ft_push_elem(stack_b, stack_a);
else if (ft_strncmp_checker(op, "pb\n", 3) == 0)
ft_push_elem(stack_a, stack_b);
else if (ft_strncmp_checker(op, "sa\n", 3) == 0)
ft_swap_elem(stack_a);
else if (ft_strncmp_checker(op, "sb\n", 3) == 0)
ft_swap_elem(stack_b);
else if (ft_strncmp_checker(op, "ss\n", 3) == 0)
ft_swap_both(stack_a, stack_b);
else if (ft_strncmp_checker(op, "ra\n", 3) == 0)
ft_rotate(stack_a);
else if (ft_strncmp_checker(op, "rb\n", 3) == 0)
ft_rotate(stack_b);
else if (ft_strncmp_checker(op, "rr\n", 3) == 0)
ft_rotate_both(stack_a, stack_b, 0);
else if (ft_strncmp_checker(op, "rra\n", 4) == 0)
ft_rev_rotate(stack_a);
else if (ft_strncmp_checker(op, "rrb\n", 4) == 0)
ft_rev_rotate(stack_b);
else if (ft_strncmp_checker(op, "rrr\n", 4) == 0)
ft_rotate_both(stack_a, stack_b, 1);
else
return (-1);
return (1);
}
Parses an op
string and executes the corresponding operation on stack_a
or stack_b
.
- It compares the input string
op
to each possible operation. Based on the matching operation, it calls the corresponding stack operation function.- If there is no match, it returns -1 to indicate invalid operation.
- Otherwise it executes the operation and returns 1.
First, clone the repository, and cd
into the project folder:
git clone [email protected]:PedroZappa/42_push_swap.git 42_push_swap_passunca
cd 42_push_swap_passunca
To build push_swap
run:
make
This command will fetch the dependencies and build the project.
Here are a couple different ways to run the program:
./push_swap 2 1 3 6 5 8
# or
./push_swap "2 1 3 6 5 8"
# or
ARG="2 1 3 6 5 8"; ./push_swap $ARG
# and to run it with the checker provided by 42:
ARG="2 1 3 6 5 8"; ./push_swap $ARG | ./checker_linux $ARG
To build checker
, run:
make bonus
To manually test checker
, run:
echo -e "rra\npb\nsa\nrra\npa" > input.txt
./checker 3 2 1 0 < input.txt
I've prepared several make rules to test the push_swap
program and the checker
in a systematic and automated way.
To run the tests present on the subject run:
make test_subject # for push_swap
make test_checker # for checker
make check_ext_func # Checks external functions in use
To test push_swap
with a custom number of input elements run:
make test_n n=42
To test push_swap
together checker
with a custom number of input elements run:
make test_checker n=42
n
is the number of input elements, in these examples, 42.
To check if push_swap
has memory leaks run:
make valgrind
To run push_swap
with GDB run:
make gdb arg="2 1 3 6 5 8"
if no argument is passed, it runs with the default argument defined in the Makefile.
To check all available commands including tests run:
make help
Important
Check the Makefile for more details about what's going on with each command. there is a lot of juicy juice gathered in it for your Make'ing enjoyment π€
This work is published under the terms of 42 Unlicense.