Linear search comparison count 2026-10-14
Count just the equality tests between the target and an entry, excluding loop conditions and index updates. If the first match is at index , linear search uses exactly equality tests. If the target is absent, it uses exactly . Thus for the best successful case costs one equality test, and the worst case costs , attained by absence or a first match at the last index. For , the sole outcome is absence with .
An expected value needs a specified probability distribution. For , condition on success, assume the target occurs exactly once, and assume its position has a uniform distribution on a finite set of indices . Then the expected value of the linear search comparison count isThis is the average of the costs under those assumptions, with no assumption that the array is sorted. It is not a universal average for arbitrary queries. For example, with , a unique successful target at index with probability and at index with probability has expected value , rather than .
Duplicates also require care about what is sampled. Choose a position uniformly in , then search for its value. All queries succeed, but the five possible sampled positions give costs : both positions holding cause an immediate match at index . The expected value is , not . Uniformly choosing an occurrence does not uniformly locate the first match.
More generally, let the success probability be and let be the conditional probability that the first matching index is , allowing duplicates. Since , the expected value isUnder the unique, uniformly located successful-target assumptions this becomes . If every query is absent, and without needing conditional probabilities. When , success is impossible and ; the successful-search conditional formula is undefined.
Converting the linear search comparison count to execution time needs a cost model. Under constant-cost equality tests, next-entry access, and loop bookkeeping, worst-case time complexity is in Big O notation for , with constant overhead even for the empty sequence. Variable-length entries can make an equality test expensive. On a linked list, advance along its links; repeatedly restarting at the head to obtain the next numeric index adds work that the comparison count does not capture. These are exact operation counts and conditional mathematical averages, not runtime measurements.
Linked list 2026-10-13
A data structure representing a sequence through linked list nodes and links between them. In the singly linked representation used here, each linked list node holds an item and a
next reference to its successor. head identifies the first linked list node; the final next is null, meaning no successor. An empty linked list has head = null. Sedgewick and Wayne use this representation to implement both a stack and a FIFO queue. [ref]https://algs4.cs.princeton.edu/13stacks/index.php. Sedgewick and Wayne, Algorithms, section 1.3: linked lists
For , place the linked list nodes at conceptual computer memory positions , respectively. These deliberately separated labels describe a toy layout, not actual pointer values. The sequence order comes from the links, not increasing positions. To read index , begin at
head and follow two next references. For index , this takes link-following steps and time complexity, hence in the worst case for linked list nodes, under the constant-cost operations assumed for the array example. [ref]https://opendatastructures.org/ods-java/3_Linked_Lists.html. Pat Morin, Open Data Structures, chapter 3: links versus indexed access
Suppose a reference to the existing linked list node is already known. To insert a fresh linked list node after it, first set
X.next = B.next, preserving the old successor , and then set B.next = X. The result is ; none of the old items moves. Reversing these assignments would read the newly changed B.next and set X.next = X, losing the route to from head. This is a two-update algorithm correctness argument, rather than a claim that finding the insertion location is free.The two pointer updates take time in Big O notation. The complete insertion has time complexity if the predecessor is already known, allocating and initializing the fixed-size linked list node costs , and copying its item has constant cost. Finding from
head can instead cost ; a position described only by an index does not confer the constant-cost advantage. This makes a linked list useful when an algorithm already holds the needed linked list node, or repeatedly works at the front as a stack does.Deletion similarly needs the incoming link: to remove after known , set
B.next = X.next. A reference to alone does not identify its predecessor in this singly linked representation. The head is a special case: after checking for a nonempty linked list, remove it with head = head.next. If a tail reference is also maintained, deleting the only linked list node must clear that reference too; a stored length must also be updated. Removing a linked list node from the sequence and reclaiming its storage are separate responsibilities. [ref]https://opendatastructures.org/ods-java/3_1_SLList_Singly_Linked_Li.html. Pat Morin, Open Data Structures, section 3.1: head removal and empty-list bookkeeping
There are practical costs beyond Big O notation. Each singly linked linked list node needs a successor reference in addition to its item; separate allocation may add further overhead, while an array can reserve unused capacity. Consecutive array slots can make a scan reuse nearby contents already brought into a memory cache; separated linked list nodes can make link following wait for successive computer memory fetches. These are layout-dependent effects, not a guarantee that every array operation beats every linked list operation on every machine. An array of references can still lead to scattered objects, and linked list nodes can be allocated close together. [ref]
https://people.freebsd.org/~lstewart/articles/cpumemory.pdf. Ulrich Drepper, What Every Programmer Should Know About Memory, sections 3.3 and 6.2: access patterns and cache use
Longest increasing subsequence Created 2026-09-28 Updated 2026-10-14
A longest increasing subsequence is an increasing subsequence of maximum length. Write that length as , with for the empty sequence; a nonempty input has . Several selections can attain the same maximum. Here uses the weak convention ; write for the maximum length of a strictly increasing subsequence. For , these lengths are three and one respectively; for an input with distinct values they agree.
For , the answer is , attained at indices , while . One useful interpretation is the smallest number of deletions needed to make the surviving sequence nondecreasing:Indeed, keeping a longest increasing subsequence attains this bound, and any valid remaining sequence is an increasing subsequence, so cannot have more than terms. In this example the answer is . This makes a measure of how much order can be recovered without moving any observation.
A table that ends at each position. Let be the maximum length of an increasing subsequence ending specifically at position . The dynamic programming recurrence relation isThe zero covers a selection consisting only of . Otherwise its penultimate index is an eligible ; append to an optimal selection ending there. Fill entries in increasing index order. For strictly increasing subsequences, change the comparison to . The strict version and reconstruction are developed in University of Toronto, CSC373 tutorial 4 solutions (2018), exercise 2; the weak version follows by allowing equality at the append step.
In the worked input, the weak table is and the strict table is . At the second , equality extends the first in the weak table. The final entry is only one, although the optimum is four: the best increasing subsequence need not finish at the last input position. Record a maximizing predecessor whenever . Following these stored indices from any position maximizing , then reversing them, gives an actual longest increasing subsequence. For the weak example the backward chain is .
Skipping positions and allowing equal values changes the optimum
. An original toy input, not measured data. The colored selection keeps indices and values . The two dynamic programming rows give the best lengths ending at every position; the backward arrows reconstruct the weak longest increasing subsequence. Details.Scanning all earlier positions for each uses time complexity and space complexity for lengths and predecessors, using Big O notation and assuming constant-time comparisons and operations on indices.
Keep the easiest ending to extend. The faster algorithm maintains a minimum-tail table for increasing subsequences: is the smallest final value of any length- increasing subsequence in the processed prefix. For equal lengths, a smaller ending permits every future extension allowed by a larger ending. Thus only one best ending per length is needed. The table is nondecreasing, since removing the last term of a length- selection gives . Start with an empty table. On reading , use binary search to find the first entry strictly greater than : replace it by , or append if there is no such entry. Its position is one beyond the longest existing length whose ending is at most . See Liben-Nowell, Vee and Zhu, section 3.1 and Figure 1.
Duplicates determine the binary search rule: weak increase replaces the first , whereas strict increase replaces the first . In Python these positions are returned by
bisect_right(T, v) and bisect_left(T, v) respectively, counting positions from zero. Replace an entry or append; do not insert and shift the remaining entries. The documented equality rules are in Python documentation, bisect_left and bisect_right. Each search takes comparisons, giving time complexity , conventionally for , and space complexity for the weak length alone.Two binary-search tie rules give different minimum-tail tables
. Original exact traces for the same toy input, not measured data. Each cell shows a tail value and the input index supplying it; the outlined cell was updated in that row. At the second , the weak algorithm appends and the strict algorithm replaces. The final weak table has length four, but its supplier indices are not in increasing order. Details.A length certificate needs separate reconstruction. The final weak tail values are , but they do not form an increasing subsequence of this input: the zero occurs last. Each table entry certifies a selection of its own length; the entries need not come from one compatible selection. To recover a longest increasing subsequence during the fast algorithm, store a supplier index for each tail and a predecessor index for each input position. Before replacing slot , save , or no predecessor if , then set . Later replacements leave saved unchanged. Follow predecessors from the final and reverse. This uses space complexity and returns indices for the weak trace. The same construction works with the strict comparator; compare the two auxiliary arrays in CP Algorithms contributors, restoring the subsequence after binary search.
Deleting or changing one term. If deletes position , thenDeleting cannot create a longer increasing subsequence; removing position from a previously optimal selection loses at most one term. If instead changes just the value at position , both inputs have the same deleted sequence, so their lengths each lie between its length and its length plus one. Consequently . These bounds also hold for strictly increasing subsequences. In the weak example, deleting either lowers the length to three, while deleting the final zero leaves it four. This is a Lipschitz bound for single-term edits, regardless of the numerical size of a replacement. Insertion has the same bound by reversing deletion, so insertions, deletions or replacements can change by at most .
For historical context, [ref]
https://doi.org/10.1016/0012-365X(75)90103-X. Fredman, On computing the length of longest increasing subsequences (1975)
studies the comparison count for distinct elements of a totally ordered set. Its distinctness assumption makes the weak and strict conventions coincide. No distinctness assumption is needed for the comparator-specific algorithms above. Memoization arithmetic and stack costs 2026-10-13
Under constant-cost lookup and integer addition, Memoized Fibonacci evaluation has time complexity in Big O notation, retaining values. A hash table needs its own lookup-cost assumptions; an array indexed by avoids hash collisions in this example. Memoization removes repeated bodies, but also pays for keys, lookups and retained answers. A workload without reuse can pay those costs without saving evaluations.
The call stack remains. Left-first recursion descends through arguments before returning, giving simultaneously active frames for . Therefore the auxiliary space complexity includes frames as well as table entries in the constant-size-value model. An iterative bottom-up dynamic programming calculation removes these recursive frames; retaining only the two previous Fibonacci numbers suffices when only the final value is wanted, but discards the table useful for later requests.
Exact Fibonacci numbers do not remain constant-size values. For , the recurrence relation gives , so the representation of as a binary number has order bits. With ordinary addition costing order bit operations on such operands, the additions cost order bit operations in total. Retaining all values also requires order bits of value storage, before keys and frame overhead. The linear bound counts additions under its stated cost model; it is not a linear bound on exact integer arithmetic in bits.[ref]
Lecture 15: Recursive Algorithms (6.006 Spring 2020) by Massachusetts Institute of Technology, 6.006 course staff. MIT 6.006 Spring 2020, Lecture 15: Recursive Algorithms, printed p. 3, final three bullets: growing Fibonacci bit lengths and addition cost
Merge algorithm 2026-10-13
The merge algorithm combines two finite sorted sequences under the same total order, retaining every entry exactly once. Keep a position at the first unconsumed entry, or "head", of each input. While both heads exist, compare their keys, append the smaller head to the output, and advance only that head's position. On equality take the left head. Once either sequence is exhausted, append the other remainder without further key comparisons. Empty inputs therefore require no head comparisons. The MIT notes illustrate this two-head combination step.[ref]
https://ocw.mit.edu/courses/6-006-introduction-to-algorithms-fall-2011/90a37c49b07b105ea7a027abc67eddc6_MIT6_006F11_lec03.pdf. MIT 6.006 (2011), lecture 3: merging sorted arrays
Five head comparisons in the merge algorithm
. Orange heads are chosen and arrows lead to the next output entry. The compared head pairs are (1,2), (4,2), (4,3), (4,8) and (7,8). The left sequence then runs out; append (8,9) without further key comparisons. Details.The head pairs , , , and select respectively. After the fifth comparison the left sequence is empty, so is appended. Seven entries are written, but only five head comparisons occur: tests for exhaustion and appending the remainder are separate operations. This distinction makes the example's count smaller than the bound .
For nonempty inputs of lengths , each head comparison consumes exactly one entry. After such removals at most one entry remains, so both heads cannot still exist. Hence the merge algorithm uses at most head comparisons. The bound can be attained: and require six head comparisons before the left sequence is exhausted. With constant-cost comparisons, access and copying, writing all entries gives time complexity in Big O notation. Writing a separate output array needs entry slots; the two positions require only constant additional storage.
The merge algorithm is useful whenever two already ordered collections must become one: it reads each input only forwards. This works with arrays and linked lists, without needing access to arbitrary middle positions. Choosing left on ties preserves each input's internal order and places equal left entries before equal right entries, which is the combination rule needed for stable sorting in merge sort.
The loop invariant behind algorithm correctness is that the emitted prefix is sorted, contains exactly the consumed entries, and every emitted key is at most every unconsumed key. Each sorted sequence has its smallest remaining key at its head, so the smaller of the two heads is safe to append. The invariant survives each removal; when one sequence ends, the other sorted remainder is safe to append in full. Each comparison removes an entry, so termination follows from the finite input lengths.
// Target: computer-science.bigb
Merge sort 2026-10-13
Merge sort is a divide-and-conquer algorithm that sorts a finite array by splitting it into contiguous halves, sorting each half using recursion, and combining the two sorted arrays with the merge algorithm. An empty or one-entry array is returned immediately. For length , put the first entries on the left and the remaining on the right; the brackets are the floor function and ceiling function. Always choose the left head on equal keys to obtain stable sorting. This is the split, sort, combine structure taught in the MIT lecture notes.[ref]
Take the concrete input arrayThe tags identify the entries originally at indices and , counting from zero. Both keys are ; the tags are never compared. Reading downward in the figure follows the splits, then the returned sorted arrays. Arrows show dependencies; the level layout groups independent calls rather than giving their chronological execution order.
Merge sort splits into base cases and returns sorted arrays
. The tagged keys 2a and 2b retain their original order through stable sorting. The merge algorithm makes 4, 6 and 7 head comparisons on the three returning levels, for 17 in total. Details.The first two halves are and . Their pairs split into one-entry base cases. Applying the merge algorithm gives the following fully worked return levels; each number in the last column counts only comparisons between two available heads.
| Returning level | Resulting sorted arrays | Head comparisons |
|---|---|---|
| Pairs | , , , | |
| Halves | , | |
| Whole array |
In the left-half merge algorithm, the head pairs are , and ; it takes , then appends . In the final merge algorithm, the head pairs are , , , , , and . It takes , then appends . Thus this precise version makes head comparisons and, ignoring identity tags, returns .
The elegance of merge sort is that it creates exactly the structure its next step needs: two smaller sorted arrays make the larger ordering inexpensive. Once the whole array is sorted, equal keys are adjacent and binary search can serve repeated queries. Stable sorting also retains information that the keys alone do not express. If the left-half merge algorithm instead takes the right head on the tie, it returns ; the keys are sorted but stable sorting has failed.
For time complexity, assume each key comparison, entry access and entry copy has constant cost. The merge algorithm processes entries in time. In Big O notation, the resulting recurrence relation isFor , the balanced levels contain entries each and there are returning levels. Each costs , so the worst-case time complexity is . For arbitrary , there are at most returning levels, each processing at most entries, giving the same bound. The worst-case number of head comparisons is also . Here is the base-two logarithm. The MIT notes give this level-by-level analysis for merge sort.[ref]
https://ocw.mit.edu/courses/6-006-introduction-to-algorithms-spring-2020/6d1ae5278d02bbecb5c4428928b24194_MIT6_006S20_lec3.pdf. MIT 6.006 (2020), lecture 3, page 5: merge sort analysis
The conventional array implementation uses a reusable buffer of entries for the merge algorithm, together with pending calls for recursion. Its auxiliary space complexity is therefore , beyond the original array. This bound describes that implementation. A linked list permits a merge algorithm that reconnects existing nodes; its storage accounting differs from copying array entries into a buffer.
For a short algorithm correctness outline, the empty and one-entry base cases are sorted. Assuming recursion correctly sorts both shorter halves, the merge algorithm returns all their entries in order. Equal keys within a half keep their relative order by the same argument applied to the smaller call; across halves, every left entry originally preceded every right entry, so taking left first preserves stable sorting. Both half-lengths decrease for , establishing termination.
Neighbour enumeration 2026-10-14
Neighbour enumeration reports the neighbours of a specified vertex. For a simple graph, scan its adjacency list and report each entry. With constant-time list access and work per entry, a list of length needs time in Big O notation, conventionally written for nonempty lists. Use vertex degree for an undirected graph and outdegree for a directed graph. Obtaining the list reference can take constant time; consuming it requires visiting every entry.[ref]
Digraph.java. Digraph.java, class documentation and adj: obtaining the collection versus iterating in time proportional to outdegree
In our undirected example, enumerating 's neighbours reads : three entries. Scanning row of the full adjacency matrix reads six entries, , to produce the same answer. In the directed variant, enumerating 's outgoing neighbours reads no neighbour entries, but still needs constant work to locate and recognize its empty list. For a multigraph, scanning lists enumerates incident edges with multiplicity; reporting only distinct neighbours additionally requires removing repetitions.
For a short time complexity argument, each reported entry requires at least one output operation, and one scan suffices. Across all vertices, the undirected entry count is by the handshaking lemma; the directed count is . A graph search that processes each vertex once and scans each of its entries once therefore does structural work, provided its visited-state access and FIFO queue or stack operations also have the required constant-time costs. With a full adjacency matrix, scanning every row instead inspects entries. This is how the representation enters the analysis of breadth-first search and depth-first search.
Past exam of the mathematics course of the University of Cambridge 2006 ia Paper 5 1 a Solution Created 2026-09-24 Updated 2026-10-14
Use the usual unit-cost model for Standard ML list constructors and fixed-size arithmetic. The input size in the list examples is the number of list elements, rather than the size of the integer entries. Inspecting only the first constructor has constant time complexity:No tail is traversed. The running time is .
sml
fun firstOrZero [] = 0
| firstOrZero (x :: _) = x; Past exam of the mathematics course of the University of Cambridge 2006 ia Paper 5 1 b Solution Created 2026-09-24 Updated 2026-10-14
A Standard ML length function visits each list constructor once:Writing for its time complexity, and . Thus . Arithmetic is treated as unit cost; the example concerns list traversal rather than arbitrary-precision arithmetic.
sml
fun length [] = 0
| length (_ :: xs) = 1 + length xs; Past exam of the mathematics course of the University of Cambridge 2006 ia Paper 5 1 c Solution Created 2026-09-24 Updated 2026-10-14
Use merge sort on a list of integers:Here
sml
fun sort xs = mergeSort (op <=) xs;mergeSort denotes the standard merge sort implementation: split into two almost equal lists, recursively sort them, and combine them using the merge algorithm. Splitting and merging take linear time, and each recursion level has total input length . There are levels, so the worst-case time complexity is . Equivalently, . Past exam of the mathematics course of the University of Cambridge 2006 ia Paper 5 1 d Solution Created 2026-09-24 Updated 2026-10-14
Traverse the whole original list once for each of its elements:The outer structural recursion runs times, and each
sml
fun scan [] = ()
| scan (_ :: xs) = scan xs;
fun scanForEach [] whole = ()
| scanForEach (_ :: rest) whole =
(scan whole; scanForEach rest whole);
fun quadratic xs = scanForEach xs xs;scan whole visits constructors. In the ordinary source-evaluation cost model, the time complexity is . This example counts executed traversals, without assuming an optimizing compiler that removes deliberately unused computations. Past exam of the mathematics course of the University of Cambridge 2006 ia Paper 5 1 e Solution Created 2026-09-24 Updated 2026-10-14
For a nonnegative integer parameter , make both branches of a structural recursion execute:The semicolon sequences two calls, so this is not a conditional choosing just one branch. The recursion tree has leaves and calls. Its time complexity satisfies , giving . Here is the parameter specified by the question, not its binary encoding length; only calls are simultaneously on the stack.
sml
fun binaryWalk 0 = ()
| binaryWalk n = (binaryWalk (n - 1); binaryWalk (n - 1)); Past exam of the mathematics course of the University of Cambridge 2006 ia Paper 5 3 Solution Created 2026-09-24 Updated 2026-10-14
Extract one four-bit digit at a time, beginning at the most significant end of the Java The logical right bit shift
int. A lookup string supplies characters, but no library performs the conversion:java
static String toHex(int value) {
String digits = "0123456789abcdef";
String answer = "";
boolean started = false;
for (int shift = 28; shift >= 0; shift -= 4) {
int digit = (value >>> shift) & 15;
if (started || digit != 0 || shift == 0) {
answer = answer + digits.charAt(digit);
started = true;
}
}
return answer;
}>>> inserts zeros, and the bit mask 15 retains exactly the low four bits of the shifted word. Each emitted character is therefore the corresponding hexadecimal digit. The started flag suppresses leading zero digits; the shift == 0 exception makes zero produce "0", rather than an empty string.A negative Java integer uses two's complement. Its top bit is set, so the first extracted digit is between eight and fifteen and all eight digits are emitted. We interpret its original 32-bit pattern, without taking an absolute value or negating a potentially unnegatable minimum integer. In particular,
toHex(19) returns "13", toHex(-1) returns "ffffffff", and toHex(Integer.MIN_VALUE) returns "80000000". There are exactly eight iterations, so for this fixed word size the time complexity is constant. Past exam of the mathematics course of the University of Cambridge 2006 ia Paper 5 6 b Solution Created 2026-09-24 Updated 2026-10-14
Represent a binary search tree by:Assume the provided For equality-type values, call
sml
datatype ('k, 'v) dict = Empty
| Branch of ('k, 'v) dict * 'k * 'v * ('k, 'v) dict;lookup d key returns NONE or SOME value, and update d (key, value) returns the updated binary search tree. Traverse the first dictionary, retaining only a key with the same value in the second:sml
fun intersection equal d1 d2 =
let
fun visit Empty result = result
| visit (Branch (left, key, value, right)) result =
let
val r1 = visit left result
val r2 =
case lookup d2 key of
NONE => r1
| SOME other =>
if equal (value, other)
then update r1 (key, value)
else r1
in
visit right r2
end
in
visit d1 Empty
end;intersection (op =) d1 d2. An explicit value comparison parameter also handles values for which Standard ML does not define built-in equality. This distinguishes absent keys from defined values without reserving a sentinel value.Every inserted entry belongs to both input dictionaries with the same value, so the result agrees with each. Conversely, each common equal-valued entry is visited in
d1, found in d2, and inserted. Any dictionary agreeing with both can contain only those entries. Thus the result is exactly the largest common dictionary, not merely an intersection of key sets. If d1 has entries, lookup height is , and maximum result-tree height during insertion is , the time complexity is for constant-cost comparisons. Logarithmic heights require a balancing assumption; an ordinary unbalanced binary search tree can have linear height. Past exam of the mathematics course of the University of Cambridge 2006 ia Paper 5 9 e Solution Created 2026-09-24 Updated 2026-10-14
A collections library can specify a
List<E> Java interface, with different implementations such as ArrayList<E> and LinkedList<E>. A method accepting List<E> can iterate or access elements according to the documented list contract without requiring one particular storage representation. Each implementation may also extend its own appropriate class superclass.The benefit is separating a usable contract from its implementation. A parameter restricted to a concrete
ArrayList<E> would exclude a linked list implementation and unnecessarily expose the storage choice. Requiring all providers to extend one concrete class would couple them to its implementation and consume Java's single class-inheritance relationship. Interfaces permit multiple implemented contracts, though clients must rely on the stated contract rather than assuming every optional operation is supported or has the same time complexity. Past exam of the mathematics course of the University of Cambridge 2007 ia Paper 5 1 c Solution Created 2026-09-24 Updated 2026-10-14
Assume head comparison and construction of one list cell take constant time. In the merge algorithm, every comparison removes one head from the recursive inputs, and evaluation stops as soon as either is empty. For lengths , at most comparisons and the same number of newly constructed cells are required; the untouched suffix is shared. Inputs which keep both lists nonempty until the last comparison attain this bound. ThusAn initially empty input makes this particular implementation constant-time; not every pair requires linear work.
The balanced merge sort spends time per call on list length, splitting and merging. Hence its time complexity satisfiesAt each recursion level the lengths of its disjoint subproblems sum to , so the work per level is , and balanced splitting gives levels. ConsequentlyThe length and splitting traversals change the constant factor, not this asymptotic order; repeatedly inserting elements into a sorted list would not give the same complexity.
Past exam of the mathematics course of the University of Cambridge 2007 ia Paper 5 9 b ii Solution Created 2026-09-24 Updated 2026-10-14
Use recursive dependency evaluation with memoization. After an edit, invalidate all cell states. To evaluate one formula, recursively obtain its dependencies before storing its own result. This is a depth-first search of the dependency graph; a visiting mark identifies a cycle, and a done mark avoids repeated evaluation of a shared dependency. The following single class implements the design, with public nested interfaces for formula extensions:The outer loop's order need not be a topological ordering, since recursion evaluates dependencies first. After an edit, callers can invoke
public final class Spreadsheet {
public enum Kind { EMPTY, LABEL, NUMBER, FORMULA }
public interface Lookup {
double valueAt(int row, int col);
}
public interface Formula {
double evaluate(Lookup values, int row, int col);
}
public static final class AddFormula implements Formula {
private final int dr1, dc1, dr2, dc2;
public AddFormula(int dr1, int dc1, int dr2, int dc2) {
this.dr1 = dr1; this.dc1 = dc1;
this.dr2 = dr2; this.dc2 = dc2;
}
public double evaluate(Lookup values, int r, int c) {
return values.valueAt(r + dr1, c + dc1)
+ values.valueAt(r + dr2, c + dc2);
}
}
private static final class Cell {
private final Kind kind;
private final String label;
private final double number;
private final Formula formula;
private Cell(Kind k, String s, double n, Formula f) {
kind = k; label = s; number = n; formula = f;
}
}
private static final Cell EMPTY =
new Cell(Kind.EMPTY, null, 0.0, null);
private final int rows, cols;
private final Cell[][] cells;
private final double[][] cache;
private final byte[][] state;
private final Lookup view = new Lookup() {
public double valueAt(int r, int c) {
return Spreadsheet.this.valueAt(r, c);
}
};
public Spreadsheet(int rows, int cols) {
if (rows < 0 || cols < 0) throw new IllegalArgumentException();
this.rows = rows; this.cols = cols;
cells = new Cell[rows][cols];
cache = new double[rows][cols];
state = new byte[rows][cols];
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++) cells[r][c] = EMPTY;
}
private boolean inside(int r, int c) {
return r >= 0 && r < rows && c >= 0 && c < cols;
}
private void check(int r, int c) {
if (!inside(r, c)) throw new IndexOutOfBoundsException();
}
private void invalidate() {
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++) state[r][c] = 0;
}
private void put(int r, int c, Cell cell) {
check(r, c); cells[r][c] = cell; invalidate();
}
public void setEmpty(int r, int c) { put(r, c, EMPTY); }
public void setLabel(int r, int c, String text) {
if (text == null) throw new IllegalArgumentException();
put(r, c, new Cell(Kind.LABEL, text, 0.0, null));
}
public void setNumber(int r, int c, double x) {
put(r, c, new Cell(Kind.NUMBER, null, x, null));
}
public void setFormula(int r, int c, Formula f) {
if (f == null) throw new IllegalArgumentException();
put(r, c, new Cell(Kind.FORMULA, null, 0.0, f));
}
public Kind kindAt(int r, int c) {
check(r, c); return cells[r][c].kind;
}
public String labelAt(int r, int c) {
check(r, c); return cells[r][c].label;
}
public double valueAt(int r, int c) {
if (!inside(r, c)) return 0.0;
if (state[r][c] == 2) return cache[r][c];
if (state[r][c] == 1)
throw new IllegalStateException("Cyclic formula dependency");
state[r][c] = 1;
try {
Cell cell = cells[r][c];
double answer;
switch (cell.kind) {
case NUMBER: answer = cell.number; break;
case FORMULA: answer = cell.formula.evaluate(view, r, c); break;
default: answer = 0.0;
}
cache[r][c] = answer;
state[r][c] = 2;
return answer;
} catch (RuntimeException error) {
state[r][c] = 0;
throw error;
}
}
public void recalculate() {
invalidate();
for (int r = 0; r < rows; r++)
for (int c = 0; c < cols; c++) valueAt(r, c);
}
}recalculate; ordinary valueAt calls also calculate lazily from the invalidated state. Empty and label cells contribute zero without destroying their display content. Relative references are interpreted at the formula's location, and off-grid references contribute zero as required.For an acyclic graph, mathematical induction over a topological ordering proves that each cached value equals the evaluation of current cell contents. Each cell becomes done at most once in a pass, so the time complexity is for cells and dependency references, plus formula arithmetic. The stack depth is at most the longest dependency path. A more elaborate implementation can record reverse dependencies and invalidate only cells affected by an edit.
The PDF gives no policy for circular formulas. Repeated numerical sweeps are not a general solution: a cell defined as itself plus one has no finite value, while a cell defined as itself plus zero has no unique value. The implementation therefore reports a cycle instead of looping or presenting an arbitrary stale value. An acyclic dependency graph is evaluated once per cell; circular dependencies are detected and rejected.





