diff --git a/.github/workflows/hashing.yml b/.github/workflows/hashing.yml new file mode 100644 index 000000000..1461e243f --- /dev/null +++ b/.github/workflows/hashing.yml @@ -0,0 +1,26 @@ +name: Hashing Algorithms CI + +on: + push: + branches: [2024_sem2] + paths-ignore: + - 'doc/**' # Ignore all files and subdirectories under "doc/" + - 'docs/**' # Ignore all files and subdirectories under "docs/" + pull_request: + branches: [2024_sem2] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout 🛎️ + uses: actions/checkout@v2.3.1 + with: + persist-credentials: false + + - name: Install and Test 🔧 + run: | + npm install + npm run test-hashinsert + npm run test-hashsearch + npm run test-hashdelete diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..94a25f7f4 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml new file mode 100644 index 000000000..8e9e168d9 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1725873310585 + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9053a771b..2f3fd9b39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@mui/lab": "latest", "@mui/material": "^5.14.4", "@mui/styles": "^5.14.4", + "@popperjs/core": "^2.11.8", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^12.1.2", "@testing-library/react-hooks": "^8.0.1", @@ -4520,6 +4521,7 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "license": "MIT", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" diff --git a/package.json b/package.json index a892d28a0..f5ead245b 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,10 @@ "test-msdrs": "npm test -- ./src/algorithms/controllers/MSDRadixSort.test.js", "test-uf": "npm test -- ./src/algorithms/controllers/unionFind.test.js", "test-234t": "npm test -- ./src/algorithms/controllers/TTFTree.test.js", + "test-hashinsert": "npm test -- ./src/algorithms/controllers/tests/HashingInsertion.test.js", + "test-hashsearch": "npm test -- ./src/algorithms/controllers/tests/HashingSearch.test.js", + "test-hashdelete": "npm test -- ./src/algorithms/controllers/tests/HashingDeletion.test.js", + "test-234t": "npm test -- ./src/algorithms/controllers/TTFTree.test.js", "test-avl": "npm test -- ./src/algorithms/controllers/AVLTree.test.js", "test-url": "npm test -- ./src/algorithms/parameters/helpers/urlHelpers.test.js" }, @@ -63,6 +67,7 @@ "@mui/lab": "latest", "@mui/material": "^5.14.4", "@mui/styles": "^5.14.4", + "@popperjs/core": "^2.11.8", "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^12.1.2", "@testing-library/react-hooks": "^8.0.1", diff --git a/src/algorithms/controllers/AStar.js b/src/algorithms/controllers/AStar.js index 0b33b7e6f..95aaa46c3 100644 --- a/src/algorithms/controllers/AStar.js +++ b/src/algorithms/controllers/AStar.js @@ -166,7 +166,7 @@ export default { 5, (vis, v) => { vis.array.set(v, algNameStr); - vis.array.getRendererClass().zoom = 8; + vis.array.setZoom(0.8); }, [[nodes, heuristics, minCosts, parents, finalCosts], 0] ); diff --git a/src/algorithms/controllers/HashingChainingInsertion.js b/src/algorithms/controllers/HashingChainingInsertion.js new file mode 100644 index 000000000..05fda6da8 --- /dev/null +++ b/src/algorithms/controllers/HashingChainingInsertion.js @@ -0,0 +1,394 @@ +import Array2DTracer from '../../components/DataStructures/Array/Array2DTracer'; +import GraphTracer from '../../components/DataStructures/Graph/GraphTracer'; +import { HashingExp } from '../explanations'; +import { + hash1, + HASH_GRAPH, + EMPTY_CHAR, + Colors, + INDEX, + POINTER, + POINTER_VALUE, + SMALL_SIZE, + VALUE, + LARGE_SIZE, + SPLIT_SIZE, + HASH_TYPE, + PRIMES, + POINTER_CUT_OFF, + newCycle +} from './HashingCommon'; +import { translateInput } from '../parameters/helpers/ParamHelper'; +import HashingDelete from './HashingDelete'; +import { createPopper } from '@popperjs/core'; + +// Bookmarks to link chunker with pseudocode +const IBookmarks = { + Init: 1, + EmptyArray: 2, + InitInsertion: 3, + NewInsertion: 4, + Hash1: 5, + Pending: 7, + PutIn: 9, + Done: 10, + BulkInsert: 1, +} + +export default { + explanation: HashingExp, + + // Initialize visualizers + initVisualisers() { + return { + array: { + instance: new Array2DTracer('array', null, 'Hash Table'), + order: 0, + }, + graph: { + instance: new GraphTracer('graph', null, 'Hashing Functions'), + order: 1, + }, + }; + }, + + /** + * Run function for insertion, using the user input to display the illustration through chunker + * @param {*} chunker the chunker for the illustrations + * @param {*} params different parameters of the algorithm insertion mode e.g. name, array size,... + * @returns a table of concluding array to serve testing purposes + */ + run(chunker, params) { + // Storing algorithms parameters as local variables + const ALGORITHM_NAME = params.name; + let inputs = params.values; + const SIZE = params.hashSize; + + // Initialize arrays + let indexArr = Array.from({ length: SIZE }, (_, i) => i); + let valueArr = Array(SIZE).fill(EMPTY_CHAR); + let nullArr = Array(SIZE).fill(''); + + // For return + let table_result; + + // Variable to keep track of insertions done and total inputs hashed into the table + let insertions = 0; + let total = 0; + + /** + * Insertion function for each key + * @param {*} table the table to keep track of the internal and illustrated array + * @param {*} key the key to insert + * @returns the index the key is assigned + */ + function hashInsert(table, key) { + + chunker.add( + IBookmarks.NewInsertion, + (vis, total) => { + vis.array.showKth({ + key: key, + type: HASH_TYPE.Insert + }) + newCycle(vis, table.length, key, ALGORITHM_NAME); // New insert cycle + }, + [total] + ) + + insertions++; // Increment insertions + total++; // Increment total + + // Get initial hash index for current key + let i = hash1(chunker, IBookmarks.Hash1, key, table.length, true); + + // Chunker for first pending slot + chunker.add( + IBookmarks.Pending, + (vis, idx) => { + + // Pointer only appear for small table + if (table.length <= PRIMES[POINTER_CUT_OFF]) { + vis.array.assignVariable(POINTER_VALUE, POINTER, idx); + } + + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Pending); // Color pending slot + + // Uncolor the hashing graph + vis.graph.deselect(HASH_GRAPH.Key); + vis.graph.deselect(HASH_GRAPH.Value); + vis.graph.removeEdgeColor(HASH_GRAPH.Key, HASH_GRAPH.Value); + if (ALGORITHM_NAME == "HashingDH") { + vis.graph.deselect(HASH_GRAPH.Key2); + vis.graph.deselect(HASH_GRAPH.Value2); + vis.graph.removeEdgeColor(HASH_GRAPH.Key2, HASH_GRAPH.Value2); + } + }, + [i] + ) + + // Internally assign the key to the index + table[i].push(key); + + // Chunker for placing the key + chunker.add( + IBookmarks.PutIn, + (vis, val, idx, insertions, table) => { + if (table[idx].length > 1) { + const popper = document.getElementById('float_box_' + idx); + if (table[idx].length == 2) { + const slot = document.getElementById('chain_' + idx); + floatingBoxes[idx] = createPopper(slot, popper, { + placement: "right-start", + strategy: "fixed", + modifiers: [ + { + name: 'preventOverflow', + options: { + boundary: document.getElementById('popper_boundary'), + }, + }, + ] + }); + } + popper.innerHTML = table[idx]; + } + + let slotCurValue = vis.array.getValueAt(VALUE, idx); + if (slotCurValue === EMPTY_CHAR) vis.array.updateValueAt(VALUE, idx, val); // Update value of that index when the slot is empty + else vis.array.updateValueAt(VALUE, idx, slotCurValue + (table[idx].length == 2 ? ".." : "")); // Update value of that index when the slot is not empty + vis.array.showKth({key: vis.array.getKth().key, type: HASH_TYPE.BulkInsert, insertions: insertions}); + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Insert); // Fill it green, indicating successful insertion + }, + [key, i, insertions, table] + ) + + // Return the insertion index + return i; + } + + + /** + * Function for bulk insertion + * @param {*} table the table to keep track of the internal and illustrated array + * @param {*} keys the keys to insert + * @returns the index the last key is assigned + */ + function hashBulkInsert(table, keys) { + let lastHash; + let inserts = {}; + let bulkInsertions = 0; + for (const key of keys) { + + bulkInsertions++; + + // hashed value + let i = hash1(null, null, key, table.length, false); + + table[i].push(key); + inserts[key] = i; + lastHash = i; + } + + insertions += bulkInsertions; + total += bulkInsertions; + chunker.add( + IBookmarks.PutIn, + (vis, keys, inserts, insertions, table) => { + for (const key of keys) { + if (table[inserts[key]].length > 1) { + const popper = document.getElementById('float_box_' + inserts[key]); + if (table[inserts[key]][2] !== null) { + const slot = document.getElementById('chain_' + inserts[key]); + floatingBoxes[inserts[key]] = createPopper(slot, popper, { + placement: "right-start", + strategy: "fixed", + modifiers: [ + { + name: 'preventOverflow', + options: { + boundary: document.getElementById('popper_boundary'), + }, + }, + ] + }); + } + popper.innerHTML = table[inserts[key]]; + } + + let slotCurValue = vis.array.getValueAt(VALUE, inserts[key]); + console.log(typeof(slotCurValue)); + if (slotCurValue === EMPTY_CHAR) vis.array.updateValueAt(VALUE, inserts[key], key); // Update value of that index when the slot is empty + else vis.array.updateValueAt(VALUE, inserts[key], slotCurValue + ((table[inserts[key]].length >= 2 && typeof(slotCurValue) == 'number') ? ".." : "")); // Update value of that index when the slot is not empty + vis.array.fill(INDEX, inserts[key], undefined, undefined, Colors.Insert); + } + vis.array.showKth({key: vis.array.getKth().key, type: HASH_TYPE.BulkInsert, insertions: insertions}); + }, + [keys, inserts, insertions, table] + ) + + return lastHash; + } + + + // Inserting inputs + let prevIdx; + + let floatingBoxes = new Array(SIZE); // List of all popper instances + + // Init hash table with dynamic array in each slot + let table = new Array(SIZE); + for (var i = 0; i < SIZE; i++) { + table[i] = []; + } + + prevIdx = null; + + // Chunker step for the inital loading state + chunker.add( + IBookmarks.Init, + (vis, size, array) => { + // Increase Array2D visualizer render space + if (SIZE === LARGE_SIZE) { + vis.array.setSize(3); + vis.array.setZoom(0.7); + vis.graph.setZoom(1.5); + } else { + vis.array.setZoom(1); + vis.graph.setZoom(1); + } + + // Initialize the array + vis.array.set(array, + params.name, + '', + INDEX, + { + rowLength: size > SMALL_SIZE ? SPLIT_SIZE : SMALL_SIZE, + rowHeader: ['Index', 'Value', ''] + } + ); + + vis.array.hideArrayAtIndex([VALUE, POINTER]); // Hide value and pointer row intially + + vis.graph.weighted(true); + + // Intialize the graphs + switch (ALGORITHM_NAME) { + case "HashingLP" : + vis.graph.set([[0, 'Hash'], [0, 0]], [' ', ' '], [[-5, 0], [5, 0]]); + break; + case "HashingCH" : + vis.graph.set([[0, 'Hash'], [0, 0]], [' ', ' '], [[-5, 0], [5, 0]]); + break; + case "HashingDH" : + vis.graph.set([ + [0, 'Hash1', 0, 0], [0, 0, 0, 0], [0, 0, 0, 'Hash2'], [0, 0, 0, 0]], // Node edges + [' ', ' ', ' ', ' '], // Node values + [[-5, 2], [5, 2], [-5, -2], [5, -2]]); // Node positions + break; + } + }, + [table.length, table.length <= PRIMES[POINTER_CUT_OFF] ? + [indexArr, valueArr, nullArr] : + [indexArr, valueArr] + ] + ); + + // Chunker to initialize empty array visually + chunker.add( + IBookmarks.EmptyArray, + (vis) => { + // Show the value row + vis.array.hideArrayAtIndex(POINTER); + }, + ); + + // Chunker for intializing insertion stat + chunker.add( + IBookmarks.InitInsertion, + (vis, insertions) => { + vis.array.showKth({ + key: "", + type: EMPTY_CHAR, + insertions: insertions, + increment: "", + } + ); + }, + [insertions] + ) + + // Magic numbers for length of splitting a postive integer string by "-", the index of "", and the number to delete when a negative integer is split by "-" + const POS_INTEGER_SPLIT_LENGTH = 1; + const EMPTY_DELETE_SPLIT_INDEX = 0; + const NUMBER_DELETE_SPLIT_INDEX = 1; + + for (const item of inputs) { + + // Different cases of insertion and deletion + let split_arr = item.split("-"); + if (split_arr.length == POS_INTEGER_SPLIT_LENGTH) { // When the input is a positive integer -> normal insert + for (const key of translateInput(item, "Array")) { + prevIdx = hashInsert(table, key, false); + } + } + else { + if (split_arr[EMPTY_DELETE_SPLIT_INDEX] === "") { // When the input is a negative integer -> delete + let key = Number(split_arr[NUMBER_DELETE_SPLIT_INDEX]); + total = HashingDelete(chunker, params, key, table, total); + } + else { // When the input is a range -> bulk insert + // Preparation for bulk insertion + chunker.add( + IBookmarks.BulkInsert, + (vis, insertions, prevIdx) => { + vis.array.unfill(INDEX, 0, undefined, table.length - 1); // Reset any coloring of slots + vis.array.showKth({key: item, type: HASH_TYPE.BulkInsert, insertions: insertions, increment: ""}); + if (table.length <= PRIMES[POINTER_CUT_OFF]) + vis.array.assignVariable("", POINTER, prevIdx, POINTER_VALUE); // Hide pointer + + vis.graph.updateNode(HASH_GRAPH.Key, ' '); + vis.graph.updateNode(HASH_GRAPH.Value, ' '); + if (ALGORITHM_NAME === "HashingDH") { + vis.graph.updateNode(HASH_GRAPH.Key2, ' '); + vis.graph.updateNode(HASH_GRAPH.Value2, ' '); + } + }, + [insertions, prevIdx] + ) + prevIdx = hashBulkInsert(table, translateInput(item, "Array")); + } + } + } + + // Chunker for resetting visualizers in case of new insertion cycle + chunker.add( + IBookmarks.Done, + (vis) => { + + vis.array.showKth({key: "", type: EMPTY_CHAR, insertions: insertions, increment: ""}) // Nullify some stats, for better UI + + // Hide pointer + if (table.length <= PRIMES[POINTER_CUT_OFF]) { + vis.array.assignVariable(POINTER_VALUE, POINTER, undefined); + } + + vis.array.unfill(INDEX, 0, undefined, table.length - 1); // Unfill all boxes + + // Reset graphs and uncolor the graph if needed + vis.graph.updateNode(HASH_GRAPH.Key, ' '); + vis.graph.updateNode(HASH_GRAPH.Value, ' '); + if (ALGORITHM_NAME === 'HashingDH') { + vis.graph.updateNode(HASH_GRAPH.Key2, ' '); + vis.graph.updateNode(HASH_GRAPH.Value2, ' '); + } + + // Extract resulting array for testing + table_result = vis.array.extractArray([1], EMPTY_CHAR) + }, + ) + + return table_result; // Return resulting array for testing + }, +}; diff --git a/src/algorithms/controllers/HashingCommon.js b/src/algorithms/controllers/HashingCommon.js new file mode 100644 index 000000000..43a3a4701 --- /dev/null +++ b/src/algorithms/controllers/HashingCommon.js @@ -0,0 +1,203 @@ +// Magic numbers used between all 3 files +export const SMALL_SIZE = 11; +export const LARGE_SIZE = 97; +const PRIME = 3457; +const PRIME2 = 1429; +const H2_SMALL_HASH_VALUE = 3; +const H2_LARGE_HASH_VALUE = 23 +export const INDEX = 0; +export const VALUE = 1; +export const POINTER = 2; +export const SPLIT_SIZE = 17; +export const FULL_SIGNAL = -1; + +// Magic character used between all 3 files +export const POINTER_VALUE = 'i' +export const EMPTY_CHAR = '-'; +export const DELETE_CHAR = 'X'; + +// Color indexes +export const Colors = { + Insert: 1, + Pending: 2, + Collision: 3, + Found: 1, + NotFound: 3, +}; + +// Graph indexes +export const HASH_GRAPH = { + Key: 0, + Value: 1, + Key2: 2, + Value2: 3 +} + +export const HASH_TYPE = { + Insert: 'I', + Search: 'S', + Delete: 'D', + BulkInsert: 'BI' +} + +// list of primes each roughly two times larger than previous +export const PRIMES = [ + 11, 23, 47, 97 +]; + +// find the table size from the table extracted from the visualizer +export function findTableSize(table) { + for (let i = 0; i < PRIMES.length; i++) { + if (table.length - PRIMES[i] < SPLIT_SIZE) { + return PRIMES[i]; + } + } + return 0; +} + +// which table size in PRIMES array to allow pointer +export const POINTER_CUT_OFF = 1; + +/** + * First hash function + * @param {*} chunker the chunker to step the visualized along with the calculation + * @param {*} bookmark the bookmark for chunker step + * @param {*} key the key to hash + * @param {*} tableSize the size of the table + * @param {*} toggleAnimate whether animation is carried out or not + * @returns the hashed value + */ +export function hash1(chunker, bookmark, key, tableSize, toggleAnimate) { + let hashed = (key * PRIME) % tableSize; // Hash the key + + if (toggleAnimate) { + // Update the graph + chunker.add( + bookmark, + (vis, val) => { + vis.graph.updateNode(HASH_GRAPH.Value, val); + vis.graph.select(HASH_GRAPH.Value); + }, + [hashed] + ) + } + + return hashed; // Return hashed value +} + +/** + * Second hash function + * @param {*} chunker the chunker to step the visualized along with the calculation + * @param {*} bookmark the bookmark for chunker step + * @param {*} key the key to hash + * @param {*} tableSize the size of the table + * @param {*} toggleAnimate whether animation is carried out or not + * @returns the hashed value + */ +export function hash2(chunker, bookmark, key, tableSize, toggleAnimate) { + let smallishPrime = tableSize == SMALL_SIZE ? H2_SMALL_HASH_VALUE : H2_LARGE_HASH_VALUE; // This variable is to limit the increment to 3 for small table and 23 for large + let hashed = (key * PRIME2) % smallishPrime + 1; // Hash the key + + if (toggleAnimate) { + // Update the graph + chunker.add( + bookmark, + (vis, val) => { + vis.graph.updateNode(HASH_GRAPH.Value2, val); + vis.graph.select(HASH_GRAPH.Value2); + }, + [hashed] + ) + } + + return hashed; // Return hashed value +} + +/** + * Calculate the increment for the key + * @param {*} chunker chunker to step the visualizer along with the calculations + * @param {*} bookmark bookmark to step chunker + * @param {*} key key to calculate the increment + * @param {*} tableSize size of the table + * @param {*} collisionHandling name of the algorithm, representing how collision is handled + * @param {*} type either search or insert because they have different stat updates + * @param {*} toggleAnimate whether animation is carried out or not + * @returns the calculated increment value + */ +export function setIncrement( + chunker, bookmark, key, tableSize, collisionHandling, type, toggleAnimate +) { + + // Increment = 1 if the algo is Linear Probing, and hashed value of second hash function if its Double Hashing + let increment; + switch (collisionHandling) { + case 'HashingLP': + increment = 1; + break; + case 'HashingDH': + increment = hash2(chunker, bookmark, key, tableSize, toggleAnimate); + break; + } + + if (toggleAnimate) { + // Show key, insertions and increment if the type is Insertion + if (type == HASH_TYPE.Insert || type == HASH_TYPE.Delete) { + chunker.add( + bookmark, + (vis, increment) => { + let kth = vis.array.getKth(); + vis.array.showKth({ + key: key, + type, + insertions: kth.insertions, + increment: increment + }); + }, + [increment] + ) + } + + // Show key\ and increment if the type is Search + else if (type == HASH_TYPE.Search) { + chunker.add( + bookmark, + (vis, increment) => { + vis.array.showKth({ + key: key, + type, + increment: increment + }); + }, + [increment] + ) + } + } + return increment; // Return calculated increment +} + +/** + * Reset the visualizations for a new cycle of either insertion, deletion or search + * @param {*} vis the visualzisers to reset + * @param {*} size table size + * @param {*} key key to insert/search/delete + * @param {*} name name of the algorithm/collision handling + */ +export function newCycle(vis, size, key, name) { + vis.array.unfill(INDEX, 0, undefined, size - 1); // Reset any coloring of slots + + if (size <= PRIMES[POINTER_CUT_OFF]) { + vis.array.resetVariable(POINTER); // Reset pointer + } + + // Update key value for the hashing graph and color them to emphasize hashing initialization + vis.graph.updateNode(HASH_GRAPH.Key, key); + vis.graph.updateNode(HASH_GRAPH.Value, ' '); + vis.graph.select(HASH_GRAPH.Key); + vis.graph.colorEdge(HASH_GRAPH.Key, HASH_GRAPH.Value, Colors.Pending) + if (name === "HashingDH") { + vis.graph.updateNode(HASH_GRAPH.Key2, key); + vis.graph.updateNode(HASH_GRAPH.Value2, ' '); + vis.graph.select(HASH_GRAPH.Key2); + vis.graph.colorEdge(HASH_GRAPH.Key2, HASH_GRAPH.Value2, Colors.Pending) + } +} diff --git a/src/algorithms/controllers/HashingDelete.js b/src/algorithms/controllers/HashingDelete.js new file mode 100644 index 000000000..7db125843 --- /dev/null +++ b/src/algorithms/controllers/HashingDelete.js @@ -0,0 +1,202 @@ +import { + hash1, + setIncrement, + HASH_GRAPH, + Colors, + INDEX, + POINTER, + POINTER_VALUE, + SMALL_SIZE, + VALUE, + DELETE_CHAR, + HASH_TYPE, + newCycle, + EMPTY_CHAR +} from './HashingCommon'; + +// Bookmarks to link chunker with pseudocode +const IBookmarks = { + ApplyHash: 16, + ChooseIncrement: 17, + InitDelete: 11, + WhileNot: 12, + MoveThrough: 13, + Found: 14, + Delete: 15, + NotFound: 18, + Pending: 19, +} + +/** + * Running function for chunker of delete, using the key provided + * @param {*} chunker the chunker for deletion + * @param {*} params parameters for deletion algorithm, e.g. name, key, insertion visualizer instances,... + * @returns whether the key is found or not + */ +export default function HashingDelete( + chunker, params, key, table, total +) { + + // Assigning parameter values to local variables + const ALGORITHM_NAME = params.name; + const SIZE = table.length; // Hash Modulo being used in the table + + // Chunker for intial state of visualizers + chunker.add( + IBookmarks.InitDelete, + (vis, target) => { + + vis.array.showKth({key: target, insertions: vis.array.getKth().insertions, type: HASH_TYPE.Delete}); // Show stats + + newCycle(vis, SIZE, key, ALGORITHM_NAME); // New delete cycle + }, + [key] + ); + + // Hashing the key + let i = hash1(chunker, IBookmarks.ApplyHash, key, SIZE, true); // Target value after being hashed + + /** This part is for Linear Probing and Double Hashing */ + if (ALGORITHM_NAME !== 'HashingCH') { + // Calculate increment for key + let increment = setIncrement(chunker, IBookmarks.ChooseIncrement, key, SIZE, params.name, HASH_TYPE.Delete, true); + + // Chunker for initial slot + chunker.add( + IBookmarks.WhileNot, + (vis, idx) => { + if (SIZE === SMALL_SIZE) { + vis.array.assignVariable(POINTER_VALUE, POINTER, idx); // Pointer only shows for small tables + } + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Pending); // Highlight initial search position + + // Uncoloring the graphs + vis.graph.deselect(HASH_GRAPH.Key); + vis.graph.deselect(HASH_GRAPH.Value); + vis.graph.removeEdgeColor(HASH_GRAPH.Key, HASH_GRAPH.Value); + if (ALGORITHM_NAME == "HashingDH") { + vis.graph.deselect(HASH_GRAPH.Key2); + vis.graph.deselect(HASH_GRAPH.Value2); + vis.graph.removeEdgeColor(HASH_GRAPH.Key2, HASH_GRAPH.Value2); + } + }, + [i] + ); + + let explored = 0; + // Search for the target key, checking each probed position + while (table[i] !== key && table[i] !== undefined && explored < SIZE) { + // Chunker for not matching + explored += 1; + chunker.add( + IBookmarks.WhileNot, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.NotFound); // Fill the slot with red if the slot does not match key + }, + [i] + ); + + // Move to the next index based on collision handling + i = (i + increment) % SIZE; + + // Chunker for probing + chunker.add( + IBookmarks.MoveThrough, + (vis, idx) => { + if (SIZE === SMALL_SIZE) { + vis.array.assignVariable(POINTER_VALUE, POINTER, idx); // Pointer is only shown for small tables + } + }, + [i] + ); + + // Chunker for searching the slots based on increment + chunker.add( + IBookmarks.WhileNot, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Pending); // Fill pending slots with yellow + }, + [i] + ); + } + + // Chunker for found + if (table[i] === key) { + chunker.add( + IBookmarks.Found, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Found); // Fill the slot with green, indicating that the key is found + }, + [i] + ); + // Replace found element with x + table[i] = DELETE_CHAR; + chunker.add( + IBookmarks.Delete, + (vis, val, idx) => { + vis.array.updateValueAt(VALUE, idx, val); + }, + [DELETE_CHAR, i] + ) + + return total - 1; // Decrement total + } + else { + chunker.add( + IBookmarks.NotFound, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.NotFound); // Fill the slot with green, indicating that the key is found + }, + [i] + ) + + return total; // Since the deletion key is not found, nothing is deleted + } + } + + /** This part is for Chaining */ + else { + + chunker.add( + IBookmarks.Pending, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Pending); // Fill pending slots with yellow + }, + [i] + ); + + if (table[i].includes(key)) { + const index = table[i].indexOf(key); + if (index > -1) table[i].splice(index, 1); // 2nd parameter means remove one item only + + chunker.add( + IBookmarks.Found, + (vis, idx, table) => { + // Modify the floating array + const popper = document.getElementById('float_box_' + idx); + popper.innerHTML = table[idx]; + + let firstItemOfChain = table[idx][0]; + if (firstItemOfChain != undefined) vis.array.updateValueAt(VALUE, idx, firstItemOfChain + '..'); + else vis.array.updateValueAt(VALUE, idx, EMPTY_CHAR); + + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Found); // Fill the slot with green, indicating that the key is found + }, + [i, table] + ); + + return total - 1; // Decrement total + } + else { + chunker.add( + IBookmarks.NotFound, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.NotFound); // Fill the slot with green, indicating that the key is found + }, + [i] + ) + + return total; // Since the deletion key is not found, nothing is deleted + } + } +} diff --git a/src/algorithms/controllers/HashingInsertion.js b/src/algorithms/controllers/HashingInsertion.js new file mode 100644 index 000000000..bdcf2e864 --- /dev/null +++ b/src/algorithms/controllers/HashingInsertion.js @@ -0,0 +1,585 @@ +import Array2DTracer from '../../components/DataStructures/Array/Array2DTracer'; +import GraphTracer from '../../components/DataStructures/Graph/GraphTracer'; +import { HashingExp } from '../explanations'; +import { + hash1, + setIncrement, + HASH_GRAPH, + EMPTY_CHAR, + Colors, + INDEX, + POINTER, + POINTER_VALUE, + SMALL_SIZE, + VALUE, + LARGE_SIZE, + SPLIT_SIZE, + DELETE_CHAR, + HASH_TYPE, + FULL_SIGNAL, + PRIMES, + POINTER_CUT_OFF, + newCycle +} from './HashingCommon'; +import { translateInput } from '../parameters/helpers/ParamHelper'; +import HashingDelete from './HashingDelete'; + +// Bookmarks to link chunker with pseudocode +const IBookmarks = { + Init: 1, + EmptyArray: 2, + InitInsertion: 3, + // IncrementInsertions: 4, + Hash1: 5, + ChooseIncrement: 6, + Probing: 7, + Collision: 8, + PutIn: 9, + Done: 10, + BulkInsert: 1, + CheckTableFull: 19, +} + +/** + * Create new arrays for expanded table + * @param {*} table the table to keep track of the internal and illustrated array + * @returns the new table, and the index, value, variable arrays for the visualiser + */ +function expandTable(table) { + let currSize = table.length; + let nextSize = PRIMES[PRIMES.indexOf(currSize) + 1]; + if (nextSize === undefined) return [null, null, null, null]; + + return [ + new Array(nextSize), + Array.from({ length: nextSize }, (_, i) => i), + Array(nextSize).fill(EMPTY_CHAR), + Array(nextSize).fill('') + ] +} + +export default { + explanation: HashingExp, + + // Initialize visualizers + initVisualisers() { + return { + array: { + instance: new Array2DTracer('array', null, 'Hash Table'), + order: 0, + }, + graph: { + instance: new GraphTracer('graph', null, 'Hashing Functions'), + order: 1, + }, + }; + }, + + /** + * Run function for insertion, using the user input to display the illustration through chunker + * @param {*} chunker the chunker for the illustrations + * @param {*} params different parameters of the algorithm insertion mode e.g. name, array size,... + * @returns a table of concluding array to serve testing purposes + */ + run(chunker, params) { + // Storing algorithms parameters as local variables + const ALGORITHM_NAME = params.name; + let inputs = params.values; + const SIZE = params.hashSize; + + // Initialize arrays + let indexArr = Array.from({ length: SIZE }, (_, i) => i); + let valueArr = Array(SIZE).fill(EMPTY_CHAR); + let nullArr = Array(SIZE).fill(''); + + // Variable to keep track of insertions done and total inputs hashed into the table + let insertions = 0; + let total = 0; + + /** + * Insertion function for each key + * @param {*} table the table to keep track of the internal and illustrated array + * @param {*} key the key to insert + * @param {*} prevIdx previous index of the previous key + * @param {*} isBulkInsert whether it is bulk insert or not + * @returns the index the key is assigned + */ + function hashInsert(table, key) { + // Chunker for when table is full + const limit = () => { + if (params.expand && table.length < LARGE_SIZE) return total + 1 === Math.round(table.length * 0.8); + return total === table.length - 1; + } + if (limit()) { + chunker.add( + IBookmarks.CheckTableFull, + (vis, total) => { + vis.array.showKth({fullCheck: "Table is filled " + total + "/" + table.length + " -> Table is full, " + + ((params.expand) ? "expanding table..." : "stopping...")}); + }, + [total] + ) + return FULL_SIGNAL; + } + + // Chunker for when the table is not full + else { + chunker.add( + IBookmarks.CheckTableFull, + (vis, total) => { + newCycle(vis, table.length, key, ALGORITHM_NAME); // New insert cycle + vis.array.showKth({fullCheck: "Table is filled " + total + "/" + table.length + " -> Table is not full, continuing..."}); + }, + [total] + ) + } + + insertions++; // Increment insertions + total++; // Increment total + + // Get initial hash index for current key + let i = hash1(chunker, IBookmarks.Hash1, key, table.length, true); + + // Calculate increment for current key + let increment = setIncrement( + chunker, + IBookmarks.ChooseIncrement, + key, + table.length, + ALGORITHM_NAME, + HASH_TYPE.Insert, + true + ); + + // Chunker for first pending slot + chunker.add( + IBookmarks.Probing, + (vis, idx) => { + + // Pointer only appear for small table + if (table.length < PRIMES[POINTER_CUT_OFF]) { + vis.array.assignVariable(POINTER_VALUE, POINTER, idx); + } + + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Pending); // Color pending slot + + // Uncolor the hashing graph + vis.graph.deselect(HASH_GRAPH.Key); + vis.graph.deselect(HASH_GRAPH.Value); + vis.graph.removeEdgeColor(HASH_GRAPH.Key, HASH_GRAPH.Value); + if (ALGORITHM_NAME == "HashingDH") { + vis.graph.deselect(HASH_GRAPH.Key2); + vis.graph.deselect(HASH_GRAPH.Value2); + vis.graph.removeEdgeColor(HASH_GRAPH.Key2, HASH_GRAPH.Value2); + } + }, + [i] + ) + + // Internal code for probing, while loop indicates finding an empty slot for insertion + while (table[i] !== undefined && table[i] !== key && table[i] !== DELETE_CHAR) { + let prevI = i; + i = (i + increment) % table.length; // This is to ensure the index never goes over table size + + // Chunker for collision + chunker.add( + IBookmarks.Collision, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Collision); // Fill the slot with red, indicating collision + }, + [prevI] + ) + + // Chunker for Probing + chunker.add( + IBookmarks.Probing, + (vis, idx) => { + + // Pointer only appears for small tables + if (table.length < PRIMES[POINTER_CUT_OFF]) { + vis.array.assignVariable(POINTER_VALUE, POINTER, idx); + } + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Pending); // Filling the pending slot with yellow + }, + [i] + ) + } + + // Internally assign the key to the index + table[i] = key; + + // Chunker for placing the key + chunker.add( + IBookmarks.PutIn, + (vis, val, idx) => { + vis.array.updateValueAt(VALUE, idx, val); // Update value of that index + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Insert); // Fill it green, indicating successful insertion + }, + [key, i, insertions] + ) + + // Return the insertion index + return i; + } + + + /** + * Function for bulk insertion + * @param {*} table the table to keep track of the internal and illustrated array + * @param {*} keys the keys to insert + * @returns the index the last key is assigned + */ + function hashBulkInsert(table, keys) { + let lastHash; + let inserts = {}; + let bulkInsertions = 0; + let prevTable = [...table]; + const limit = () => { + if (params.expand && table.length < LARGE_SIZE) return total + 1 === Math.round(table.length * 0.8); + return total === table.length - 1; + } + for (const key of keys) { + if (limit()) { + inserts[key] = FULL_SIGNAL; + lastHash = FULL_SIGNAL; + break; + } + + bulkInsertions++; + + // hashed value + let i = hash1(null, null, key, table.length, false); + + // increment for probing + let increment = setIncrement( + null, + null, + key, + table.length, + ALGORITHM_NAME, + HASH_TYPE.Insert, + false + ); + + while (table[i] !== undefined) { + i = (i + increment) % table.length; // This is to ensure the index never goes over table size + } + + table[i] = key; + inserts[key] = i; + lastHash = i; + } + + if (params.expand && (lastHash === FULL_SIGNAL)) { + table = [...prevTable]; + return lastHash; + } + + insertions += bulkInsertions; + total += bulkInsertions; + chunker.add( + IBookmarks.PutIn, + (vis, keys, inserts, insertions) => { + for (const key of keys) { + vis.array.updateValueAt(VALUE, inserts[key], key); // Update value of that index + vis.array.fill(INDEX, inserts[key], undefined, undefined, Colors.Insert); + } + vis.array.showKth({key: vis.array.getKth().key, type: HASH_TYPE.BulkInsert, insertions: insertions}); + }, + [keys, inserts, insertions] + ) + + return lastHash; + } + + + const REINSERT_CAPTION_LEN = 5; + + /** + * ReInsertion function for inserted key to new table + * @param {*} table the table to keep track of the internal and illustrated array + * @param {*} key the key to reinsert + * @param {*} prevTable rrray of emaining keys from old table to be inserted + * @returns the index the key is assigned + */ + function hashReinsert(table, key, prevTable) { + chunker.add( + IBookmarks.CheckTableFull, + (vis, prevTable) => { + newCycle(vis, table.length, key, ALGORITHM_NAME); // New insert cycle + vis.array.showKth({ + reinserting: key, + toReinsert: `${prevTable.slice(0, REINSERT_CAPTION_LEN)}` + + ((prevTable.length > REINSERT_CAPTION_LEN) ? `,...` : ``) + }); + }, + [prevTable] + ) + + + // Get initial hash index for current key + let i = hash1( + chunker, + IBookmarks.CheckTableFull, + key, + table.length, + false + ); + + // Calculate increment for current key + let increment = setIncrement( + chunker, + IBookmarks.CheckTableFull, + key, + table.length, + ALGORITHM_NAME, + HASH_TYPE.Insert, + false + ); + + // Chunker for first pending slot + chunker.add( + IBookmarks.CheckTableFull, + (vis, idx) => { + + // Pointer only appear for small table + if (table.length <= PRIMES[POINTER_CUT_OFF]) { + vis.array.assignVariable(POINTER_VALUE, POINTER, idx); + } + + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Pending); // Color pending slot + + // Uncolor the hashing graph + vis.graph.deselect(HASH_GRAPH.Key); + vis.graph.deselect(HASH_GRAPH.Value); + vis.graph.removeEdgeColor(HASH_GRAPH.Key, HASH_GRAPH.Value); + if (ALGORITHM_NAME == "HashingDH") { + vis.graph.deselect(HASH_GRAPH.Key2); + vis.graph.deselect(HASH_GRAPH.Value2); + vis.graph.removeEdgeColor(HASH_GRAPH.Key2, HASH_GRAPH.Value2); + } + }, + [i] + ) + + // Internal code for probing, while loop indicates finding an empty slot for insertion + while (table[i] !== undefined && table[i] !== key && table[i] !== DELETE_CHAR) { + let prevI = i; + i = (i + increment) % table.length; // This is to ensure the index never goes over table size + + // Chunker for collision + chunker.add( + IBookmarks.CheckTableFull, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Collision); // Fill the slot with red, indicating collision + }, + [prevI] + ) + + // Chunker for Probing + chunker.add( + IBookmarks.CheckTableFull, + (vis, idx) => { + + // Pointer only appears for small tables + if (table.length <= PRIMES[POINTER_CUT_OFF]) { + vis.array.assignVariable(POINTER_VALUE, POINTER, idx); + } + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Pending); // Filling the pending slot with yellow + }, + [i] + ) + } + + // Internally assign the key to the index + table[i] = key; + + // Chunker for placing the key + chunker.add( + IBookmarks.CheckTableFull, + (vis, val, idx) => { + vis.array.updateValueAt(VALUE, idx, val); // Update value of that index + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Insert); // Fill it green, indicating successful insertion + }, + [key, i, insertions] + ) + + // Return the insertion index + return i; + } + + + // Inserting inputs + let prevIdx; + // Init hash table + let table = new Array(SIZE); + let prevTable; + // Last input index + let lastInput = 0; + + // main loop allowing table extension + do { + prevIdx = null; + + chunker.add( + IBookmarks.Init, + (vis, size, array) => { + // Increase Array2D visualizer render space + if (SIZE === LARGE_SIZE) { + vis.array.setSize(3); + vis.array.setZoom(0.7); + vis.graph.setZoom(1.5); + } else { + vis.array.setZoom(1); + vis.graph.setZoom(1); + } + + // Initialize the array + vis.array.set(array, + params.name, + '', + INDEX, + { + rowLength: size > SMALL_SIZE ? SPLIT_SIZE : SMALL_SIZE, + rowHeader: ['Index', 'Value', ''] + } + ); + + vis.array.hideArrayAtIndex([VALUE, POINTER]); // Hide value and pointer row intially + + vis.graph.weighted(true); + + // Intialize the graphs + switch (ALGORITHM_NAME) { + case "HashingLP" : + vis.graph.set([[0, 'Hash'], [0, 0]], [' ', ' '], [[-5, 0], [5, 0]]); + break; + case "HashingDH" : + vis.graph.set([ + [0, 'Hash1', 0, 0], [0, 0, 0, 0], [0, 0, 0, 'Hash2'], [0, 0, 0, 0]], // Node edges + [' ', ' ', ' ', ' '], // Node values + [[-5, 2], [5, 2], [-5, -2], [5, -2]]); // Node positions + break; + } + }, + [table.length, table.length <= PRIMES[POINTER_CUT_OFF] ? + [indexArr, valueArr, nullArr] : + [indexArr, valueArr] + ] + ); + + // Chunker to initialize empty array visually + chunker.add( + IBookmarks.EmptyArray, + (vis) => { + // Show the value row + vis.array.hideArrayAtIndex(POINTER); + }, + ); + + // Chunker for intializing insertion stat + chunker.add( + IBookmarks.InitInsertion, + (vis, insertions) => { + vis.array.showKth( + (params.expand && (lastInput !== 0)) ? { + fullCheck: "Expanding Table" + } : { + key: "", + type: EMPTY_CHAR, + insertions: insertions, + increment: "", + } + ); + }, + [insertions] + ) + + // Magic numbers for length of splitting a postive integer string by "-", the index of "", and the number to delete when a negative integer is split by "-" + const POS_INTEGER_SPLIT_LENGTH = 1; + const EMPTY_DELETE_SPLIT_INDEX = 0; + const NUMBER_DELETE_SPLIT_INDEX = 1; + + if (params.expand && (lastInput !== 0)) { + while (prevTable.length > 0) { + let key = prevTable[0]; + prevTable.shift(); + hashReinsert(table, key, prevTable); + } + } + + for (let i = lastInput; i < inputs.length; i++) { + let item = inputs[i]; + + // Different cases of insertion and deletion + let split_arr = item.split("-"); + if (split_arr.length == POS_INTEGER_SPLIT_LENGTH) { // When the input is a positive integer -> normal insert + for (const key of translateInput(item, "Array")) { + prevIdx = hashInsert(table, key, false); + } + } + else { + if (split_arr[EMPTY_DELETE_SPLIT_INDEX] === "") { // When the input is a negative integer -> delete + let key = Number(split_arr[NUMBER_DELETE_SPLIT_INDEX]); + total = HashingDelete(chunker, params, key, table, total); + } + else { // When the input is a range -> bulk insert + // Preparation for bulk insertion + chunker.add( + IBookmarks.BulkInsert, + (vis, insertions, prevIdx) => { + vis.array.unfill(INDEX, 0, undefined, table.length - 1); // Reset any coloring of slots + vis.array.showKth({key: item, type: HASH_TYPE.BulkInsert, insertions: insertions, increment: ""}); + if (table.length <= PRIMES[POINTER_CUT_OFF]) + vis.array.assignVariable("", POINTER, prevIdx, POINTER_VALUE); // Hide pointer + + vis.graph.updateNode(HASH_GRAPH.Key, ' '); + vis.graph.updateNode(HASH_GRAPH.Value, ' '); + if (ALGORITHM_NAME === "HashingDH") { + vis.graph.updateNode(HASH_GRAPH.Key2, ' '); + vis.graph.updateNode(HASH_GRAPH.Value2, ' '); + } + }, + [insertions, prevIdx] + ) + prevIdx = hashBulkInsert(table, translateInput(item, "Array")); + } + } + + // when table is full or almost full + if (prevIdx === FULL_SIGNAL) { + lastInput = i; + prevTable = table.filter(n => n !== undefined); + if (params.expand && (table.length < LARGE_SIZE)) [table, indexArr, valueArr, nullArr] = expandTable(table); + break; + } + } + } while (params.expand && (prevIdx === FULL_SIGNAL) && (table.length < LARGE_SIZE)); + + // Chunker for resetting visualizers in case of new insertion cycle + chunker.add( + IBookmarks.Done, + (vis) => { + + vis.array.showKth({key: "", type: EMPTY_CHAR, insertions: insertions, increment: ""}) // Nullify some stats, for better UI + + // Hide pointer + if (table.length <= PRIMES[POINTER_CUT_OFF]) { + vis.array.assignVariable(POINTER_VALUE, POINTER, undefined); + } + + vis.array.unfill(INDEX, 0, undefined, table.length - 1); // Unfill all boxes + + // Reset graphs and uncolor the graph if needed + vis.graph.updateNode(HASH_GRAPH.Key, ' '); + vis.graph.updateNode(HASH_GRAPH.Value, ' '); + if (ALGORITHM_NAME === 'HashingDH') { + vis.graph.updateNode(HASH_GRAPH.Key2, ' '); + vis.graph.updateNode(HASH_GRAPH.Value2, ' '); + } + }, + ) + + return table; // Return resulting array for testing + }, +}; diff --git a/src/algorithms/controllers/HashingSearch.js b/src/algorithms/controllers/HashingSearch.js new file mode 100644 index 000000000..ffa5deb80 --- /dev/null +++ b/src/algorithms/controllers/HashingSearch.js @@ -0,0 +1,223 @@ +import { + hash1, + setIncrement, + HASH_GRAPH, + EMPTY_CHAR, + Colors, + INDEX, + POINTER, + POINTER_VALUE, + SMALL_SIZE, + DELETE_CHAR, + HASH_TYPE, + PRIMES, + POINTER_CUT_OFF, + newCycle, + findTableSize +} from './HashingCommon'; + +// Bookmarks to link chunker with pseudocode +const IBookmarks = { + Init: 1, + ApplyHash: 5, + ChooseIncrement: 6, + WhileNot: 2, + Probing: 3, + CheckValue: 4, + Found: 7, + NotFound: 8, + Pending: 9 +} + +export default { + + // Initialize visualizers + initVisualisers({ visualisers }) { + return { + array: { + instance: visualisers.array.instance, + order: 0, + }, + graph: { + instance: visualisers.graph.instance, + order: 1, + }, + }; + }, + + /** + * Running function for chunker of search, using the key provided + * @param {*} chunker the chunker for searching + * @param {*} params parameters for searching algorithm, e.g. name, key, insertion visualizer instances,... + * @returns whether the key is found or not + */ + run(chunker, params) { + + // Assigning parameter values to local variables + const ALGORITHM_NAME = params.name; + const TARGET = params.target; // Target value we are searching for + let table = params.visualisers.array.instance.extractArray(1, EMPTY_CHAR); // The table with inserted values + const SIZE = findTableSize(table); // Hash Modulo being used in the table + + // Variable for testing + let found = true; + + // Chunker for intial state of visualizers + chunker.add( + IBookmarks.Init, + (vis, target) => { + + vis.array.showKth({key: target, type: HASH_TYPE.Search}); // Show stats + + newCycle(vis, SIZE, target, ALGORITHM_NAME); + }, + [TARGET] + ); + + // Hashing the key + let i = hash1(chunker, IBookmarks.ApplyHash, TARGET, SIZE, true); // Target value after being hashed + + /** This part is for Linear Probing and Double Hashing */ + if (ALGORITHM_NAME !== 'HashingCH') { + // Calculate increment for key + let increment = setIncrement(chunker, IBookmarks.ChooseIncrement, TARGET, SIZE, params.name, HASH_TYPE.Search, true); + + // Chunker for initial slot + chunker.add( + IBookmarks.WhileNot, + (vis, idx) => { + if (SIZE <= PRIMES[POINTER_CUT_OFF]) { + vis.array.assignVariable(POINTER_VALUE, POINTER, idx); // Pointer only shows for small tables + } + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Pending); // Highlight initial search position + + // Uncoloring the graphs + vis.graph.deselect(HASH_GRAPH.Key); + vis.graph.deselect(HASH_GRAPH.Value); + vis.graph.removeEdgeColor(HASH_GRAPH.Key, HASH_GRAPH.Value); + if (ALGORITHM_NAME == "HashingDH") { + vis.graph.deselect(HASH_GRAPH.Key2); + vis.graph.deselect(HASH_GRAPH.Value2); + vis.graph.removeEdgeColor(HASH_GRAPH.Key2, HASH_GRAPH.Value2); + } + }, + [i] + ); + + let explored = 0; + // Search for the target key, checking each probed position + while (table[i] !== TARGET && table[i] !== undefined && explored < SIZE) { + explored += 1; + + // Chunker for not matching + chunker.add( + IBookmarks.WhileNot, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Collision); // Fill the slot with red if the slot does not match key + }, + [i] + ); + + // Move to the next index based on collision handling + i = (i + increment) % SIZE; + + // Chunker for probing + chunker.add( + IBookmarks.Probing, + (vis, idx) => { + if (SIZE <= PRIMES[POINTER_CUT_OFF]) { + vis.array.assignVariable(POINTER_VALUE, POINTER, idx); // Pointer is only shown for small tables + } + }, + [i] + ); + + // Chunker for searching the slots based on increment + chunker.add( + IBookmarks.WhileNot, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Pending); // Fill pending slots with yellow + }, + [i] + ); + } + + // Chunker for found + if (table[i] === TARGET) { + chunker.add( + IBookmarks.Found, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Insert); // Fill the slot with green, indicating that the key is found + }, + [i] + ); + found = true; // Set testing variable + } + + // Chunker for not found + else { + chunker.add( + IBookmarks.NotFound, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Collision); // Fill last slot with red + }, + [i] + ); + found = false; // Set testing variable + } + return found; // Return found or not for testing + } + + /** This part is for Chaining */ + else { + + chunker.add( + IBookmarks.Pending, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Pending); // Fill pending slots with yellow + }, + [i] + ); + + // Chunker for found + if (table[i] != undefined) { + if (Array.isArray(table[i])) { + if (table[i].includes(TARGET)) { + chunker.add( + IBookmarks.Found, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Insert); // Fill the slot with green, indicating that the key is found + }, + [i] + ); + found = true; // Set testing variable + } + if (table[i] === TARGET) { + chunker.add( + IBookmarks.Found, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Insert); // Fill the slot with green, indicating that the key is found + }, + [i] + ); + found = true; // Set testing variable + } + } + } + + // Chunker for not found + else { + chunker.add( + IBookmarks.NotFound, + (vis, idx) => { + vis.array.fill(INDEX, idx, undefined, undefined, Colors.Collision); // Fill last slot with red + found = false; // Set testing variable + }, + [i] + ); + found = false; // Set testing variable + } + return found; // Return found or not for testing + } + }, +}; diff --git a/src/algorithms/controllers/index.js b/src/algorithms/controllers/index.js index 0032e15a8..3d126dad7 100644 --- a/src/algorithms/controllers/index.js +++ b/src/algorithms/controllers/index.js @@ -23,3 +23,7 @@ export { default as DFSrec } from './DFSrec'; export { default as prim_old } from './prim_old'; export { default as prim } from './prim'; export { default as kruskal } from './kruskal'; +export { default as HashingInsertion } from './HashingInsertion'; +export { default as HashingSearch } from './HashingSearch'; +export { default as HashingDelete } from './HashingDelete'; +export { default as HashingChainingInsertion} from './HashingChainingInsertion' diff --git a/src/algorithms/controllers/tests/HashingDeletion.test.js b/src/algorithms/controllers/tests/HashingDeletion.test.js new file mode 100644 index 000000000..9822ad389 --- /dev/null +++ b/src/algorithms/controllers/tests/HashingDeletion.test.js @@ -0,0 +1,80 @@ +/* The purpose of the test here is to detect whether the correct result is generated + under the legal input, not to test its robustness, because this is not considered + in the implementation process of the algorithm. +*/ + +/* eslint-disable no-undef */ + +import { LARGE_SIZE, SMALL_SIZE } from '../HashingCommon'; +import HashingInsertion from '../HashingInsertion'; + +// Simple stub for the chunker +const chunker = { + add: () => {}, +}; + +describe('HashingDeletion', () => { + // Test cases for Linear Probing + it('LP insert small table, delete 1 element after all has been inserted', () => { + const input = ["12", "10", "18", "6", "21", "48", "47", "49", "24", "26", "-18"]; + const result = [47, 48, 26, 12, 49, undefined, 24, 6, 10, 21, "X"]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingLP" })).toEqual(result); + }); + it('LP delete element in the middle of insertion', () => { + const input = ["3", "2", "10", "18", "-10", "28", "36", "25", "17","22","44"]; + const result = [36, 25, 22, 44, undefined, undefined, 2, 28, 17, 3, 18]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingLP" })).toEqual(result); + }); + it('LP delete element, table contains no empty indices', () => { + const input = ["29","40","15","14","43","10","16","48","12","18","-12","46"]; + const result = [40, 15, 10, 48, 16, "X", 18, 46, 43, 14, 29]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingLP" })).toEqual(result); + }); + it('LP attempt to delete non-existent element from non-empty table', () => { + const input = ["29","40","15","14","43","10","16","48","12","18","-12","46","-99"]; + const result = [40, 15, 10, 48, 16, "X", 18, 46, 43, 14, 29]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingLP" })).toEqual(result); + }); + it('LP delete everything', () => { + const input = ["23", "11", "38", "22", "19", "3", "26", "35", "14", "37","-23", "-11", "-38", "-22", "-19", "-3", "-26", "-35", "-14", "-37"]; + const result = ["X", "X", "X", "X", "X", "X", "X", "X", undefined, "X", "X"]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingDH" })).toEqual(result); + }); + it('LP delete multiple elements during insertion in large table', () => { + const input = ["34","57","55","43","42","100","71","62","-55","78","97","-43","45","59","87","-34","70","73","67","68","20","77","26","4","46","32","95","49","56","51","58","2","89","-77","66","18","98","27","48","13","36","84","10","74","-67","63","28","39","54","65","61","35","7","82","72","14","93","50","79","8","37","1","80","76","44","94","11","52","86","33","31","96","81","64","40","85","21","17","90","83","38","22","23","53","30","60","41","29","16","9","15","19","47","99","69","25","12","5","75","6","91"]; + const result = [97, 36, 72, 11, 47, 83, 22, 58, 94, 33, 69, 8, 44, 80, 19, "X", 91, 30, 66, 5, 41, "X", 16 , 52, undefined, 27, 63, 2, 38, 74, 13, 49, 85, 99, 60, 96, 35, 71, 10, 46, 82, 21, 57, 93, 32, 68, 7, "X", 79, 18, 54, 90, 29, 65, 4, 40, 76, 15, 51, 87, 26, 62, 98, 37, 73, 1, 48, 84, 23, 59, 95, 12, 70, 9, 45, 81, 20, 56, undefined, 31, "X", 6, 42, 78, 17, 53, 89, 28, 64, 100, 39, 75, 14, 50, 86, 25, 61]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: LARGE_SIZE, name: "HashingLP" })).toEqual(result); + }); + + // Test cases for Double Hashing + it('DH insert small table, delete 1 element after all has been inserted', () => { + const input = ["31","13","9","17","6","43","2","15","3","50","-9"]; + const result = [3, 43, 15, 50, undefined, 31, 13, "X", 6, 2, 17]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingDH" })).toEqual(result); + }); + it('DH delete element in the middle of insertion', () => { + const input = ["9", "29", "12", "46", "33", "2", "-46", "15", "37", "6", "18"]; + const result = [33, 15, 18, 12, undefined, 9, "X", 37, 6, 2, 29]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingDH" })).toEqual(result); + }); + it('DH delete element, table contains no empty indices and deletion probe passes through a deleted index', () => { + const input = ["30", "11", "24", "27", "14", "46", "44", "41", "50", "12", "-14", "92", "-12"]; + const result = [11, 44, 30, 41, 27, "X", 24, 46, 50, 92, undefined]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingLP" })).toEqual(result); + }); + it('DH attempt to delete non-existent element from non-empty table', () => { + const input = ["14", "16", "25", "42", "32", "10", "18", "20", "5", "29", "-20", "97", "-53"]; + const result = [25, 18, 29, "X", 16, 42, 97, 5, 32, 14, 10]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingDH" })).toEqual(result); + }); + it('DH delete everything', () => { + const input = ["48","50","36","27","49","19","41","24","45","32","-48","-50","-36","-27","-49","-19","-41","-24","-45","-32"]; + const result = ["X", "X", "X", "X", "X", "X", "X", "X", "X", "X", undefined]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingDH" })).toEqual(result); + }); + it('DH delete multiple elements during insertion in large table', () => { + const input = ["44", "26", "83", "49", "95", "22", "1", "84", "-83", "4", "31", "63", "33", "51", "-95", "-22", "47", "53", "68", "81", "72", "7", "23", "34", "32", "69", "6", "-34", "-81", "40", "5", "99", "52", "79", "14", "-40", "50", "15", "73", "86", "98", "70", "19", "39","61", "38", "96", "30", "54", "-86", "67", "12", "17", "62", "-73", "75", "80", "94", "82", "13", "85", "78", "45", "-67", "88", "24", "42","89", "28", "56", "21", "74", "92", "97", "27", "25", "3", "100", "43", "18", "91", "37", "93", "11", "87", "29", "8", "20", "16", "58", "9", "65", "10", "36", "60", "2", "66", "59", "64", "77", "57", "48", "76", "46", "55"]; + const result = [97, 36, 72, 98, 47, "X", 100, 58, 94, 33, 69, 8, 44, 80, 19, 55, 91, 30, 66, 5, undefined, 77, 16, 52, 88, 27, 63, 99, 38, 74, 13, 49, 85, 24, 60, 96, 11, undefined, 10, 46, 82, 21, 57, 93, 32, 68, 7, 43, 79, 18, 54, undefined, 29, 65, 4, 2, 76, 15, 51, 87, 26, 62, 1, 37, "X", 12, 48, 84, 23, 59, "X", "X", 70, 9, 45, "X", 20, 56, 92, 31, "X", 6, 42, 78, 17, 53, 89, 28, 64, 3, 39, 75, 14, 50, "X", 25, 61]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: LARGE_SIZE, name: "HashingDH" })).toEqual(result); + }); +}); diff --git a/src/algorithms/controllers/tests/HashingInsertion.test.js b/src/algorithms/controllers/tests/HashingInsertion.test.js new file mode 100644 index 000000000..a932418ea --- /dev/null +++ b/src/algorithms/controllers/tests/HashingInsertion.test.js @@ -0,0 +1,60 @@ +/* The purpose of the test here is to detect whether the correct result is generated + under the legal input, not to test its robustness, because this is not considered + in the implementation process of the algorithm. +*/ + +/* eslint-disable no-undef */ + +import { LARGE_SIZE, SMALL_SIZE } from '../HashingCommon'; +import HashingInsertion from '../HashingInsertion'; + +// Simple stub for the chunker +const chunker = { + add: () => {}, +}; + +describe('HashingInsertion', () => { + // Test cases for Linear Probing + it('LP insert small table', () => { + const input = ["42", "87", "16", "59", "23", "74", "31", "5", "68", "90"]; + const result = [undefined, 59, 74, 23, 16, 42, 31, 5, 87, 68, 90]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingLP" })).toEqual(result); + }); + it('LP insert with duplicates', () => { + const input = ["14", "62", "14", "33", "57", "62", "85", "33"]; + const result = [33, undefined, 85, undefined, undefined, undefined, 57, undefined, undefined, 14, 62]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingLP" })).toEqual(result); + }); + it('LP insert with bulk insert', () => { + const input = ["14", "2-6-2", "8-10", "3"]; + const result = [undefined, 4, 8, undefined, undefined, 9, 2, 6, 10, 14, 3]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingLP" })).toEqual(result); + }); + it('LP insert large table', () => { + const input = ["1", "57", "84", "39", "12", "93", "66", "2", "48", "76", "35", "26", "49", "19", "87", "73", "62", "28", "17", "8", "94", "33", "70", "30", "11", "45", "38", "81", "15", "5", "60", "46", "32", "88", "27", "86", "69", "3", "54", "24", "77", "22", "72", "91", "41", "78", "25", "90", "34", "44", "52", "130", "196", "14", "23", "31", "42", "125"]; + const result = [undefined, undefined, 72, 11, undefined, undefined, 22, undefined, 94, 33, 69, 8, 44, 130, 19, undefined, 91, 30, 66, 5, 41, 77, undefined, 52, 88, 27, undefined, 2, 38, 196, undefined, 49, undefined, 24, 60, undefined, 35, undefined, undefined, 46, undefined, undefined, 57, 93, 32, undefined, undefined, undefined, undefined, undefined, 54, 90, undefined, undefined, undefined, undefined, 76, 15, undefined, 87, 26, 62, 1, undefined, 73, 12, 48, 84, 23, undefined, undefined, 34, 70, undefined, 45, 81, undefined, undefined, undefined, 31, undefined, undefined, 42, 78, 17, undefined, undefined, 28, 125, 3, 39, undefined, 14, undefined, 86, 25, undefined]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: LARGE_SIZE, name: "HashingLP" })).toEqual(result); + }); + + // Test cases for Double Hashing + it('DH insert small table', () => { + const input = ["42", "87", "16", "59", "23", "74", "31", "5", "68", "90"]; + const result = [undefined, 59, 74, 23, 16, 42, 68, 31, 87, 90, 5]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingDH" })).toEqual(result); + }); + it('DH insert with duplicates', () => { + const input = ["14", "62", "14", "33", "57", "62", "85", "33"]; + const result = [33, undefined, 85, undefined, undefined, undefined, 57, undefined, undefined, 14, 62]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingDH" })).toEqual(result); + }); + it('DH insert with bulk insert', () => { + const input = ["14", "2-6-2", "8-10", "3"]; + const result = [undefined, 4, 8, undefined, undefined, 9, 2, 6, 10, 14, 3]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: SMALL_SIZE, name: "HashingDH" })).toEqual(result); + }); + it('DH insert large table', () => { + const input = ["1", "57", "84", "39", "12", "93", "66", "2", "48", "76", "35", "26", "49", "19", "87", "73", "62", "28", "17", "8", "94", "33", "70", "30", "11", "45", "38", "81", "15", "5", "60", "46", "32", "88", "27", "86", "69", "3", "54", "24", "77", "22", "72", "91", "41", "78", "25", "90", "34", "44", "52", "130", "196", "14", "23", "31", "42", "125"]; + const result = [undefined, undefined, 72, 11, undefined, undefined, 22, undefined, 94, 33, 69, 8, 44, undefined, 19, undefined, 91, 30, 66, 5, 41, 77, 125, 52, 88, 27, undefined, 2, 38, undefined, undefined, 49, 130, 24, 60, undefined, 35, undefined, undefined, 46, undefined, 196, 57, 93, 32, undefined, undefined, undefined, undefined, undefined, 54, 90, undefined, undefined, undefined, undefined, 76, 15, undefined, 87, 26, 62, 1, undefined, 73, 12, 48, 84, 23, undefined, undefined, 34, 70, undefined, 45, 81, undefined, undefined, undefined, 31, undefined, undefined, 42, 78, 17, undefined, undefined, 28, undefined, 3, 39, undefined, 14, undefined, 86, 25, undefined]; + expect(HashingInsertion.run(chunker, { values: input, hashSize: LARGE_SIZE, name: "HashingDH" })).toEqual(result); + }); +}); diff --git a/src/algorithms/controllers/tests/HashingSearch.test.js b/src/algorithms/controllers/tests/HashingSearch.test.js new file mode 100644 index 000000000..bb36acd23 --- /dev/null +++ b/src/algorithms/controllers/tests/HashingSearch.test.js @@ -0,0 +1,148 @@ +/* +The purpose of the test here is to detect whether the correct result is generated +*/ + +/* eslint-disable no-undef */ + +import { LARGE_SIZE, SMALL_SIZE } from '../HashingCommon'; +import HashingInsertion from '../HashingInsertion'; +import HashingSearch from '../HashingSearch'; + +// Simple stub for the chunker +const chunker = { + add: () => {}, +}; + +describe('HashingSearch', () => { + // Search tests for Linear Probing + it('Search small table found LP', () => { + const arr = ["x", 59, 74, 23, 16, 42, 31, 5, 87, 68, 90]; + const target = 16; + const found = true; + const hashSize = SMALL_SIZE; + const visualisers = { + array: { + instance: { + extractArray(row = [1], empty = "x") { + return arr; + } + }, + }, + }; + expect(HashingSearch.run(chunker, { name: "HashingLP", visualisers, target, hashSize })).toEqual(found); + }); + it('Search small table not found LP', () => { + const arr = ["x", 59, 74, 23, 16, 42, 31, 5, 87, 68, 90]; + const target = 20; + const found = false; + const hashSize = SMALL_SIZE; + const visualisers = { + array: { + instance: { + extractArray(row = [1], empty = "x") { + return arr; + } + }, + }, + }; + expect(HashingSearch.run(chunker, { name: "HashingLP", visualisers, target, hashSize })).toEqual(found); + }); + it('Search with duplicates LP', () => { + const arr = [14, 33, 62, 85, 33, "x", 57, "x", "x", 14, 62]; + const target = 33; + const found = true; + const hashSize = SMALL_SIZE; + const visualisers = { + array: { + instance: { + extractArray(row = [1], empty = "x") { + return arr; + } + }, + }, + }; + expect(HashingSearch.run(chunker, { name: "HashingLP", visualisers, target, hashSize })).toEqual(found); + }); + it('Search large table LP', () => { + const arr = ["x", "x", 72, 11, "x", "x", 22, "x", 94, 33, 69, 8, 44, 130, 19, "x", 91, 30, 66, 5, 41, 77, "x", 52, 88, 27, "x", 2, 38, 196, "x", 49, "x", 24, 60, "x", 35, "x", "x", 46, "x", "x", 57, 93, 32, "x", "x", "x", "x", "x", 54, 90, "x", "x", "x", "x", 76, 15, "x", 87, 26, 62, 1, "x", 73, 12, 48, 84, 23, "x", "x", 34, 70, "x", 45, 81, "x", "x", "x", 31, "x", "x", 42, 78, 17, "x", "x", 28, 125, 3, 39, "x", 14, "x", 86, 25, "x"]; + const target = 66; + const found = true; + const hashSize = LARGE_SIZE; + const visualisers = { + array: { + instance: { + extractArray(row = [1], empty = "x") { + return arr; + } + }, + }, + }; + expect(HashingSearch.run(chunker, { name: "HashingLP", visualisers, target, hashSize })).toEqual(found); + }); + + // Search tests for Double Hashing + it('Search small table found DH', () => { + const arr = ["x", 59, 74, 23, 16, 42, 68, 31, 87, 90, 5]; + const target = 16; + const found = true; + const hashSize = SMALL_SIZE; + const visualisers = { + array: { + instance: { + extractArray(row = [1], empty = "x") { + return arr; + } + }, + }, + }; + expect(HashingSearch.run(chunker, { name: "HashingDH", visualisers, target, hashSize })).toEqual(found); + }); + it('Search small table not found DH', () => { + const arr = ["x", 59, 74, 23, 16, 42, 68, 31, 87, 90, 5]; + const target = 20; + const found = false; + const hashSize = SMALL_SIZE; + const visualisers = { + array: { + instance: { + extractArray(row = [1], empty = "x") { + return arr; + } + }, + }, + }; + expect(HashingSearch.run(chunker, { name: "HashingDH", visualisers, target, hashSize })).toEqual(found); + }); + it('Search with duplicates DH', () => { + const arr = [33, 14, 62, 33, 85, "x", 57, "x", "x", 14, 62]; + const target = 33; + const found = true; + const hashSize = SMALL_SIZE; + const visualisers = { + array: { + instance: { + extractArray(row = [1], empty = "x") { + return arr; + } + }, + }, + }; + expect(HashingSearch.run(chunker, { name: "HashingDH", visualisers, target, hashSize })).toEqual(found); + }); + it('Search large table DH', () => { + const arr = [97, 36, "x", 11, "x", "x", 22, 58, 94, 33, "x", 8, "x", "x", 19, "x", 128, "x", 66, 5, "x", 77, 113, 59, 88, 27, "x", 2, 16, 171, "x", 24, "x", "x", "x", 96, 35, "x", 174, 143, 179, "x", "x", 93, "x", 74, 10, "x", "x", 18, 54, "x", "x", 65, 101, 137, 173, "x", 51, "x", 123, "x", 1, "x", "x", 12, 145, 84, 120, 156, 95, 34, 167, 106, 142, 178, "x", "x", "x", 31, 4, 200, 181, "x", "x", "x", 89, "x", "x", "x", 39, 75, "x", 186, "x", "x", 61]; + const target = 66; + const found = true; + const hashSize = LARGE_SIZE; + const visualisers = { + array: { + instance: { + extractArray(row = [1], empty = "x") { + return arr; + } + }, + }, + }; + expect(HashingSearch.run(chunker, { name: "HashingDH", visualisers, target, hashSize })).toEqual(found); + }); +}); diff --git a/src/algorithms/controllers/TTFTree.test.js b/src/algorithms/controllers/tests/TTFTree.test.js similarity index 96% rename from src/algorithms/controllers/TTFTree.test.js rename to src/algorithms/controllers/tests/TTFTree.test.js index 34f769a11..4b169113e 100644 --- a/src/algorithms/controllers/TTFTree.test.js +++ b/src/algorithms/controllers/tests/TTFTree.test.js @@ -1,7 +1,7 @@ /* eslint-disable no-undef */ -import TTFTreeInsertion from './TTFTreeInsertion'; -import TTFTreeSearch from './TTFTreeSearch'; -import VariableTreeNode from '../../components/DataStructures/Graph/NAryTreeTracer/NAryTreeVariable'; +import TTFTreeInsertion from '../TTFTreeInsertion'; +import TTFTreeSearch from '../TTFTreeSearch'; +import VariableTreeNode from '../../../components/DataStructures/Graph/NAryTreeTracer/NAryTreeVariable'; // simple stub for the chunker const chunker = { diff --git a/src/algorithms/controllers/binaryTreeInsertion.test.js b/src/algorithms/controllers/tests/binaryTreeInsertion.test.js similarity index 96% rename from src/algorithms/controllers/binaryTreeInsertion.test.js rename to src/algorithms/controllers/tests/binaryTreeInsertion.test.js index a53ba213d..c4914b913 100644 --- a/src/algorithms/controllers/binaryTreeInsertion.test.js +++ b/src/algorithms/controllers/tests/binaryTreeInsertion.test.js @@ -5,7 +5,7 @@ /* eslint-disable no-undef */ -import binaryTreeInsertion from './binaryTreeInsertion'; +import binaryTreeInsertion from '../binaryTreeInsertion'; // Simple stub for the chunker const chunker = { diff --git a/src/algorithms/controllers/binaryTreeSearch.test.js b/src/algorithms/controllers/tests/binaryTreeSearch.test.js similarity index 96% rename from src/algorithms/controllers/binaryTreeSearch.test.js rename to src/algorithms/controllers/tests/binaryTreeSearch.test.js index b55edebfb..27524bec5 100644 --- a/src/algorithms/controllers/binaryTreeSearch.test.js +++ b/src/algorithms/controllers/tests/binaryTreeSearch.test.js @@ -4,7 +4,7 @@ */ /* eslint-disable no-undef */ -import binaryTreeSearch from './binaryTreeSearch'; +import binaryTreeSearch from '../binaryTreeSearch'; // Simple stub for the chunker const chunker = { add: () => {}, diff --git a/src/algorithms/controllers/heapSort.test.js b/src/algorithms/controllers/tests/heapSort.test.js similarity index 97% rename from src/algorithms/controllers/heapSort.test.js rename to src/algorithms/controllers/tests/heapSort.test.js index 3e7539555..c12f70e0f 100644 --- a/src/algorithms/controllers/heapSort.test.js +++ b/src/algorithms/controllers/tests/heapSort.test.js @@ -5,7 +5,7 @@ /* eslint-disable no-undef */ -import heapSort from './heapSort'; +import heapSort from '../heapSort'; // Simple stub for the chunker const chunker = { diff --git a/src/algorithms/controllers/prim.test.js b/src/algorithms/controllers/tests/prim.test.js similarity index 98% rename from src/algorithms/controllers/prim.test.js rename to src/algorithms/controllers/tests/prim.test.js index 8514c272f..afdf4afac 100644 --- a/src/algorithms/controllers/prim.test.js +++ b/src/algorithms/controllers/tests/prim.test.js @@ -5,7 +5,7 @@ /* eslint-disable no-undef */ -import prim from './prim'; +import prim from '../prim'; // Simple stub for the chunker const chunker = { diff --git a/src/algorithms/controllers/prim_old.test.js b/src/algorithms/controllers/tests/prim_old.test.js similarity index 98% rename from src/algorithms/controllers/prim_old.test.js rename to src/algorithms/controllers/tests/prim_old.test.js index 8514c272f..afdf4afac 100644 --- a/src/algorithms/controllers/prim_old.test.js +++ b/src/algorithms/controllers/tests/prim_old.test.js @@ -5,7 +5,7 @@ /* eslint-disable no-undef */ -import prim from './prim'; +import prim from '../prim'; // Simple stub for the chunker const chunker = { diff --git a/src/algorithms/controllers/quickSort.test.js b/src/algorithms/controllers/tests/quickSort.test.js similarity index 97% rename from src/algorithms/controllers/quickSort.test.js rename to src/algorithms/controllers/tests/quickSort.test.js index ed4ca5322..871a09380 100644 --- a/src/algorithms/controllers/quickSort.test.js +++ b/src/algorithms/controllers/tests/quickSort.test.js @@ -5,7 +5,7 @@ /* eslint-disable no-undef */ -import quickSort from './quickSort'; +import quickSort from '../quickSort'; // Simple stub for the chunker const chunker = { diff --git a/src/algorithms/controllers/transitiveClosure.test.js b/src/algorithms/controllers/tests/transitiveClosure.test.js similarity index 87% rename from src/algorithms/controllers/transitiveClosure.test.js rename to src/algorithms/controllers/tests/transitiveClosure.test.js index b7b02557f..7bbff06b1 100644 --- a/src/algorithms/controllers/transitiveClosure.test.js +++ b/src/algorithms/controllers/tests/transitiveClosure.test.js @@ -5,10 +5,10 @@ /* eslint-disable no-undef */ -import Array2DTracer from '../../components/DataStructures/Array/Array2DTracer'; -import GraphTracer from '../../components/DataStructures/Graph/GraphTracer'; -import Chunker from '../../context/chunker'; -import transitiveClosure from './transitiveClosure'; +import Array2DTracer from '../../../components/DataStructures/Array/Array2DTracer'; +import GraphTracer from '../../../components/DataStructures/Graph/GraphTracer'; +import Chunker from '../../../context/chunker'; +import transitiveClosure from '../transitiveClosure'; // Simple stub for the chunker diff --git a/src/algorithms/controllers/unionFind.test.js b/src/algorithms/controllers/tests/unionFind.test.js similarity index 96% rename from src/algorithms/controllers/unionFind.test.js rename to src/algorithms/controllers/tests/unionFind.test.js index 30016a0a5..f9f9f2e02 100644 --- a/src/algorithms/controllers/unionFind.test.js +++ b/src/algorithms/controllers/tests/unionFind.test.js @@ -5,8 +5,8 @@ /* eslint-disable no-undef */ -import unionFindUnion from './unionFindUnion'; -import unionFindFind from './unionFindFind'; +import unionFindUnion from '../unionFindUnion'; +import unionFindFind from '../unionFindFind'; // simple stub for the chunker const chunker = { diff --git a/src/algorithms/explanations/HashingExp.md b/src/algorithms/explanations/HashingExp.md new file mode 100644 index 000000000..2865677a1 --- /dev/null +++ b/src/algorithms/explanations/HashingExp.md @@ -0,0 +1,44 @@ +# Hashing + +--- + +### Hashing Introduction + +Hashing is a popular method for storing and looking up records. This +module visualises a data structure called a hash table, which utilises +hashing to enable quick insertions, searches and deletion. + +Hashing is based on the transformation of the record key via a +'Hashing Function' into a table address. + +For hashing to be efficient, the hashed keys should spread out over the +table evenly and avoid clusters from forming. To achieve this, the +hash function should use as much of the key as possible, and the hashed +key and the table size should be relatively prime. +* For the small table we have chosen 11 +* For the larger table we have chosen 97 + +### Collision + +Even in sparse tables, sometimes two keys will hash to the same value. +Provisions must be taken to resolve these collisions. Two commonly used +methods for collision resolution are depicted in this module: +* Linear Probing + * Checks the next available slot sequentially using step size 1 +* Double Hashing + * Uses a secondary hashing function to determine the sequential step size + +### Time complexity and Rehashing + +Hashing allows for very fast data retrieval, often achieving an O(1) +time complexity in the average cases for insertion, searches and +deletions. However this performance degrades quite dramatically as +the table starts to get full, particularly for unsuccessful searches +which can effectively search the whole table. + +Due to this issue it is necessary to keep track of the number of records +in the table, and to make sure this is well below the table size. One +tactic used is to increase the table size every time the number of +records gets above the capacity. This strategy is called rehashing, and +it involves transforming existing keys in the table using an updated +hash function and have their new keys relocated to a larger table. \ No newline at end of file diff --git a/src/algorithms/explanations/index.js b/src/algorithms/explanations/index.js index 6345de4de..9b79e343b 100644 --- a/src/algorithms/explanations/index.js +++ b/src/algorithms/explanations/index.js @@ -19,3 +19,4 @@ export { default as ASTARExp } from './ASTExp.md'; export { default as BFSExp } from './BFSExp.md'; export { default as DFSExp } from './DFSExp.md'; export { default as DFSrecExp } from './DFSrecExp.md'; +export { default as HashingExp } from './HashingExp.md'; diff --git a/src/algorithms/extra-info/HashingInfo.md b/src/algorithms/extra-info/HashingInfo.md new file mode 100644 index 000000000..bdbb8895c --- /dev/null +++ b/src/algorithms/extra-info/HashingInfo.md @@ -0,0 +1,44 @@ + + +## Extra Info + +----- + +### For a comprehensive explanation of Hashing algorithm: + +Geeks for Geeks Hashing introductory video: + + + +### Extra Reading: + +Geeks for Geeks Hashing Links: + +* [**Hashing**][G4GHashing] +* [**Linear Probing**][G4GLP] +* [**Double Hashing**][G4GDH] + +### Real-life appliation of Hashing + +Geeks for Geeks: [**Hashing Applications**][G4GApplication] + +[G4GHashing]: "https://www.geeksforgeeks.org/hashing-data-structure/" +[G4GLP]: "https://www.geeksforgeeks.org/implementing-hash-table-open-addressing-linear-probing-cpp/" +[G4GDH]: "https://www.geeksforgeeks.org/double-hashing/" +[G4GApplication]: "https://www.geeksforgeeks.org/applications-of-hashing/" + diff --git a/src/algorithms/extra-info/index.js b/src/algorithms/extra-info/index.js index 2bbee1d41..f69fee4a7 100644 --- a/src/algorithms/extra-info/index.js +++ b/src/algorithms/extra-info/index.js @@ -19,3 +19,5 @@ export { default as ASTARInfo } from './ASTInfo.md'; export { default as BFSInfo } from './BFSInfo.md'; export { default as DFSInfo } from './DFSInfo.md'; export { default as DFSrecInfo } from './DFSrecInfo.md'; +export { default as HashingInfo } from './HashingInfo.md'; + diff --git a/src/algorithms/index.js b/src/algorithms/index.js index b1170ee47..1ef7fb742 100644 --- a/src/algorithms/index.js +++ b/src/algorithms/index.js @@ -31,7 +31,7 @@ import * as Instructions from './instructions'; // Also: the key for the algorithm MUST be the same as the "name" // of the top level Param block returned by the parameter function. // Eg, parameters/msort_arr_td.js has -// +// // function MergesortParam() { // ... // return ( @@ -163,6 +163,58 @@ const allalgs = { search: Controller.TTFTreeSearch, }, }, + + 'HashingLP': { + name: 'Hashing (Linear Probing)', + category: 'Insert/Search', + param: , + instructions: Instructions.HashingLPDHInstruction, + explanation: Explanation.HashingExp, + extraInfo: ExtraInfo.HashingInfo, + pseudocode: { + insertion: Pseudocode.linearProbing, + search: Pseudocode.linearSearch, + }, + controller: { + insertion: Controller.HashingInsertion, + search: Controller.HashingSearch, + }, + }, + + 'HashingDH': { + name: 'Hashing (Double Hashing)', + category: 'Insert/Search', + param: , + instructions: Instructions.HashingLPDHInstruction, + explanation: Explanation.HashingExp, + extraInfo: ExtraInfo.HashingInfo, + pseudocode: { + insertion: Pseudocode.doubleHashing, + search: Pseudocode.doubleSearch, + }, + controller: { + insertion: Controller.HashingInsertion, + search: Controller.HashingSearch, + }, + }, + + 'HashingCH': { + name: 'Hashing (Chaining)', + category: 'Insert/Search', + param: , + instructions: Instructions.HashingCHInstruction, + explanation: Explanation.HashingExp, + extraInfo: ExtraInfo.HashingInfo, + pseudocode: { + insertion: Pseudocode.chaining, + search: Pseudocode.chainingSearch, + }, + controller: { + insertion: Controller.HashingChainingInsertion, + search: Controller.HashingSearch, + }, + }, + 'AVLTree': { name: 'AVL Tree', category: 'Insert/Search', @@ -235,7 +287,7 @@ const allalgs = { find: Controller.dijkstra, }, - }, + }, 'aStar': { name: 'A* (heuristic search)', category: 'Graph', @@ -250,7 +302,7 @@ const allalgs = { find: Controller.AStar, }, - }, + }, 'prim': { noDeploy: false, name: 'Prim\'s (min. spanning tree)', diff --git a/src/algorithms/instructions/index.js b/src/algorithms/instructions/index.js index 3f8747272..8460afdfd 100644 --- a/src/algorithms/instructions/index.js +++ b/src/algorithms/instructions/index.js @@ -13,9 +13,10 @@ const KEY_UF_UNION = 'UNION'; const KEY_UF_FIND = 'FIND'; const KEY_UF_PC_ON = 'ON'; const KEY_UF_PC_OFF = 'OFF'; +const KEY_INSDEL = 'INSERT/DELETE'; export const KEY_WORDS = [ - KEY_CODE, KEY_INSERT, KEY_PLAY, KEY_SEARCH, KEY_SORT, KEY_LOAD, + KEY_CODE, KEY_INSERT, KEY_PLAY, KEY_SEARCH, KEY_SORT, KEY_LOAD, KEY_INSDEL ]; const bstInstructions = [ @@ -51,6 +52,75 @@ const stringInstructions = [{ ], }]; +const hashingInstructions1 = [ + { + title: 'Insert/Delete Mode', + content: [ + `Click on ${KEY_CODE} on the right panel.`, + `Select small or larger table via the radio buttons.`, + `Enter a comma separated list of integers into the Insert parameter. + There should be less than 11 integers if it is a small table, and less than 97 if it is a large table. + + **Valid inputs**: + + - x : Insert x into table. + - x - y: Bulk insert from integers x to y. + - x - y - z: Bulk insert from integers x to y in steps of z. + - -x: Delete x from table. + + Only for small table, if you wish to input more integers, select the Expand radio button. + The table will now expand after reaching 80% capacity until it reaches 97 slots, after which it will + stop at one slot left`, + + `Click on ${KEY_INSERT} to enter Insert mode and load the algorithm.`, + `Click on ${KEY_PLAY} to watch the algorithm run. The speed may be adjusted using the speed slider.`, + ], + }, + { + title: 'Search Mode', + content: [ + 'Make sure table has inserted values before searching.', + `Click on ${KEY_CODE} on the right panel.`, + 'Enter an Integer in the Search parameter.', + `Click on ${KEY_SEARCH} to enter Search mode and load the algorithm.`, + `Click on ${KEY_PLAY} to watch the algorithm run. The speed may be adjusted using the speed slider.`, + ], + }, +]; + +const hashingInstructions2 = [ + { + title: 'Insert/Delete Mode', + content: [ + `Click on ${KEY_CODE} on the right panel.`, + `Select small or larger table via the radio buttons.`, + `Enter a comma separated list of integers into the Insert parameter. + + **Valid inputs**: + + - x : Insert x into table. + - x - y: Bulk insert from integers x to y. + - x - y - z: Bulk insert from integers x to y in steps of z. + - -x: Delete x from table. + + You can hover over a slot to see the chain when you see a .. in the slot`, + + `Click on ${KEY_INSERT} to enter Insert mode and load the algorithm.`, + `Click on ${KEY_PLAY} to watch the algorithm run. The speed may be adjusted using the speed slider.`, + ], + }, + { + title: 'Search Mode', + content: [ + 'Make sure table has inserted values before searching.', + `Click on ${KEY_CODE} on the right panel.`, + 'Enter an Integer in the Search parameter.', + `Click on ${KEY_SEARCH} to enter Search mode and load the algorithm.`, + `Click on ${KEY_PLAY} to watch the algorithm run. The speed may be adjusted using the speed slider.`, + ], + }, +]; + const sortInstructions = [{ title: 'Sorting Numbers', content: [ @@ -129,3 +199,5 @@ export const ASTARInstruction = graphInstructions; export const BFSInstruction = graphInstructions; export const DFSInstruction = graphInstructions; export const DFSrecInstruction = graphInstructions; +export const HashingLPDHInstruction = hashingInstructions1; +export const HashingCHInstruction = hashingInstructions2; diff --git a/src/algorithms/parameters/HashingCHParam.js b/src/algorithms/parameters/HashingCHParam.js new file mode 100644 index 000000000..03c1b37ef --- /dev/null +++ b/src/algorithms/parameters/HashingCHParam.js @@ -0,0 +1,223 @@ +import React, { useState, useContext, useEffect } from 'react'; +import { GlobalContext } from '../../context/GlobalState'; +import { GlobalActions } from '../../context/actions'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Radio from '@mui/material/Radio'; +import { withStyles } from '@mui/styles'; +import ListParam from './helpers/ListParam'; +import SingleValueParam from './helpers/SingleValueParam'; +import '../../styles/Param.scss'; +import { + genUniqueRandNumList, + singleNumberValidCheck, + successParamMsg, + errorParamMsg, + commaSeparatedPairTripleCheck, + checkAllRangesValid, +} from './helpers/ParamHelper'; +import { SMALL_SIZE, LARGE_SIZE } from '../controllers/HashingCommon'; + +// Algotiyhm information and magic phrases +const ALGORITHM_NAME = 'Hashing (linear probing)'; +const HASHING_INSERT = 'Hashing Insertion'; +const HASHING_SEARCH = 'Hashing Search'; +const HASHING_EXAMPLE = 'PLACE HOLDER ERROR MESSAGE'; + +// Default inputs +const DEFAULT_ARRAY = genUniqueRandNumList(10, 1, 50); +const DEFAULT_SEARCH = 2 + +const UNCHECKED = { + smallTable: false, + largeTable: false +}; + +// Styling of radio buttons +const BlueRadio = withStyles({ + root: { + color: '#2289ff', + '&$checked': { + color: '#027aff', + }, + }, + checked: {}, + // eslint-disable-next-line react/jsx-props-no-spreading +})((props) => ) + +// Error messages +const ERROR_INVALID_INPUT_INSERT = 'Please enter a list containing positive integers, pairs or triples'; +const ERROR_INVALID_INPUT_SEARCH = 'Please enter a positive integer'; +const ERROR_TOO_LARGE = `Please enter the right amount of inputs`; +const ERROR_INVALID_RANGES = 'If you had entered ranges, please input valid ranges' + +/** + * Chaining input component + * @returns the component + */ +function HashingCHParam() { + const [message, setMessage] = useState(null); + const { algorithm, dispatch } = useContext(GlobalContext); + const [array, setArray] = useState(DEFAULT_ARRAY); + const [search, setSearch] = useState(DEFAULT_SEARCH); + const [HASHSize, setHashSize] = useState({ + smallTable: true, + largeTable: false, + }); + + /** + * Handle changes to input + * @param {*} e the input box component + */ + const handleChange = (e) => { + setHashSize({ ...UNCHECKED, [e.target.name]: true }) + } + + /** + * Handle insert box inputs + * @param {*} e the insert box component + */ + const handleInsertion = (e) => { + e.preventDefault(); + const inputs = e.target[0].value; // Get the value of the input + + let removeSpace = inputs.split(' ').join(''); + + // Check if the inputs are either positive integers, pairs or triples + if (commaSeparatedPairTripleCheck(true, true, removeSpace)) { + let values = removeSpace.split(","); // Converts input to array + if (checkAllRangesValid(values)) { + let hashSize = HASHSize.smallTable ? SMALL_SIZE : LARGE_SIZE; // Table size + + // Dispatch algo + dispatch(GlobalActions.RUN_ALGORITHM, { + name: 'HashingCH', + mode: 'insertion', + hashSize: hashSize, + values, + expand: false + }); + setMessage(successParamMsg(ALGORITHM_NAME)); + } + else { + setMessage(errorParamMsg(ALGORITHM_NAME, ERROR_INVALID_RANGES)); + } + } else { + setMessage(errorParamMsg(ALGORITHM_NAME, ERROR_INVALID_INPUT_INSERT)); + } + } + + /** + * Handle search box input + * @param {*} e search box component + */ + const handleSearch = (e) => { + e.preventDefault(); + const inputValue = e.target[0].value; + let hashSize = HASHSize.smallTable ? SMALL_SIZE : LARGE_SIZE; // Table size + + const visualisers = algorithm.chunker.visualisers; // Visualizers from insertion + if (singleNumberValidCheck(inputValue)) { // Check if input is a single positive number + const target = parseInt(inputValue); + + // Dispatch algorithm + dispatch(GlobalActions.RUN_ALGORITHM, { + name: 'HashingCH', + mode: 'search', + hashSize: hashSize, + visualisers, + target + }); + setMessage(successParamMsg(ALGORITHM_NAME)); + } else { + setMessage(errorParamMsg(ALGORITHM_NAME, ERROR_INVALID_INPUT_SEARCH)); + } + } + + // Use effect to detect changes in radio box choice + useEffect( + () => { + document.getElementById('startBtnGrp').click(); + }, + [HASHSize], + ); + + + return ( + <> +
+ { + if (HASHSize.smallTable) { + return () => genUniqueRandNumList(SMALL_SIZE-1, 1, 50); + } + else if(HASHSize.largeTable) { + return () => genUniqueRandNumList(LARGE_SIZE-1, 1, 100); + } + })() + } + ALGORITHM_NAME = {HASHING_INSERT} + EXAMPLE={HASHING_EXAMPLE} + handleSubmit={handleInsertion} + setMessage={setMessage} + /> + + + {} +
+ +
+
+ + } + label="Small Table" + className="checkbox" + /> + + } + label="Larger Table" + className="checkbox" + /> +
+
+ + {/* render success/error message */} + {message} + + ); +} + +export default HashingCHParam; diff --git a/src/algorithms/parameters/HashingDHParam.js b/src/algorithms/parameters/HashingDHParam.js new file mode 100644 index 000000000..30418a9be --- /dev/null +++ b/src/algorithms/parameters/HashingDHParam.js @@ -0,0 +1,279 @@ +import PropTypes from 'prop-types'; +import { withAlgorithmParams } from './helpers/urlHelpers' + +import { URLContext } from '../../context/urlState.js'; + +import React, { useState, useContext, useEffect } from 'react'; +import { GlobalContext } from '../../context/GlobalState'; +import { GlobalActions } from '../../context/actions'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Radio from '@mui/material/Radio'; +import { withStyles } from '@mui/styles'; +import ListParam from './helpers/ListParam'; +import SingleValueParam from './helpers/SingleValueParam'; +import '../../styles/Param.scss'; +import { + genUniqueRandNumList, + singleNumberValidCheck, + successParamMsg, + errorParamMsg, + commaSeparatedPairTripleCheck, + checkAllRangesValid +} from './helpers/ParamHelper'; +import { SMALL_SIZE, LARGE_SIZE } from '../controllers/HashingCommon'; + +// Algotiyhm information and magic phrases +const ALGORITHM_NAME = 'Hashing (double hashing)'; +const HASHING_INSERT = 'Hashing Insertion'; +const HASHING_SEARCH = 'Hashing Search'; +const HASHING_EXAMPLE = 'PLACE HOLDER ERROR MESSAGE'; + +// Default inputs +const DEFAULT_ARRAY = genUniqueRandNumList(10, 1, 50); +const DEFAULT_SEARCH = 2 + +const UNCHECKED = { + smallTable: false, + largeTable: false +}; + +const DEFAULT_EXPAND = false; + +// Styling of radio buttons +const BlueRadio = withStyles({ + root: { + color: '#2289ff', + '&$checked': { + color: '#027aff', + }, + }, + checked: {}, + // eslint-disable-next-line react/jsx-props-no-spreading +})((props) => ) + +// Error messages +const ERROR_INVALID_INPUT_INSERT = 'Please enter a list containing positive integers, pairs or triples'; +const ERROR_INVALID_INPUT_SEARCH = 'Please enter a positive integer'; +const ERROR_TOO_LARGE = `Please enter the right amount of inputs`; +const ERROR_INVALID_RANGES = 'If you had entered ranges, please input valid ranges' + +/** + * Double Hashing input component + * @returns the component + */ +function HashingDHParam({ mode, list, value }) { + const [message, setMessage] = useState(null); + const { algorithm, dispatch } = useContext(GlobalContext); + const [array, setLocalArray] = useState(list || DEFAULT_ARRAY); + const [search, setLocalSearch] = useState(DEFAULT_SEARCH); + const [HASHSize, setHashSize] = useState({ + smallTable: true, + largeTable: false, + }); + + const [expand, setExpand] = useState(DEFAULT_EXPAND); + const { setNodes, setSearchValue } = useContext(URLContext); + + useEffect(() => { + setNodes(array); + setSearchValue(search); + }, [array, search]) + + /** + * Handle changes to input + * @param {*} e the input box component + */ + const handleChange = (e) => { + setHashSize({ ...UNCHECKED, [e.target.name]: true }) + } + + /** + * Handle changes to input + * @param {*} e the input box component + */ + const handleExpand = (e) => { + setExpand(!expand) + } + + /** + * Handle insert box inputs + * @param {*} e the insert box component + */ + const handleInsertion = (e) => { + e.preventDefault(); + const inputs = e.target[0].value; // Get the value of the input + + let removeSpace = inputs.split(' ').join(''); + + + // Check if the inputs are either positive integers, pairs or triples + if (commaSeparatedPairTripleCheck(true, true, removeSpace)) { + let values = removeSpace.split(","); // Converts input to array + if (checkAllRangesValid(values)) { + let hashSize = HASHSize.smallTable ? SMALL_SIZE : LARGE_SIZE; // Table size + + // Dispatch algo + dispatch(GlobalActions.RUN_ALGORITHM, { + name: 'HashingDH', + mode: 'insertion', + hashSize: hashSize, + values, + expand: expand + }); + setMessage(successParamMsg(ALGORITHM_NAME)); + } + else { + setMessage(errorParamMsg(ALGORITHM_NAME, ERROR_INVALID_RANGES)); + } + } else { + setMessage(errorParamMsg(ALGORITHM_NAME, ERROR_INVALID_INPUT_INSERT)); + } + } + + /** + * Handle search box input + * @param {*} e search box component + */ + const handleSearch = (e) => { + e.preventDefault(); + const inputValue = e.target[0].value; + let hashSize = HASHSize.smallTable ? SMALL_SIZE : LARGE_SIZE; // Table size + + const visualisers = algorithm.chunker.visualisers; // Visualizers from insertion + if (singleNumberValidCheck(inputValue)) { // Check if input is a single positive number + const target = parseInt(inputValue); + + // Dispatch algorithm + dispatch(GlobalActions.RUN_ALGORITHM, { + name: 'HashingDH', + mode: 'search', + hashSize: hashSize, + visualisers, + target + }); + setMessage(successParamMsg(ALGORITHM_NAME)); + } else { + setMessage(errorParamMsg(ALGORITHM_NAME, ERROR_INVALID_INPUT_SEARCH)); + } + } + + // Use effect to detect changes in radio box choice + useEffect( + () => { + document.getElementById('startBtnGrp').click(); + }, + [HASHSize], + ); + + // Use effect to detect changes in expand radio box choice + useEffect( + () => { + document.getElementById('startBtnGrp').click(); + }, + [expand], + ); + + + return ( + <> +
+ { + if (HASHSize.smallTable) { + return () => genUniqueRandNumList(SMALL_SIZE-1, 1, 50); + } + else if(HASHSize.largeTable) { + return () => genUniqueRandNumList(LARGE_SIZE-1, 1, 100); + } + })() + } + ALGORITHM_NAME = {HASHING_INSERT} + EXAMPLE={HASHING_EXAMPLE} + handleSubmit={handleInsertion} + setMessage={setMessage} + /> + + + {} +
+ +
+
+ + } + label="Small Table" + className="checkbox" + /> + + } + label="Larger Table" + className="checkbox" + /> +
+ + +
+ {HASHSize.smallTable && ( + + } + label="Expand" + className="checkbox" + /> + )} +
+
+ + {/* render success/error message */} + {message} + + ); +} + +// Define the prop types for URL Params +HashingDHParam.propTypes = { + alg: PropTypes.string.isRequired, // keep alg for all algorithms + mode: PropTypes.string.isRequired, //keep mode for all algorithms + list: PropTypes.string.isRequired, + value: PropTypes.string.isRequired + }; +export default withAlgorithmParams(HashingDHParam); + diff --git a/src/algorithms/parameters/HashingLPParam.js b/src/algorithms/parameters/HashingLPParam.js new file mode 100644 index 000000000..35ff04250 --- /dev/null +++ b/src/algorithms/parameters/HashingLPParam.js @@ -0,0 +1,277 @@ +import PropTypes from 'prop-types'; +import { withAlgorithmParams } from './helpers/urlHelpers' + +import { URLContext } from '../../context/urlState.js'; + +import React, { useState, useContext, useEffect } from 'react'; +import { GlobalContext } from '../../context/GlobalState'; +import { GlobalActions } from '../../context/actions'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Radio from '@mui/material/Radio'; +import { withStyles } from '@mui/styles'; +import ListParam from './helpers/ListParam'; +import SingleValueParam from './helpers/SingleValueParam'; +import '../../styles/Param.scss'; +import { + genUniqueRandNumList, + singleNumberValidCheck, + successParamMsg, + errorParamMsg, + commaSeparatedPairTripleCheck, + checkAllRangesValid, +} from './helpers/ParamHelper'; +import { SMALL_SIZE, LARGE_SIZE } from '../controllers/HashingCommon'; + +// Algotiyhm information and magic phrases +const ALGORITHM_NAME = 'Hashing (linear probing)'; +const HASHING_INSERT = 'Hashing Insertion'; +const HASHING_SEARCH = 'Hashing Search'; +const HASHING_EXAMPLE = 'PLACE HOLDER ERROR MESSAGE'; + +// Default inputs +const DEFAULT_ARRAY = genUniqueRandNumList(10, 1, 50); +const DEFAULT_SEARCH = 2 + +const UNCHECKED = { + smallTable: false, + largeTable: false +}; + +const DEFAULT_EXPAND = false; + +// Styling of radio buttons +const BlueRadio = withStyles({ + root: { + color: '#2289ff', + '&$checked': { + color: '#027aff', + }, + }, + checked: {}, + // eslint-disable-next-line react/jsx-props-no-spreading +})((props) => ) + +// Error messages +const ERROR_INVALID_INPUT_INSERT = 'Please enter a list containing positive integers, pairs or triples'; +const ERROR_INVALID_INPUT_SEARCH = 'Please enter a positive integer'; +const ERROR_TOO_LARGE = `Please enter the right amount of inputs`; +const ERROR_INVALID_RANGES = 'If you had entered ranges, please input valid ranges' + +/** + * Linear probing input component + * @returns the component + */ +function HashingLPParam({ mode, list, value }) { + const [message, setMessage] = useState(null); + const { algorithm, dispatch } = useContext(GlobalContext); + const [array, setLocalArray] = useState(list || DEFAULT_ARRAY); + const [search, setLocalSearch] = useState(DEFAULT_SEARCH); + const [HASHSize, setHashSize] = useState({ + smallTable: true, + largeTable: false, + }); + const [expand, setExpand] = useState(DEFAULT_EXPAND); + const { setNodes, setSearchValue } = useContext(URLContext); + + useEffect(() => { + setNodes(array); + setSearchValue(search); + }, [array, search]) + + + /** + * Handle changes to input + * @param {*} e the input box component + */ + const handleChange = (e) => { + setHashSize({ ...UNCHECKED, [e.target.name]: true }) + } + + /** + * Handle expand + * @param {*} e the input box component + */ + const handleExpand = (e) => { + setExpand(!expand) + } + + /** + * Handle insert box inputs + * @param {*} e the insert box component + */ + const handleInsertion = (e) => { + e.preventDefault(); + const inputs = e.target[0].value; // Get the value of the input + + let removeSpace = inputs.split(' ').join(''); + + // Check if the inputs are either positive integers, pairs or triples + if (commaSeparatedPairTripleCheck(true, true, removeSpace)) { + let values = removeSpace.split(","); // Converts input to array + if (checkAllRangesValid(values)) { + let hashSize = HASHSize.smallTable ? SMALL_SIZE : LARGE_SIZE; // Table size + + // Dispatch algo + dispatch(GlobalActions.RUN_ALGORITHM, { + name: 'HashingLP', + mode: 'insertion', + hashSize: hashSize, + values, + expand: expand + }); + setMessage(successParamMsg(ALGORITHM_NAME)); + } + else { + setMessage(errorParamMsg(ALGORITHM_NAME, ERROR_INVALID_RANGES)); + } + } else { + setMessage(errorParamMsg(ALGORITHM_NAME, ERROR_INVALID_INPUT_INSERT)); + } + } + + /** + * Handle search box input + * @param {*} e search box component + */ + const handleSearch = (e) => { + e.preventDefault(); + const inputValue = e.target[0].value; + let hashSize = HASHSize.smallTable ? SMALL_SIZE : LARGE_SIZE; // Table size + + const visualisers = algorithm.chunker.visualisers; // Visualizers from insertion + if (singleNumberValidCheck(inputValue)) { // Check if input is a single positive number + const target = parseInt(inputValue); + + // Dispatch algorithm + dispatch(GlobalActions.RUN_ALGORITHM, { + name: 'HashingLP', + mode: 'search', + hashSize: hashSize, + visualisers, + target + }); + setMessage(successParamMsg(ALGORITHM_NAME)); + } else { + setMessage(errorParamMsg(ALGORITHM_NAME, ERROR_INVALID_INPUT_SEARCH)); + } + } + + // Use effect to detect changes in radio box choice + useEffect( + () => { + document.getElementById('startBtnGrp').click(); + }, + [HASHSize], + ); + + // Use effect to detect changes in expand radio box choice + useEffect( + () => { + document.getElementById('startBtnGrp').click(); + }, + [expand], + ); + + + return ( + <> +
+ { + if (HASHSize.smallTable) { + return () => genUniqueRandNumList(SMALL_SIZE-1, 1, 50); + } + else if(HASHSize.largeTable) { + return () => genUniqueRandNumList(LARGE_SIZE-1, 1, 100); + } + })() + } + ALGORITHM_NAME = {HASHING_INSERT} + EXAMPLE={HASHING_EXAMPLE} + handleSubmit={handleInsertion} + setMessage={setMessage} + /> + + + {} +
+ +
+
+ + } + label="Small Table" + className="checkbox" + /> + + } + label="Larger Table" + className="checkbox" + /> +
+ + +
+ {HASHSize.smallTable && ( + + } + label="Expand" + className="checkbox" + /> + )} +
+
+ + {/* render success/error message */} + {message} + + ); +} + +// Define the prop types for URL Params +HashingLPParam.propTypes = { + alg: PropTypes.string.isRequired, // keep alg for all algorithms + mode: PropTypes.string.isRequired, //keep mode for all algorithms + list: PropTypes.string.isRequired, + value: PropTypes.string.isRequired + }; +export default withAlgorithmParams(HashingLPParam); diff --git a/src/algorithms/parameters/helpers/ParamHelper.js b/src/algorithms/parameters/helpers/ParamHelper.js index 2b827c2ae..c0c7c9d1f 100644 --- a/src/algorithms/parameters/helpers/ParamHelper.js +++ b/src/algorithms/parameters/helpers/ParamHelper.js @@ -441,3 +441,77 @@ export const shuffleArray = (array) => { } return array; }; + +/** + * Check if the input string are comma-separated numbers, pairs and triples + * @param {*} allowPosInteger is a toggle, if true it allows positive integers + * @param {*} allowNegInteger is a toggle, if true it allows negative integers + * @param {*} input the input string + * @returns whether the check is true + */ +export const commaSeparatedPairTripleCheck = (allowPosInteger, allowNegInteger, input) => { + const regex_pos_num = /^[0-9]+(-[0-9]+){0,2}$/g; + const regex_all_num = /^[0-9]+(-[0-9]+){0,2}$|^-[0-9]+$/g; + const regex_no_num = /^[0-9]+(-[0-9]+){1,2}$/g; + let array = input.split(","); + for (let item of array) { + if (!item.match(allowPosInteger ? (allowNegInteger ? regex_all_num : regex_pos_num) : regex_no_num)) return false; + } + return true; +} + +/** + * return an array of number according to the range specified + * @param {*} str the string of range, e.g."2-7-4", "2-5" + * @param {*} mode "Array" or "Count", return array of inputs or count of inputs, respectively (delete "Count" returns -1 and "Array return array of that negative number") + * @returns the array of number in that range + */ +export const translateInput = (str, mode) => { + let arr = str.split("-"); + switch (mode) { + case "Count": + if (arr.length == 1) return 1; + else if (arr.length == 2) { + if (arr[0] === "") return 0; + else return arrayRange(Number(arr[0]), Number(arr[1]), 1).length; + } + else if (arr.length == 3) return arrayRange(Number(arr[0]), Number(arr[1]), Number(arr[2])).length; + break; + case "Array": + if (arr.length == 1) return arr.map(Number); + else if (arr.length == 2) { + if (arr[0] === "") return [str].map(Number); + else return arrayRange(Number(arr[0]), Number(arr[1]), 1); + } + else if (arr.length == 3) return arrayRange(Number(arr[0]), Number(arr[1]), Number(arr[2])); + break; + } +} + +/** + * return an array of number according to the range specified + * @param {*} start start point(inclusive) + * @param {*} stop end point(inclusive) + * @param {*} step the step + * @returns an array of number + */ +const arrayRange = (start, stop, step) => + Array.from( + { length: (stop - start) / step + 1 }, + (value, index) => start + index * step + ); + +/** + * Check if all ranges in array of inputs are valid (e.g for a-b, a must < b) + * @param {*} values the array of inputs + * @returns whether the check is true or not + */ +export const checkAllRangesValid = (values) => { + for (let item of values) { + let rangesItems = item.split("-").map(Number); + if ((rangesItems.length == 2 || rangesItems.length == 3) && rangesItems[0] > rangesItems[1]) { + return false; + } + } + return true; +} diff --git a/src/algorithms/parameters/index.js b/src/algorithms/parameters/index.js index b5e3fffa2..b6dbd493d 100644 --- a/src/algorithms/parameters/index.js +++ b/src/algorithms/parameters/index.js @@ -20,3 +20,6 @@ export { default as ASTARParam } from './ASTParam'; export { default as BFSParam } from './BFSParam'; export { default as DFSParam } from './DFSParam'; export { default as DFSrecParam } from './DFSrecParam'; +export { default as HashingLPParam } from './HashingLPParam'; +export { default as HashingDHParam } from './HashingDHParam'; +export { default as HashingCHParam } from './HashingCHParam'; diff --git a/src/algorithms/pseudocode/HashingInsertion.js b/src/algorithms/pseudocode/HashingInsertion.js index 06b087790..dab59287b 100644 --- a/src/algorithms/pseudocode/HashingInsertion.js +++ b/src/algorithms/pseudocode/HashingInsertion.js @@ -8,6 +8,7 @@ import parse from '../../pseudocode/parse'; // NOTE: code now no longer explicitly keeps track of number of // insertions and CheckTableFullness is modified so some bookmarks (eg, // 4, 19, 20) no longer exist and controller code will have to change + const main = ` \\Code{ @@ -53,7 +54,7 @@ prevent infinite loops when searching. T[i] <- k // unoccupied slot found so we put k in it \\B 9 // Done \\B 10 \\In} - + //======================================================= HashDelete(T, k) // Delete key k in table T \\B 11 @@ -82,7 +83,7 @@ prevent infinite loops when searching. \\In{ // Do nothing \\In} - \\In} + \\In} \\Code} \\Code{ @@ -181,5 +182,86 @@ export const doubleHashingIncrement = ` \\Expl} \\Code} ` + +let chainingInsert = ` + \\Code{ + NullTable + i <- 0 + while i { + const elRef = useRef(null); + const executeScroll = () => elRef.current.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + + return [executeScroll, elRef]; +}; + +const ScrollToHighlight = ({col, j, toString}) => { + const [executeScroll, elRef] = useScroll(); + useEffect(executeScroll, []); + + return ( + + + {toString(col.value)} + + + ); +} + +ScrollToHighlight.propTypes = { + col: PropTypes.object.isRequired, + j: PropTypes.number.isRequired, +} + +const handleMouseEnter = (id) => { + console.log(id); +} + class Array2DRenderer extends Renderer { constructor(props) { super(props); @@ -53,9 +102,29 @@ class Array2DRenderer extends Renderer { renderData() { // For DFSrec+msort_arr_td,... listOfNumbers is actually a list of // pairs of numbers, or strings such as '(2,5)' - const { data, algo, kth, listOfNumbers, motionOn, hideArrayAtIdx } = - this.props.data; + const { + data, + algo, + kth, + listOfNumbers, + motionOn, + hideArrayAtIdx, + splitArray, + highlightRow, + // newZoom, + } = this.props.data; + let centerX = this.centerX; + let centerY = this.centerY; + let zoom = this.zoom; + + // // Change Renderer's zoom on newZoom change + // if (newZoom != this.zoom && newZoom !== undefined) { + // this.zoom = newZoom; + // this.refresh(); + // } + const isArray1D = true; + let render = []; // eslint-disable-next-line camelcase let data_T; if (algo === 'tc') { @@ -63,269 +132,426 @@ class Array2DRenderer extends Renderer { data_T = data[0].map((col, i) => data.map((row) => row[i])); } // const isArray1D = this instanceof Array1DRenderer; + + // These are for setting up the floating boxes + const firstColIsHeaders = ALGOS_WITH_FIRST_COL_AS_HEADERS.includes(algo); + // XXX sometimes caption (listOfNumbers) is longer than any row... - let longestRow = data.reduce( - (longestRow, row) => (longestRow.length < row.length ? row : longestRow), - [] - ); - return ( - + function createArray(data, toString, longestRow, currentSub, subArrayNum) { + return ( - {algo === 'unionFind' && ( // adding the array indicies for union find - - - {data[0].map((col, idx) => ( - + {data[0].map((col, idx) => ( + - ))} - - - )} + {v} + + ))} + + + ))} + + + )} - - {!isArray1D && - ); - })} - - {data.map((row, i) => { - let pointer = false; + 1 && + (algo === 'HashingLP' || algo === 'HashingDH' || algo === 'HashingCH') + ) ? 5 : styles.row.height + }} + > + {!isArray1D && + ); + }) + } + + {data.map((row, i) => { + let pointer = false; - if (i === hideArrayAtIdx) return null; + if ( + (Array.isArray(hideArrayAtIdx) && hideArrayAtIdx.includes(i)) + || (i === hideArrayAtIdx) + ) return null; - // eslint-disable-next-line no-plusplus - for (let j = 0; j < row.length; j++) { - if (row[j].selected) { - pointer = true; - } + // eslint-disable-next-line no-plusplus + for (let j = 0; j < row.length; j++) { + if (row[j].selected) { + pointer = true; } - return ( - - {algo === 'tc' && ( // generate vertical index, which starts from 1 - - )} - {!isArray1D && algo !== 'tc' && ( - - )} - {row.map((col, j) => { - const varGreen = col.fill === 1; // for simple fill - const varOrange = col.fill === 2; - const varRed = col.fill === 3; + } + return ( + + {algo === 'tc' && ( // generate vertical index, which starts from 1 + + )} + {!isArray1D && algo !== 'tc' && ( + + )} + {row.map((col, j) => { + const varGreen = col.fill === 1; // for simple fill + const varOrange = col.fill === 2; + const varRed = col.fill === 3; + if (varOrange) { return ( - + ); - })} - { - (pointer && algo === 'tc' && ( - - )) - || - (algo === 'aStar' && i === 1 && ( - - )) - || - (algo === 'aStar' && i === 2 && ( - - )) - || - (((algo === 'prim' && i === 2) || - (algo === 'dijkstra' && i === 2) - ) && ( - - )) - || - ); - })} - {algo === 'tc' && ( - // Don't remove "j-tag='transitive_closure'" - - - )) || ); - })} + }) + } + { + (pointer && algo === 'tc' && ( + + )) + || + (algo === 'aStar' && i === 1 && ( + + )) + || + (algo === 'aStar' && i === 2 && ( + + )) + || + (((algo === 'prim' && i === 2) || + (algo === 'dijkstra' && i === 2) + ) && ( + + )) + || - )} - {(algo === 'prim' || - algo === 'kruskal' || - algo === 'dijkstra' || - algo === 'aStar' || - algo === 'DFS' || - algo === 'DFSrec' || - algo === 'msort_lista_td' || - algo === 'BFS') && - data.map( - (row, i) => - i === 2 && ( - - - {row.map((col, j) => ( - + )) || + )} + {(algo === 'prim' || + algo === 'kruskal' || + algo === 'dijkstra' || + algo === 'aStar' || + algo === 'DFS' || + algo === 'DFSrec' || + algo === 'msort_lista_td' || + algo === 'BFS' || + algo === 'HashingLP' || + algo === 'HashingDH' || + algo === 'HashingCH') && + data.map( + (row, i) => + i === 2 && ( + + + {row.map((col, j) => ( + + {v} + ))} - - - ) - )} + + ))} + + + ) + )} - {algo === 'tc' && ( - - )} - {algo == 'unionFind' && ( // bottom centre caption for union find - - )} - {algo === 'DFS' && ( - - )} - {algo === 'DFSrec' && ( - - )} - {algo === 'msort_arr_td' && ( - - )} - {algo === 'msort_lista_td' && listOfNumbers && ( - - )} - {algo === 'BFS' && ( - + )} + {algo == 'unionFind' && ( // bottom centre caption for union find + + )} + {algo === 'DFS' && ( + + )} + {algo === 'DFSrec' && ( + + )} + {algo === 'msort_arr_td' && ( + + )} + {algo === 'msort_lista_td' && listOfNumbers && ( + + )} + {algo === 'BFS' && ( + + )} +
-
+
+
+ {col.variables.map((v) => ( + - {col.variables.map((v) => ( - - {v} - - ))} -
-
} - {algo === 'tc' && ( // Leave a blank cell at the header row - - )} - { /* XXX really should have a displayIndex flag for this */ - algo !== 'BFS' && - algo !== 'DFSrec' && - algo !== 'DFS' && - algo !== 'kruskal' && - algo !== 'dijkstra' && - algo !== 'aStar' && - algo !== 'aStar' && - algo !== 'msort_lista_td' && - longestRow.map((_, i) => { - if (algo === 'tc') { - i += 1; - } - if (algo === 'prim' || algo == 'unionFind') { - return ; - } - return ( - - {i} -
} + {algo === 'tc' && ( // Leave a blank cell at the header row + + )} + { /* XXX really should have a displayIndex flag for */ + algo !== 'BFS' && + algo !== 'DFSrec' && + algo !== 'DFS' && + algo !== 'kruskal' && + algo !== 'dijkstra' && + algo !== 'aStar' && + algo !== 'aStar' && + algo !== 'msort_lista_td' && + algo !== 'HashingLP' && + algo !== 'HashingDH' && + algo !== 'HashingCH' && + longestRow.map((_, i) => { + if (algo === 'tc') { + i += 1; + } + if (algo === 'prim' || algo == 'unionFind') { + return ; + } + return ( + + {i} +
- {i + 1} - - {i} -
+ {i + 1} + + {i} + - - {this.toString(col.value)} - - - i - - )Priority - - )Queue  - - Priority Queue - } -
- {data_T.map((row) => { - let pointer = false; - // eslint-disable-next-line no-plusplus - for (let j = 0; j < row.length; j++) { - if (row[j].selected1) { - pointer = true; - } } + return ( - (pointer && ( - - j - + { + let element = e.target.children[1]; + if (element && element.innerHTML !== "") { + element.style.display = 'block' + } + }} + onMouseLeave={(e) => { + let element = e.target.children[1]; + if (element && element.innerHTML !== "") { + element.style.display = 'none' + } + }} + > +
+ + {toString(col.value)} + +
+ { + (i == 1 && ALGOS_USING_FLOAT_BOX.includes(algo) && (!(firstColIsHeaders && j == 0)) && ( + + )) + } +
+ i + + )Priority + + )Queue  + + Priority Queue + }
+ + {data_T.map((row) => { + let pointer = false; + // eslint-disable-next-line no-plusplus + for (let j = 0; j < row.length; j++) { + if (row[j].selected1) { + pointer = true; + } + } + return ( + (pointer && ( + + j + + ); + })} +
+ {col.variables.map((v) => ( + - {col.variables.map((v) => ( - - {v} - - ))} -
k = {kth} - Union({kth}) - - Nodes (stack):  {listOfNumbers}        - - Call stack (n,p):  {listOfNumbers}   - - Call stack (n,p):  {listOfNumbers}   - - Call stack (L, len):  {listOfNumbers}   - + {render} + {algo === 'tc' && ( + k = {kth} + Union({kth}) + + Nodes (stack):  {listOfNumbers}        + + Call stack (n,p):  {listOfNumbers}   + + Call stack (n,p):  {listOfNumbers}   + + Call stack (L, len):  {listOfNumbers}   + + Nodes (queue): {listOfNumbers} +
+ ) + } + + if (!splitArray.doSplit) { + let longestRow = data.reduce( + (longestRow, row) => (longestRow.length < row.length ? row : longestRow), + [] + ); + render.push(createArray(data, this.toString, longestRow)); + return createRender(render); + + } else { + for (let i = 0; i < data.length; i++) { + let longestRow = data[i].reduce( + (longestRow, row) => (longestRow.length < row.length ? row : longestRow), + [] + ); + render.push(createArray( + data[i], + this.toString, + longestRow, + i, + data.length + )); + } + + return ( +
+
- Nodes (queue): {listOfNumbers} - - )} - - ); + {createRender(render)} +
+
+ {(algo === 'HashingLP' || + algo === 'HashingDH' || + algo === 'HashingCH') && + kth !== '' && + ((kth.fullCheck === undefined) && (kth.reinserting === undefined) ? + ( + + {(kth.type == 'I' || kth.type == 'BI') ? 'Inserting' : (kth.type == 'S' ? 'Searching' : (kth.type == 'D' ? 'Deleting' : '')) } Key{kth.type == 'BI' ? 's' : ''}: {kth.key} + {kth.insertions !== undefined && ( + +      + Insertions: {kth.insertions} + + )} + {algo !== 'HashingCH' && ( + +      + Increment: {kth.increment} + + )} + + ) : ((kth.fullCheck !== undefined) ? ( + + {kth.fullCheck} + + ) : ( + + Reinserting: {kth.reinserting} +      + To reinsert: {kth.toReinsert} + + ) + ) + ) + } +
+
+ ); + } + } } diff --git a/src/components/DataStructures/Array/Array2DTracer.js b/src/components/DataStructures/Array/Array2DTracer.js index db48d4488..b116e1bcb 100644 --- a/src/components/DataStructures/Array/Array2DTracer.js +++ b/src/components/DataStructures/Array/Array2DTracer.js @@ -56,16 +56,73 @@ class Array2DTracer extends Tracer { /** * @param {array} array2d * @param {string} algo used to mark if it is a specific algorithm + * @param {any} kth used to display kth + * @param {number} highlightRow used mark the row to highlight + * @param {Object} splitArray determine how to split the array + * @param {number} splitArray.rowLength determine the length of a split array + * @param {string[]} splitArray.rowHeader determine the header of each row of a split array */ - set(array2d = [], algo, kth = 1) { - this.data = array2d.map((array1d) => - [...array1d].map((value, i) => new Element(value, i)) - ); + set(array2d = [], algo, kth = 1, highlightRow, splitArray) { + // set the array2d based of the splitArray values + if (splitArray === undefined || splitArray.rowLength < 1) { + this.splitArray = {doSplit: false}; + + // set the value of array cells + this.data = array2d.map((array1d) => + [...array1d].map((value, i) => new Element(value, i)) + ); + } else { + this.data = []; + this.splitArray = splitArray; + this.splitArray.doSplit = true; + + // check if the rows have headers + if (Array.isArray(splitArray.rowHeader) && splitArray.rowHeader.length) { + this.splitArray.hasHeader = true; + } else { + this.splitArray.hasHeader = false; + } + let split = []; + + // splitting the array into multiple arrays of length rowLength + let step = 0; + while (step < array2d[0].length) { + // one smaller array + let arr2d = []; + for (let i = 0; i < array2d.length; i++ ) { + arr2d.push([ + splitArray.rowHeader[i], + ...array2d[i].slice(step, step + splitArray.rowLength), + ...( + ( + (array2d[0].length - step) > 0 && + (array2d[0].length - step) < splitArray.rowLength + ) + ? Array(step + splitArray.rowLength - array2d[0].length) + : Array(0) + ) + ]); + } + + step += splitArray.rowLength; + + // push to a main array of multiple split arrays + split.push(arr2d); + } + + // set the value of array cells + for (const item of split) { + this.data.push(item.map((array1d) => + [...array1d].map((value, i) => new Element(value, i)) + )); + } + } this.algo = algo; this.kth = kth; this.motionOn = true; // whether to use animation this.hideArrayAtIdx = null; // to hide array at given index this.listOfNumbers = ''; + this.highlightRow = highlightRow; super.set(); } @@ -118,21 +175,145 @@ class Array2DTracer extends Tracer { } } - // a simple fill function based on aia themes - // where green=1, yellow=2, and red=3 + /** + * a simple fill function based on aia themes + * @param {number} sx the starting row to fill + * @param {number} sy the starting row to fill + * @param {number} ex the ending row to fill, defaults to sx + * @param {number} ey the ending row to fill, defaults to sy + * @param {number} c the color value, where green=1, yellow=2, and red=3 + */ fill(sx, sy, ex = sx, ey = sy, c = 0) { - for (let x = sx; x <= ex; x++) { - for (let y = sy; y <= ey; y++) { - this.data[x][y].fill = c === 1 || c === 2 || c === 3 ? c : 0; + if (!this.splitArray.doSplit) { + for (let x = sx; x <= ex; x++) { + for (let y = sy; y <= ey; y++) { + this.data[x][y].fill = c === 1 || c === 2 || c === 3 ? c : 0; + } + } + } else { + for (let i = 0; i < this.data.length; i++) { + // when it is just one cell for each row + if (sy === ey) { + let relativeY = sy + (this.splitArray.hasHeader ? 1 : 0); + + // if the relative start position is over the split array length, wrap to next split array + if (relativeY > this.splitArray.rowLength) { + sy -= this.splitArray.rowLength; + ey -= this.splitArray.rowLength; + continue; + } + + for (let x = sx; x <= ex; x++) { + this.data[i][x][relativeY].fill = + c === 1 || + c === 2 || + c === 3 ? + c : 0; + } + + break; + } + + + // when there are multiple columns + // if the relative start position is over the split array length, wrap to next split array + let relativeSY = sy + (this.splitArray.hasHeader ? 1 : 0); + if (relativeSY > this.splitArray.rowLength) { + sy -= this.splitArray.rowLength; + ey -= this.splitArray.rowLength; + continue; + } + + + // if the relative start position is over the split array length, limit + let relativeEY = ey + (this.splitArray.hasHeader ? 1 : 0); + if (relativeEY > this.splitArray.rowLength) { + relativeEY = this.splitArray.rowLength; + } + + // out of range, stop + if (relativeEY < 0) { + break; + } + + // start at the first index of subarray + if (relativeSY < 0) { + relativeSY = 0; + } + + for (let x = sx; x <= ex; x++) { + for (let y = relativeSY; y <= relativeEY; y++) { + this.data[i][x][y].fill = c === 1 || c === 2 || c === 3 ? c : 0; + } + } + + sy -= this.splitArray.rowLength; + ey -= this.splitArray.rowLength; } } } - // unfills the given element (used with fill) + /** + * unfills the given element (used with fill) + * @param {number} sx the starting row to unfill + * @param {number} sy the starting row to unfill + * @param {number} ex the ending row to unfill, defaults to sx + * @param {number} ey the ending row to unfill, defaults to sy + */ unfill(sx, sy, ex = sx, ey = sy) { - for (let x = sx; x <= ex; x++) { - for (let y = sy; y <= ey; y++) { - this.data[x][y].fill = 0; + if (!this.splitArray.doSplit) { + for (let x = sx; x <= ex; x++) { + for (let y = sy; y <= ey; y++) { + this.data[x][y].fill = 0; + } + } + } else { + for (let i = 0; i < this.data.length; i++) { + if (sy === ey) { + let relativeSY = sy + (this.splitArray.hasHeader ? 1 : 0); + if (relativeSY > this.splitArray.rowLength) { + sy -= this.splitArray.rowLength; + continue; + } + + for (let x = sx; x <= ex; x++) { + this.data[i][x][relativeSY].fill = 0; + } + + break; + } + + + let relativeSY = sy + (this.splitArray.hasHeader ? 1 : 0); + if (relativeSY > this.splitArray.rowLength) { + sy -= this.splitArray.rowLength; + ey -= this.splitArray.rowLength; + continue; + } + + let relativeEY = ey + (this.splitArray.hasHeader ? 1 : 0); + if (relativeEY > this.splitArray.rowLength) { + relativeEY = this.splitArray.rowLength; + } + + // out of range + if (relativeEY < 0) { + break; + } + + // start at the first index of subarray + if (relativeSY < 0) { + relativeSY = 0; + } + + for (let x = sx; x <= ex; x++) { + for (let y = relativeSY; y <= relativeEY; y++) { + this.data[i][x][y].fill = 0; + } + } + + sy -= this.splitArray.rowLength; + ey -= this.splitArray.rowLength; } } } @@ -158,7 +339,7 @@ class Array2DTracer extends Tracer { // XXX for some reason, variables only seem to be displayed if // row==2, and if you don't have enough rows in the table you are // stuck unless you add an extra dummy row and hide it using hideArrayAtIndex - assignVariable(v, row, idx) { + assignVariable(v, row, idx, changeFrom) { // deep clone data so that changes to this.data are all made at the same time which will allow for tweening // eslint-disable-next-line consistent-return function customizer(val) { @@ -168,24 +349,98 @@ class Array2DTracer extends Tracer { if (val.selected) newEl.selected = true; if (val.sorted) newEl.sorted = true; newEl.variables = val.variables; + newEl.fill = val.fill; return newEl; } } - const newData = cloneDeepWith(this.data, customizer); - // remove all current occurences of the variable - for (let y = 0; y < newData[row].length; y++) { - newData[row][y].variables = newData[row][y].variables.filter( - (val) => val !== v - ); + if (!this.splitArray.doSplit) { + const newData = cloneDeepWith(this.data, customizer); + + // remove all current occurences of the variable + for (let y = 0; y < newData[row].length; y++) { + newData[row][y].variables = newData[row][y].variables.filter( + (val) => val !== v + ); + } + + // add variable to item if not undefined or null + if (idx !== null && idx !== undefined) + newData[row][idx].variables.push(v); + + // update this.data + this.data = newData; + + } else { + let newData = []; + for (let i = 0; i < this.data.length; i++) { + let _newData = cloneDeepWith(this.data[i], customizer); + + // remove all current occurences of the variable + for (let y = 0; y < _newData[row].length; y++) { + _newData[row][y].variables = _newData[row][y].variables.filter( + (val) => val !== ((changeFrom !== undefined) ? changeFrom : v) + ); + } + + // add variable to item if not undefined or null + if (idx !== null && idx !== undefined) { + // check if idx is in subarray + // account for header offset + let relativeIdx = idx + (this.splitArray.hasHeader ? 1 : 0); + if (relativeIdx > 0 && relativeIdx <= this.splitArray.rowLength) + _newData[row][relativeIdx].variables.push(v); + } + + newData.push(_newData); + idx -= this.splitArray.rowLength; + } + + // update this.data + this.data = newData; } + } - // add variable to item if not undefined or null - if (idx !== null && idx !== undefined) - newData[row][idx].variables.push(v); + resetVariable(row) { + // deep clone data so that changes to this.data are all made at the same time which will allow for tweening + // eslint-disable-next-line consistent-return + function customizer(val) { + if (val instanceof Element) { + const newEl = new Element(val.value, val.key); + if (val.patched) newEl.patched = true; + if (val.selected) newEl.selected = true; + if (val.sorted) newEl.sorted = true; + newEl.variables = val.variables; + newEl.fill = val.fill; + return newEl; + } + } - // update this.data - this.data = newData; + if (!this.splitArray.doSplit) { + const newData = cloneDeepWith(this.data, customizer); + + // remove all current occurences of the variable + for (let y = 0; y < newData[row].length; y++) { + newData[row][y].variables = [] + } + + this.data = newData; + } else { + let newData = []; + for (let i = 0; i < this.data.length; i++) { + let _newData = cloneDeepWith(this.data[i], customizer); + + // remove all current occurences of the variable + for (let y = 0; y < _newData[row].length; y++) { + _newData[row][y].variables = []; + } + + newData.push(_newData); + } + + // update this.data + this.data = newData; + } } /** @@ -269,10 +524,134 @@ class Array2DTracer extends Tracer { * @param {*} newValue the new value. */ updateValueAt(x, y, newValue) { - if (!this.data[x] || !this.data[x][y]) { - return; + if (!this.splitArray.doSplit) { + if (!this.data[x] || !this.data[x][y]) { + return; + } + this.data[x][y].value = newValue; + } else { + for (let i = 0; i < this.data.length; i++) { + if (y !== null || y !== undefined || y >= 0) { + // check if y is in subarray + // add 1 to account for header offset + let relativeY = y + (this.splitArray.hasHeader ? 1 : 0); + if (relativeY > 0 && relativeY <= this.splitArray.rowLength) { + if (!this.data[i][x] || !this.data[i][x][relativeY]) continue; + this.data[i][x][relativeY].value = newValue; + } + y -= this.splitArray.rowLength; + } + } + } + } + + /** + * Get the value at the given position of the array. + * @param {*} x the row index. + * @param {*} y the column index. + */ + getValueAt(x, y) { + if (!this.splitArray.doSplit) { + if (!this.data[x] || !this.data[x][y]) { + return; + } + + return this.data[x][y].value; + } else { + for (let i = 0; i < this.data.length; i++) { + if (y !== null || y !== undefined || y >= 0) { + // check if y is in subarray + // add 1 to account for header offset + let relativeY = y + (this.splitArray.hasHeader ? 1 : 0); + if (relativeY > 0 && relativeY <= this.splitArray.rowLength) { + if (!this.data[i][x] || !this.data[i][x][relativeY]) continue; + return this.data[i][x][relativeY].value; + } + y -= this.splitArray.rowLength; + } + } } - this.data[x][y].value = newValue; + } + + /** + * Extract the array at the given row(s) of the array. + * @param {*} row the row index(es). + * @param {*} empty the character to change to empty. + */ + extractArray(row, empty) { + let extract = []; + // currently does not support empty character replacement + // to implement later + if (!this.splitArray.doSplit) { + if (Array.isArray(row) && row.length) { + for (const i of row) { + extract.push(this.data[i].map((e) => e.value)); + } + } else { + extract = this.data[row].map((e) => e.value); + } + + } else { + // combine the split array and remove the headers if exist + let combined = []; + if (this.splitArray.hasHeader) { + for (const array of this.data) { + // get the first subarray + if (!combined.length) { + combined = array.map((arr) => arr.slice(1)); + continue; + } + + // append the next subarray + for (let i = 0; i < combined.length; i++) { + combined[i] = [...combined[i], ...array[i].slice(1)]; + } + } + } else { + for (const array of this.data) { + // get the first subarray + if (!combined.length) { + combined = array; + continue; + } + + // append the next subarray + for (let i = 0; i < combined.length; i++) { + combined[i] = [...combined[i], ...array[i]]; + } + } + } + + // extract the value array + if (Array.isArray(row) && row.length) { + // extracting multiple rows + for (const i of row) { + // get the value + extract.push(combined[i].map((e) => e.value)); + } + } else { + // get the value + extract = combined[row].map((e) => e.value); + } + } + + // change an empty character to undefined + // also extract a chaining array for hash chaining + for (let i = 0; i < extract.length; i++) { + extract[i] = (extract[i] === empty) ? undefined : extract[i]; + if (typeof extract[i] === 'string') { + if (extract[i].includes("..")) { + let popper = document.getElementById('float_box_' + i); + let array = popper.innerHTML.split(',').map(Number); + extract[i] = array; + } + } + } + return extract; + } + + setHighlightRow(row) { + this.highlightRow = row; } } diff --git a/src/components/DataStructures/Graph/GraphRenderer/index.js b/src/components/DataStructures/Graph/GraphRenderer/index.js index dcf142aa9..5c25f99ce 100644 --- a/src/components/DataStructures/Graph/GraphRenderer/index.js +++ b/src/components/DataStructures/Graph/GraphRenderer/index.js @@ -449,7 +449,7 @@ class GraphRenderer extends Renderer { } renderData() { - const { nodes, edges, isDirected, isWeighted, dimensions, text, functionInsertText, functionNode, functionBalance, rectangle, radius, tagInfo } = + const { nodes, edges, isDirected, isWeighted, dimensions, text, functionInsertText, functionNode, functionBalance, rectangle, radius, tagInfo, newZoom } = this.props.data; const { baseWidth, @@ -472,6 +472,12 @@ class GraphRenderer extends Renderer { rootX = root.x; rootY = root.y; } + // + // // Change Renderer's zoom on newZoom change + // if (newZoom != this.zoom && newZoom !== undefined) { + // this.zoom = newZoom; + // this.refresh(); + // } return ( {title} { this.renderData() } diff --git a/src/components/DataStructures/common/Tracer.jsx b/src/components/DataStructures/common/Tracer.jsx index 08dac1800..186a88784 100644 --- a/src/components/DataStructures/common/Tracer.jsx +++ b/src/components/DataStructures/common/Tracer.jsx @@ -10,6 +10,7 @@ class Tracer { if (options !== undefined) { this.arrayItemMagnitudes = options.arrayItemMagnitudes; this.largestValue = options.largestValue; + this.size = options.size; } this.init(); this.reset(); @@ -25,13 +26,35 @@ class Tracer { render() { const RendererClass = this.getRendererClass(); return ( - + ); } set() { } + /** + * Set visualiser size (flex value for renderer) + * @param {*} size + */ + setSize(size) { + this.size = size; + } + + /** + * Change the zoom of the visualizer + * @param {*} zoom the new zoom + */ + setZoom(zoom) { + this.newZoom = zoom; + window.setTimeout(() => {this.newZoom = undefined}, 200) + } + reset() { this.set(); } diff --git a/src/components/mainmenu/InsertSearchAlgorithms.js b/src/components/mainmenu/InsertSearchAlgorithms.js index 9b26e37cb..2275c55b1 100644 --- a/src/components/mainmenu/InsertSearchAlgorithms.js +++ b/src/components/mainmenu/InsertSearchAlgorithms.js @@ -1,5 +1,5 @@ import React from 'react'; -import '../../styles/InsertSearchAlgorithms.scss'; +import '../../styles/InsertSearchAlgorithms.scss'; // Get the base URL dynamically const baseUrl = window.location.origin; @@ -7,6 +7,9 @@ const baseUrl = window.location.origin; const insertSearchAlgorithms = [ { name: 'Binary Search Tree', url: `${baseUrl}/?alg=binarySearchTree&mode=search` }, { name: '2-3-4 Tree', url: `${baseUrl}/?alg=TTFTree&mode=search` }, + { name: 'Hashing (Linear Probing)', url: `${baseUrl}/?alg=HashingLP&mode=insertion` }, + { name: 'Hashing (Double Hashing)', url: `${baseUrl}/?alg=HashingDH&mode=insertion` }, + { name: 'Hashing (Chaining)', url: `${baseUrl}/?alg=HashingCH&mode=insertion` }, ]; const InsertSearchAlgorithms = () => { @@ -22,4 +25,4 @@ const InsertSearchAlgorithms = () => { ); }; -export default InsertSearchAlgorithms; \ No newline at end of file +export default InsertSearchAlgorithms;