This answer focuses in particular on the case of a recursive palindrome check for lists (not vectors), as a follow-up to comments on Gwang-Jin Kim's answer. There is a recursive way to check lists for palindromes that does not allocate a new list, and only iterates the given list in O(n) time, ie. in linear time, not quadratic.
In order to explain this approach, let me define an auxiliary function, map-opposites
, which is a high-level function that maps a function to values at both ends of the list. For example, (map-opposites f '(a b c))
calls f
on the following couples of arguments: (c a)
, (b b)
and (a c)
(in that order). Then, the palindrome
check function will just be an application of map-opposites
with a closure (with early exit).
In order for map-opposites
to be useful for palindrome check (and maybe other functions), it also keeps track of the current index of each element in the list.
The function called by map-opposite
should accept two values x
and y
, as well as two indices x-index
and y-index
.
map-opposites
(defun map-opposites% (fun l1 whole i1)
(if l1
(destructuring-bind (h1 . t1) l1
(multiple-value-bind (i2 l2) (map-opposites% fun t1 whole (1+ i1))
(if l2
(destructuring-bind (h2 . t2) l2
(funcall fun h1 h2 i1 i2)
(values (1+ i2) t2))
i2)))
(values 0 whole)))
(defun map-opposites (fun list)
(map-opposites% fun list list 0))
Let's define an auxiliary function and trace it, along with map-opposites%
:
(defun dbg (&rest args)
(declare (ignore args)))
(trace map-opposites% dbg)
Calling the main function with this input:
(map-opposites 'dbg '(a b c d e))
Gives the following trace (package prefix edited out for clarity):
0: (MAP-OPPOSITES% DBG (A B C D E) (A B C D E) 0)
1: (MAP-OPPOSITES% DBG (B C D E) (A B C D E) 1)
2: (MAP-OPPOSITES% DBG (C D E) (A B C D E) 2)
3: (MAP-OPPOSITES% DBG (D E) (A B C D E) 3)
4: (MAP-OPPOSITES% DBG (E) (A B C D E) 4)
5: (MAP-OPPOSITES% DBG NIL (A B C D E) 5)
5: MAP-OPPOSITES% returned 0 (A B C D E)
5: (DBG E A 4 0)
5: DBG returned NIL
4: MAP-OPPOSITES% returned 1 (B C D E)
4: (DBG D B 3 1)
4: DBG returned NIL
3: MAP-OPPOSITES% returned 2 (C D E)
3: (DBG C C 2 2)
3: DBG returned NIL
2: MAP-OPPOSITES% returned 3 (D E)
2: (DBG B D 1 3)
2: DBG returned NIL
1: MAP-OPPOSITES% returned 4 (E)
1: (DBG A E 0 4)
1: DBG returned NIL
0: MAP-OPPOSITES% returned 5 NIL
There are two main regions:
First the function recurses down to the final empty list, while counting the size of the list; notice that the whole original list is passed unmodified as a third argument.
When unwinding the stack, the whole list is being traversed a second time (see secondary return value), while the call stack is going upward. In fact one may argue that the call stack plays the role of a copy of the list.
More precisely, notice that:
At the bottom of the recursion, the function returns the size of the list, as well as the whole list. This whole list will be visited down while the stack unwinds upward.
At intermediate levels of the recursion, we have the current value h1
(the one obtained while recursing down), and the second value h2
obtained by iterating on the whole list (called l2). They correspond to opposite indices in the list, as can be seen by the calls to dbg
. The intermediate level of recursion returns two values: an increasing index for the element of the left (i2
) and the tail of the list, t2
.
palindrome
The palindrome check is defined as follows:
(defun palindrome (list)
(prog1 t
(map-opposites (lambda (x y ix iy)
(when (<= ix iy)
(return-from palindrome t))
(unless (eql x y)
(return-from palindrome nil)))
list)))
It returns t
for empty lists, and otherwise returns early from the lambda
if values differ (case nil
) or when we try to visit the first half of the list, which is superfluous for this check.
With only indices
By using only the indices, the structure of the traversal is a bit more clear I hope; here I only compute the associated indices.
(defun map-opposite-indices% (fun list i1)
(if list
(let ((i2 (map-opposite-indices% fun (rest list) (1+ i1))))
(funcall fun i1 i2)
(1+ i2))
0))
(defun map-opposite-indices (fun list)
(map-opposite-indices% fun list 0))
(defun dbg (&rest args)
(print args))
(map-opposite-indices 'dbg '(a b c d e))
This prints:
(4 0)
(3 1)
(2 2)
(1 3)
(0 4)
Using only elements
The version that only tracks the list, and not indices:
(defun map-opposites% (fun l1 whole)
(if l1
(destructuring-bind (h1 . t1) l1
(let ((l2 (map-opposites% fun t1 whole)))
(when l2
(destructuring-bind (h2 . t2) l2
;; in the base case l2 is the whole list (a b c d e)
;; then one level up in the recursion l2 is bound to
;; (b c d e); one level up it is (c d e),
;; then (d e), etc.
;; At the same levels of recursion, l1 is bound
;; respectively to (), then (e), then (d e),
;; then (c d e), etc.
(prog1 t2
(funcall fun h1 h2))))))
whole))
(defun map-opposites (fun list)
(map-opposites% fun list list))
(defun dbg (&rest args)
(print args))
(map-opposites 'dbg '(a b c d e))
Prints:
(E A)
(D B)
(C C)
(B D)
(A E)