在 XSLT 中对记录进行分组时如何避免 O(n^2) 复杂性?
Posted
技术标签:
【中文标题】在 XSLT 中对记录进行分组时如何避免 O(n^2) 复杂性?【英文标题】:How to avoid O(n^2) complexity when grouping records in XSLT? 【发布时间】:2011-12-26 00:00:48 【问题描述】:当我通过 XSL 将大量数据转换为 html 时,我经常遇到性能问题。这些数据通常只是几个非常大的表格,大致是这种形式:
<table>
<record>
<group>1</group>
<data>abc</abc>
</record>
<record>
<group>1</group>
<data>def</abc>
</record>
<record>
<group>2</group>
<data>ghi</abc>
</record>
</table>
在转换过程中,我想像这样直观地对记录进行分组
+--------------+
| Group 1 |
+--------------+
| abc |
| def |
+--------------+
| Group 2 |
+--------------+
| ghi |
+--------------+
一个愚蠢的实现是这个(集合来自http://exslt.org。实际的实现有点不同,这只是一个例子):
<xsl:for-each select="set:distinct(/table/record/group)">
<xsl:variable name="group" select="."/>
<!-- This access needs to be made faster : -->
<xsl:for-each select="/table/record[group = $group]">
<!-- Do the table stuff -->
</xsl:for-each>
</xsl:for-each>
很容易看出这往往具有O(n^2)
的复杂性。更糟糕的是,因为每条记录中都有很多字段。操作的数据可达几十MB,记录数可达5000条。最坏的情况下,每条记录都有自己的组和50个字段。更糟糕的是,还有另一个级别的分组可能,这就是O(n^3)
现在会有很多选择:
-
我可以找到一个涉及映射和嵌套数据结构的 Java 解决方案。但我想提高我的 XSLT 技能,所以这实际上是最后的选择。
我可能没有注意到 Xerces/Xalan/Exslt 中有一个很好的功能,它可以更好地处理分组
我也许可以为
/table/record/group
建立某种索引
您可以向我证明,在这个用例中,<xsl:apply-templates/>
方法明显快于 <xsl:for-each/>
方法。
您认为如何降低O(n^2)
的复杂性?
【问题讨论】:
【参考方案1】:您可以只使用 XSLT 1.0 中著名的 Muenchian 分组方法——无需探索已排序的数据并实现更复杂和更慢的算法:
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output omit-xml-declaration="yes" indent="yes"/>
<xsl:strip-space elements="*"/>
<xsl:key name="kGroupByVal" match="group" use="."/>
<xsl:template match="node()|@*">
<xsl:copy>
<xsl:apply-templates select="node()|@*"/>
</xsl:copy>
</xsl:template>
<xsl:template match=
"group
[generate-id()
=
generate-id(key('kGroupByVal', .)[1])
]">
<group gid=".">
<xsl:apply-templates select="key('kGroupByVal', .)/node()"/>
</group>
</xsl:template>
<xsl:template match="group/text()"/>
</xsl:stylesheet>
当此转换应用于您提供的文本(甚至不是格式正确的 XML 文档!!!)在将其更正为格式正确后,
3 个 record
元素需要 80 毫秒。
对于具有 1000 个 record
元素的类似文本,转换在 136 毫秒内完成。
使用 10000 个record
元素所需的时间为 284 毫秒。
使用 100000 个record
元素所需的时间为 1667 毫秒。
观察到的复杂性显然是亚线性的。
很难(如果可能的话)找到比 XSLT 1.0 中的 Muenchian 分组更有效的解决方案。
【讨论】:
感谢您的解释。不要担心格式是否正确,这只是保持简单的一个示例。在这种情况下,@IvanDugic 的解决方案可能会更快一些,因为确实,这些组已经在数据库中进行了排序。因此可以使用<xsl:if test="not(preceding-sibling::record[1]/group = group)"/>
创建分组标题,但这显然是需要牢记的
@LukasEder:您为什么不尝试两种解决方案并进行测量?
我正要这样做。我会告诉你的
令我惊讶的是,这两种解决方案在
@LukasEder:是的,而且 Muenchian 分组几乎可以机械地编码,不需要任何专业知识。【参考方案2】:
如果数据是按组预排序的(如您的示例),您可以循环记录集并检查记录的组是否与前面的记录组不同。如果组发生变化,您可以添加组标题。这将以 O(n) 时间复杂度执行。
【讨论】:
【参考方案3】:你当前的算法:
for every [group] record
for every [data] record
// actions
我假设如果您对所有元素执行简单的迭代并且
for every [record]
take [data]
take [group]
add [data] to [group]
对于组表示,您可以使用树或地图。
如你所见,这个算法的复杂度是O(n)
【讨论】:
我知道这个选项,我可以很容易地在 Java 中实现它。但是如何使用 XSLT 做到这一点? 我不是 xslt 专家,但您可以使用推荐的分组方法是 XSLT 2.0 中的 xsl:for-each-group 和 XSLT 1.0 中的 Muenchian 分组。使用任何半体面的处理器,这两者都将具有 (n*log(n)) 性能。
或者您可以简单地将"/table/record[group = $group]"
替换为对 key() 函数的调用。
如果您准备为 Saxon-EE 等企业级 XSLT 处理器付费,这些优化很有可能会自动为您完成,因此您不必担心。
【讨论】:
我应该说我正在使用 XSLT 1.0...不过,关键的方法很有趣!我必须仔细检查一下以上是关于在 XSLT 中对记录进行分组时如何避免 O(n^2) 复杂性?的主要内容,如果未能解决你的问题,请参考以下文章