Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

After this page, you should be able to:

Builds on: BST lookup/insertion, ordered queries, ranges and deletion supply the functions used below.

Code languages: Python defines the explanations, figures and self-checks. Use the language guide for native conventions and dependencies. Complete runnable topic examples: Python · JavaScript · Julia.

Python and JavaScript positions are zero-based; Julia vector positions and insertion boundaries are one-based. A missing lookup is None/null/nothing, while a boundary query can return the one-past-end position. Julia public ordered-set operations use functions such as add!, ordered_keys and range_keys.

Purpose and operation contracts

An ordered set retains distinct keys and supports queries by their ordering as well as equality. BST and AVL trees can implement this same ADT. These chapters store keys only; adding associated values would require a map contract and corresponding node fields.

Public operationReturned resultState change / exceptional policy
add(key)NoneInsert if absent; equal duplicates change nothing
contains(key)BooleanObserve membership
remove(key)NoneDelete key; absence raises KeyError
discard(key)NoneDelete if present; absence is a no-op
minimum() / maximum()Extreme keyObserve; empty set raises KeyError
successor(key) / predecessor(key)Smallest stored key strictly greater / largest strictly smallerObserve; no such key raises KeyError; query need not be stored
extract_min() / extract_max()Removed extreme keyRemove and return it; empty set raises KeyError
keys()New ascending listSnapshot of all keys; empty gives []
range_keys(low, high)New ascending listClosed interval; invalid low>high raises ValueError
size() / len(s); is_empty()Count; BooleanObserve only

Keys and queries must share a stable, consistent total order: equality must agree with the less/greater comparisons. Hashability is unnecessary. Do not change a stored key in a way that changes its order. No insertion-order or node-identity guarantee is part of this public contract.

Set remove deletes an already specified key and returns None, as in Topic 6. Extraction chooses an extreme and returns that removed key. The two operations have different purposes.

Representation, invariant and trace

The adapter owns a private root and a cached total count. Reachable nodes hold each logical key exactly once under strict BST ordering; the count equals that node count. An AVL implementation additionally maintains correct cached heights and balance. This total count is not a subtree-size augmentation for rank/select.

OperationReturnKeys afterwards
Add 50,30,70,20,40,60,80Each returns None20,30,40,50,60,70,80
add(30)NoneUnchanged; size seven
successor(55)60Unchanged; query 55 need not be present
extract_min()2030,40,50,60,70,80
remove(50)None30,40,60,70,80
range_keys(35,70)[40,60,70]Unchanged

Python implementation

Read the preceding helper listings in one session first. Both kinds of nodes expose key, left and right, so the read-only helpers can work on either. The constructor receives the chosen pair of insert/delete algorithms; use a matching pair for one representation throughout its lifetime.

Python
JavaScript
Julia
class OrderedSet:
    def __init__(self, insert_key, delete_key):
        self._root = None
        self._size = 0
        self._insert_key = insert_key
        self._delete_key = delete_key

    def __len__(self):
        return self._size

    def size(self):
        return len(self)

    def is_empty(self):
        return len(self) == 0

    def contains(self, key):
        return bst_search(self._root, key) is not None

    def add(self, key):
        if not self.contains(key):
            self._root = self._insert_key(self._root, key)
            self._size += 1

    def remove(self, key):
        if not self.contains(key):
            raise KeyError(key)
        self._root = self._delete_key(self._root, key)
        self._size -= 1

    def discard(self, key):
        if self.contains(key):
            self.remove(key)

    def _required_key(self, node):
        if node is None:
            raise KeyError("no key satisfies the query")
        return node.key

    def minimum(self):
        return self._required_key(bst_minimum(self._root))

    def maximum(self):
        return self._required_key(bst_maximum(self._root))

    def successor(self, key):
        return self._required_key(bst_successor(self._root, key))

    def predecessor(self, key):
        return self._required_key(bst_predecessor(self._root, key))

    def extract_min(self):
        key = self.minimum()
        self.remove(key)
        return key

    def extract_max(self):
        key = self.maximum()
        self.remove(key)
        return key

    def keys(self):
        return inorder_keys(self._root)

    def range_keys(self, low, high):
        return bst_range_keys(self._root, low, high)

Create ordinary = OrderedSet(bst_insert, bst_delete). After reading Topic 8, create balanced = OrderedSet(avl_insert, avl_delete) instead. The adapter starts empty; it does not convert an existing BST into an AVL tree.

Implementation choices and correctness

Membership checks make the size change exactly once for a new insertion or successful removal. They add path traversals but do not change the O(h+1) update bound. discard deliberately favours reuse and clarity over avoiding a repeated membership check. Every update retains the returned root, so empty-tree, root-replacement and rotation cases are handled uniformly.

Queries leave links unchanged; keys and range_keys create new lists of key references, not deep copies. The public interface does not expose nodes. Bypassing it to edit _root, keys or metadata would invalidate these promises.

Time, space and suitability

OperationOrdinary BST, height hAVL tree
Contains; add/remove/discardO(h+1)O(log(n+2))
Minimum/maximum; successor/predecessorO(h+1)O(log(n+2))
Extract an extremeO(h+1), including lookup and deletionO(log(n+2))
Sorted snapshotO(n+1) time; O(n) resultSame
Closed range with r resultsO(h+r+1)O(log(n+2)+r)
Size/emptyO(1), cached countSame

Persistent storage is O(n+1); AVL adds one cached height per node. Iterative single-path queries use O(1) workspace. Recursive updates use O(h+1) stack space, logarithmic for AVL. Snapshot/range traversals also use O(h+1) stack plus their returned output. Empty operations take constant time, with the specified errors or empty results.

Choose this ADT for changing keys with ordered output, neighbours and ranges. A hash set is a different implementation choice for exact membership; a mostly static sorted array trades cheap indexing for shifting on ordered updates. A set of keys does not itself implement a directory’s key–value association.

Check your understanding

1. Does successor(55) require inserting 55 first in the seven-key reference set?

Show explanation

No. It asks for the smallest stored key greater than the query, which is 60. The query itself need not be stored.

2. After extract_min() on a singleton set, what is returned and what remains?

Show explanation

The sole key is returned and the set becomes empty with size zero. Calling it again raises KeyError rather than returning an ambiguous missing-key marker.

Next: Search costs and representation choices.