【问题标题】:Reversing a string without using the REVERSE function in Common LISP在 Common LISP 中不使用 REVERSE 函数反转字符串
【发布时间】:2020-11-18 14:57:55
【问题描述】:

我正在创建一个程序,该程序可以确定一个字符串是否为最终项目的回文。但是,我不允许使用REVERSE 函数。

这是我迄今为止对 REVERSE 函数的尝试。

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

我想出的解决这个问题的方法如下。这可能非常复杂。

-将字符串拆分为单独的字符并将它们连接到一个变量中(最后一个字符先出现),然后与输入进行比较。

-通过递归将每个字符写入从最后一个字符开始的另一个变量,然后与输入进行比较。

我只是不知道如何实现上述任何 lisp 方法。

【问题讨论】:

标签: string lisp common-lisp palindrome


【解决方案1】:

这个答案特别关注 lists(不是向量)的递归回文检查的情况,作为Gwang-Jin Kim 答案的 cmets 的后续行动。有一种递归方法可以检查回文列表,它不分配新列表,并且仅在 O(n) 时间内迭代给定列表,即。在线性时间,而不是二次。

为了解释这种方法,让我定义一个辅助函数map-opposites,它是一个高级函数,将函数映射到列表两端的值。例如,(map-opposites f '(a b c)) 在以下几个参数上调用 f(c a)(b b)(a c)(按此顺序)。然后,palindrome 检查函数将只是一个带有闭包的map-opposites 应用程序(提前退出)。

为了使map-opposites 可用于回文检查(可能还有其他功能),它还跟踪列表中每个元素的当前索引。 map-opposite 调用的函数应该接受两个值xy,以及两个索引x-indexy-index

地图对面

(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))

让我们定义一个辅助函数并跟踪它,以及map-opposites%

(defun dbg (&rest args)
  (declare (ignore args)))

(trace map-opposites% dbg)

用这个输入调用主函数:

(map-opposites 'dbg '(a b c d e))

提供以下跟踪(为清楚起见,删除了包前缀):

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

主要有两个区域:

  1. 首先,函数递归到最终的空列表,同时计算列表的大小;请注意,整个原始列表作为第三个参数未经修改地传递。

  2. 当展开堆栈时,整个列表将被第二次遍历(参见第二次返回值),同时调用堆栈向上移动。事实上,有人可能会争辩说,调用堆栈扮演着列表副本的角色。

更准确地说,请注意:

  1. 在递归的底部,函数返回列表的大小,以及整个列表。当堆栈向上展开时,将向下访问整个列表。

  2. 在递归的中间级别,我们有当前值h1(向下递归时获得的值)和第二个值h2 通过迭代整个列表(称为l2)获得。它们对应于列表中的相反索引,如对dbg 的调用所示。递归的中间级别返回两个值:左侧元素的递增索引 (i2) 和列表尾部 t2

回文

回文检查定义如下:

(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)))

它为空列表返回t,否则如果值不同(案例nil)或当我们尝试访问列表的前半部分时从lambda 提前返回,这对于此检查是多余的。

只有索引

通过只使用索引,我希望遍历的结构更清晰一些;这里我只计算相关的索引。

(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))

打印出来:

(4 0) 
(3 1) 
(2 2) 
(1 3) 
(0 4) 

仅使用元素

只跟踪列表而不跟踪索引的版本:

(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))

打印:

(E A) 
(D B) 
(C C) 
(B D) 
(A E) 

【讨论】:

  • 酷,谢谢!现在我更接近于理解这段代码 xD。能不能半途而废?因为不需要遍历整个列表,而是遍历其中的一半来知道整个事情是否是回文,不是吗?我凭直觉猜到traceing 会帮助我理解你的映射器功能。
  • 是的,事实上(return-from palindrome t) 发生在索引重叠时,在第二次遍历的中点。
  • 啊,现在看到了——你的(when (&lt;= ix iy) (return-from ...))中有这个
  • 对不起。这应该有助于忘记回文并专注于遍历,以及从递归出去时大部分工作是如何完成的(另见 OCaml:stackoverflow.com/a/47100848/124319)。另见编辑
  • 谢谢!这非常有趣——我会在后天消化它——因为我必须赶上截止日期。但这非常有趣。我希望我只能使用 lisp :D 。
【解决方案2】:

反转字符串

我很确定在你的练习中不使用reverse 的限制是为了避免像(string= s (reverse s)) 这样的琐碎答案(请注意,这种方法做的工作太多,因为你只需要检查一半的字符是相等)并避免产生中间字符串。所以我猜你可能不会使用copy-seq,或make-string,或任何其他分配字符串的标准函数。因此,即使有一种方法可以在给定约束的情况下编写自定义逆向操作,也可能会违背练习的精神。

例如,这是一个依赖DO*MAKE-STRINGmy-reverse 函数。再说一次,我不认为你应该用它来解决这个问题,这只是为了展示它是如何完成的:

(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))))

基本上,i 从零开始并增加,j 从字符串中的最后一个位置开始并减少,并且循环的每次迭代将结果字符串中位置 i 处的字符设置为位置处的字符j 在原始字符串中。

回文检查

您可以通过查看字符串来编写回文检查,而无需创建新字符串并将其反转。例如,您可以调整上述循环,以便它检查回文而不是创建新字符串。

实现它的另一种可能方式是遵循回文的递归定义:

  • 空字符串是回文
  • 一个字符的字符串是回文
  • 如果 S 是回文,C 是字符,则 CSC 是回文

根据这个定义,可以定义一个相应的递归过程来检查回文。但是请注意,您不需要构建中间字符串。您只需要比较字符串中不同索引处的字符即可。

例如,我有以下痕迹:

空字符串:

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

单字母字符串:

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

长度超过 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

非回文:

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

另一个有一些共同字母的失败测试:

  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

【讨论】:

  • 我唯一不能使用的功能是 REVERSE。我的错。
  • 1+ 和 1- 如何作用于 i 和 j,因为它们是空变量?
  • 这里的“do*”引入了变量n、z、i和j。每个变量由 (var init) 或 (var init step) 声明,其中 init 和 step 是表达式; init 表达式用于初始化变量,并在每个循环结束时对 step 进行评估,然后再进入另一个迭代。因此,例如 i 从 0 开始,然后绑定到 (1+ i),即。 1,然后是 2,然后是 3,以此类推。同样,j 从 (1- n) 开始,然后在循环的每次迭代中按其 step 表达式递减。
  • 这很好 - 只使用索引来做递归 - 我会做一些低效的事情,比如剪裁字符串来做递归
  • 顺便说一句,((&gt;= i n) z)do* 构造中的用途是什么?
【解决方案3】:

我想说,最简单的方法是简单的循环,像这样:

(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))))

如果每一步都缩小界限,则从末尾开始,检查字符是否相等并在遇到不相等的字符时立即停止(end 迭代部分保证循环在到达字符串中间时立即结束, 意思是回文)

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

更新

正如@coredump 建议的那样,可以这样简化:

(defun palindrome? (s)
  (loop for c1 across s
        for end from (1- (length s)) downto (/ (length s) 2)
        always (char= c1 (char s end))))

【讨论】:

  • 如果我可以建议, (not (loop ...)) 可能是 (loop ... always ...)
  • @coredump 哇。完全忘记了那个。谢谢!
【解决方案4】:

通过递归

查找第一个和最后一个元素 字符串的字符列表,它们是否相等,直到第一个不相等 发生并返回nil。 测试的第一个和最后一个元素从列表中删除 cdrbutlast

;; 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)))

【讨论】:

  • 这对列表进行了很多迭代(last、butlast、recursion),但可以通过在展开堆栈时尝试进行比较来改进;例如,请参阅pastebin.com/raw/pthmRZqk(随意复制它)以获取不分配列表且具有线性时间的列表的回文函数
  • @coredump 哎呀我的大脑还不能理解它......也许你应该写一个新的答案并解释一下代码? xD - 如果真的只通过一次 - 这太棒了!
  • 没问题,我添加了另一个答案:它实际上不止一次,但仍然是线性的
猜你喜欢
  • 1970-01-01
  • 2017-10-29
  • 1970-01-01
  • 2021-12-08
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-30
  • 1970-01-01
相关资源
最近更新 更多