-1

I'm creating a program that can determine if a string is palindromic or not for a final project. However, I'm not allowed to use the REVERSE function.

Here's my attempt so far with the REVERSE function.

(defun palindrome(x)
    (if (string= x (reverse x))
        (format t "~d" ": palindrome" (format t x))
        (format t "~d" ": not palindrome" (format t x)))
)

The methods that I've come up to solve this problem are the following. This might be very convoluted.

-Split the string into separate chars and concatenate them inside a variable (the last char goes first) and then compare to the input.

-Write each characters into another variable starting with the last char via recursion and then compare to the input.

I just don't have any idea how to implement any of those aforementioned methods to lisp.

coredump
  • 37,664
  • 5
  • 43
  • 77
Sieg
  • 31
  • 5
  • Reversing is actually wasteful. To check whether a string is a palindrome, we just need to check whether its first half is a mirror image of its second half. – Kaz Nov 20 '20 at 04:28
  • Is your attempt a copy-paste from rosettacode? https://rosettacode.org/wiki/Palindrome_detection#Common_Lisp – coredump Nov 23 '20 at 15:14

4 Answers4

2

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:

  1. 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.

  2. 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:

  1. 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.

  2. 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) 
coredump
  • 37,664
  • 5
  • 43
  • 77
  • cool, thanks! Now I am closer to understand this code xD. Would it be possible to stop halfway? Because it is not necessary to traverse the entire list but half of it to know whether the whole thing is palindrome or not, isn't it? I somehow intuitively guessed that `trace`ing would help me to understand your mapper function. – Gwang-Jin Kim Nov 23 '20 at 15:35
  • in this case, it could already stop right after comparing `C` with `C`. while unwinding. The last number after first traversal `5` in this case will tell when to stop. – Gwang-Jin Kim Nov 23 '20 at 15:37
  • 1
    yes exactly, in fact `(return-from palindrome t)` happens when the indices overlap, at midpoint during the second traversal. – coredump Nov 23 '20 at 15:39
  • `(floor (/ 5 2))` would be the number where to stop, isn't it? – Gwang-Jin Kim Nov 23 '20 at 15:41
  • `(if (< (floor (/ 5 2)) i2) )` – Gwang-Jin Kim Nov 23 '20 at 15:43
  • 1
    ah now is see - you have this in your `(when (<= ix iy) (return-from ...))` – Gwang-Jin Kim Nov 23 '20 at 15:46
  • Because `map-opposites` was so general, I didn't got it. – Gwang-Jin Kim Nov 23 '20 at 15:59
  • 1
    Sorry. This was supposed ot help forget palindromes and focus on the traversal, and how most of the work is done when going out from recursion (see also in OCaml: https://stackoverflow.com/a/47100848/124319). See also edit – coredump Nov 23 '20 at 16:06
  • 1
    Thank you! It is super interesting - I will digest it day after tomorrow - because of a deadline I have to match. But this is super interesting. I wished I could work only with lisp :D . – Gwang-Jin Kim Nov 23 '20 at 17:00
1

Reversing a string

I am pretty sure the constraint for not using reverse in your exercise is there to avoid a trivial answer like (string= s (reverse s)) (note that this approach does too much work, since you only need to check if half the characters are equal) and to avoid producing intermediate strings. So I guess you are probably not expected to use copy-seq either, or make-string, or any other standard function that allocates a string. So even if there is a way to write a custom reverse that does what you want given your constraints, that probably would go against the spirit of the exercise.

For example, here is a my-reverse function that relies on DO* and MAKE-STRING. Again, I don't think you should be using it for this problem, this is only to show how it could be done:

(defun my-reverse (s)
  (do* ((n (length s))
        (z (make-string n))
        (i 0 (1+ i))
        (j (1- n) (1- j)))
       ((>= i n) z)
    (setf (char z i) (char s j))))

Basically, i starts from zero and increases, j from the last position in the string and decreases, and each iteration of the loop sets the character at position i in the resulting string as the one at position j in the original string.

Palindrome check

You can write a palindrome check by looking at the string, without creating a new string and reversing it. For example you could adapt the above loop so that instead of creating a new string it checks for palindromes.

Another possible way to implement it is to follow this recursive definition of a palindrome:

  • the empty string is a palindrome
  • a string of one character is a palindrome
  • if S is a palindrome and C a character, then CSC is a palindrome

From this definition there is a corresponding recursive procedure that can be defined to check for palindromes. Note however that you don't need to build intermediate strings. You only need to compare characters at different indices in the string.

For example, I have the following traces:

Empty string:

  0: (PALINDROMEP "")
    1: (BOUNDED-PALINDROME-P "" 0 -1)
    1: BOUNDED-PALINDROME-P returned T
  0: PALINDROMEP returned T

Single-letter string:

  0: (PALINDROMEP "A")
    1: (BOUNDED-PALINDROME-P "A" 0 0)
    1: BOUNDED-PALINDROME-P returned T
  0: PALINDROMEP returned T

Palindrome of length over 1:

  0: (PALINDROMEP "ABCBA")
    1: (BOUNDED-PALINDROME-P "ABCBA" 0 4)
      2: (BOUNDED-PALINDROME-P "ABCBA" 1 3)
        3: (BOUNDED-PALINDROME-P "ABCBA" 2 2)
        3: BOUNDED-PALINDROME-P returned T
      2: BOUNDED-PALINDROME-P returned T
    1: BOUNDED-PALINDROME-P returned T
  0: PALINDROMEP returned T

Non-palindrome:

  0: (PALINDROMEP "ABCDEF")
    1: (BOUNDED-PALINDROME-P "ABCDEF" 0 5)
    1: BOUNDED-PALINDROME-P returned NIL
  0: PALINDROMEP returned NIL

Another failing test that has some letters in common:

  0: (PALINDROMEP "ABCDEFBA")
    1: (BOUNDED-PALINDROME-P "ABCDEFBA" 0 7)
      2: (BOUNDED-PALINDROME-P "ABCDEFBA" 1 6)
        3: (BOUNDED-PALINDROME-P "ABCDEFBA" 2 5)
        3: BOUNDED-PALINDROME-P returned NIL
      2: BOUNDED-PALINDROME-P returned NIL
    1: BOUNDED-PALINDROME-P returned NIL
  0: PALINDROMEP returned NIL
coredump
  • 37,664
  • 5
  • 43
  • 77
  • The only function I'm not allowed to use is the REVERSE. My bad. – Sieg Nov 18 '20 at 15:57
  • How does 1+ and 1- work on i and j since they are empty variables? – Sieg Nov 18 '20 at 19:22
  • the "do*" here introduces the variables n, z, i and j. Each variable is declared by either (var init) or (var init step), where init and step are expressions; the init expression is used to intialize the variable, and step is evaluated at the end of each loop, before going into another iteration. So for example i starts at 0, then is bound to (1+ i), ie. 1, then 2, then 3, etc. Likewise, j starts at (1- n), then is decremented by its step expression at each iteration of the loop. – coredump Nov 18 '20 at 22:46
  • 1
    This is nice - using just indices to do recursion - I would have done something inefficient like clipping the strings to do recursion – rajashekar Nov 19 '20 at 03:51
  • Btw what's the purpose of `((>= i n) z)` inside the `do*` construct? – Sieg Dec 02 '20 at 15:09
  • `(>= i n)` is the *termination condition*, it is an expression that is evaluated at each beginning of the loop, and if it evaluates to true, then iteration stops. The second value, `z` is the value returned by the do-loop when it stops. Here, `z` is the reversed string, and `(>= i n)` is true when the index `i` reaches the size of the string. Technically it is enough to write `(= i n)`, the `>=` is something I write out of habit (for example, if I wanted to step `i` by 2, then for odd lengths of vectors the `=` case would never be reached (n-1, n+1, n+3, ... infinite loop), whereas `>=` is) – coredump Dec 02 '20 at 15:43
1

i would say, the easiest way would be simple looping, like this:

(defun palindrome? (s)
  (not
   (loop for c1 across s
         for end from (1- (length s)) downto (/ (length s) 2)
         when (char/= c1 (char s end))
           do (return t))))

starting from the ends if narrows bounds on every step, checking that chars are equal and stopping as soon as it meets unequal characters (the end iteration part guarantees that the loop ends as soon as it reaches the middle of the string, meaning it is a palindrome)

CL-USER> (palindrome? "")
T

CL-USER> (palindrome? "a")
T

CL-USER> (palindrome? "aa")
T

CL-USER> (palindrome? "ab")
NIL

CL-USER> (palindrome? "aba")
T

CL-USER> (palindrome? "abda")
NIL

CL-USER> (palindrome? "abba")
T

CL-USER> (palindrome? "abcba")
T

CL-USER> (palindrome? "abcsba")
NIL

UPDATE

as @coredump suggested, this one could be simplified this way:

(defun palindrome? (s)
  (loop for c1 across s
        for end from (1- (length s)) downto (/ (length s) 2)
        always (char= c1 (char s end))))
leetwinski
  • 17,408
  • 2
  • 18
  • 42
1

By recursion

Look for first and last element of the character list of the string, whether they are equal until first unequality occurs and nil returned. The tested first and last elements are chopped away from the list by cdr and butlast.

;; helper function working with list of chars
(defun %palindromep (chars)
  (cond ((null chars) t)
        ((char= (first chars) (car (last chars))) 
         (%palindromep (butlast (cdr chars))))
        (t nil)))

;; function working with strings
(defun palindomep (s)
  (%palindromep (coerce s 'list)))
Gwang-Jin Kim
  • 9,303
  • 17
  • 30
  • 1
    this iterates over the list a lot (last, butlast, recursion), but this might be improved by trying to do the comparison when unwinding the stack; see for example https://pastebin.com/raw/pthmRZqk (feel free to copy it) for a palindrome function for lists that do not allocate the list and with linear time – coredump Nov 22 '20 at 16:10
  • @coredump phew my brain can't understand it yet ... maybe you should write a new answer and explain the code a little? xD - if that really goes just once through - it is amazing! – Gwang-Jin Kim Nov 22 '20 at 21:36
  • 1
    No problem, I added another answer: it actually goes a bit more than once, but still linearly – coredump Nov 23 '20 at 11:19