<name>Student 1</name>
<classes>
<class>
<title>soccer</title>
<category>sport</category>
</class>
<class>
<title>football</title>
<category>sport</category>
</class>
So Student 1 has two class elements under classes, each of which
falls under category sport.
<p>Student group by Category</p>
<xsl:for-each select="/category[not(. = preceding::category)]">
<p><xsl:value-of select="."/></p>
<xsl:for-each select="key('categories',.)">
<p><xsl:value-of select="../../name"/></p>
</xsl:for-each>
</xsl:for-each>
The inner for-each you specify selects all categories matching the current
category and then outputs the name. That means when category is 'sport',
Student 1 matches twice.
To solve the problem you posted, I found the techniques discussed in
http://www.jenitennison.com/xslt/grouping/index.html to be useful.
If you use the Muenchian technique discussed under that URL, you can
select the first occurance of each unique title, category, and class
with a unique title-category combination, and then apply templates to
the student/name element for each match:
<?xml version='1.0' encoding='ISO-8859-1'?>
<xsl:stylesheet
xmlns:xsl='http://www.w3.org/1999/XSL/Transform'
version='1.0'>
<xsl:key name='title' match='title' use='.'/>
<xsl:key name='category' match='category' use='.'/>
<xsl:key name='class' match='class' use='concat(./title, " - ", ./category)'/>
<xsl:key name='by_title' match='student' use='./classes/class/title'/>
<xsl:key name='by_category' match='student' use='./classes/class/category'/>
<xsl:output method='text' indent='no'/>
<xsl:template match="students">
<xsl:text> Student group by title </xsl:text>
<xsl:apply-templates select='./student/classes/class/title
[generate-id(.)=generate-id(key("title", .))]'>
<xsl:sort select="."/>
</xsl:apply-templates>
<xsl:text> Student group by category </xsl:text>
<xsl:apply-templates select='./student/classes/class/category
[generate-id(.)=generate-id(key("category", .))]'>
<xsl:sort select="."/>
</xsl:apply-templates>
<xsl:text> Student group by title and category </xsl:text>
<xsl:apply-templates select='./student/classes/class
[generate-id(.)=generate-id(key("class", concat(./title, " - ",
./category)))]'>
<xsl:sort select="."/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="title">
<xsl:value-of select='.'/>
<xsl:text> </xsl:text>
<xsl:apply-templates select='key("by_title", .)/name'>
<xsl:sort select='.'/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="category">
<xsl:value-of select='.'/>
<xsl:text> </xsl:text>
<xsl:apply-templates select='key("by_category", .)/name'>
<xsl:sort select='.'/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match="class">
<xsl:variable name='title_category' select='concat(./title, " - ",
./category)'/>
<xsl:value-of select='$title_category'/>
<xsl:text> </xsl:text>
<xsl:apply-templates select='key("class", $title_category)/../../name'>
<xsl:sort select='.'/>
</xsl:apply-templates>
</xsl:template>
<xsl:template match='name'>
<xsl:text>	</xsl:text>
<xsl:value-of select='.'/>
<xsl:text> </xsl:text>
</xsl:template>
</xsl:stylesheet>
XSL-List info and archive: http://www.mulberrytech.com/xsl/xsl-list