Clarification of Intent
- Recursion here is used on purpose for practice - note that iteration is preferred in this case (I'll be more clear about where my intentions are!)
- Factory function was also used in place of
class
for practice - The project is educational, for the purposes of learning the ins and outs of Linked Lists
This is most likely used in my next project HashMap (as a part of The Odin Project https://www.theodinproject.com/lessons/javascript-hashmap-data-structure#growth-of-a-hash-table ) , and I wanted to ensure that the code is good.
Here are some of my concerns which I'd love to get some feedback on:
I implemented a generalized traverse() function and made each operation call it with a "mode" parameter. I'm not sure about this implementation.
I struggled to implement a generalized evaluateCondition function. Part of the evaluateCondition parameter is set indirectly by using "mode" + switch statements (condition2). The other part would be set by passing a conditionObject right when any linkedList operation calls traverse(). The challenge was that condition2 needed to be either value/index, which would change only within traverse(). I am not sure if my solution was okay. Concerned about passing too many parameters into traverse() as it is recursive.
is it good practice to track [head,tail,size] as variables inside my module? I'd decided the tradeoff is worth it, since the alternative would be to re-calculate all 3 using traverse() every time I need any one of these values.
I had some concerns about the space complexity. Articles such as this one https://www.geeksforgeeks.org/time-and-space-complexity-of-linked-list/ would claim O(1), but I have a feeling this is assuming a non-recursive solution. I'd imagine that height/depth would basically be the length of the linked list. So max space would be O(n), with a coefficient that correlates to the number and space complexity of the parameters being passed. This made me try and use the minimal # of arguments for traverse(). If anyone could clarify what the space complexity will be, and if it is a genuine concern in this case, that would be helpful!
I found that every time I made any changes / added new operations, I'd also have to alter traverse() and this bothered me - it made me think coupling was too tight.
Any other criticisms welcome!
function linkedList() {
let head;
let tail;
let size = 0;
function getTail() {
// MAYBE: I could traverse instead of storing tail in memory
return tail;
}
function getSize() {
return size;
}
function getHead() {
return head;
}
function checkIfSizeIs1(newNodeReference) {
if (getSize() === 0) {
head = newNodeReference;
tail = newNodeReference;
return true;
}
return false;
}
function traverse(
conditionObject,
mode = null,
currentNode = head,
currentIndex = 0,
resultString = '',
) {
// TODO: calculate big O for time & space
// MAYBE: use loop instead
let stopConditionMet = false;
const currentValue = currentNode.value;
stopConditionMet = evaluateCondition(conditionObject, mode, currentIndex, currentValue);
// BASE CASE
if (stopConditionMet === true) {
if (mode === 'find') {
return currentIndex;
}
if (mode === 'toString') {
return `${resultString}( ${currentNode.value} ) -> ` + ' null';
// return resultString + ' null';
}
return currentNode;
}
if (currentNode === tail) return currentNode;
// RECURSIVE CASE
resultString = resultString.concat(`( ${currentNode.value} )`, '->');
currentNode = currentNode.next;
return traverse(conditionObject, mode, currentNode, currentIndex + 1, resultString);
}
function evaluateCondition(conditionObject, mode, currentIndex, currentValue) {
// MAYBE: instead of using mode, assign "this" to static variable (which poiints to the caller function)
let {
condition1,
condition2
} = conditionObject;
let meetsCondition = false;
switch (mode) {
case 'at':
case 'pop':
case 'toString':
case 'insertAt':
case 'removeAt':
{
condition2 = currentIndex;
break;
}
case 'contains':
case 'find':
{
condition2 = currentValue;
break;
}
default:
}
meetsCondition = condition1 === condition2;
return meetsCondition;
}
function at(targetIndex) {
try {
isIndexValid(targetIndex);
const result = traverse({
condition1: targetIndex
}, 'at');
return result;
} catch (error) {
console.error(error);
}
}
function pop() {
try {
if (size <= 0) throw new Error('Cannot pop, size is 0');
const last = { ...tail
};
tail.value = null;
const secondToLast = traverse({
condition1: size - 2
}, 'pop'); // find 2nd to the last element
secondToLast.next = null;
// set new tail value
tail = secondToLast;
size -= 1;
return last;
} catch (error) {
console.error(error);
}
}
function contains(value) {
const currentNode = traverse({
condition1: value
}, 'contains');
return currentNode.value === value;
}
function find(value) {
const index = traverse({
condition1: value
}, 'find');
return Number.isInteger(index) ? index : undefined; // traverse will return currentNode (tail) if nothing is found. If found, it will return an index,
}
function toString() {
return traverse({
condition1: size - 1
}, 'toString');
}
function append(value) {
const newNodeReference = node(value);
checkIfSizeIs1(newNodeReference);
getTail().next = newNodeReference;
tail = newNodeReference;
size++;
return newNodeReference;
}
function prepend(value) {
const newNodeReference = node(value, head);
checkIfSizeIs1(newNodeReference);
head = newNodeReference;
size++;
return newNodeReference;
}
function insertAt(value, index) {
try {
isIndexValid(index);
if (index === 0) {
prepend(value);
return;
}
if (index === size - 1) {
append(value);
return;
}
const insertionPoint = traverse({
condition1: index
}, 'insertAt');
const newNode = node(value, insertionPoint.next);
insertionPoint.next = newNode;
size += 1;
return newNode;
} catch (error) {
console.error(error);
}
}
function removeAt(index) {
try {
isIndexValid(index);
if (index === 0) {
aList.pop;
return;
}
const nodeBeforeTarget = traverse({
condition1: index - 1
}, 'removeAt');
const removalTarget = { ...nodeBeforeTarget.next
};
nodeBeforeTarget.next = nodeBeforeTarget.next.next;
if (index === size - 1) {
tail = nodeBeforeTarget;
console.log('NEW tail:', tail);
}
size -= 1;
return removalTarget;
} catch (error) {
console.error(error);
}
}
function isIndexValid(targetIndex) {
if (targetIndex + 1 > size) {
throw new Error('Invalid Index');
}
}
return {
append,
prepend,
getHead,
getSize,
getTail,
at,
pop,
contains,
find,
toString,
insertAt,
removeAt,
};
}
// MAYBE: use undefined instead of null
function node(value = null, next = null) {
return {
value,
next
};
}
//TESTS
const aList = linkedList();
aList.append(2);
aList.append(5);
aList.append(4);
aList.prepend(10);
aList.prepend(7);
aList.prepend(8);
const at = aList.at(4);
const contains = aList.contains(8);
const find = aList.find(1000);
const toString = aList.toString();
console.log('toString:', toString);
const [head, tail, size] = [aList.getHead(), aList.getTail(), aList.getSize()];
console.log('head:', head);
console.log('tail:', tail);
console.log('size:', size);