Sets and Sorting

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

Written against MATLAB R2026b. The sessions below were run in GNU Octave 11.3.0 (MATLAB code that Octave also accepts) and show Octave’s output, shortened where noted. Octave has no maxk, mink, topkrows, groupcounts, allunique or numunique, and its tables and strings differ, so those parts are checked against the R2026b documentation and marked "not run here". Octave prints the index outputs ia and ic as columns, as MATLAB does.

Two families of functions answer "which distinct values are there?", "what do these collections have in common?" and "in what order?". They are used together constantly: the distinct sensor names in a log, the readings of one sensor, the three hottest readings. The vector, matrix and index notation used here is on Matrices and Arrays and Indexing; text handling is on Strings and Character Arrays; table sorting is on Tables.

unique

C = unique(A) returns the values of A without repetitions, in sorted order. For a row vector the result is a row, for a column a column, otherwise a column of the elements of A(:). It accepts numbers, character vectors, string arrays, cell arrays of character vectors (cellstr), categorical arrays, tables and timetables.

Option Meaning

"sorted" (default), "stable"

"stable" keeps the values in the order of first appearance instead of sorting them

"first" (default), "last"

which occurrence the index output ia points at when a value is repeated

"rows"

treat each row as one item and return the unique rows, sorted. Not supported for cell arrays of character vectors

"legacy"

the behaviour of R2012b and earlier (output orientation and index class); avoid in new code

TreatMissingAsDistinct=tf

since R2026a: true (the default) keeps every NaN or other missing value as distinct, false keeps at most one

With several outputs, [C, ia, ic] = unique(A) returns two index vectors, both columns: ia locates the chosen occurrence of each unique value in A, so C = A(ia); ic maps every element of A to its position in C, so A = C(ic) for vectors (for a matrix, A(:) = C(ic); with "rows", C = A(ia,:) and A = C(ic,:)).

a = [5 3 5 1 3 9];
[u, ia, ic] = unique(a)
[us, ias] = unique(a, 'stable')
ul = unique(a, 'last')

Octave 11.3.0 output, condensed (each vector is printed on its own lines by Octave):

u   = 1 3 5 9
ia  = 4 2 1 6        (first occurrences)
ic  = 3 2 3 1 2 4    (so that a = u(ic))
us  = 5 3 1 9        (order of first appearance)
ias = 1 2 4 6
ul  = 1 3 5 9

ic is the key to group-by work: it assigns every element a group number from 1 to numel©. Grouping and then aggregating with accumarray is shown below.

Unique rows, and unique text:

A = [1 2; 3 4; 1 2];
unique(A, 'rows')
names = {'S2','S1','S2','S3'};
unique(names)
unique(names, 'stable')

Octave 11.3.0 output: 1 2 / 3 4 for the rows; S1 S2 S3 for the sorted cell array; S2 S1 S3 for "stable".

Three details are easy to miss. First, NaN values are not equal to each other, so unique([4 NaN 2 NaN 2]) keeps both NaN (Octave 11.3.0 prints 2 4 NaN NaN); from R2026a TreatMissingAsDistinct=false collapses them. Second, floating-point values are compared exactly, so 0.1 + 0.2 and 0.3 are two values; uniquetol does the same job with a tolerance. Third, sorting follows sort (below), so a cell array of character vectors is sorted case-sensitively by character code.

Since R2025a, allunique(A) (true if there are no repeated values) and numunique(A) (the count of unique values) avoid building the result when only a yes or a count is needed. Not run here.

Membership and the set operations

The operations compare exact equality. They take numbers, text (character vectors, cell arrays of character vectors, string arrays), categorical arrays, dates and tables, and the inputs must be compatible (two numeric arrays, two cell arrays of text, and so on).

Call Result

ismember(a, S)

logical array with the size of a: is each element of a in S? [tf, loc] = ismember(a, S) also gives the index in S (0 if absent)

union(A, B)

all values in either set, sorted, without repetitions

intersect(A, B)

values in both

setdiff(A, B)

values in A and not in B

setxor(A, B)

values in exactly one of the two

Every operation accepts "rows" (compare whole rows), "stable" (order of appearance instead of sorted order; for union it means A first, then new values of B) and "sorted" (the default). The index outputs are [C, ia, ib] = intersect(A, B) with C = A(ia) = B(ib); union and setxor give indices into whichever input a value came from; setdiff gives ia only. Index outputs are columns.

A = [1 2 3 4]; B = [3 4 5];
union(A,B), intersect(A,B), setdiff(A,B), setxor(A,B)
[c, ia, ib] = intersect(A,B)
[tf, loc] = ismember([3 7 1], A)
ismember({'Neumann','Robin'}, {'Dirichlet','Neumann','Periodic'})
ismember([1 2; 5 6], [1 2; 3 4], 'rows')
union([3 1 2],[2 9 0],'stable')

Octave 11.3.0 output, condensed, in order:

union     = 1 2 3 4 5
intersect = 3 4
setdiff   = 1 2
setxor    = 1 2 5
c = 3 4,  ia = 3 4 (column),  ib = 1 2 (column)
tf  = 1 0 1
loc = 3 0 1
ismember of the two names = 1 0
ismember of the two rows  = 1 0 (column)
union stable = 3 1 2 9 0

ismember with a "rows" option returns a column with one answer per row of the first argument. For text, ismember is the readable replacement for a chain of strcmp calls: in heatflow, ismember(kind, ["Dirichlet" "Neumann" "Periodic"]) validates a boundary kind (see Logical Type and Conditions).

Tolerances. Floating-point results rarely match exactly. ismembertol(a, S, tol) and uniquetol(A, tol) perform membership and uniqueness with a tolerance (a relative tolerance by default). They are listed with the set operations in the documentation and not run here; the comparison rules are those of Numeric Types and Floating Point.

sort

B = sort(A) sorts ascending. For a vector it sorts the elements; for a matrix it sorts each column independently (the first dimension whose size is not 1); sort(A, dim) picks the dimension, so sort(A, 2) sorts each row. sort(..., "descend") reverses the order ("ascend" is the default). The second output [B, I] = sort(A) gives the permutation, so B = A(I) for a vector.

  • The sort is stable: equal elements keep their original order, so I for [2 1 2 1] is 2 4 1 3 (confirmed in Octave 11.3.0).

  • Missing values. NaN (and NaT, <undefined>, missing) are placed last for ascending order and first for descending order by default; MissingPlacement="first" or "last" overrides this. Octave 11.3.0 sorts [3 1 NaN 2 5] to 1 2 3 5 NaN, and descending to NaN 5 3 2 1.

  • Complex numbers are sorted by magnitude (and by phase angle on a tie) unless you pass ComparisonMethod="real", which sorts by real part and then imaginary part. ComparisonMethod="abs" forces magnitude for real data too. Octave shows the default case: sort([3+1i, 1-2i, -2]) gives -2, 1-2i, 3+1i.

  • Text. A cell array of character vectors or a string array is sorted by UTF-16 code order and case-sensitively, so uppercase letters sort before lowercase letters. Octave 11.3.0 gives Apple banana pear for {'pear','Apple','banana'}, which is already in code order. To ignore case, sort an index computed on a lowercased copy: [~, i] = sort(lower(c)); c(i). dim and direction are not supported for cell arrays (they sort ascending along the first non-singleton dimension). A string array sorts the elements and does not reorder the characters inside the strings.

  • Other classes. sort also orders logical, char, datetime, duration and categorical values (a categorical array follows the order of its categories). sort('hello') is ehllo.

  • Natural ordering is not built in. sort({'S10','S2','S1'}) gives S1 S10 S2, as in Octave 11.3.0. To order by the number, sort on an extracted number: [~, k] = sort(cellfun(@(c) str2double(c(2:end)), names)) (see Type Identification and Conversion).

x = [3 1 NaN 2 5];
sort(x), sort(x,'descend')
M = [3 1 2; 9 7 8; 4 6 5];
sort(M), sort(M,2,'descend')
[s, k] = sort(x)

Octave 11.3.0 output, condensed:

sort(x)          = 1 2 3 5 NaN
sort(x,'descend')= NaN 5 3 2 1
sort(M)          = 3 1 2 / 4 6 5 / 9 7 8      (each column sorted)
sort(M,2,'descend') = 3 2 1 / 9 8 7 / 6 5 4  (each row sorted, largest first)
k = 2 4 1 5 3

sortrows

sortrows(A) sorts the rows of a matrix by the first column, then by the second to break ties, and so on. sortrows(A, col) chooses columns: a negative number sorts that column descending, so sortrows(A, [1 -2]) is "first column ascending, ties by second column descending". The same can be written with a direction argument, sortrows(A, [1 2], ["ascend" "descend"]). [B, index] = sortrows(A, ...) returns the row order, so B = A(index,:). The ComparisonMethod and MissingPlacement options behave as for sort. For tables and timetables sortrows(T, vars) sorts by named variables, and a timetable sorts by its row times (see Tables and Timetables).

A heatflow example: a log with columns sensor number and temperature. Sort by sensor, hottest first:

data = [3 21.5; 1 22.4; 2 20.9; 1 21.7];
sortrows(data, [1 -2])
[B, idx] = sortrows([3 1; 1 2; 3 0; 1 1], [1 -2])

Octave 11.3.0 output:

1.0000  22.4000
1.0000  21.7000
2.0000  20.9000
3.0000  21.5000

B = 1 2 / 1 1 / 3 1 / 3 0
idx = 2 4 1 3    (as a column)

sortrows on a cell array works for a cell array of character vectors (one column). To sort rows of mixed data, put the data in a table.

issorted(A) is true if the vector is already sorted (ascending by default, "descend" for the reverse, and "rows" for rows); it is a cheap check before an algorithm that needs sorted input (issorted([1 1 2]) is 1, issorted([1 3 2]) is 0, as in Octave 11.3.0). Searching with ismember or find does not need sorted data.

Top k: maxk, mink, topkrows

When only the largest or smallest few values are needed, sorting everything is wasteful and clumsy.

  • maxk(A, k) returns the k largest elements (a vector for a vector input; for a matrix, a matrix whose columns hold the largest of each column). mink(A, k) returns the smallest. maxk(A, k, dim) chooses the dimension, [B, I] = maxk(A, k) also returns the indices, and ComparisonMethod="abs" or "real" is accepted. The result is sorted: the largest first for maxk, the smallest first for mink.

  • topkrows(X, k) returns the top k rows of a matrix, sorted descending by the first column and then by following columns; topkrows(X, k, col) picks columns, and topkrows(X, k, col, ["ascend" "descend"]) gives a direction per column. The table form topkrows(T, k, vars) works on table variables.

A heatflow use is the three hottest readings of a run: [hot, where] = maxk(temp, 3) returns the values and the positions of the largest three. Not run here: Octave 11.3.0 does not implement maxk (it reports that the function is not yet implemented). A portable version that runs in both is [~, order] = sort(temp, "descend"); order(1:3), which Octave 11.3.0 confirms: for readings [22.1 21.5 22.4 20.9 21.7 22.0] with sensor names S2 S1 S2 S3 S1 S2, the names of the three hottest are S2 S2 S2.

Grouping: ic with accumarray, and groupcounts

The ic output of unique is a group index, and accumarray aggregates by group: it is the loop-free way to count or average per category. With the heatflow readings from above:

ids  = {'S2','S1','S2','S3','S1','S2'};
temp = [22.1 21.5 22.4 20.9 21.7 22.0];
[u, ~, ic] = unique(ids);
counts = accumarray(ic(:), 1)'
meanT  = accumarray(ic(:), temp(:), [], @mean)'

Octave 11.3.0 output: u is S1 S2 S3, counts is 2 3 1 and meanT is about 21.600 22.167 20.900, so sensor S2 appears three times with mean 22.167.

groupcounts does the counting directly. For array data, [B, BG, BP] = groupcounts(A) returns the count of each unique value, the unique values and the percentage of the data each represents; for a table G = groupcounts(T, groupvars) returns a table with one row per group and the columns GroupCount and Percent. A groupbins argument bins a variable first (for example by year) and IncludeMissingGroups controls whether missing values form a group. Not run here (the functions are in current MATLAB, Octave has no groupcounts). Other group summaries are groupsummary and findgroups with splitapply, covered where tables are described.

Pitfalls

  • unique sorts by default. Pass "stable" to keep the order of appearance.

  • NaN is never equal to NaN, so each NaN survives unique (and ismember(NaN, A) is false) unless you remove or treat missing values first (see Missing Data).

  • ia points at the first occurrence by default, not the last.

  • Floating-point values are compared exactly in every set operation; use uniquetol and ismembertol.

  • sort on a matrix sorts each column independently, which destroys the rows. Use sortrows to keep the rows together.

  • Text sorts by character code, case-sensitively, and numbers embedded in text sort as text (S10 before S2).

  • sortrows with -column sorts that column descending; the minus sign is on the column number, not on the data.

  • maxk and topkrows are not the same order as sort: topkrows sorts descending by default, sortrows ascending.

  • Set operations need compatible types: a numeric array with a cell array of text is an error, and "rows" needs the same number of columns.

Where older books differ

Older books use sort and find, or write a loop, for top-k selection and for counting per group; maxk and mink (R2017b) and topkrows (R2016b), both introduction releases taken from the reference pages, now do the first, and groupcounts the second. They describe the behaviour of unique, union, intersect, setdiff and setxor as it was in R2012b and earlier (the documentation says "legacy" preserves it; the details, such as the orientation of the outputs, are not reproduced here): the "legacy" option still gives that behaviour, but new code should not use it. The "stable" option is newer than most books, and so are allunique and numunique (R2025a) and TreatMissingAsDistinct (R2026a). Before R2021a, name-value pairs of sort and sortrows were written with commas and quoted names (sort(A,"ComparisonMethod","abs")); the comma form still works.