DEV Community

Cover image for Solving The Same Algorithmic Challenge In 13 Different Ways
Carl Mungazi
Carl Mungazi

Posted on

Solving The Same Algorithmic Challenge In 13 Different Ways

Given an array (the first argument in the destroyer function) remove all elements from the initial array that are of the same value as these arguments in the function. Note: You have to use the arguments object

Algorithmic challenges such as the one above (from FreeCodeCamp) are fun ways to stretch your problem solving muscles. You can usually come up with a few solutions but what if you tried solving it in as many ways as you can?

I have always wanted to do that so when I found myself with some free time, I began dreaming of ways I could torture JavaScript into doing things which would be a sackable offence if they ever found their way into a production codebase.

The challenge

The problem itself is fairly straightforward. You have a function which takes multiple inputs (the target array plus one or more values) and returns an array which contains the target array minus the values entered as the other arguments. From this explanation we can deduce the following:

  • The solution will involve array manipulation
  • The solution has to be able to handle any number of arguments (via the arguments object)

Solution 1: Make it work

function destroyerForEach(arr, ...args) {
  return arr.filter((el) => {
    let passed = el;
    args.forEach((num) => {
      if (num === el) passed = null;
    });
    return passed !== null;
  });
}
Enter fullscreen mode Exit fullscreen mode

Whenever I am solving a problem, I create a working solution as fast as possible and then improve it afterwards. destroyerForEach takes the more long-winded approach of filtering through the target array and then looping through the rest of the arguments for each item in the target array. It is not pretty but it works. You could improve your programming street cred with this one liner args.forEach(num => num === el ? passed = null: null) in the .forEach function.

Solution 2: Filter and find

function shouldItemBeDestroyed(targetElement, comparisonArr) {
  return comparisonArr.find((el) => el === targetElement);
}

function destroyer(arr, ...args) {
  return arr.filter((el) => el !== shouldItemBeDestroyed(el, args));
}
Enter fullscreen mode Exit fullscreen mode

If the .forEach and .filter combination is not to your taste, you can reach for .find. This solution has the added benefit of splitting the logic between different functions, thus improving the testability of the code. Again, you could unleash your inner one line maximalist with this:

const destroyer = (arr, ...args) =>
  arr.filter((el) => el !== args.find((item) => item === el));
Enter fullscreen mode Exit fullscreen mode

Solution 3: Short and simple

function destroyerIncludes(arr, ...args) {
  return arr.filter((item) => !args.includes(item));
}
Enter fullscreen mode Exit fullscreen mode

This solution gets to the crux of the matter without much ceremony. You will notice that .filter has been a mainstay in each solution thus far. This is because it is perfectly suited to tasks such as this. An interesting thing to note is that .includes returns a boolean whilst .filter's testing function returns a value which coerces to either true or false. This is useful if you like to avoid implicit coercions. You can take this solution to new heights by indulging both your ES6 and one liner tendencies to create this beauty:

const destroyerIncludes = (arr, ...args) =>
  arr.filter((item) => !args.includes(item));
Enter fullscreen mode Exit fullscreen mode

Solution 4 & 5: Indexing

function destroyerIndexOf(arr, ...args) {
  return arr.filter((item) => args.indexOf(item) < 0);
}

// OR

function destroyerLastIndexOf(arr, ...args) {
  return arr.filter((item) => args.lastIndexOf(item) < 0);
}
Enter fullscreen mode Exit fullscreen mode

We can continue keeping things simple by using array indexes to determine which values need purging. This solution only works if we use the spread syntax to turn the arguments object from an Array-like object into an array. We also need to perform this spread operation in the parameter declaration. Had we done it like this, for example:

function destroyerIndexOf(arr) {
  const args = [...arguments];
  // ... rest of the code goes here
}

destroyerIndexOf([1, 2, 3, 4], 2, 3);
// args would be [ [ 1, 2, 3, 4 ], 2, 3 ]
Enter fullscreen mode Exit fullscreen mode

We would be including the target array in our array of elimination values.

Solution 6: Give the filter some

function shouldItemBeDestroyed(target, comparisonArr) {
  return comparisonArr.some((el) => el === target);
}

function destroyerSome(arr, ...args) {
  return arr.filter((el) => !shouldItemBeDestroyed(el, args));
}
Enter fullscreen mode Exit fullscreen mode

A similar solution to the one using .find, with the difference being .some returns a boolean instead of a value.

Solution 7: #nofilter

function destroyerValuesIterator(arr, ...args) {
  let finalArr = [];
  const iterator = arr.values();

  for (const value of iterator) {
    if (!args.includes(value)) finalArr.push(value);
  }

  return finalArr;
}
Enter fullscreen mode Exit fullscreen mode

What's that? No filtering?! Yes, it is possible to live without .filter and we do that by relying on for..of to handle the iteration. The .values methods returns an Array Iterator object which contains the values for each index in the array.

Solution 8: ?!?!?!

function destroyerArrOwnProp(arr, ...args) {
  args.forEach((item) => {
    Object.defineProperties(Array, {
      [item]: {
        value: item,
        writable: true,
        configurable: true, // so we can use delete to clean up after ourselves
      },
    });
  });

  return arr.filter((item) => {
    return !Array.hasOwnProperty(item);
  });

Enter fullscreen mode Exit fullscreen mode

I cannot think of a scenario where this is even an option but it is reassuring to know we can create such monstrosities should the mood strike. Here we are extending the built-in Array object so we can use the .hasOwnProperty method later to weed out the repeated values. In this solution's defence, it sets the configurable property to true so we can cover our tracks by deleting the properties and pretending this never happened.

Solution 9: Splicin' it up

function destroyerSpliceAndFromAndForEach(arr, ...args) {
  const copiedArr = Array.from(arr);

  arr.forEach((item) => {
    args.forEach((num) => {
      if (num === item) {
        const index = copiedArr.indexOf(item);
        copiedArr.splice(index, 1);
      }
    });
  });

  return copiedArr;
}
Enter fullscreen mode Exit fullscreen mode

Here we use Array.from to create a shallow copy of the target array and then enlist the services of .splice to hack away the repeat elements. We can safely perform surgery on copiedArr because whilst it has the same values as arr, they are different array objects, so we don't have to worry about any mutation.

Solution 10: Functional preparation

function destroyerFromMap(arr, ...args) {
  const mapFn = (item) => ({ value: item, isSameVal: args.includes(item) });
  const copiedArr = Array.from(arr, mapFn);

  return copiedArr.filter((item) => !item.isSameVal).map((item) => item.value);
}
Enter fullscreen mode Exit fullscreen mode

We're not done with .from just yet. This method has two optional arguments, the first of which is a map function that is called on every element of the array being copied. We can take advantage of this to prepare our array during the copy process by creating an object and adding a property on it which checks if the item being filtered against the arguments.

Solution 11: Let's get reducin'

function destroyerReducerConcat(arr, ...args) {
  return arr.reduce((seedArray, elementFromSourceArr) => {
    if (!args.includes(elementFromSourceArr)) {
      return seedArray.concat(elementFromSourceArr);
    }

    return seedArray;
  }, []);
}
Enter fullscreen mode Exit fullscreen mode

This is one of my favourite solutions because it taught me a new way of using the .reduce method. For a more in-depth and comprehensive explanation of the mechanics behind this solution, this article has you covered. With the .reduce method, we can either provide a second argument or omit it, in which case it defaults to the first element of the array being worked on. In our case, we can "seed it" with an empty array and then populate that array with the passing elements. The other new method which makes its first appearance is .concat and we use it to merge values into the seed array.

Solution 12: Let's get fancy with our reducin'

const destroyerReducerOneLinerSpread = (arr, ...args) =>
  arr.reduce(
    (seedArray, elementFromSourceArr) =>
      !args.includes(elementFromSourceArr)
        ? (seedArray = [...seedArray, elementFromSourceArr])
        : seedArray,
    []
  );
Enter fullscreen mode Exit fullscreen mode

As if solution 11 wasn't fancy enough, we can really flex our ES6 muscles by swapping .concat for the spread operator and using a ternary to really drive home that we can write one-liners with the best of them.

const destroyerReducerOneLinerSpread = (arr, ...args) =>
  arr.reduce(
    (seedArray, elementFromSourceArr) =>
      !args.includes(elementFromSourceArr)
        ? (seedArray = [...seedArray, elementFromSourceArr])
        : seedArray,
    []
  );
Enter fullscreen mode Exit fullscreen mode

Solution 13: Settin' things up

function destroyerSet(arr, ...args) {
  const argsSet = new Set(args);
  let uniqueVals = [];

  for (let i = 0; i < arr.length; i++) {
    if (!argsSet.has(arr[i])) uniqueVals = [...uniqueVals, arr[i]];
  }

  return uniqueVals;
}
Enter fullscreen mode Exit fullscreen mode

The final solution is another gratuitous use of a random JavaScript feature. Here we have cajoled a Set into storing our arguments and then used a for loop to iterate through the Set and find the unique values.

Discussion (0)