Main problem
You are evaluating expressions inside a loop, in a do
clause. The
returned value is never going to be used anywhere.
The function's return value is given by the local variable listRet
,
which is set at the beginning of the function and affected at exactly
2 places, the two calls to push
. The first one happens only for an
input list of size 1. The second push happen only at step 4.
We can easily see that all other operations have no effect whatsoever
on the local listRet
variable. Also, because sort-by-freq
as well
as your auxiliary functions are pure (they never destroy parts of the
list structure pointed by listRet
), you know also that the list is
not going to be modified by having its cons cells linked differently
over time.
Let's confirm this by tracing your code with your example:
(trace sort-by-freq last-element-by-pair remove-element)
When you evaluate your test, the following trace is emitted (output varies among implementations, here it is using SBCL):
0: (SORT-BY-FREQ ((C . 2) (B . 3) (A . 1)) (3 2 1))
1: (LAST-ELEMENT-BY-PAIR ((C . 2) (B . 3) (A . 1)))
1: LAST-ELEMENT-BY-PAIR returned A
1: (LAST-ELEMENT-BY-PAIR ((C . 2) (B . 3) (A . 1)))
1: LAST-ELEMENT-BY-PAIR returned A
1: (LAST-ELEMENT-BY-PAIR ((C . 2) (B . 3) (A . 1)))
1: LAST-ELEMENT-BY-PAIR returned A
1: (REMOVE-ELEMENT ((C . 2) (B . 3) (A . 1)) (B . 3))
1: REMOVE-ELEMENT returned ((A . 1) (C . 2))
1: (SORT-BY-FREQ ((A . 1) (C . 2)) (2 1) ((B . 3)))
2: (LAST-ELEMENT-BY-PAIR ((A . 1) (C . 2)))
2: LAST-ELEMENT-BY-PAIR returned C
2: (LAST-ELEMENT-BY-PAIR ((A . 1) (C . 2)))
2: LAST-ELEMENT-BY-PAIR returned C
2: (LAST-ELEMENT-BY-PAIR ((A . 1) (C . 2)))
2: LAST-ELEMENT-BY-PAIR returned C
2: (REMOVE-ELEMENT ((A . 1) (C . 2)) (C . 2))
2: REMOVE-ELEMENT returned ((A . 1))
2: (SORT-BY-FREQ ((C . 2) (A . 1)) (2 1) ((B . 3)))
3: (LAST-ELEMENT-BY-PAIR ((C . 2) (A . 1)))
3: LAST-ELEMENT-BY-PAIR returned A
3: (LAST-ELEMENT-BY-PAIR ((C . 2) (A . 1)))
3: LAST-ELEMENT-BY-PAIR returned A
3: (REMOVE-ELEMENT ((C . 2) (A . 1)) (C . 2))
3: REMOVE-ELEMENT returned ((A . 1))
3: (SORT-BY-FREQ ((A . 1)) (1) ((C . 2) (B . 3)))
3: SORT-BY-FREQ returned ((A . 1) (C . 2) (B . 3))
2: SORT-BY-FREQ returned ((C . 2) (B . 3))
1: SORT-BY-FREQ returned ((B . 3))
0: SORT-BY-FREQ returned ((B . 3))
A minor point that can be seen right there is that
last-element-by-pair
is called many times with the same input, which
is wasteful. It could be called once before looping.
At level 0, the function iterates over pairs until it finds (B . 3)
,
whose frequency equals the first frequency in the second list. This is
step 4, and the pair is pushed in front of the list designated by the
local variable listRet
in that invocation of sort-by-freq
.
When the loop reaches the last element of the unordered list, the
functions is called recursively with (i) a new list of unordered pairs,
computed using remove-element
, (ii) one less frequency, and (iii) the current
list bound to listRet
.
But whatever happens during the recursive steps, and in particular no matter what result the recursive calls yield, the binding currently
in scope for listRet
is not going to be modified anymore. Also, the
structure of the list currently pointed by listRet
is not modified
(e.g. using nconc
or rplacd
).
All the work that is done at level 2 and below is about pushing values in
front of temporary variables locally named listRet
, and then discarding them.
What am I missing?
The data flow from recursive invocations of sort-by-freq
to the
current invocation of that function is broken, you have to express the
current result in terms of recursive results, OR you need to mutate
things (but this is discouraged).
Naming and user interfaces
Taking a list of unordered pairs, a list of sorted frequencies, and
an optional startList for recursive calls. It first sets listRet
equal to the startList.
Based on your specification, I would define the function as follows:
(defun sort-pairs-by-frequencies (pairs frequencies)
...)
Whether or not there need to be additional parameters for recursive
invocations is not a concern for the user of the function. That kind
of details should be left hidden, except if you really want your user
to be able to pass a list as a third argument.
Complex corner cases
If the unordered list has only one element, that element is pushed
to listRet. [...] If it is not the last element, it is pushed to the
listRet.
Functions that recurses over lists typically need only consider two
cases: empty and non-empty lists. The fact that you consider more
corner cases, like a list of one element or like when you check if an
element is the last one, is a huge red flag. There are problems that
needs to have complex base conditions, but this one can be done in a
simpler way.
Redundant tests
In addition to the many calls to last-element-by-pair
, notice also
that you repeat tests at different points of your functions, even when
they are necessarily true in a given context.
(if (and (equal (cdr pair) (car listOrdFreqs))
(not (equal (last-element-by-pair listUnord)(car pair))))
(push pair listRet) ;;step 4
(if (and (equal (cdr pair) (car listOrdFreqs))
(equal (last-element-by-pair listUnord)(car pair)))
(sort-by-freq (append (list (cons (car pair) (cdr pair)))
(remove-element listUnord pair))
listOrdFreqs listRet) ;;step 6
(if (equal (last-element-by-pair listUnord)(car pair))
(sort-by-freq (remove-element listUnord (car listRet))
(cdr listOrdFreqs)
listRet))))
Given appropriate definitions, the above could be written:
(if (and A (not B))
(step-4)
(if (and A B)
(step-6)
(if B (step-5))))
Let's write what conditions are true for each step, based on which
branch they belong in the trees of if expressions:
For step 4: (and a (not b))
necessarily holds since it is in the "then" branch of the if.
For step 5: (and b (not a))
, based on the following simplifications over the path predicate:
(and b
(not (and a b))
(not (and a (not b))))
=> (and b
(or (not a) (not b))
(or (not a) b))
=> (and b (or (not a) nil) t)
=> (and b (not a))
For step 6: (and a b)
Thus, you could write the test as:
(cond
(a (if b (step-6) (step-4)))
(b (step-5)))
Simpler version
If the list is larger than 1 each pair of the unordred list is
looped through and checked if it equals the first element of the
ordered frequency list.
This above is the core of the algorithm. For each frequency F, you
partition the unordered elements into two lists: the one where the
frequency equals to F, and the other ones.
Let's define just this: how to remove or keep an item based on whether
their frequency matches a given frequency. The following is not
necessarily efficient, but it works and is simple. Also, it errs on
the side of caution by using immutable operations only:
(defun remove-frequency (pairs frequency)
(remove frequency pairs :test #'= :key #'cdr))
(defun keep-frequency (pairs frequency)
(remove frequency pairs :test-not #'= :key #'cdr))
(remove-frequency '((a . 20) (b . 10) (c . 5) (d . 20)) 20)
=> ((B . 10) (C . 5))
(keep-frequency '((a . 20) (b . 10) (c . 5) (d . 20)) 20)
=> ((A . 20) (D . 20))
Then, your main function recurses over frequencies:
(defun sort-pairs-by-frequencies (pairs frequencies)
(if (null frequencies)
pairs
(destructuring-bind (frequency . frequencies) frequencies
(append (keep-frequency pairs frequency)
(sort-pairs-by-frequencies (remove-frequency pairs frequency)
frequencies)))))
When given no frequencies to sort by, the unsorted list of pairs is
returned. Otherwise, frequencies
can be destructured as a cons cell,
where the first item is a frequency
, and the remaining items are
frequencies
. Note that the previous binding of frequencies
is
shadowed, because from this point on we do not need to refer to the
whole list of frequencies anymore.
Then, the function's return value in the general case is computed by
appending pairs that have the given frequency and the list of
pairs that did not match frequency
, recursively sorted according to
the remaining frequencies.
(sort-pairs-by-frequencies '((a . 20) (b . 10) (c . 5) (d . 20))
'(5 10 20))
=> ((C . 5) (B . 10) (A . 20) (D . 20))
By evaluating first (trace sort-pairs-by-frequencies)
, you also
obtain the following trace:
0: (SORT-PAIRS-BY-FREQUENCIES ((A . 20) (B . 10) (C . 5) (D . 20)) (5 10 20))
1: (SORT-PAIRS-BY-FREQUENCIES ((A . 20) (B . 10) (D . 20)) (10 20))
2: (SORT-PAIRS-BY-FREQUENCIES ((A . 20) (D . 20)) (20))
3: (SORT-PAIRS-BY-FREQUENCIES NIL NIL)
3: SORT-PAIRS-BY-FREQUENCIES returned NIL
2: SORT-PAIRS-BY-FREQUENCIES returned ((A . 20) (D . 20))
1: SORT-PAIRS-BY-FREQUENCIES returned ((B . 10) (A . 20) (D . 20))
0: SORT-PAIRS-BY-FREQUENCIES returned ((C . 5) (B . 10) (A . 20) (D . 20))
The above functions were written so that they could be easily readable
and "trivially" correct, but they are still wasting memory and stack. Other approaches are possible to sort elements in place (no memory allocation) using a loop (constant stack usage).