I have written LSD radix sort implementation in JavaScript (6 functions in total).
It sorts positive and negative integers:
// returns [-4935, -511, 1, 48, 333, 689]
radixSort([-511, -4935, 333, 689, 1, 48]);
Could you please review it?
I prefixed my concerns with: "Concern".
Performance figures sneak peek:
// 100k elements, range: -10k to 10k
// Node v8.9.0
radixSort(): 136.767ms
Array.prototype.sort(): 72.909ms
1. Full code and tests
Run the code snippet below to test it.
Tests:
- run 100 times: generate 1k integers between -10k and 10k, sort them with
radixSort()
and compare the results toArray.prototype.sort()
- compare execution times of
radixSort()
,Array.prototype.sort()
andotherRadixSort()
for 100k integers between 0 and 10k (other implementation found on GitHub). - test each function
/** @module */
/**
* Get the nth digit from a number.
* Rounds the number (12.5 -> 13).
* Returns 0 when the specified index is out of range.
* @example
* // returns 4
* getDigit(1234, 0);
* @see {@link https://stackoverflow.com/a/7784664 Get digits out of a number}
* @param {Number} num Number
* @param {Number} [index=0] The index of the number's digit to return.
* Digits are zero-indexed from right to left.
* @returns {Number} The digit of a number at specified index.
*/
function getDigit(num, index = 0) {
let idx = index;
let number = Math.round(num);
let remainder = 0;
let lastIteration = false;
// best condition: (index >= 0 && index =< number.length)
while (idx >= 0) {
remainder = (number % 10);
number = ((number - remainder) / 10);
if (lastIteration) {
break;
}
/**
* If the passed index is bigger than the number's length,
* make only one unnecessary loop to return 0.
* i.e `getDigit(900, 50)` should return 0 after 4 loops instead 50. */
if (number === 0) {
// if we stopped here, `getDigit(900, 50)` would return 9.
lastIteration = true;
}
idx -= 1;
}
return remainder;
}
/**
* Create an array of X empty arrays.
* @example
* const buckets = createBuckets(10);
* // returns 10
* buckets.length
* // returns true
* Array.isArray(buckets[9]);
* @param {Number} x Number of arrays to create
* @returns {Array} Array of arrays.
*/
function createBuckets(x) {
// `new Array(10).fill([])` would reference the same Array object
const buckets = new Array(x);
// `Array.forEach()` ignores array holes
for (let i = 0; i < buckets.length; i += 1) {
buckets[i] = [];
}
return buckets;
}
/**
* Sort positive integers using nth digit/exponent.
* @example
* const integers = [1020, 680, 870, 90, 1];
* // sort by 10's place
* const sorted = sort(integers, 1);
* // returns [1, 1020, 870, 680, 90]
* console.log(sorted);
* @param {Array} intgs Array of positive integers
* @param {Number} [nth=0] Digit/exponent
* @returns {Array} Sorted array of integers by nth digit.
*/
function sort(intgs, nth = 0) {
if (intgs.length === 0) {
return intgs;
}
const integers = [...intgs];
const currentPlaceValue = 10 ** nth;
const buckets = createBuckets(10);
const sorted = [];
// add number to bucket or skip it if it's sorted
integers.forEach((integer) => {
/**
* If we're sorting the integers using 2nd digit (10's place),
* integers have been already sorted by LSD (1's place).
*/
if (integer >= currentPlaceValue) {
const digit = getDigit(integer, nth);
buckets[digit].push(integer);
} else {
sorted.push(integer);
}
});
// empty each bucket into the auxiliary array
buckets.forEach((bucket) => {
if (bucket.length > 0) {
sorted.push(...bucket);
}
});
// copy elements back from the auxiliary to original array
sorted.forEach((item, j) => {
if (item !== integers[j]) {
integers[j] = item;
}
});
return integers;
}
/**
* Count digits of a positive or negative number.
* @example
* // returns 3
* countDigits(-124.785);
* @see {@link https://stackoverflow.com/a/28203456 Count digits of a number}
* @param {Number} number Number
* @returns {Number} Number of digits.
*/
function countDigits(number) {
const abs = Math.abs(number);
// how many 10s have to be multiplied to get `abs`
const log10 = Math.log10(abs);
// `Math.log10(0)` returns `-Infinity`
const finite = Number.isFinite(log10) ? log10 : 0;
const floor = Math.floor(finite);
// `Math.ceil(0) = 0` vs `Math.floor(0) + 1 = 1` (correct count)
const count = (floor + 1);
return count;
}
/**
* Find the biggest integer in the array of integers.
* @example
* // returns 150
* getMax([-150, 100]);
* @param {Array} integers Array of integers
* @returns {Number} The biggest integer from the array.
*/
function getMax(integers) {
const absArr = integers.map(Math.abs);
const maxNum = Math.max(...absArr);
return maxNum;
}
/**
* LSD radix sort that sorts positive and negative integers.
* @static
* @example
* import radixSort from './radixSort';
* const integers = [-511, -4935, 333, 689, 1, 48];
* const sorted = radixSort(integers);
* // returns [-4935, -511, 1, 48, 333, 689]
* console.log(sorted);
* @see {@link https://en.wikipedia.org/wiki/Radix_sort#Least_significant_digit_radix_sorts Wiki: LSD radix sort}
* @see {@link https://codereview.stackexchange.com/a/150288 Radix sort implementation in JS}
* @param {Array} integers Array of integers
* @returns {Array} The sorted array.
*/
function radixSort(integers) {
const maxInteger = getMax(integers);
const digits = countDigits(maxInteger);
let positiveIntegers = integers.filter(v => v >= 0);
let negativeIntegers = integers.filter(v => v < 0).map(Math.abs);
// sort the integers starting at the LSD (ones, next tens, then hundreds...)
for (let i = 0; i < digits; i += 1) {
positiveIntegers = sort(positiveIntegers, i);
negativeIntegers = sort(negativeIntegers, i);
}
// reverse the sorted negative integers, add the negative sign to each integer
negativeIntegers = negativeIntegers.reverse().map(v => -Math.abs(v));
// merge the positive and negative integers
const results = negativeIntegers.concat(positiveIntegers);
return results;
}
// Tests
// Test functions
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random#Getting_a_random_integer_between_two_values_inclusive
function getRandomIntInclusive(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
function compareNumbers(a, b) {
return a - b;
}
function generateArrayIntegers(length, min, max) {
return Array.from({ length: length }, () => getRandomIntInclusive(min, max));
}
// Mocha
mocha.setup('bdd');
const { expect } = chai;
describe('Radix Sort', function() {
describe('radixSort()', function() {
it('Sorts the array of 1k integers between -10k and 10k', () => {
for (let i = 100; i >= 0; i -= 1) {
const generated = generateArrayIntegers(1000, -10000, 10000);
const result = radixSort(generated);
generated.sort(compareNumbers);
expect(result).to.deep.equal(generated);
}
});
});
describe('sort([1020, 680, 870, 90, 1], 1)', () => {
it('Sort the integers by 10\'s place', () => {
const result = sort([1020, 680, 870, 90, 1], 1);
expect(result).to.deep.equal([1, 1020, 870, 680, 90]);
});
});
describe('getDigit(1234, 1)', () => {
it('Gets the nth digit from a number', () => {
const result = getDigit(1234, 1);
expect(result).equal(3);
});
});
describe('getDigit(12.5, 0)', () => {
it('Rounds the number then gets the nth digit', () => {
const result = getDigit(12.5, 0);
expect(result).equal(3);
});
});
describe('getDigit(10, 3)', () => {
it('Returns 0 when the specified index is out of range', () => {
const result = getDigit(10, 3);
expect(result).equal(0);
});
});
describe('countDigits(12345)', () => {
it('Counts digits of the number', () => {
const result = countDigits(12345);
expect(result).equal(5);
});
});
describe('countDigits(-65)', () => {
it('Counts digits of the negative number', () => {
const result = countDigits(-65);
expect(result).equal(2);
});
});
describe('countDigits(0)', () => {
it('Counts digits properly when the number is 0', () => {
const result = countDigits(0);
expect(result).equal(1);
});
});
describe('getMax([-150, 100, 50])', () => {
it('Finds the biggest absolute number in the array', () => {
const result = getMax([-150, 100, 50]);
expect(result).equal(150);
});
});
describe('createBuckets(10)', () => {
it('Create buckets array in which each bucket is an empty array itself', () => {
const result = createBuckets(10);
expect(result).to.have.lengthOf(10);
for (let i = 0; i < result.length; i += 1) {
expect(result[i]).to.be.an('array').that.is.empty;
}
});
});
});
mocha.run();
/**
* Time
*
* Measure execution times for this and other implementation (to compare).
* Other implementation: https://github.com/mgechev/javascript-algorithms/blob/master/src/sorting/radixsort.js
*/
const generated = generateArrayIntegers(100000, 0, 10000);
const otherRadixSort = window.radixSort;
console.time('radixSort()');
radixSort(generated);
console.timeEnd('radixSort()');
console.time('otherRadixSort()');
otherRadixSort(generated);
console.timeEnd('otherRadixSort()');
console.time('Array.prototype.sort()');
generated.sort(compareNumbers);
console.timeEnd('Array.prototype.sort()');
<script src="https://rawgit.com/mgechev/javascript-algorithms/master/src/sorting/radixsort.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chai/4.1.2/chai.min.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/mocha/4.0.1/mocha.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/4.0.1/mocha.min.js"></script>
<div id="mocha"></div>
2. Isolated code - 6 functions
The code consists of 6 functions, the hierarchy is following:
radixSort()
- the main functiongetMax()
- find the biggest integercountDigits()
- how many digits the number hassort()
- the heart, sort an array using nth digit (ones, tens...)createBuckets()
- create array of empty arraysgetDigit()
- get the nth digit from a number
radixSort()
- the main
- Find the biggest integer, count its digits.
- Split the passed integers into two arrays: positive and negative (for negatives get the absolute value).
- Sort both arrays for each digit.
Reverse the sorted negative array, add negative sign, return them both merged.
function radixSort(integers) { const maxInteger = getMax(integers); const digits = countDigits(maxInteger); let positiveIntegers = integers.filter(v => v >= 0); let negativeIntegers = integers.filter(v => v < 0).map(Math.abs); // sort the integers starting at the LSD (ones, next tens, then hundreds...) for (let i = 0; i < digits; i += 1) { positiveIntegers = sort(positiveIntegers, i); negativeIntegers = sort(negativeIntegers, i); } /** reverse the sorted negative integers, * add the negative sign to each integer */ negativeIntegers = negativeIntegers.reverse().map(v => -Math.abs(v)); // merge the positive and negative integers const results = negativeIntegers.concat(positiveIntegers); return results; }
getMax()
Concern 1: I use .map(Math.abs)
twice: in getMax()
and radixSort()
.
/**
* Find the biggest integer in the array of integers.
* @example
* // returns 150
* getMax([-150, 100]);
* @param {Array} integers Array of integers
* @returns {Number} The biggest integer from the array.
*/
function getMax(integers) {
const absArr = integers.map(Math.abs);
const maxNum = Math.max(...absArr);
return maxNum;
}
// returns 150
console.log(getMax([-150, 100]));
countDigits()
/**
* Count digits of a positive or negative number.
* @example
* // returns 3
* countDigits(-124.785);
* @see {@link https://stackoverflow.com/a/28203456 Count digits of a number}
* @param {Number} number Number
* @returns {Number} Number of digits.
*/
function countDigits(number) {
const abs = Math.abs(number);
// how many 10s have to be multiplied to get `abs`
const log10 = Math.log10(abs);
// `Math.log10(0)` returns `-Infinity`
const finite = Number.isFinite(log10) ? log10 : 0;
const floor = Math.floor(finite);
// `Math.ceil(0) = 0` vs `Math.floor(0) + 1 = 1` (correct count)
const count = (floor + 1);
return count;
}
// returns 3
console.log(countDigits(-124.785));
sort()
- the heart
integers
array -> each integer -> buckets[digit] -> empty buckets
contents to sorted
array -> copy elements from sorted
to integers
It expects only positive integers but the main function radixSort()
takes care of that.
Concern 2: it creates the same buckets array every loop. It could be passed and cleared every loop with buckets.splice(0)
. On the other hand, GC should take care of that, shouldn't it?
Concern 3: I'm using auxiliary array sorted
, should it be avoided?
function sort(intgs, nth = 0) {
if (intgs.length === 0) {
return intgs;
}
const integers = [...intgs];
const currentPlaceValue = 10 ** nth;
const buckets = createBuckets(10);
const sorted = [];
// add number to bucket or skip it if it's sorted
integers.forEach((integer) => {
/**
* If we're sorting the integers using 2nd digit (10's place),
* integers have been already sorted by LSD (1's place).
*/
if (integer >= currentPlaceValue) {
const digit = getDigit(integer, nth);
buckets[digit].push(integer);
} else {
sorted.push(integer);
}
});
// empty each bucket into the auxiliary array
buckets.forEach((bucket) => {
if (bucket.length > 0) {
sorted.push(...bucket);
}
});
// copy elements back from the auxiliary to original array
sorted.forEach((item, j) => {
if (item !== integers[j]) {
integers[j] = item;
}
});
return integers;
}
createBuckets()
/**
* Create an array of X empty arrays.
* @example
* const buckets = createBuckets(10);
* // returns 10
* buckets.length
* // returns true
* Array.isArray(buckets[9]);
* @param {Number} x Number of arrays to create
* @returns {Array} Array of arrays.
*/
function createBuckets(x) {
// `new Array(10).fill([])` would reference the same Array object
const buckets = new Array(x);
// `Array.forEach()` ignores array holes
for (let i = 0; i < buckets.length; i += 1) {
buckets[i] = [];
}
return buckets;
}
const buckets = createBuckets(10);
// returns 10
console.log(buckets.length);
// returns true
console.log(Array.isArray(buckets[9]));
getDigit()
/**
* Get the nth digit from a number.
* Rounds the number (12.5 -> 13).
* Returns 0 when the specified index is out of range.
* @example
* // returns 4
* getDigit(1234, 0);
* @see {@link https://stackoverflow.com/a/7784664 Get digits out of a number}
* @param {Number} num Number
* @param {Number} [index=0] The index of the number's digit to return.
* Digits are zero-indexed from right to left.
* @returns {Number} The digit of a number at specified index.
*/
function getDigit(num, index = 0) {
let idx = index;
let number = Math.round(num);
let remainder = 0;
let lastIteration = false;
// best condition: (index >= 0 && index =< number.length)
while (idx >= 0) {
remainder = (number % 10);
number = ((number - remainder) / 10);
if (lastIteration) {
break;
}
/**
* If the passed index is bigger than the number's length,
* make only one unnecessary loop to return 0.
* i.e `getDigit(900, 50)` should return 0 after 4 loops instead 50. */
if (number === 0) {
// if we stopped here, `getDigit(900, 50)` would return 9.
lastIteration = true;
}
idx -= 1;
}
return remainder;
}
// returns 4
console.log(getDigit(1234, 0));
Concern 4: is it efficient enough for a typical radix sort implementation in JS?