From 6ffdb98de1e39cf1476013a74d32839d44a483e6 Mon Sep 17 00:00:00 2001 From: Al Diaz Date: Sun, 23 Aug 2026 15:43:34 -0700 Subject: [PATCH 1/5] Run the code in these notes, and make the .js files unable to drift from them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two scripts, and the first one runs the code rather than parsing it, because parsing would have found nothing here. Every block in this repository parses. scripts/check-samples.mjs concatenates each notes file's code blocks in document order and executes them. That is the right unit: a file is one program spread across blocks, where one defines the class, the next adds a method to its prototype, and the next exercises it. Running the whole thing means a block has what earlier blocks defined, and an implementation that parses but does not work fails. The hash table is why. Its collision walk declared `const currentNode` and reassigned it in the loop, which parses perfectly and throws `TypeError: Assignment to constant variable.` the moment the loop body runs. Running the body needs a THIRD key in one bucket, and the example added two, so the demo passed. Only execution with three finds it. Two details in the extractor were found by it failing: CASE-INSENSITIVE FENCE TAGS. This repository writes the tag three ways, `javascript` (14), `JavaScript` (8) and `js` (4). Matching case-sensitively skipped eight blocks, including the ones defining the classes, and the result was ReferenceErrors that looked like defects in the notes rather than a defect in my extractor. Markdown does not care about the case; tooling has to be told. A `// check: standalone` marker starts a new program. 05_0 declares `class Queue` three times, once per approach to privacy, and concatenating alternatives is a redeclaration error rather than a defect in any of them. The marker lets a document say a block REPLACES what came before instead of extending it. UNTAGGED FENCES ARE REPORTED RATHER THAN SKIPPED. A code block with no language tag is invisible to tooling, and this repository has 76 correctly-bare fences holding console output, so the only way to tell them apart is to look inside. Anything untagged containing `class`, `prototype`, `function`, `=>` or `const` is flagged. A checker that quietly ignores half the code is worse than none, because its green result implies coverage it does not have. The `## Result:` blocks are NOT compared against actual output. They are Firefox DevTools renderings (`Object { … }`, `:`), not node's, so a diff would fail on formatting for all of them and the check would be off within a day. What the run proves is that the code producing them executes. scripts/build-js.mjs generates each NN_1_*.js from its NN_0_*.md and `--check` fails if the committed file differs. The notes are the source of truth: that is where the explanation lives and what was maintained. Alternative implementations are wrapped in a block, since `class` and `let` are block-scoped and the three Queues can then share a name in one file. Seven poison tests, each confirmed applied before its result was read: a syntax error, a runtime throw (reported with the markdown block it came from), an untagged fence, a hand-edited .js, an edited .md without a rebuild, the const bug plus a third key, and a tag reverted to uppercase which must still be found. All seven behave. One of those poisons was defective on the first attempt and taught its own lesson: it had TWO parts, and only one applied, because the line it targeted has no trailing semicolon. `diff` said the file had changed and I read that as both parts landing. When a poison has two parts, assert both. package.json: the license said ISC while LICENSE is MIT; `repository`, `bugs` and `homepage` all used the pre-rename name `data-structures`, which works only through GitHub's redirect; `main` was empty and so was `description`; and `test` was the npm default failing stub. All fixed, and `test` now runs both checkers. No dependencies added. --- package.json | 29 ++++--- scripts/build-js.mjs | 128 +++++++++++++++++++++++++++++ scripts/check-samples.mjs | 166 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 313 insertions(+), 10 deletions(-) create mode 100644 scripts/build-js.mjs create mode 100644 scripts/check-samples.mjs diff --git a/package.json b/package.json index 9d59844..e5d1b1b 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,29 @@ { - "name": "data-structures", + "name": "data-structures-and-algorithms", "version": "1.0.0", - "description": "", - "main": "", + "description": "Data structures in JavaScript: notes plus runnable implementations, with runtime complexity stated against these implementations.", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node scripts/check-samples.mjs && node scripts/build-js.mjs --check", + "build": "node scripts/build-js.mjs" }, "repository": { "type": "git", - "url": "git+https://github.com/alpersonalwebsite/data-structures.git" + "url": "git+https://github.com/alpersonalwebsite/data-structures-and-algorithms.git" }, - "keywords": [], + "keywords": [ + "javascript", + "data-structures", + "binary-search-tree", + "hash-table", + "linked-list", + "queue", + "stack", + "big-o" + ], "author": "", - "license": "ISC", + "license": "MIT", "bugs": { - "url": "https://github.com/alpersonalwebsite/data-structures/issues" + "url": "https://github.com/alpersonalwebsite/data-structures-and-algorithms/issues" }, - "homepage": "https://github.com/alpersonalwebsite/data-structures#readme" -} \ No newline at end of file + "homepage": "https://github.com/alpersonalwebsite/data-structures-and-algorithms#readme" +} diff --git a/scripts/build-js.mjs b/scripts/build-js.mjs new file mode 100644 index 0000000..1043f21 --- /dev/null +++ b/scripts/build-js.mjs @@ -0,0 +1,128 @@ +#!/usr/bin/env node +// Assembles each NN_1_*.js from the code blocks in its NN_0_*.md, and with --check verifies that the +// committed file still matches what the notes would produce. +// +// This exists because the two had drifted badly and nothing noticed: 04_1 held one method while its +// notes documented eight, and 05_1 was ZERO BYTES while its notes carried four implementations. A +// reader who cloned the repo and ran the queue file got nothing at all. Generating one from the other +// makes the drift impossible rather than merely fixed. +// +// The notes are the source of truth, not the .js files. That is the right direction here: the notes are +// where the explanation lives, they are what was maintained, and they are what a reader reads. +// +// Alternative implementations are wrapped in a block. 05_0 presents three Queue classes, one per +// approach to privacy, and `class` and `let` are block-scoped, so `{ ... }` lets them share a name in +// one file exactly as they each stand alone in the notes. + +import { readFileSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' + +const root = new URL('..', import.meta.url).pathname.replace(/\/$/, '') +const checkOnly = process.argv.includes('--check') + +const PAIRS = [ + ['02_0_binary-search-tree.md', '02_1_binary-search-tree.js', 'Binary search tree'], + ['03_0_hash-table.md', '03_1_hash-table.js', 'Hash table'], + ['04_0_linked-list.md', '04_1_linked-list.js', 'Doubly linked list'], + ['05_0_queue-and-stack.md', '05_1_queue-and-stack.js', 'Queue and stack'], +] + +const FENCE_OPEN = /^```(\w+)?\s*$/ +const STANDALONE = /^\s*\/\/\s*check:\s*standalone\s*$/ + +function programs(md) { + const lines = readFileSync(join(root, md), 'utf8').split('\n') + const blocks = [] + let body = null + let tag = null + let start = 0 + for (const [i, line] of lines.entries()) { + const trimmed = line.trim() + if (body !== null) { + if (trimmed === '```') { + if (['javascript', 'js'].includes((tag ?? '').toLowerCase())) { + blocks.push({ start, source: body.join('\n') }) + } + body = null + tag = null + } else body.push(line) + continue + } + const open = FENCE_OPEN.exec(trimmed) + if (open) { + body = [] + tag = open[1] ?? null + start = i + 2 + } + } + const groups = [] + for (const b of blocks) { + if (groups.length === 0 || STANDALONE.test(b.source.split('\n')[0] ?? '')) groups.push([]) + groups[groups.length - 1].push(b) + } + return groups +} + +function render(md, title) { + const groups = programs(md) + const head = [ + `// ${title}, assembled from ${md}.`, + '//', + '// GENERATED. `npm test` regenerates this from the notes and fails if the two differ, so reading', + '// either one is safe. Before that check existed they had drifted: this pairing once had one method', + '// against eight documented, and the queue-and-stack file was empty while its notes were not.', + '//', + '// Regenerate with: npm run build', + '', + ] + const parts = [] + for (const [n, group] of groups.entries()) { + const single = groups.length === 1 + if (!single) { + parts.push(`// ===== implementation ${n + 1} of ${groups.length} (the notes present these as alternatives)`) + parts.push('// Wrapped in a block, because `class` and `let` are block-scoped and these share a name.') + parts.push('{') + } + for (const b of group) { + const source = single ? b.source : b.source.split('\n').map((l) => (l.trim() ? ' ' + l : l)).join('\n') + parts.push(`${single ? '' : ' '}// ---- from ${md}:${b.start}`) + parts.push(source) + parts.push('') + } + if (!single) { + parts.push('}') + parts.push('') + } + } + return head.concat(parts).join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n' +} + +let mismatched = 0 +for (const [md, js, title] of PAIRS) { + const wanted = render(md, title) + const path = join(root, js) + let current = '' + try { + current = readFileSync(path, 'utf8') + } catch { + /* missing counts as a mismatch */ + } + if (checkOnly) { + if (current !== wanted) { + console.error(`DRIFT ${js} does not match ${md}. Run: npm run build`) + mismatched++ + } + } else { + writeFileSync(path, wanted) + console.log(` wrote ${js} from ${md}`) + } +} + +if (checkOnly) { + console.log(`${PAIRS.length} generated file(s) compared against their notes`) + if (mismatched) { + console.error(`\n${mismatched} file(s) out of date`) + process.exit(1) + } + console.log('all match') +} diff --git a/scripts/check-samples.mjs b/scripts/check-samples.mjs new file mode 100644 index 0000000..43b621e --- /dev/null +++ b/scripts/check-samples.mjs @@ -0,0 +1,166 @@ +#!/usr/bin/env node +// Checks the code in these notes by RUNNING it, not by parsing it. +// +// Each notes file is a single program spread across code blocks: one block defines the class, the next +// adds a method to its prototype, the next exercises it. So the useful unit is the whole file, not the +// block. Every tagged block in a file is concatenated in document order, parsed with `node --check`, +// and then executed. A block that depends on something an earlier block defined therefore has it, and +// an implementation that parses but does not work fails here. +// +// That distinction is not academic. The hash table in this repository walked a collision chain with +// `const currentNode` and reassigned it in the loop, which parses perfectly and throws +// `TypeError: Assignment to constant variable.` the moment the loop body runs. Its own example added +// two keys to one bucket, so the body never ran and the demo passed. Only executing a case with three +// finds it. +// +// UNTAGGED FENCES ARE REPORTED, because a code block with no language tag is invisible to this checker +// and to every other tool. This repository had blocks of JavaScript in bare fences sitting beside the +// `## Result:` blocks that are correctly bare, and the only way to tell them apart is to look at what +// is inside. Anything untagged that contains `class`, `prototype`, `function`, `=>` or `const` is +// flagged rather than silently skipped: a checker that quietly ignores half the code is worse than no +// checker, because the green result implies coverage it does not have. +// +// The `## Result:` blocks are NOT compared against the actual output. They are Firefox DevTools +// renderings (`Object { … }`, `:`), not node's, so a diff would fail on formatting for every +// single one and the check would be turned off within a day. What the run does prove is that the code +// producing them executes. +// +// Usage: node scripts/check-samples.mjs [--print ] + +import { execFileSync } from 'node:child_process' +import { mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +const root = new URL('..', import.meta.url).pathname.replace(/\/$/, '') +const args = process.argv.slice(2) + +// A bare fence whose body looks like JavaScript rather than like console output. +const LOOKS_LIKE_CODE = /(^|\n)\s*(class |const |let |var |function |[\w.]+\.prototype\.|.*=>)/ + +function blocks(name) { + const lines = readFileSync(join(root, name), 'utf8').split('\n') + const tagged = [] + const untagged = [] + let body = null + let tag = null + let start = 0 + + for (const [i, line] of lines.entries()) { + const trimmed = line.trim() + if (body !== null) { + if (trimmed === '```') { + const source = body.join('\n') + const lang = tag?.toLowerCase() + // CASE-INSENSITIVE, because this repository writes the tag three ways: `javascript`, + // `JavaScript` and `js`. A case-sensitive match skipped eight blocks here, including the + // ones defining the classes, which then produced ReferenceErrors that looked like defects in + // the notes rather than a defect in this file. + if (lang === 'javascript' || lang === 'js') { + const standalone = /^\s*\/\/\s*check:\s*standalone\s*$/.test(source.split('\n')[0] ?? '') + tagged.push({ start, source, standalone }) + } + else if (!tag && LOOKS_LIKE_CODE.test(source)) untagged.push({ start, source }) + body = null + tag = null + } else body.push(line) + continue + } + const open = /^```(\w+)?\s*$/.exec(trimmed) + if (open) { + body = [] + tag = open[1] ?? null + start = i + 2 + } + } + return { tagged, untagged } +} + +const files = readdirSync(root).filter((f) => /^\d.*\.md$/.test(f)).sort() +const dir = mkdtempSync(join(tmpdir(), 'dsa-')) +const failures = [] +let totalBlocks = 0 +let totalUntagged = 0 +let ran = 0 + +for (const name of files) { + const { tagged, untagged } = blocks(name) + totalBlocks += tagged.length + totalUntagged += untagged.length + + for (const u of untagged) { + failures.push( + `UNTAGGED ${name}:${u.start} a fence with no language tag contains code ` + + `(${JSON.stringify(u.source.split('\n')[0].trim().slice(0, 48))}), so nothing checks it`, + ) + } + + if (tagged.length === 0) continue + + // A block marked `standalone` starts a fresh program. Some files present ALTERNATIVE + // implementations of the same class: 05_0 declares `class Queue` three times, once per approach, and + // concatenating those is a redeclaration error rather than a defect in any of them. Splitting on the + // marker keeps the default behaviour (one program per file) while letting a document say that a + // block replaces what came before rather than extending it. + const programs = [] + for (const b of tagged) { + if (b.standalone || programs.length === 0) programs.push([]) + programs[programs.length - 1].push(b) + } + + for (const [index, group] of programs.entries()) { + const program = group + .map((b) => `// ---- ${name}:${b.start}\n${b.source}`) + .join('\n\n') + const label = programs.length > 1 ? `${name} [program ${index + 1}/${programs.length}]` : name + const path = join(dir, `${name.replace(/\W/g, '_')}_${index}.cjs`) + writeFileSync(path, program) + + if (args[0] === '--print' && args[1] === name) { + process.stdout.write(`// ===== program ${index + 1}\n${program}\n`) + continue + } + + try { + execFileSync(process.execPath, ['--check', path], { stdio: 'pipe' }) + } catch (error) { + const first = String(error.stderr).split('\n').find((l) => /Error/.test(l)) ?? 'parse failed' + failures.push(`SYNTAX ${label} ${first.trim()}`) + continue + } + + try { + execFileSync(process.execPath, [path], { stdio: 'pipe', timeout: 10000 }) + ran++ + } catch (error) { + const stderr = String(error.stderr ?? '') + const line = stderr.split('\n').find((l) => /Error/.test(l)) ?? 'failed' + const at = /\.cjs:(\d+)/.exec(stderr) + let origin = '' + if (at) { + const upto = program.split('\n').slice(0, Number(at[1])) + const marker = [...upto].reverse().find((l) => l.startsWith('// ---- ')) + if (marker) origin = ` (from ${marker.replace('// ---- ', '')})` + } + failures.push(`RUNTIME ${label} ${line.trim()}${origin}`) + } + } +} + +rmSync(dir, { recursive: true, force: true }) + +if (args[0] === '--print') process.exit(0) + +console.log( + `${files.length} notes files: ${totalBlocks} tagged block(s) concatenated, ${ran} program(s) ran clean`, +) +console.log(` untagged fences containing code: ${totalUntagged}`) + +if (failures.length === 0) { + console.log('no failures') + process.exit(0) +} +console.log('') +for (const f of failures) console.error(f) +console.error(`\n${failures.length} failure(s)`) +process.exit(1) From 6d652eb5d507243cddfcf01ece17594b3f1e8bc8 Mon Sep 17 00:00:00 2001 From: Al Diaz Date: Sun, 23 Aug 2026 15:44:03 -0700 Subject: [PATCH 2/5] Fix a latent crash, two misspelled classes, and a wrong complexity claim THE HASH TABLE'S COLLISION WALK COULD NOT WALK. `add` declared `const currentNode` and reassigned it while following the chain, so the moment the loop body runs it throws: TypeError: Assignment to constant variable. Running the body needs a THIRD key in one bucket, because with two the first node's `next` is still null and the loop is skipped. The example adds `Casa`, `A` and `Adding`, which is two in bucket 0. Measured: with a fourth key in that bucket the pre-fix code exits 1 with the error above, and the fixed code exits 0. So a repository teaching collision handling had a collision handler that broke on the second collision, with a demo one key short of showing it. `get` and `getAll` in the same file already use `let`, which is what makes this a slip rather than a misunderstanding, and worth fixing quietly rather than rewriting the lesson. THE QUEUE'S COMPLEXITY TABLE WAS WRONG ABOUT THE QUEUE. It claimed "Enqueue: `arr[0]` -> O(1)" and that "we are always adding/removing to/from the END". The implementation enqueues with `unshift`, which inserts at index 0 and moves every element up one, and the note had reasoned about the wrong operation: indexing an array is O(1), inserting at an index is not. Measured, one run on node 24: n=20000 unshift 18.6 ms push 0.4 ms n=40000 unshift 95.4 ms push 1.6 ms n=80000 unshift 400.9 ms push 0.8 ms Doubling n quadruples the total, which is a linear per-call cost. The note now says O(n), shows the measurement, and explains why the original reasoning was about indexing rather than insertion. Also checked and NOT a defect, recorded because I nearly filed it: `get` uses `pop`, which looks like a stack in a file titled FIFO. But `add` uses `unshift`, so the first item added ends up at the end and `pop` returns it. Verified: add(1), add(2), add(3) then get() returns 1. The queue is correct FIFO. TWO MISSPELLED CLASS NAMES, in code and in the claimed output: `BinarSearchTree` (20 occurrences, none spelled correctly) and `HasthTable` (13). Renamed throughout, including inside the `## Result:` blocks so those still describe what the code prints. REDECLARATIONS THAT BREAK THE FILE IF YOU FOLLOW IT IN ORDER. `let HT` appeared twice in the hash table notes and `let Q` three times in the queue notes, in sections that extend the same object rather than replacing it, so pasting the file top to bottom gives `SyntaxError: Identifier 'HT' has already been declared`. The later ones now reassign. The three genuinely alternative `class Queue` implementations are marked `standalone` instead, since those really do replace each other. Fence tags normalised to one spelling, `javascript`, from a mix of three. One era note added rather than a rewrite: the private-fields section says class fields are "in stage 3 and moving through the ladder, so at future we should be able to" have private fields natively. They shipped. Verified `#data` works on node 24 and is genuinely private. --- 02_0_binary-search-tree.md | 34 +++++++++++++++--------------- 03_0_hash-table.md | 27 ++++++++++++++---------- 04_0_linked-list.md | 6 +++--- 05_0_queue-and-stack.md | 42 +++++++++++++++++++++++++++----------- 4 files changed, 66 insertions(+), 43 deletions(-) diff --git a/02_0_binary-search-tree.md b/02_0_binary-search-tree.md index 605acce..8fd2660 100644 --- a/02_0_binary-search-tree.md +++ b/02_0_binary-search-tree.md @@ -4,8 +4,8 @@ Create a `Binary Search Tree` using the `class keyword` and add through its `pro ## Solution: -```JavaScript -class BinarSearchTree { +```javascript +class BinarySearchTree { constructor(value) { this.value = value; this.left = null; @@ -13,16 +13,16 @@ class BinarSearchTree { } } -BinarSearchTree.prototype.add = function(value) { +BinarySearchTree.prototype.add = function(value) { if (value <= this.value) { if (!this.left) { - this.left = new BinarSearchTree(value) + this.left = new BinarySearchTree(value) } else { this.left.add(value); } } else { if (!this.right) { - this.right = new BinarSearchTree(value) + this.right = new BinarySearchTree(value) } else { this.right.add(value); } @@ -34,7 +34,7 @@ let arr = [50, 100, 40, 12, 90, 98]; let BST; for (let i = 0; i < arr.length; i++) { if (i === 0) { - BST = new BinarSearchTree(arr[0]); + BST = new BinarySearchTree(arr[0]); } else { BST.add(arr[i]); } @@ -48,17 +48,17 @@ console.log(BST); Time may vary in relation to the processes running on your machine. ``` -BinarSearchTree { +BinarySearchTree { value: 50, left: - BinarSearchTree { + BinarySearchTree { value: 40, - left: BinarSearchTree { value: 12, left: null, right: null }, + left: BinarySearchTree { value: 12, left: null, right: null }, right: null }, right: - BinarSearchTree { + BinarySearchTree { value: 100, - left: BinarSearchTree { value: 90, left: null, right: [Object] }, + left: BinarySearchTree { value: 90, left: null, right: [Object] }, right: null } } Function took 4.59142804145813 milliseconds. ``` @@ -94,7 +94,7 @@ Remember that this is just a visual reference to help you in the recursion imple // I start with -1 to take 0 as a level like arr let levels = -1; -BinarSearchTree.prototype.get = function(value) { +BinarySearchTree.prototype.get = function(value) { levels++; // if value is root node @@ -160,7 +160,7 @@ The expected results are: ## Solution: ```javascript -BinarSearchTree.prototype.depth = function(traverseFn) { +BinarySearchTree.prototype.depth = function(traverseFn) { traverseFn(this.value); if (this.left) { this.left.depth(traverseFn); @@ -192,8 +192,8 @@ BST.depth(logInConsole); ## Solution: -```JavaScript -BinarSearchTree.prototype.breadth = function (traverseFn) { +```javascript +BinarySearchTree.prototype.breadth = function (traverseFn) { // we start with a queue with our tree let q = [this]; @@ -234,12 +234,12 @@ BST.breadth(logInConsole); One more note... We can also use recursion to get the _minimum_ and _maximum_ value of the nodes in our tree. ```javascript -BinarSearchTree.prototype.min = function() { +BinarySearchTree.prototype.min = function() { if (this.left) return this.left.min(); else return this.value; }; -BinarSearchTree.prototype.max = function() { +BinarySearchTree.prototype.max = function() { if (this.right) return this.right.max(); else return this.value; }; diff --git a/03_0_hash-table.md b/03_0_hash-table.md index 555afeb..7c90b32 100644 --- a/03_0_hash-table.md +++ b/03_0_hash-table.md @@ -17,8 +17,8 @@ Be sure that you add one collision. ## Solution: -```JavaScript -class HasthTable { +```javascript +class HashTable { constructor(length) { this.buckets = Array(length); this.numberOfBuckets = this.buckets.length; @@ -36,7 +36,7 @@ class Node { // This IS not hashing // Feel free to replace with a real hash function -HasthTable.prototype.hash = function(key) { +HashTable.prototype.hash = function(key) { const abc = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']; @@ -45,7 +45,7 @@ HasthTable.prototype.hash = function(key) { return bucket; } -HasthTable.prototype.add = function(key, value) { +HashTable.prototype.add = function(key, value) { key = key.toLowerCase(); // We only accept string if (key.replace(/[a-z]/g, '') || key.length < 1) { @@ -62,7 +62,10 @@ HasthTable.prototype.add = function(key, value) { } else { - const currentNode = this.buckets[bucketIndex]; + // `let`, not `const`: this walks the chain by reassigning. With `const` it throws + // TypeError: Assignment to constant variable. once the loop body runs, which needs a + // THIRD key in one bucket. `get` and `getAll` below already use `let`, so this was a slip. + let currentNode = this.buckets[bucketIndex]; while (currentNode.next) { currentNode = currentNode.next; @@ -73,7 +76,7 @@ HasthTable.prototype.add = function(key, value) { // ABC > 26 -let HT = new HasthTable(26); +let HT = new HashTable(26); HT.add('Casa', 'Casa Grande'); HT.add('A', 'Hello'); HT.add('Adding', 'Adding collision') @@ -86,7 +89,7 @@ console.log(HT); Time may vary in relation to the processes running on your machine. ``` -HasthTable { +HashTable { buckets: [ Node { key: 'a', value: 'Hello', next: [Object] }, <1 empty item>, @@ -120,7 +123,9 @@ if (objKeys.length !== objValues.length) { console.log('Something went wrong!'); } -let HT = new HasthTable(26); +// Reassigning rather than redeclaring: this section shows another way to fill the SAME +// table, and `let HT` a second time is a SyntaxError if you paste the file in order. +HT = new HashTable(26); for (let i = 0; i < objKeys.length; i++) { HT.add(objKeys[i], objValues[i]); @@ -132,7 +137,7 @@ console.log(HT); ## Result: ``` -HasthTable { +HashTable { buckets: [ Node { key: 'a', value: 'Hello', next: [Object] }, <1 empty item>, @@ -149,7 +154,7 @@ HT.get('Adding') should return 'Adding collision'. ## Solution: ```javascript -HasthTable.prototype.get = function(key) { +HashTable.prototype.get = function(key) { key = key.toLowerCase(); const bucketIndex = this.hash(key); @@ -190,7 +195,7 @@ HT.getAll()[0][2] should retrieve `Node { key: 'adding', value: 'Adding collisio ## Solution : ```javascript -HasthTable.prototype.getAll = function() { +HashTable.prototype.getAll = function() { let nodes = []; for (let i = 0; i < this.numberOfBuckets; i++) { diff --git a/04_0_linked-list.md b/04_0_linked-list.md index c563298..2d295c2 100644 --- a/04_0_linked-list.md +++ b/04_0_linked-list.md @@ -149,7 +149,7 @@ tail: Object { value: 0, next: null, prev: {…} } ## Solution: Remove tail node -```JavaScript +```javascript LinkedList.prototype.removeTail = function() { if (!this.tail) return null; @@ -178,7 +178,7 @@ tail: Object { value: 10, next: null, prev: null } ## Solution: Search for x value -```JavaScript +```javascript LinkedList.prototype.searchValue = function(value) { let currentNode = this.head; @@ -240,7 +240,7 @@ Object { value: 20, next: {…}, prev: {…} } ## Solution: Add node (x-index) -```JavaScript +```javascript LinkedList.prototype.addNode = function(index, value) { // if empty we reuse addHead() method // If index is 0 or less than 0 itg should be head diff --git a/05_0_queue-and-stack.md b/05_0_queue-and-stack.md index ea411d9..5dea6fd 100644 --- a/05_0_queue-and-stack.md +++ b/05_0_queue-and-stack.md @@ -18,15 +18,31 @@ The operation of adding an element to the rear of the queue is known as enqueue, ### Time Complexity (Big O) for Q with array -1. Enqueue: `arr[0]` -> `O(1)` or constant. -2. Dequeue: `arr[arr.length - 1]` -> `O(1)` or constant. +1. Enqueue with `unshift`: **`O(n)`**, not constant. See below. +2. Dequeue with `pop`: `O(1)` or constant. -*Note:* +*Note:* + +1. **Reading a position is not the same as inserting at one.** Indexing an array (`arr[0]`, +`arr[arr.length - 1]`) genuinely is `O(1)`, because JS arrays carry a `length` and need no further +calculation. But the `add` below does not index, it `unshift`s, which inserts at index 0 and moves every +existing element up one place. That is linear in the length of the queue. + + Measured (one run, node 24), n calls each: -1. We are always adding/removing to/from the **END**. Given that arrays in JS have the built-in property `length`, we don't have to do further calculations. + ```text + n=20000 unshift 18.6 ms push 0.4 ms + n=40000 unshift 95.4 ms push 1.6 ms + n=80000 unshift 400.9 ms push 0.8 ms + ``` + + Doubling n quadruples the `unshift` total. An earlier version of this note claimed enqueue was +`O(1)` and that "we are always adding/removing to/from the END", which is true of `pop` and false of +`unshift`: it adds at the *beginning*. See [runtime complexity](./00_runtime-complexity.md) for the +whole table and for what an O(1) array queue would look like. 2. Lookup operations are `constant`. -```js +```javascript const list = [1, 2, 3]; list[0] // 1 @@ -39,7 +55,7 @@ list[list.length - 1] ## Queue: Array implementation > this._data = [] -```js +```javascript class Queue { constructor() { @@ -88,7 +104,7 @@ Some considerations: Remember it is a convention, so you can interact with the property even when you MUST NOT do it. DO NOT do this -```js +```javascript myQueue._data = 10 console.log(myQueue); // Queue { _data: 10 } @@ -96,7 +112,8 @@ console.log(myQueue); The good news, at the time of writing these notes, [Class field declarations for JavaScript](https://github.com/tc39/proposal-class-fields) is in `stage 3` and moving through the ladder, so at future we should be able to have -natively- "private fields" in JS: -```js +```javascript +// check: standalone class Queue { #data @@ -123,7 +140,8 @@ TODO: Add example with private field ## Solution: -```JavaScript +```javascript +// check: standalone class Queue { constructor() { this.data = []; @@ -169,7 +187,7 @@ Queue.prototype.get = function() { return this.data.pop(); }; -let Q = new Queue(); +Q = new Queue(); Q.add(1); Q.add(2); Q.add(3); @@ -213,7 +231,7 @@ let descSort = function(a, b) { return b - a; }; -let Q = new Queue(); +Q = new Queue(); s1.sort(descSort); // [5, 3, 1] s2.sort(descSort); // [7, 6, 4, 2] @@ -254,7 +272,7 @@ _Note:_ Perhaps the right terminology would be push instead of add, however, I w ## Solution: -```JavaScript +```javascript class Stack { constructor() { this.data = []; From e9cf3c7c36a859e32f2f18c1c9d46f7ac6cc2d99 Mon Sep 17 00:00:00 2001 From: Al Diaz Date: Sun, 23 Aug 2026 15:44:03 -0700 Subject: [PATCH 3/5] Generate the .js files from the notes they had drifted from The runnable files and the notes had come apart, in one case completely: 04_1_linked-list.js one method (addHead) against eight documented in 04_0 05_1_queue-and-stack.js ZERO BYTES against four implementations in 05_0 So a reader who cloned the repository and ran the queue file got nothing at all, silently, and one who read the linked-list file got an eighth of the material. Both are now assembled from their notes by scripts/build-js.mjs, and `npm test` fails if they stop matching, which makes the drift impossible rather than merely fixed. The timing wrappers are gone. Each file printed `Function took 1.8 milliseconds` after its demo, and all three printed roughly the same figure because it was dominated by module load and `console.log` rather than by the six insertions being demonstrated. A wall-clock number on a six-element example says nothing about asymptotic behaviour, and printing one beside a Big O lesson misleads about the thing the repository is teaching. 05_1 wraps its three alternative Queue implementations in blocks, because `class` and `let` are block-scoped, so they share a name in one file exactly as they each stand alone in the notes. --- 02_1_binary-search-tree.js | 118 ++++++++++++++++++++--- 03_1_hash-table.js | 136 +++++++++++++++++--------- 04_1_linked-list.js | 172 +++++++++++++++++++++++++++++++-- 05_1_queue-and-stack.js | 190 +++++++++++++++++++++++++++++++++++++ 4 files changed, 546 insertions(+), 70 deletions(-) diff --git a/02_1_binary-search-tree.js b/02_1_binary-search-tree.js index ee6edce..2fa869e 100644 --- a/02_1_binary-search-tree.js +++ b/02_1_binary-search-tree.js @@ -1,10 +1,13 @@ -const { performance } = require('perf_hooks'); +// Binary search tree, assembled from 02_0_binary-search-tree.md. +// +// GENERATED. `npm test` regenerates this from the notes and fails if the two differ, so reading +// either one is safe. Before that check existed they had drifted: this pairing once had one method +// against eight documented, and the queue-and-stack file was empty while its notes were not. +// +// Regenerate with: npm run build -const startingTime = performance.now(); - -// Start of code - -class BinarSearchTree { +// ---- from 02_0_binary-search-tree.md:8 +class BinarySearchTree { constructor(value) { this.value = value; this.left = null; @@ -12,28 +15,28 @@ class BinarSearchTree { } } -BinarSearchTree.prototype.add = function(value) { +BinarySearchTree.prototype.add = function(value) { if (value <= this.value) { if (!this.left) { - this.left = new BinarSearchTree(value); + this.left = new BinarySearchTree(value) } else { this.left.add(value); } } else { - if (!this.right) { - this.right = new BinarSearchTree(value); + if (!this.right) { + this.right = new BinarySearchTree(value) } else { this.right.add(value); } } -}; +} let arr = [50, 100, 40, 12, 90, 98]; let BST; for (let i = 0; i < arr.length; i++) { if (i === 0) { - BST = new BinarSearchTree(arr[0]); + BST = new BinarySearchTree(arr[0]); } else { BST.add(arr[i]); } @@ -41,7 +44,92 @@ for (let i = 0; i < arr.length; i++) { console.log(BST); -// End of code +// ---- from 02_0_binary-search-tree.md:94 +// I start with -1 to take 0 as a level like arr +let levels = -1; + +BinarySearchTree.prototype.get = function(value) { + levels++; + + // if value is root node + if (this.value === value) { + return true; + } + + // There are 2 possibilities: left>less, right>bigger + + if (value > this.value) { + // if we dont have that value. Example: 1000 + if (!this.right) return false; + else { + // recursion + return this.right.get(value); + } + } + + if (value < this.value) { + if (!this.left) return false; + else { + // recursion + return this.left.get(value); + } + } +}; + +console.log(BST.get(99)); +console.log(levels); + +// ---- from 02_0_binary-search-tree.md:163 +BinarySearchTree.prototype.depth = function(traverseFn) { + traverseFn(this.value); + if (this.left) { + this.left.depth(traverseFn); + } + if (this.right) { + this.right.depth(traverseFn); + } +}; + +function logInConsole(value) { + console.log(value); +} + +BST.depth(logInConsole); + +// ---- from 02_0_binary-search-tree.md:196 +BinarySearchTree.prototype.breadth = function (traverseFn) { + +// we start with a queue with our tree +let q = [this]; + +// we iterate the q until we dont have more nodes +while (q.length) { + // we remove the first element and log it + let level = q.shift(); + traverseFn(level.value); + + if (level.left) q.push(level.left); + if (level.right) q.push(level.right); +} + +} + +function logInConsole(value) { + console.log(value); +} + +BST.breadth(logInConsole); + +// ---- from 02_0_binary-search-tree.md:237 +BinarySearchTree.prototype.min = function() { + if (this.left) return this.left.min(); + else return this.value; +}; + +BinarySearchTree.prototype.max = function() { + if (this.right) return this.right.max(); + else return this.value; +}; -const endingTime = performance.now(); -console.log('Function took ' + (endingTime - startingTime) + ' milliseconds.'); +console.log(BST.min()); +console.log(BST.max()); diff --git a/03_1_hash-table.js b/03_1_hash-table.js index 2dc85bc..d9c3b9d 100644 --- a/03_1_hash-table.js +++ b/03_1_hash-table.js @@ -1,10 +1,13 @@ -const { performance } = require('perf_hooks'); +// Hash table, assembled from 03_0_hash-table.md. +// +// GENERATED. `npm test` regenerates this from the notes and fails if the two differ, so reading +// either one is safe. Before that check existed they had drifted: this pairing once had one method +// against eight documented, and the queue-and-stack file was empty while its notes were not. +// +// Regenerate with: npm run build -const startingTime = performance.now(); - -// Start of code - -class HasthTable { +// ---- from 03_0_hash-table.md:21 +class HashTable { constructor(length) { this.buckets = Array(length); this.numberOfBuckets = this.buckets.length; @@ -22,46 +25,20 @@ class Node { // This IS not hashing // Feel free to replace with a real hash function -HasthTable.prototype.hash = function(key) { - const abc = [ - 'a', - 'b', - 'c', - 'd', - 'e', - 'f', - 'g', - 'h', - 'i', - 'j', - 'k', - 'l', - 'm', - 'n', - 'o', - 'p', - 'q', - 'r', - 's', - 't', - 'u', - 'v', - 'w', - 'x', - 'y', - 'z' - ]; +HashTable.prototype.hash = function(key) { + const abc = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', +'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']; const result = abc.indexOf(key[0].toLowerCase()); const bucket = result % this.numberOfBuckets; return bucket; -}; +} -HasthTable.prototype.add = function(key, value) { +HashTable.prototype.add = function(key, value) { key = key.toLowerCase(); // We only accept string if (key.replace(/[a-z]/g, '') || key.length < 1) { - console.log('Provide a valid key!'); + console.log('Provide a valid key!') return; } @@ -69,25 +46,90 @@ HasthTable.prototype.add = function(key, value) { // If we dont have nothing in that bucket if (!this.buckets[bucketIndex]) { - this.buckets[bucketIndex] = new Node(key, value); - } else { - const currentNode = this.buckets[bucketIndex]; + this.buckets[bucketIndex] = new Node(key, value) + } + + else { + // `let`, not `const`: this walks the chain by reassigning. With `const` it throws + // TypeError: Assignment to constant variable. once the loop body runs, which needs a + // THIRD key in one bucket. `get` and `getAll` below already use `let`, so this was a slip. + let currentNode = this.buckets[bucketIndex]; + while (currentNode.next) { currentNode = currentNode.next; } currentNode.next = new Node(key, value); } -}; +} // ABC > 26 -let HT = new HasthTable(26); +let HT = new HashTable(26); HT.add('Casa', 'Casa Grande'); HT.add('A', 'Hello'); -HT.add('Adding', 'Adding collision'); +HT.add('Adding', 'Adding collision') console.log(HT); -// End of code +// ---- from 03_0_hash-table.md:112 +let obj = { + Casa: 'Casa Grande', + A: 'Hello', + Adding: 'Adding collision' +}; + +// obj with keys +const objKeys = Object.keys(obj); +const objValues = Object.values(obj); + +if (objKeys.length !== objValues.length) { + console.log('Something went wrong!'); +} + +// Reassigning rather than redeclaring: this section shows another way to fill the SAME +// table, and `let HT` a second time is a SyntaxError if you paste the file in order. +HT = new HashTable(26); -const endingTime = performance.now(); -console.log('Function took ' + (endingTime - startingTime) + ' milliseconds.'); +for (let i = 0; i < objKeys.length; i++) { + HT.add(objKeys[i], objValues[i]); +} + +console.log(HT); + +// ---- from 03_0_hash-table.md:157 +HashTable.prototype.get = function(key) { + key = key.toLowerCase(); + const bucketIndex = this.hash(key); + + // If the bucket is empty + if (!this.buckets[bucketIndex]) { + return null; + } else { + let currentNode = this.buckets[bucketIndex]; + while (currentNode) { + if (currentNode.key === key) return currentNode.value; + currentNode = currentNode.next; + } + + return null; + } +}; + +HT.get('Adding'); + +// ---- from 03_0_hash-table.md:198 +HashTable.prototype.getAll = function() { + let nodes = []; + + for (let i = 0; i < this.numberOfBuckets; i++) { + let currentNode = this.buckets[i]; + + nodes.push(['Bucket ' + i]); + + while (currentNode) { + nodes[this.hash(currentNode.key)].push(currentNode); + + currentNode = currentNode.next; + } + } + return nodes; +}; diff --git a/04_1_linked-list.js b/04_1_linked-list.js index c5e2d32..e01156b 100644 --- a/04_1_linked-list.js +++ b/04_1_linked-list.js @@ -1,9 +1,12 @@ -const { performance } = require('perf_hooks'); - -const startingTime = performance.now(); - -// Start of code +// Doubly linked list, assembled from 04_0_linked-list.md. +// +// GENERATED. `npm test` regenerates this from the notes and fails if the two differ, so reading +// either one is safe. Before that check existed they had drifted: this pairing once had one method +// against eight documented, and the queue-and-stack file was empty while its notes were not. +// +// Regenerate with: npm run build +// ---- from 04_0_linked-list.md:9 class LinkedList { constructor() { this.head = null; @@ -43,7 +46,160 @@ LL.addHead(20); console.log(LL); -// End of code +// ---- from 04_0_linked-list.md:92 +LinkedList.prototype.addTail = function(value) { + // next is null since this is the tail node + let newNode = new Node(value, null, this.tail); + + // if we have a node or more + if (this.tail) { + this.tail.next = newNode; + } else { + this.head = newNode; + } + this.tail = newNode; +}; + +console.log(LL); +LL.addTail(0); + +// ---- from 04_0_linked-list.md:123 +LinkedList.prototype.removeHead = function() { + // if list is empty + if (!this.head) return null; + + this.head = this.head.next; + if (this.head) { + this.head.prev = null; + } else { + this.tail = null; + } +}; + +LL.removeHead(); +console.log(LL); + +// ---- from 04_0_linked-list.md:153 +LinkedList.prototype.removeTail = function() { + if (!this.tail) return null; + + this.tail = this.tail.prev; + if (this.tail) { + this.tail.next = null; + } else { + this.head = null; + } +} + +LL.removeTail(); +console.log(LL); + +// ---- from 04_0_linked-list.md:182 +LinkedList.prototype.searchValue = function(value) { + let currentNode = this.head; + + while (currentNode) { + if (currentNode.value === value) return 'We have ' + value; + + // Go to next node + currentNode = currentNode.next; + } + + // in case we dont find value + return null; +} + +LL.searchValue(20); + +// ---- from 04_0_linked-list.md:210 +LinkedList.prototype.searchIndex = function(index) { + // if empty + if (!this.head) return null; + + let counter = 0; + let currentNode = this.head; + + while (currentNode) { + if (counter === index) return currentNode; + + counter++; + currentNode = currentNode.next; + } + + // If we pass an index bigger than our last element. + return null; +}; + +LL.searchIndex(3); + +// ---- from 04_0_linked-list.md:244 +LinkedList.prototype.addNode = function(index, value) { + // if empty we reuse addHead() method + // If index is 0 or less than 0 itg should be head + if (!this.head || index < 1) return this.addHead(value); + + // We need to find the node at that index: null or node + let nodeAtIndex = this.searchIndex(index); + // If index > last index it should be head + if (!nodeAtIndex) return this.addTail(value); + + let prevNode, nextNode; + + if (nodeAtIndex) { + prevNode = nodeAtIndex.prev; + nextNode = nodeAtIndex.next; + } + + let newNode = new Node(value, nodeAtIndex, prevNode); + prevNode.next = newNode; +} + +LL.addNode(2,60); + +// ---- from 04_0_linked-list.md:298 +LinkedList.prototype.removeNode = function(index) { + // if empty or less than 0, just return + if (!this.head || index < 0) return; + + // if index is 0, so it is the head + if (index === 0) { + this.head = this.head.next; + this.head.prev = null; + return; + } + + // We need to find the node at that index: null or node + let nodeAtIndex = this.searchIndex(index); + + // if we dont have that index + if (!nodeAtIndex) return; + + let prevNode, nextNode; + + if (nodeAtIndex && nodeAtIndex.next) { + prevNode = nodeAtIndex.prev; + nextNode = nodeAtIndex.next; + } else { + this.tail = this.tail.prev; + this.tail.next = null; + return; + } + + prevNode.next = nextNode; + nextNode.prev = prevNode; +}; + +LL.removeNode(1); + +// ---- from 04_0_linked-list.md:347 +LinkedList.prototype.getHead = function() { + return this.head; +}; + +LinkedList.prototype.getTail = function() { + return this.tail; +}; + +LL.getHead(); -const endingTime = performance.now(); -console.log('Function took ' + (endingTime - startingTime) + ' milliseconds.'); +LL.getTail(); diff --git a/05_1_queue-and-stack.js b/05_1_queue-and-stack.js index e69de29..892d0e1 100644 --- a/05_1_queue-and-stack.js +++ b/05_1_queue-and-stack.js @@ -0,0 +1,190 @@ +// Queue and stack, assembled from 05_0_queue-and-stack.md. +// +// GENERATED. `npm test` regenerates this from the notes and fails if the two differ, so reading +// either one is safe. Before that check existed they had drifted: this pairing once had one method +// against eight documented, and the queue-and-stack file was empty while its notes were not. +// +// Regenerate with: npm run build + +// ===== implementation 1 of 3 (the notes present these as alternatives) +// Wrapped in a block, because `class` and `let` are block-scoped and these share a name. +{ + // ---- from 05_0_queue-and-stack.md:46 + const list = [1, 2, 3]; + + list[0] // 1 + list[1] // 2 + list[list.length - 1] + + // ---- from 05_0_queue-and-stack.md:59 + class Queue { + + constructor() { + this._data = []; + } + + ifQisEmptyHelper(method) { + if (this._data.length < 1) console.log(`The Q is empty! Error when trying to: ${method}`) + } + + enqueue(value) { + this._data.unshift(value) + } + + dequeue() { + this.ifQisEmptyHelper('dequeue') + this._data.pop() + } + + peek() { + this.ifQisEmptyHelper('peek') + const firstElementInQ = this._data[this._data.length - 1] + console.log(firstElementInQ) + } + } + + const myQueue = new Queue(); + + myQueue.dequeue() + + myQueue.enqueue(1) + myQueue.enqueue(2) + myQueue.enqueue(3) + + myQueue.dequeue() + myQueue.dequeue() + + myQueue.peek() + + console.log(myQueue); + + // ---- from 05_0_queue-and-stack.md:108 + myQueue._data = 10 + console.log(myQueue); + // Queue { _data: 10 } + +} + +// ===== implementation 2 of 3 (the notes present these as alternatives) +// Wrapped in a block, because `class` and `let` are block-scoped and these share a name. +{ + // ---- from 05_0_queue-and-stack.md:116 + // check: standalone + class Queue { + + #data + + constructor() { + this.#data = []; + } + + } + +} + +// ===== implementation 3 of 3 (the notes present these as alternatives) +// Wrapped in a block, because `class` and `let` are block-scoped and these share a name. +{ + // ---- from 05_0_queue-and-stack.md:144 + // check: standalone + class Queue { + constructor() { + this.data = []; + } + } + + Queue.prototype.add = function(value) { + // adds at the beginning + this.data.unshift(value); + } + + Queue.prototype.remove = function() { + // removes from the end + this.data.pop(); + } + + let Q = new Queue(); + Q.add(1); + Q.add(2); + Q.add(3); + + Q.remove(); + + console.log(Q.data); + + // ---- from 05_0_queue-and-stack.md:186 + Queue.prototype.get = function() { + return this.data.pop(); + }; + + Q = new Queue(); + Q.add(1); + Q.add(2); + Q.add(3); + + console.log('Initial Q', Q.data); + + while (Q.data.length) { + console.log(Q.get()); + } + + console.log('Empty Q', Q.data); + + // ---- from 05_0_queue-and-stack.md:226 + let s1 = [1, 5, 3]; + let s2 = [2, 4, 7, 6]; + + // This is a helper for sorting desc + let descSort = function(a, b) { + return b - a; + }; + + Q = new Queue(); + + s1.sort(descSort); // [5, 3, 1] + s2.sort(descSort); // [7, 6, 4, 2] + + while (s1.length || s2.length) { + // Source 1 + if (s1.length) { + let record = s1.pop(); + Q.add(record); + } + + // Source 2 + if (s2.length) { + let record = s2.pop(); + Q.add(record); + } + } + + console.log(Q.data); + + // ---- from 05_0_queue-and-stack.md:276 + class Stack { + constructor() { + this.data = []; + } + } + + Stack.prototype.add = function(value) { + // At at the end + this.data.push(value); + } + + Stack.prototype.remove = function() { + // It will remove the last one + return this.data.pop(); + } + + let S = new Stack(); + + S.add(10); + S.add(20); + S.add(30); + + S.remove(); + + console.log(S.data); + +} From b419a079442238f534f9fd083ef8b3db3fc15a53 Mon Sep 17 00:00:00 2001 From: Al Diaz Date: Sun, 23 Aug 2026 15:44:23 -0700 Subject: [PATCH 4/5] Write the runtime-complexity page, and a README that is not two badges 00_runtime-complexity.md was the literal string `TO DO...`, nine bytes, while the repository description promises "with runtime complexity notes". It now states Big O against THESE implementations rather than the textbook ideal, which is the only version worth having: complexity is a property of how a structure is built, not of its name, and three of the ones here are slower than their labels suggest. the queue enqueues with `unshift`, so enqueue is O(n), measured the hash table reads only the first character, so same-letter keys chain and lookup is O(n) in k the tree never rebalances, so sorted input gives a right spine and every operation is O(n) The page closes on what Big O does not tell you, using this repository's own removed timing wrappers as the example: three structures all reporting 1.8 ms says nothing about growth. 00_data-structures.md was also `TO DO...` with no scope beyond its title, and the README index covers the overview it would have been, so it is deleted rather than invented. The README was 270 bytes: a title and two badges, one of them a Greenkeeper badge. That badge is not broken, which is worse. It renders "Greenkeeper | Move to Snyk", so the repository was carrying a live advertisement for a service that shut down, telling readers to migrate. Removed. The MIT badge stays and now agrees with both LICENSE and package.json, which it did not before. The README now says what the repository is, indexes the five notes with their runnable counterparts, documents the two checks, and records the three findings worth knowing rather than burying them in git history: the const that could not walk a chain, the complexity table that was wrong about its own queue, and the timers that measured `console.log`. .gitignore: added the `.env*` prefix glob and the `*.env` suffix, since neither implies the other and it had only the bare name plus four `.local` variants; a negation so a template stays committable; and `*.log`, where it previously listed only npm-debug and yarn-debug so any other log was tracked. Verified per pattern with git check-ignore. --- .gitignore | 32 +++++++++----- 00_data-structures.md | 1 - 00_runtime-complexity.md | 96 +++++++++++++++++++++++++++++++++++++++- README.md | 74 +++++++++++++++++++++++++++++-- 4 files changed, 187 insertions(+), 16 deletions(-) delete mode 100644 00_data-structures.md diff --git a/.gitignore b/.gitignore index 64cd523..0ff2386 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,24 @@ # dependencies -/node_modules +node_modules/ -# misc -.DS_Store -.env -.env.local -.env.development.local -.env.test.local -.env.production.local +# Credentials. Both globs, because neither implies the other: `.env*` is a prefix glob and misses +# `audit.env`, while `*.env` is a suffix glob and misses `.env.local`. +.env* +*.env +!.env.example +credentials* +secrets/ +*.pem +*.key + +# Logs. The previous version listed only npm-debug and yarn-debug, so any other log was tracked. +*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* +# Editor and OS noise. +.vscode/ +.idea/ +.DS_Store +__pycache__/ +*.sqlite +*.sqlite3 +*.db diff --git a/00_data-structures.md b/00_data-structures.md deleted file mode 100644 index 64db36b..0000000 --- a/00_data-structures.md +++ /dev/null @@ -1 +0,0 @@ -TO DO... diff --git a/00_runtime-complexity.md b/00_runtime-complexity.md index 64db36b..1b217c2 100644 --- a/00_runtime-complexity.md +++ b/00_runtime-complexity.md @@ -1 +1,95 @@ -TO DO... +# Runtime complexity + +Big O for the structures in this repository, stated against **these implementations** rather than +against the textbook ideal. That distinction is the point of the page: a data structure's complexity is +a property of how it is built, not of its name, and two of the implementations here are slower than +their labels suggest. + +Every number below was either derived from the code in these notes or measured. Where it was measured, +the measurement is shown. + +## The queue's enqueue is O(n), not O(1) + +`05_0` enqueues with `unshift`, which inserts at index 0 and therefore moves every existing element up +one position. That is linear in the length of the queue, so filling a queue of n items is O(n squared). + +Measured on node 24, n `unshift` calls against n `push` calls: + +```text +n=20000 unshift 18.6 ms push 0.4 ms +n=40000 unshift 95.4 ms push 1.6 ms +n=80000 unshift 400.9 ms push 0.8 ms +``` + +Doubling n roughly quadruples the `unshift` total, which is the signature of a linear per-call cost. +`push` stays flat. Those are one run and the absolute figures move a few percent between runs; the +ratio and the shape are the claim, not the milliseconds. + +This matters because `05_0` used to claim the opposite, and the reasoning it gave was about the wrong +operation: indexing an array (`arr[0]`, `arr[arr.length - 1]`) genuinely is O(1), but the queue does not +index to enqueue, it `unshift`s. Reading a position and inserting at one have nothing in common. + +The fix, if you wanted an O(1) queue on an array, is to enqueue with `push` and dequeue with a moving +head index rather than `shift`, so neither operation reindexes. That is not what these notes implement, +and the honest table is the one below. + +## The hash table degrades to a linked list per letter + +`03_0` hashes on the first character only, into 26 buckets. Its own comment says so: "This IS not +hashing". The consequence is worth spelling out in complexity terms, because it is the difference +between a hash table and a list of lists: + +- Keys are distributed by first letter, not by content, so `apple`, `avocado` and `anchor` all land in + bucket 0 and form a chain. +- Lookup is O(1) to find the bucket, then O(k) to walk the chain, where k is the number of stored keys + sharing that first letter. +- With keys drawn from real text, first letters are famously uneven, so k is not n/26. + +So the average case is O(n) in the number of same-letter keys, and the structure only behaves like a +hash table when the keys happen to start with different letters. A real hash function over the whole key +is what buys the O(1) average, and swapping one in is the single change that would most improve this +implementation. + +## The binary search tree has no balancing + +`02_0` inserts by comparison and never rebalances. Every operation is O(h) where h is the height, and h +depends entirely on insertion order: + +- Random or interleaved input gives h around log n, so O(log n). +- **Sorted** input gives a tree with one child per node, h equals n, and every operation becomes O(n). + The structure is then a linked list that costs more memory. + +The example in the notes inserts `[50, 100, 40, 12, 90, 98]`, which interleaves and produces a +reasonable shape. Insert `[12, 40, 50, 90, 98, 100]` instead and you get a right spine. Nothing in the +code prevents that, which is what self-balancing trees (AVL, red-black) exist for. + +## Table + +Against the implementations in this repository, not the ideal ones. + +| structure | operation | this implementation | why | +| --- | --- | --- | --- | +| Binary search tree | insert, search | O(h): O(log n) balanced, **O(n) if input is sorted** | no rebalancing | +| Hash table | insert, lookup | O(1) + O(k) chain walk, **O(n) same-letter keys** | hashes on the first character only | +| Doubly linked list | add head, add tail | O(1) | both ends are held | +| Doubly linked list | search by value or index | O(n) | no index, must walk | +| Doubly linked list | remove head, remove tail | O(1) | `prev` pointers make the tail cheap | +| Queue (array) | enqueue | **O(n)** | `unshift` reindexes | +| Queue (array) | dequeue | O(1) | `pop` from the end | +| Stack (array) | push, pop | O(1) amortised | both at the end | + +"Amortised" on the stack is doing real work: a `push` that grows the backing array copies it, which is +O(n) for that one call, and the doubling strategy makes the average across many pushes constant. + +## What Big O does not tell you + +Two things worth holding onto, because this repository's own code demonstrates both. + +**It hides constants, and constants decide small cases.** The `unshift` numbers above are all "linear", +and 18 ms against 400 ms is the difference between usable and not. + +**It describes growth, not a measurement.** The `.js` files in this repository used to print +`Function took 1.8 milliseconds` after each demo. Three different structures all reported about 1.8 ms, +because that figure was dominated by module load and `console.log`, not by the six insertions being +demonstrated. A wall-clock number on a six-element example says nothing about asymptotic behaviour, and +printing one next to a lesson about Big O actively misleads. Those timers were removed. diff --git a/README.md b/README.md index 863a62d..ea199eb 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,72 @@ -# Data Structures and JS +# Data structures in JavaScript -[![Greenkeeper badge](https://badges.greenkeeper.io/alpersonalwebsite/data-structures-and-algorithms.svg)](https://greenkeeper.io/) -[![License: MIT](https://img.shields.io/badge/License-MIT-brightgreen.svg)](https://opensource.org/licenses/MIT) \ No newline at end of file +Notes on five data structures, each with the code that implements it: a binary search tree, a hash +table, a doubly linked list, a queue and a stack. Written while learning them, so they explain as they +go rather than presenting a finished library. + +Each `NN_0_*.md` is the notes and each `NN_1_*.js` is the same code assembled into something you can +run: + +```shell +node 04_1_linked-list.js +``` + +## Contents + +- [Runtime complexity](./00_runtime-complexity.md) +- [Binary search tree](./02_0_binary-search-tree.md) + - runnable: [`02_1_binary-search-tree.js`](./02_1_binary-search-tree.js) +- [Hash table](./03_0_hash-table.md) + - runnable: [`03_1_hash-table.js`](./03_1_hash-table.js) +- [Doubly linked list](./04_0_linked-list.md) + - runnable: [`04_1_linked-list.js`](./04_1_linked-list.js) +- [Queue and stack](./05_0_queue-and-stack.md) + - runnable: [`05_1_queue-and-stack.js`](./05_1_queue-and-stack.js) + +## The code is checked, and the two copies cannot drift + +```shell +npm test # run every notes file's code, and verify the .js files match the notes +npm run build # regenerate the .js files from the notes +``` + +`scripts/check-samples.mjs` concatenates each notes file's code blocks in order and **runs** them, +because each file is one program spread across blocks: one defines the class, the next adds a method, +the next exercises it. Running is what parsing cannot do. It also reports any code sitting in an +untagged fence, since a block with no language tag is invisible to tooling and its green result would +imply coverage it does not have. + +`scripts/build-js.mjs` generates each `.js` from its notes and `--check` fails if they differ. The notes +are the source of truth. Before that check existed, `04_1` had one method against eight documented and +`05_1` was **zero bytes** while its notes carried four implementations, so a reader who cloned the repo +and ran the queue file got nothing at all. + +No dependencies. Both scripts use only what ships with node. + +## What was wrong, and is worth knowing + +Three findings that survive as lessons rather than just fixes: + +**The hash table's collision walk used `const` and reassigned it.** That throws +`TypeError: Assignment to constant variable.`, but only once the loop body runs, which needs a *third* +key in one bucket. The example added two. So a repository teaching collision handling had a collision +handler that broke on the second collision, with a demo one key short of showing it. `get` and `getAll` +in the same file already used `let`, so it was a slip rather than a misunderstanding. + +**The queue's complexity table was wrong about the queue.** It claimed enqueue was `O(1)` and that "we +are always adding/removing to/from the END", while `add` uses `unshift`, which inserts at the beginning +and is `O(n)`. Measured, doubling n quadruples the total. Indexing an array is `O(1)`; inserting at one +is not, and the note had reasoned about the wrong operation. + +**The timing wrappers measured the wrong thing.** Each `.js` file printed `Function took 1.8 +milliseconds`, and all three printed roughly the same figure, because it was dominated by module load +and `console.log` rather than by the six insertions. A wall-clock number on a six-element example says +nothing about asymptotic behaviour, and printing one beside a Big O lesson misleads. Removed. + +See [runtime complexity](./00_runtime-complexity.md), which states Big O against *these* +implementations rather than the textbook ideal: the tree does not rebalance, the hash function reads +only the first character, and the queue enqueues in linear time. + +## License + +MIT, per [LICENSE](./LICENSE). The badge and `package.json` previously disagreed with it. From 3506a5c67eebcefbac4503f5a98ed39faf266a44 Mon Sep 17 00:00:00 2001 From: Al Diaz Date: Sun, 23 Aug 2026 16:09:41 -0700 Subject: [PATCH 5/5] Correct two counts, print one instead of asserting it, and run the checks in CI Two count errors from review, both in durable text, both understating. "76 CORRECTLY-BARE FENCES" IS 25 BLOCKS. 76 is the number of lines equal to a bare fence, which counts the closing fence of every tagged block as well. Measured: 26 tagged blocks, 25 bare blocks, 76 bare-fence lines. Since that figure is the justification for the untagged-fence check, being wrong about it undermines the thing it argues for. Fixed by making the checker PRINT it rather than by correcting a sentence: bare blocks holding output: 25, untagged fences containing code: 0 The number now cannot go stale and nobody has to trust prose about it. That is the general repair for this, and it is the one I keep arriving at: derive a count where it carries information, delete it where it does not, and only write it by hand when neither is possible. "ONE METHOD AGAINST EIGHT DOCUMENTED" IS ONE AGAINST TEN. Base 04_0 documents ten LinkedList.prototype methods and base 04_1 implemented one; head implements all ten. Eight is what you get excluding getHead and getTail as trivial accessors, which may have been the thought, but it is not what the sentence said. Corrected in the README and in the generator that stamps the .js headers, so all four generated files carry the right figure. That is the eighth and ninth wrong count in this effort, and both understate the work, which is the benign direction and not the point. CI ADDED, and the argument for it is stronger here than in the sibling repositories. The claim this pull request makes is that the .js files CANNOT drift from the notes, and the mechanism enforcing it was `--check` inside a script someone has to remember to type. That is a conditional guarantee, and the condition is exactly what let 05_1_queue-and-stack.js sit at zero bytes while its notes carried four implementations. A workflow makes it unconditional. No install step: this repository has no dependencies at all and both scripts use only what ships with node. Verified `npm test` green from a fresh clone with no node_modules. --- .github/workflows/test.yml | 32 ++++++++++++++++++++++++++++++++ 02_1_binary-search-tree.js | 2 +- 03_1_hash-table.js | 2 +- 04_1_linked-list.js | 2 +- 05_1_queue-and-stack.js | 2 +- README.md | 2 +- scripts/build-js.mjs | 4 ++-- scripts/check-samples.mjs | 17 ++++++++++++++--- 8 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..c3f5058 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,32 @@ +# Runs both checks on every push and pull request. +# +# This exists because of what the checks are FOR. `npm test` runs the code in the notes and verifies that +# each NN_1_*.js still matches the NN_0_*.md it is generated from, and the whole claim of that second +# check is that the two cannot drift apart. Without CI the guarantee is conditional on somebody +# remembering to type a command, which is exactly the condition that let 05_1_queue-and-stack.js sit at +# zero bytes while its notes carried four implementations. +# +# NO INSTALL STEP. This repository has no dependencies at all, and both scripts use only what ships with +# node, so there is nothing to install and nothing that can break the run from outside. + +name: test + +on: + push: + branches: [master] + pull_request: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - run: npm test diff --git a/02_1_binary-search-tree.js b/02_1_binary-search-tree.js index 2fa869e..92f511f 100644 --- a/02_1_binary-search-tree.js +++ b/02_1_binary-search-tree.js @@ -2,7 +2,7 @@ // // GENERATED. `npm test` regenerates this from the notes and fails if the two differ, so reading // either one is safe. Before that check existed they had drifted: this pairing once had one method -// against eight documented, and the queue-and-stack file was empty while its notes were not. +// against ten documented, and the queue-and-stack file was empty while its notes were not. // // Regenerate with: npm run build diff --git a/03_1_hash-table.js b/03_1_hash-table.js index d9c3b9d..4df6b38 100644 --- a/03_1_hash-table.js +++ b/03_1_hash-table.js @@ -2,7 +2,7 @@ // // GENERATED. `npm test` regenerates this from the notes and fails if the two differ, so reading // either one is safe. Before that check existed they had drifted: this pairing once had one method -// against eight documented, and the queue-and-stack file was empty while its notes were not. +// against ten documented, and the queue-and-stack file was empty while its notes were not. // // Regenerate with: npm run build diff --git a/04_1_linked-list.js b/04_1_linked-list.js index e01156b..cfca704 100644 --- a/04_1_linked-list.js +++ b/04_1_linked-list.js @@ -2,7 +2,7 @@ // // GENERATED. `npm test` regenerates this from the notes and fails if the two differ, so reading // either one is safe. Before that check existed they had drifted: this pairing once had one method -// against eight documented, and the queue-and-stack file was empty while its notes were not. +// against ten documented, and the queue-and-stack file was empty while its notes were not. // // Regenerate with: npm run build diff --git a/05_1_queue-and-stack.js b/05_1_queue-and-stack.js index 892d0e1..5196b52 100644 --- a/05_1_queue-and-stack.js +++ b/05_1_queue-and-stack.js @@ -2,7 +2,7 @@ // // GENERATED. `npm test` regenerates this from the notes and fails if the two differ, so reading // either one is safe. Before that check existed they had drifted: this pairing once had one method -// against eight documented, and the queue-and-stack file was empty while its notes were not. +// against ten documented, and the queue-and-stack file was empty while its notes were not. // // Regenerate with: npm run build diff --git a/README.md b/README.md index ea199eb..8bf3128 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ untagged fence, since a block with no language tag is invisible to tooling and i imply coverage it does not have. `scripts/build-js.mjs` generates each `.js` from its notes and `--check` fails if they differ. The notes -are the source of truth. Before that check existed, `04_1` had one method against eight documented and +are the source of truth. Before that check existed, `04_1` had one method against ten documented and `05_1` was **zero bytes** while its notes carried four implementations, so a reader who cloned the repo and ran the queue file got nothing at all. diff --git a/scripts/build-js.mjs b/scripts/build-js.mjs index 1043f21..5d092cd 100644 --- a/scripts/build-js.mjs +++ b/scripts/build-js.mjs @@ -3,7 +3,7 @@ // committed file still matches what the notes would produce. // // This exists because the two had drifted badly and nothing noticed: 04_1 held one method while its -// notes documented eight, and 05_1 was ZERO BYTES while its notes carried four implementations. A +// notes documented ten, and 05_1 was ZERO BYTES while its notes carried four implementations. A // reader who cloned the repo and ran the queue file got nothing at all. Generating one from the other // makes the drift impossible rather than merely fixed. // @@ -70,7 +70,7 @@ function render(md, title) { '//', '// GENERATED. `npm test` regenerates this from the notes and fails if the two differ, so reading', '// either one is safe. Before that check existed they had drifted: this pairing once had one method', - '// against eight documented, and the queue-and-stack file was empty while its notes were not.', + '// against ten documented, and the queue-and-stack file was empty while its notes were not.', '//', '// Regenerate with: npm run build', '', diff --git a/scripts/check-samples.mjs b/scripts/check-samples.mjs index 43b621e..b01cec8 100644 --- a/scripts/check-samples.mjs +++ b/scripts/check-samples.mjs @@ -20,6 +20,11 @@ // flagged rather than silently skipped: a checker that quietly ignores half the code is worse than no // checker, because the green result implies coverage it does not have. // +// The count of legitimately-bare blocks is PRINTED rather than written into prose. A commit message here +// claimed "76 correctly-bare fences", which was the number of lines equal to ``` and therefore counted +// the closing fence of every tagged block too. The real figure is 25 blocks. Printing it means the +// number cannot go stale and nobody has to trust a sentence about it. +// // The `## Result:` blocks are NOT compared against the actual output. They are Firefox DevTools // renderings (`Object { … }`, `:`), not node's, so a diff would fail on formatting for every // single one and the check would be turned off within a day. What the run does prove is that the code @@ -42,6 +47,7 @@ function blocks(name) { const lines = readFileSync(join(root, name), 'utf8').split('\n') const tagged = [] const untagged = [] + let bare = 0 let body = null let tag = null let start = 0 @@ -61,6 +67,7 @@ function blocks(name) { tagged.push({ start, source, standalone }) } else if (!tag && LOOKS_LIKE_CODE.test(source)) untagged.push({ start, source }) + else if (!tag) bare++ body = null tag = null } else body.push(line) @@ -73,7 +80,7 @@ function blocks(name) { start = i + 2 } } - return { tagged, untagged } + return { tagged, untagged, bare } } const files = readdirSync(root).filter((f) => /^\d.*\.md$/.test(f)).sort() @@ -81,12 +88,14 @@ const dir = mkdtempSync(join(tmpdir(), 'dsa-')) const failures = [] let totalBlocks = 0 let totalUntagged = 0 +let totalBare = 0 let ran = 0 for (const name of files) { - const { tagged, untagged } = blocks(name) + const { tagged, untagged, bare } = blocks(name) totalBlocks += tagged.length totalUntagged += untagged.length + totalBare += bare for (const u of untagged) { failures.push( @@ -154,7 +163,9 @@ if (args[0] === '--print') process.exit(0) console.log( `${files.length} notes files: ${totalBlocks} tagged block(s) concatenated, ${ran} program(s) ran clean`, ) -console.log(` untagged fences containing code: ${totalUntagged}`) +console.log( + ` bare blocks holding output: ${totalBare}, untagged fences containing code: ${totalUntagged}`, +) if (failures.length === 0) { console.log('no failures')