Skip to content

Comparative Ordering

So far this chapter has dealt primarily with structural ordering; that is, the basic concepts around memory layouts and access mechanisms used to store and organize data. The material has not yet addressed any meaningful ways to arrange elements by their realtive values. This page introduces the concept of comparative ordering, which defines the semantics of comparison-based operations and the correctness conditions under which order-dependent algorithms operate. Order-dependent operations include sort operations on sequences, insert and remove operations on structures that maintain sorted invariants, and query operations such as find (search), and find_kth ranked elements (selection) within a collection.

The first half of this page explores the mathematical foundations of comparison-based ordering, specifically the order and equivalence relations used to define comparison semantics. Readers already familiar with these concepts can skip ahead to the next section entitled Comparative Ordering in Practice which attempts to bridge domain gaps between discrete mathematics and actual Rust code. The next page applies the concept of ordered types for elementary sorting, searching, and selection algorithms and culminates in the module’s first sorted data structure.

Comparison Foundations

Most DSA texts state that sorting a collection assumes a total order on the values to be sorted, or more generally, a that there exists a consistent comparison relation on those values. The text might even define a total order as a partially ordered set where every pair of values is directly comparable, but not much more is typically provided about ordering schemes. These texts assume prior knowledge of discrete mathematics topics such as set and order theory, so the terms are not typically discussed in further depth.

This section explores the mathematical concepts of binary relations through order and equivalence relations, and uses them to define ordering schemes like total and partial order.

Order Relations

The idea of comparative ordering as defined in this module is based loosely on elementary order theory, which itself is formulated in terms of binary relations to establish a comparative ordering of values. One of the most familiar examples of order theory, and math in general, is numerical ordering via magnitude. This is often introduced early on through story problems concerning two or more parties who possess and/or exchange multiples of the same object. For example, “If Peter had 3 drinks and Nina had 2 drinks, who had more drinks?” In this way, it is possible to establish the concept of numerical ordering through a binary relation.

Mathematically, a binary relation R is a subset of the Cartesian product of two sets A and B, which can be written in mathematical notation as R ⊆ A × B. This relation defines the collection of all ordered pairs for which the relation holds true. In ordering and comparison contexts, it is most common to work with relations over a single set, typically as a single type, so that R ⊆ A × A, which means . In this setting, a comparison between two elements (values) x and y can be expressed as (x, y) ∈ R, meaning that the ordered pair (x, y) is an element of the relation. This can also be written equivalently using infix notation as xRy.

A binary relation can be described in two equivalent ways; extensionally, as a set of ordered pairs, or intensionally, as a predicate (or comparative function in computer science) P(x, y) that determines whether a given pair belongs to that set. In practice, algorithms evaluate the predicate for individual pairs, but the underlying relation is defined mathematically as the set of all pairs for which the predicate evaluates to true. This can be re-stated by saying that the predicate P(x, y) evaluates to true if and only if (x, y) ∈ R. These two perspectives are equivalent; the predicate characterizes the relation, while the relation is the set of all pairs for which the predicate evaluates to true.

As an analogy, consider a coordinate plane (2d graph) representing a relation defined by the predicate “less than”, written as P(x, y) = (x < y). The graph illustrates the relation as a region encompassing a set of all points (x, y) such that x < y. Naturally, the position of elements in an ordered pair matters. For example, the relation R = “less than” on the set A = { 1, 2, 3 } can be written as R = { (1,2), (1,3), (2,3) } ⊆ A × A, and does not include reversed pairs such as (2,1) or (3, 1). This makes sense because 2 !< 1 and 3 !< 1. For the relation “less than”, if (x,y) ∈ R, then (y,x) ∉ R, illustrating the relation’s asymmetry. Stated slightly differently, evaluating an expression in code like 2 < 3 corresponds mathematically to checking whether the pair (2, 3) belongs to this relation.

As illustrated, the “less than” relation is asymmetric. Asymmetry is only one of many possible properties that a relation may satisfy. While not exhaustive, the following properties are useful for classifying relations relevant to comparative ordering:

  • Transitivity
    If k1 <= k2 and k2 <= k3, then k1 <= k3. For strict relations, this applies to the < operator such that if k1 < k2 and k2 < k3, then k1 < k3.
  • Reflexivity & Irreflexivity
    Reflexivity of an order relation holds when k <= k. In contrast, irreflexivity holds when k !< k, and is a defining property of strict relations. This can be expressed in mathematical notation as ¬(k < k). Reflexivity of equivalency holds when k == k.
  • Antisymmetry
    Antisymmetry occurs when k1 <= k2 and k2 <= k1 imply k1 == k2. This ensures that no two distinct elements relate in both directions unless they are the same.
  • Asymmetry
    Asymmetry holds when k1 < k2 implies k2 !< k1. This enforces a one-way relationship and implies irreflexivity.
  • Comparability
    Comparability, sometimes called connectedness, holds when every pair of elements can be ordered relative to each other. This property distinguishes total ordering from partial ordering: k1 <= k2 || k2 <= k1. That is, for all elements x and y, either x <= y or y <= x.

Equivalence Relations

Thus far, this page has taken for granted what it means to establish that two objects are equal. Any binary relation that is symmetric (that is, for all a, b ∈ S, if aRb then bRa), reflexive, and transitive is known as an equivalence relation, which partitions a set into equivalence classes of elements considered equivalent under that relation.

Equivalence is not the same as equality. An equivalence relation may group distinct elements together based on some shared property. For example, numbers with the same rounded value are equivalent under rounding, even though the unrounded numbers are not equal. Equality is the strictest (finest grained) equivalence relation possible, and represents the identity set. It relates an element only to itself as R = { (a, a) | a ∈ S }. Thus, each equivalence class under the equality relation contains exactly one element. As an analogy, you may prepare a meal using the same process each time, producing meals that are roughly equivalent, but not identical; only a specific meal is strictly equal to itself. Alternatively, consider an analogy by way of the parable about never being able to step into the same river twice. The river changes moment to moment, so while wading in one day may feel equivalent to a previous experience, it is not strictly equal.

In computer science, equality is defined by the semantics of a type rather than purely by its raw memory representation. This means that two values are considered equal if they satisfy the equality predicate implemented for that type. For some types, this corresponds to comparing underlying data (e.g., element-wise comparison for arrays or strings), while for others it may involve more abstract notions of equivalence. It is therefore important to understand how equality is implemented for a given type. For example, a type with multiple fields may define equality in a way that does not consider all fields. Similarly, for types involving references or pointers, equality may be defined either in terms of the referenced contents (value equality) or the pointer identity (reference equality).

In Rust, equality is defined via the PartialEq and Eq traits (covered in the Comparative Ordering In Practice section below), which typically implement structural (field-wise) comparison rather than raw bit/byte comparison. As a result, equality reflects the logical value of a type rather than its in-memory representation. For now, just know that PartialEq::eq() is a required method that tests self vs other for equality which is used by the == operator.

Ordering Schemes

An ordering scheme is a classification of binary relations characterized by the structural properties they satisfy. Different schemes arise by varying these properties, yielding relations with different expressive power and constraints. Terminology is not entirely uniform across sources, but the underlying distinctions remain standardized.

One key axis of classification is whether a relation is strict or non-strict. A non-strict ordering relation (typically as <=) is reflexive and transitive, whereas a strict relation (typically as <) is irreflexive and transitive, and therefore asymmetric. These two forms are interdefinable when they describe the same ordering structure under appropriate conditions, such as in total orders. In such cases, one can define x < y ⟺ (x <= y ∧ x != y), and, assuming totality, x <= y ⟺¬ (y < x). Thus, a single ordering scheme may admit multiple equivalent formulations depending on whether it is expressed in strict or non-strict form.

Another key axis of classification is totality, also called comparability or connexity. A relation is total if for every pair of elements a and b, either a <= b or b <= a holds. This property is part of what distinguishes total relations from partial relations. A total preorder is a reflexive, transitive, and total relation that allows distinct elements to be equivalent. A total order strengthens a total preorder by requiring antisymmetry, ensuring that equivalence collapses to equality. This should sound familiar, as it is equivalent to the definition of a total order as a partial order in which any two elements are comparable.

Ordering schemes can also be constructed via projections (or key functions). Given a function f: X → Y where Y is totally ordered, one can define a relation on X with x <= y ⟺ f(x) <= f(y). This induces a total preorder on X, where elements may be distinct but equivalent whenever f(x) = f(y). For example, ordering students by rounding their numerical grade to a letter grade yields a total preorder. The letter grade B- includes a range of percentage values so multiple students can occupy the same “rank” as a letter grade even though they are not identical or have identical scores.

Different ordering schemes emerge as properties are added or removed:

  • A strict partial order is irreflexive and transitive, but does not require that all elements be comparable. For example, the subset relation on sets is a strict partial order: some sets are incomparable.
  • A total preorder is reflexive and transitive and requires totality, but does not require antisymmetry. This permits ties: distinct elements may be equivalent without being equal.
  • A total order strengthens a total preorder with antisymmetry, ensuring that equivalence collapses to equality. For example, the usual ordering on integers is a total order.

For comparative ordering in computer science, the most important ordering schemes are categorized in the following table:

Ordering SchemeTransitiveReflexiveAntisymmetricComparable (Connex)
Total OrderXXXX
Total PreorderXXX
Partial OrderXXX

NOTE: All orders in this table are assumed to be non-strict (weak) orders, usually as <=. Strict expressions (usually as <) for partial and total orders are irreflexive, not reflexive. Additionally, strict total preorders are asymmetric.

Compositional Ordering

Compositional ordering constructions are not a formally categorized type of ordering, but for the purposes of this module describe how comparison relations are defined over structured values. These constructions determine whether values are compared as indivisible units or as structured collections of components.

  • Atomic ordering represents the simplest kind of compositional ordering, and refers to comparisons performed on values treated as indivisible units using a defined comparison relation. Numerical ordering, one of the most common instances of atomic ordering, is where values are compared according to their numeric magnitude. A simple example of numerical ordering is the inequality 2 < 3. For example, the following array of integers is sorted under numerical (and thus atomic) ordering: [1, 2, 5, 10, 11, 23]

  • Lexicographical ordering, sometimes called dictionary ordering, represents another common ordering construction which generalizes alphabetical ordering to sequences over a totally ordered set. Lexicographical ordering is used to compare composite objects of elements. This is generally done in sequential order, one at a time. In Rust, the String type represents a contiguous UTF-8 encoded sequence of bytes, so comparisons between strings are performed lexicographically over UTF-8 bytes in sequence. For example, imagine two files named file_6 and file_10. It might be natural for a human to assume that file #6 comes before file #10, but that is not necessarily how a computer compares the values. When computing a lexicographical ordering, the first five UTF-8 bytes of each file name are equal and therefore do not affect the comparison. However, the sixth byte differs, and since “1” < “6” in ASCII, file_10 comes before file_6.

    "10" < "6" // lexicographically true
    10 > 6 // numerically true

Comparative Ordering In Practice

In programming, it is more common to work with types than with sets, though the two concepts are closely related. Informally, a type can be viewed as the set of values that may inhabit it. For example, the mathematical set of natural numbers, N = { 0, 1, 2, … }, contains all non-negative integers, while an 8-bit unsigned integer type (u8) contains only the integers from 0 to 2^8 − 1. In this sense, a type corresponds to a particular set of representable values. For example, the u8 type corresponds to the finite subset of natural numbers { 0, 1, 2, …, 255 }.

Because values of different types may have incompatible representations or semantics, statically typed languages generally do not allow arbitrary comparisons across type boundaries without explicit conversion. Some dynamically typed languages do permit such comparisons through implicit type coercion, which can lead to weird places when left unchecked (I’m looking at you, Javascript).

Javascript
[] == ![] // true

Most programming languages provide mechanisms for comparing values, but differ in how explicitly they expose them. Depending on language design, ordering may be expressed through interfaces, operator overloading, or comparator functions. Rust uses a trait-based system, similar in role to interfaces in other languages, to define comparison-based ordering behavior for custom types.

Comparison Tools

Rust’s standard library includes the cmp (“comparison”) module which provides the necessary tools required to implement ordering in Rust. These tools consist primarily of the four traits PartialEq, Eq, PartialOrd, Ord, and the Ordering enum.

- PartialEq

The PartialEq trait provides a predicate for equality comparisons by defining the behavior of the == and != operators on the implemented type. The trait accomplishes this through a single required method, eq, which determines whether two values are considered equal. In this way, the expressions a == b and a.eq(b) evaluate equivalently. The PartialEq trait also has a method ne or “not equal”, which derives its implementation from eq, so a != b and a.ne(b) come for free.

Although it is not strictly enforced, PartialEq::eq implementations are expected to represent a partial equivalence relation which satisfies symmetry (if a == b, then b == a) and transitivity (if a == b, and b == c, then a == c). A “full” equivalence relation is symmetric, transitive, and reflexive (a == a), but the PartialEq::eq method does not require reflexivity, so it is considered “partial”. This allows types like floats to assume a partial equality because NaN != NaN.

The PartialEq can be derived, but when done so, the compiler assumes a component-wise partial equivalence relation, meaning that all fields in the type are compared. This is not always desirable when the type contains data that is considered irrelevant for comparison.

- Eq

The Eq trait is a marker trait that indicates that a type’s PartialEq implementation forms a “full” equivalence relation by asserting that its equality predicate also satisfies reflexivity (if a == a). Since the compiler cannot verify reflexivity for all types, Eq has no methods and serves purely as a semantic type guarantee.

For example, Rust’s ordering traits rely on this guarantee when defining ordering semantics. As such, requiring Eq trait bounds for ordering traits signal a requirement for the implementor to ensure that this semantic guarantee holds true. Implementing a partial equivalence relation with PartialEq and slapping on an Eq without ensuring reflexivity to use with Rust’s ordering traits may lead to incorrect type behavior.

- PartialOrd

On the ordering side, the std::cmp module provides the PartialOrd and (total) Ord traits, which define the behavior of the <, <=, >, and >= operators. PartialOrd is intended to model a partial order via the required partial_cmp method, which returns Option<Ordering> to account for incomparable values such as NaN.

PartialOrd implementations are expected to be transitive and consistent with PartialEq, such that a == b if and only if partial_cmp(a, b) == Some(Ordering::Equal). As a result, types that implement PartialOrd must also implement PartialEq. PartialOrd is derivable and performs a lexicographic comparison over struct fields in order of declaration. For example struct Ex { a: u8, b: u8 } comparisons would first compare a and then b if necessary. Additionally, implementations are expected to satisfy duality which states that a < b if and only if b > a, and a <= b if and only if b >= a. The < and > operators correspond to a strict ordering, while <= and >= correspond to a non-strict (reflexive) ordering.

Rust’s trait names can be slightly misleading: PartialOrd does not enforce that the relation satisfies the mathematical definition of a partial order, which would be reflexive, antisymmetric, and transitive. Instead, it provides a flexible interface that can represent relations with possible incomparability, including partial orders.

- Ord

The Ord trait requires that the comparison forms a total order, where all value pairs are comparable. Implementations of Ord must be consistent with PartialOrd, and violating this can lead to incorrect behavior in generic algorithms and data structures. When implementing Ord, Rust recommends implementing PartialOrd::partial_cmp as Some(self.cmp(other)), delegating to the total ordering defined by Ord.

Together, these traits and the supporting enum Ordering represent the core tools necessary to establish comparative ordering on types. Reading through the official Rust documentation is highly recommended, though it may be helpful to read through this page and the next page on sorting first to establish a baseline for the concepts involved.

Type Bounds

It is important to constrain operations and data structures with appropriate ordering bounds to ensure correctness and well-defined operational behavior. The next page covers sorting and a sorted structures in more detail, but as an example, consider defining a sort method or a sorted data structure that typically require a total order. Proper trait bounds ensure that routines and structures are generic over T where T is Ord, meaning that they only take types that implement a total order:

fn sort<T: Ord>(list: &mut [T]) { ... }
impl<T: Ord> SkipList<T> { ... }

Rust induces a total order via Ord on most of its primitive numerical types, as well as String, by default. One notable exception is floating point types. Floats do not implement Ord because their comparison semantics do not define a total order. Specifically, NaN values are not comparable, and since NaN != NaN they also do not satisfy reflexivity. As a result, floating point types only implement PartialOrd and NaN comparisons return None, indicating the lack of totality.

Ordering Impelmentation

The rest of this section implements a partial order, a total preorder, and a total order as illustrative examples of comparative ordering. Specifically, the examples represent three different approaches to comparing students and their grades. The base Student struct does not change across the ordering implementations and is defined as follows:

pub struct Student {
pub name: &'static str,
pub grade: f32,
}

The struct field types are included specifically because they pose some extra “gotchas” that help illustrate the implementation methodologies. It is possible to further define each ordering implementation here as strict or non-strict (weak) by the use of comparison operators on the types themselves. That is, strict/weak ordering does not happen as a result of trait implementation, but rather in the algorithms that take these types.

Partial Order

The distinguishing characteristic of a partial order is its lack of totality. That is, the possibility that not all type values are comparable. In this example, PartialEq is manually implemented to override the derived/default behavior of a full componentwise comparison between Students. The PartialOrd implementation returns Option<Ordering> which may result in a None indicating that the objects are not comparable. For example, in the off-chance that a student’s grade is NaN, comparisons would return a None indicating that they are not comparable.

Handling incomparability in floats while remaining fully reflexive presents this section’s first big “gotcha”. Partial orders are reflexive, meaning that a == a for all values, but since NaN != NaN, there needs to be some other way to guarantee global reflexivity. This is handled by comparing the memory address of the comparators directly. That way, if a student has a NaN grade, it is still possible to prove that a == a.

use std::cmp::Ordering;
pub struct Student {
pub name: &'static str,
pub grade: f32,
}
// Specifies a partial equivalence relation in which two objects
// are only equal if their grades are strictly equal. This impl
// also guarantees global reflexivity by only establishing
// equivalence for NANs if they represent the exact
// same memory address
impl PartialEq for Student {
fn eq(&self, other: &Self) -> bool {
if std::ptr::eq(self, other) { return true; }
self.grade.eq(&other.grade)
}
}
// The PartialOrd impl must be semantically consistent
// with the PartialEq impl so only grades get compared,
// and because not all floats are directly comparable, the
// comparison may return None.
impl PartialOrd for Student {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
if std::ptr::eq(self, other) { return Some(Ordering::Equal); }
self.grade.partial_cmp(&other.grade)
}
}

Total Preorder

A total preorder is transitive, reflexive, and connex such that any two values of a type are comparable under a consistent ordering relation. The difference between a total preorder and a total order is that a total preorder does not require antisymmetry, so it may group multiple distinct values into the same equivalence class. This implementation includes a Grade enum with a single eq_class method that maps values into equivalence classes as letter grades.

Like the partial order implementation, this total preorder implements PartialEq to override the default full component-wise comparison, effecitvely excluding the student’s name as a comparator. The implementation defines equivalence against only mapped Grade values. In this way, two Students are considered equal if they have the same equivalence class. For example, Peter and Paul are considered equivalent if they both have a B-, regardless of the specific point value and name.

The Grade enum derives equivalence and ordering traits to reduce boilerplate impls. In Rust, the ordering of the discrimiants is important, such that the first listed discrimiant carries a value of 0, the next discrimiant a 1, and so on. In this way it is possible to implement ordering on the discriminants and provides a convenient way to assert that Grade::Bminus < Grade::A, for example.

The Ord implementation assumes totality via the required cmp method, which returns a direct Ordering type as Less, Equal, or Greater, and notably does not allow for None values like PartialOrd. Incomparablility for floats is handled by mapping them to a “incomplete” (INC) equivalence class, meaning that any Student grade that does not map to a standard letter grade is considered incomplete, and thus comparable.

PartialOrd::partial_cmp is implemented with a boilerplate Some(ord::cmp(self, other)) as per the Rust docs to ensure consistency with the Ord::cmp implementation. Diverging logic between the Ord::cmp and PartialOrd::partial_cmp methods can have unintended consequences and cause order-based algorithms to exhibit inconsistent behavior.

Ord requires that the type also implement Eq. Eq is a marker trait indicating that PartialEq forms a valid equivalence relation, that is, reflexive, symmetric, and transitive. These properties cannot be verified by the compiler, so Eq has no required methods and serves as a semantic guarantee for generic code. However, the equivalence class mapping for floats guarantees reflexivity, so this derivation is both legal and semantically correct.

use std::cmp::Ordering;
pub struct Student {
pub name: &'static str,
pub grade: f32,
}
/// An equivalence class to categorize grades based
/// on ranges of percentage point values.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Grade {
INC, // 0
F, // 1
DMinus, // 2
D,
DPlus,
CMinus,
C,
CPlus,
BMinus,
B,
BPlus,
AMinus,
A,
APlus
}
// Gets rid of the possibility of None for a total preorder,
// adds cases where NAN and _ == INC
impl Grade {
/// Maps a score into its corresponding equivalence class
pub fn eq_class(value: f32) -> Grade {
match value {
val if val.is_nan() => Grade::INC,
..59.5 => Grade::F,
59.5..62.8 => Grade::DMinus,
62.8..66.1 => Grade::D,
66.1..69.5 => Grade::DPlus,
69.5..72.8 => Grade::CMinus,
72.8..76.1 => Grade::C,
76.1..79.5 => Grade::CPlus,
79.5..82.8 => Grade::BMinus,
82.8..86.1 => Grade::B,
86.1..89.5 => Grade::BPlus,
89.5..92.8 => Grade::AMinus,
92.8..96.1 => Grade::A,
96.1.. => Grade::APlus,
_ => Grade::INC, // catch-all because the compiler doesn't trust floats
}
}
}
// Determines equivalence via mapped Grade values, and implements
// global reflexivity by mapping otherwise incomparable floats
// (like NaN) to the INC (incomplete) equivalence class.
impl PartialEq for Student {
fn eq(&self, other: &Self) -> bool {
Grade::eq_class(self.grade) == Grade::eq_class(other.grade)
}
}
// Implements Eq because grade is a float and cannot be derived.
impl Eq for Student {}
// Imposes a generic comparison because the
// comparison is actually defined in cmp() for Ord.
impl PartialOrd for Student {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
// Implements a total preorder by comparing mapped equivalence classes.
impl Ord for Student {
// Implements a total preorder
// Only the grade's equivalence class is compared
fn cmp(&self, other: &Self) -> Ordering {
Grade::eq_class(self.grade).cmp(&Grade::eq_class(other.grade))
}
}

Total Order

A total order is transitive, reflexive, connex, and antisymmetric (if k1 <= k2 and k2 <= k1, the relation implies k1 == k2). This implementation defines a total order on Student by first comparing rounded grade values using floating-point total ordering semantics (total_cmp), and breaking ties via lexicographic ordering of names. This induces an ordering equivalent to comparing the tuple (rounded grade, name) lexicographically.

The Ord implementation defines this ordering via the required cmp method. PartialOrd is implemented as a boilerplate delegation to Ord::cmp, as recommended in the Rust standard library when a total order is defined.

PartialEq is implemented manually to match the Ord implementation, otherwise there would exist a logical contradiction between eq and cmp. Eq is implemented because Student satisfies the requirements for a valid equivalence relation including global reflexivity.

use std::cmp::Ordering;
pub struct Student {
pub name: &'static str,
pub grade: f32,
}
// Defines a full equivalence realtion for Student where two Students
// are only equal if their rounded grade and lexicographic names are equal.
// PartialEq can be derived, but would use structural equality,
// which may not match the intended equivalence relation.
// NOTE: Must remain consistent with Ord::cmp if both are implemented.
impl PartialEq for Student {
fn eq(&self, other: &Self) -> bool {
self.grade.round().total_cmp(&other.grade.round()) == Ordering::Equal &&
self.name.cmp(other.name) == Ordering::Equal
}
}
// Eq marks that equality is reflexive under the PartialEq impl.
// Cannot be derived because Eq is not implemented for floats.
impl Eq for Student {}
// Delegates ordering to Ord::cmp.
// NOTE: Though the code compiles, Rust does not like it when you derive
// PartialOrd when implementing Ord
impl PartialOrd for Student {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
// First compares grades rounded to the nearest whole number,
// and then breaks ties by comparaing the names lexicographically.
impl Ord for Student {
fn cmp(&self, other: &Self) -> Ordering {
let grade_order = self.grade.round().total_cmp(&other.grade.round());
if grade_order == std::cmp::Ordering::Equal {
self.name.cmp(other.name)
} else {
grade_order
}
}
}