从列表中删除重复项

从列表中删除重复项

是否有一个 LaTeX 内核命令可以从列表中删除重复项?

该列表的结构如下

\def\alist{john,mary,george,australia,australia}

答案1

几年后…

首先,最好不要使用宏来存储列表,而是创建一种命名空间。

使用 可以轻松完成此操作以及删除重复项expl3。可以利用标准expl3功能添加用于管理和使用列表的其他功能。

\documentclass{article}

\ExplSyntaxOn

\NewDocumentCommand{\listdefine}{mm}
 {% #1 = list name, #2 = items
  \clist_gclear_new:c { g_yannisl_list_#1_clist }
  \clist_gset:cn { g_yannisl_list_#1_clist } { #2 }
 }
\NewDocumentCommand{\listremoveduplicates}{mo}
 {% #1 = list name, #2 = optional new list name
  \IfNoValueTF{#2}
   {% change the list
    \clist_gremove_duplicates:c { g_yannisl_list_#1_clist }
   }
   {% put in a new one
    \clist_clear_new:c { g_yannisl_list_#2_clist }
    \clist_gset_eq:cc { g_yannisl_list_#2_clist } { g_yannisl_list_#1_clist }
    \clist_gremove_duplicates:c { g_yannisl_list_#2_clist }
   }
 }

% Just for the example
\NewDocumentCommand{\listshow}{m}
 {% #1 = list name
  \clist_show:c { g_yannisl_list_#1_clist }
 }

\ExplSyntaxOff

\listdefine{alist}{john,mary,george,australia,australia,john}
\listshow{alist}
\listremoveduplicates{alist}[blist]
\listshow{alist}
\listshow{blist}

\stop

控制台输出:

The comma list \g_yannisl_list_alist_clist contains the items (without outer
braces):
>  {john}
>  {mary}
>  {george}
>  {australia}
>  {australia}
>  {john}.
<recently read> }

l.32 \listshow{alist}

?
The comma list \g_yannisl_list_alist_clist contains the items (without outer
braces):
>  {john}
>  {mary}
>  {george}
>  {australia}
>  {australia}
>  {john}.
<recently read> }

l.34 \listshow{alist}

?
The comma list \g_yannisl_list_blist_clist contains the items (without outer
braces):
>  {john}
>  {mary}
>  {george}
>  {australia}.
<recently read> }

l.35 \listshow{blist}

如果\listremoveduplicates调用时没有可选参数,那么它本身将被重新定义且不会有重复。

原始答案

\makeatletter
\def\removeduplicates#1#2{\begingroup
  \let\@tempa#1%
  \def\@tempb{}%
  \@for\next:=\@tempa\do
    {\@ifundefined{lstel@\next}
      {\edef\@tempb{\@tempb,\next}
       \expandafter\let\csname lstel@\next\endcsname\@empty}
      {}%
    }%
  \edef\x{\endgroup\def\noexpand#2{\@tempb}}\x
  \expandafter\strip@comma#2\@nil#2}
\def\strip@comma,#1\@nil#2{\def#2{#1}}
\makeatother
\def\alist{john,mary,george,australia,australia,john}

\removeduplicates\alist\blist

\show\blist
\show\alist

\removeduplicates\alist\alist
\show\alist

当我们查看每个列表元素(例如)时john,我们会看到是否\lstel@john未定义;在这种情况下,我们将列表元素添加到新列表并定义相应的命令。最后,我们删除初始逗号。它不适用于空列表,但添加该测试很容易。

\removeduplicates可以接收相同的参数,在这种情况下,新列表当然会覆盖原始列表。

注意:已编辑以避免使用\xdef

答案2

可以在几乎线性的时间内删除重复项,而不受元素的限制(当然,它们不能包含逗号,除非将它们隐藏在括号后面)。

困难之处在于,一次添加一个元素会自动导致二次时间,因为向逗号列表(或实际上任何标记列表)添加元素所需的时间与列表的长度成正比。所以我们需要以某种方式一次放入所有元素。

此外,我能想到的唯一在几乎恒定的时间内检查项目是否重复的方法是为每个项目定义一个控制序列。我从 egreg 的回答中得到了这个想法。这样做的问题是,它将 catcode 不同的项目视为相同的项目。为了克服这个障碍,我们需要一个双程系统:

  • 一步创建包含\detokenize返回相同值的项目列表的控制序列;

  • 另一个步骤是将所有这些控制序列放在一起。

在这两个步骤中,我们都需要避免重复,最终更简单的方法是先执行第二步,利用如果为 则\csname foo\endcsname让其局部\foo为的事实。如果未定义,则构造将扩展为,并让其为,否则将扩展为空。通过循环遍历逗号列表(使用可扩展的),我们构建了一个列表,形式为\relaxundefined\ifcsname foo\endcsname \csname foo\endcsname \fi\foo\relax\clist_map_function:NN

\l_Xclist_<item1>_seq \l_Xclist_<item2>_seq ... \l_Xclist_<itemN>_seq

没有重复。

在第二遍中,我们定义每一个\l_Xclist_<itemK>_seq。其中每一个最初都是\relax,我们不喜欢这样,所以我们测试是否是这种情况(使用\if_meaning:w #1 \relax,即\ifx),如果是,则让它们为空宏。一旦我们确定是\l_Xclist_<itemK>_seq一个“好”的序列,我们就可以使用 LaTeX3 的 seq 宏来测试(未去标记化)是否在 中\l_Xclist_<itemK>_seq。如果不是,我们就添加它。

最终,我们构建了所有的\l_Xclist_<itemK>_seq,每个都包含一个所有项目的序列,这些项目在去标记化时会给出。然后我做了一些坏事,通过定义并执行本质上的<itemK>来达到模块的内部,这会将每个序列扩展为正确的逗号列表。小技巧是以一种相对便宜的方式删除第一个逗号。l3seq\seq_item:n\xdef \g_Xclist_remove_clist {\g_Xclist_remove_clist}\romannumeral

\RequirePackage{expl3}
\ExplSyntaxOn
%
% Now we use a global clist to return the value.
% 
\clist_new:N \g_Xclist_remove_clist
%
% In the same way as in l3clist, we use a common auxiliary function
% for removing duplicates locally or globally.
%
\cs_new_protected_nopar:Npn \Xclist_remove_duplicates:N #1 {
  \Xclist_remove_duplicates_aux:N #1
  \clist_set_eq:NN #1 \g_Xclist_remove_clist
}
\cs_new_protected_nopar:Npn \Xclist_gremove_duplicates:N #1 {
  \Xclist_remove_duplicates_aux:N #1 
  \clist_gset_eq:NN #1 \g_Xclist_remove_clist
}
%
% The rough idea is to define one variable per element "#1"
% of the clist, and only add the element if the corresponding
% variable is not defined. 
% 
% with name "\g_clist_remove_\tl_to_str:n{#1}_seq",
% containing the sequence of all "#1" with the same detokenization.
% This is necessary to cater for the possibility that two different
% 
\cs_new_protected_nopar:Npn \Xclist_remove_duplicates_aux:N #1 
  {
    \group_begin:
    \tl_gset:Nx \g_Xclist_remove_clist 
      {
        \clist_map_function:NN #1 \Xclist_remove_duplicates_aux_ii:n
      }
    \clist_map_inline:Nn #1 
      {
        \exp_args:Nc \Xclist_remove_duplicates_aux_iii:Nn
        {l_Xclist_\tl_to_str:n{##1}_seq} {##1}
      }
    \cs_set:Npn \seq_item:n ##1 { , \exp_not:n {##1} }
    \cs_set:Npn\seq_elt:w##1\seq_elt_end:{\seq_item:n{##1}}%for older l3seq.
    \tl_gset:Nx \g_Xclist_remove_clist 
      { 
        \exp_after:wN \use_none:n \tex_romannumeral:D -`\0% remove leading ","
        \g_Xclist_remove_clist 
      }
    \group_end:
  }
\cs_new:Npn \Xclist_remove_duplicates_aux_ii:n #1 
  {
    \reverse_if:N \if_cs_exist:w l_Xclist_\tl_to_str:n {#1}_seq\cs_end:
      \cs:w l_Xclist_\tl_to_str:n{#1}_seq \cs_end:
    \fi:
  }
\cs_new:Npn \Xclist_remove_duplicates_aux_iii:Nn #1 #2
  {
    \if_meaning:w #1 \tex_relax:D
       \seq_clear:N #1
    \fi:
    \seq_if_in:NnF #1 {#2} {\seq_put_right:Nn #1 {#2}}
  }

一个非常基本的测试:

\clist_new:N \l_my_clist
\clist_set:Nn \l_my_clist {a,b,c,d,e,\f,\g\h,a,b,c,\f,d,e,\g\h}
\clist_put_right:Nx \l_my_clist {\string a}
\clist_put_right:Nx \l_my_clist {\string b}
\clist_put_right:Nx \l_my_clist {\string c}
\clist_put_right:Nx \l_my_clist {\string d}
\clist_put_right:Nx \l_my_clist {\string e}

\clist_show:N \l_my_clist
\Xclist_remove_duplicates:N \l_my_clist
\clist_show:N \l_my_clist

答案3

我受到 Bruno 解决方案的启发,但我建议使用仅一个完全可扩展通行证。主要效果是通过\expandafter\ifx\csname foo\endcsname\fi将 扩展为 ,但将 设置\foo为 来实现的\relax。这只是在扩展处理器级别完成的唯一分配。所有其他分配都在 TeX 的主处理器中完成。

\def\noduplicate#1,{\ifx,#1,\else
   \ifcsname dup:#1\endcsname
   \else #1,\expandafter\ifx \csname dup:#1\endcsname \relax \fi
   \fi
   \expandafter\noduplicate \fi
}
\def\alist{john,mary,george,australia,australia,mary}

\edef\alist{\expandafter\noduplicate\alist,,}
\message{\meaning\alist}

\end

由于使用,因此需要 e-TeX \ifcsname。字符串池和哈希表(不幸的是)已分配,但没有办法释放它们。

答案4

LaTeX2e 内核仅提供\@removeelement,它将删除特定元素(并用于从全局列表中删除类选项)。因此,您需要编写自己的代码或使用包中预先编写的版本(例如\clist_remove_duplicates:Nn来自expl3)。

相关内容