36

I have a challenge in JavaScript that I’m trying to figure out for a while already.

Consider this array:

let arr = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5];

I have to output this result:

arr = [0, 0, 0, 0, 0, 5, 4, 3, 2, 1]

I’m following this line of logic to position the zeros in front, adjusting the index value:

arr.sort((x, y) => {
    if (x !== 0) {
        return 1;
    }

    if (x === 0) {
        return -1;
    }

    return y - x;
});

But I’m stuck at this result:

arr = [0, 0, 0, 0, 0, 1, 2, 3, 4, 5]

Does anyone have any tips on how to solve this?

Sebastian Simon
  • 18,263
  • 7
  • 55
  • 75
lianbwl
  • 398
  • 4
  • 6
  • 6
    Is it guaranteed there are no negative numbers? – Michael Nov 19 '19 at 20:57
  • 2
    Doesn't it fix if the last line is switched to `return x - y;`? – Mooing Duck Nov 19 '19 at 22:24
  • 2
    Would it be efficient in JavaScript to count and remove zeros, sort the remaining elements normally? (without a custom comparator so hopefully the JS engine can use a built-in number-sorting function). Then prepend the right number of zeros to the result. If there are many zeros, removing them before sorting makes a smaller problem. Or swap the zeros to the start of the array with one pass, then sort the tail of the array? – Peter Cordes Nov 20 '19 at 04:23
  • 2
    Does this ever even get to `return y - x;`? Even in javascript, I can't think of anything that would be neither `===0` nor `!==0`. – George T Nov 20 '19 at 09:42
  • @GeorgeT: Not for numbers at least. Even NaN is `!= 0` and not `== 0`. There's already [an answer](https://stackoverflow.com/a/58934188/224132) that explains what the bugs is in the question's attempt. – Peter Cordes Nov 20 '19 at 22:53
  • 2
    JSPerf for answers: https://jsperf.com/stackoverflow-question-58933996-v2 – Salman A Nov 21 '19 at 07:01

9 Answers9

38

You could sort by the delta of b and a (for descending sorting) and take Number.MAX_VALUE, for falsy values like zero.

This:

Number.MAX_VALUE - Number.MAX_VALUE

is equal to zero.

let array = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5];

array.sort((a, b) => (b || Number.MAX_VALUE) - (a || Number.MAX_VALUE));

console.log(...array);
S.S. Anne
  • 15,171
  • 8
  • 38
  • 76
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • 15
    Your comparison function will return `NaN` if both `a` and `b` are zero. This may be undesired behavior. – dan04 Nov 19 '19 at 23:06
  • 22
    The results of `Array.prototype.sort` are implementation-defined if the comparator ever returns `NaN`, so this comparator is a bad idea. It tries to be clever, and gets it wrong. – user2357112 Nov 20 '19 at 00:11
  • then how about `array.sort((a, b) => ((b || Infinity) - (a || Infinity)) || 0);` to avoid this NaN problem ? – jonatjano Nov 20 '19 at 07:51
  • @dan04, no this is intended. – Nina Scholz Nov 20 '19 at 08:02
  • 1
    @user2357112supportsMonica, plese see edit. the standard requires only positive or begative values, not zero. – Nina Scholz Nov 20 '19 at 08:03
  • @jonatjano, right but superfluous. please see edit. – Nina Scholz Nov 20 '19 at 08:03
  • 3
    What if an element inside the array is Infinity? The comparison function returns 0 when comparing an Infinity number to 0. – Marco Nov 20 '19 at 08:15
  • @Marco thank you for the hint. i was waiting for this argument. op is asking for sorting positive naturals with zero. i see no other values, as objects, functions and other types then numbers in a given range. I suppose there is no other number area involved. – Nina Scholz Nov 20 '19 at 08:19
  • @Nina: The undefined shouldn't be bolded in *If the argument comparefn is not undefined, then*. That's talking about the `comparefn` arg itself. That's not in question and is a distraction to have bolded. I don't know enough JavaScript to understand the "undefined" in the `a.` line about getting a value by calling the compare fn. But yes it looks like `b.` guarantees that NaN is treated the same as `+0`. Still this method requires a lot of thinking to be sure it's correct, and spends time sorting the zeros. I posted an answer that filters+counts zeros so it can use a simple smaller sort. – Peter Cordes Nov 20 '19 at 08:22
  • @NinaScholz: While the part you quoted is in the standard, the behavior is still implementation defined because of the following other sections of the standard: "If comparefn is not undefined and is not a consistent comparison function for the elements of this array (see below), the sort order is implementation-defined. ... A function comparefn is a consistent comparison function for a set of values S if all of the requirements below are met for all values a, b, and c (possibly the same value) in the set S: ..." [cont.] – user2357112 Nov 20 '19 at 08:26
  • 2
    "Calling comparefn(a, b) always returns the same value v when given a specific pair of values a and b as its two arguments. Furthermore, Type(v) is Number, and v is not NaN." – user2357112 Nov 20 '19 at 08:26
  • 4
    thank you all. i take the greatest number which can be substracted by itself, and hope this number isn't used in op's array. – Nina Scholz Nov 20 '19 at 08:37
  • 2
    @Salman A: I find this solution very readable. Maybe because I've written lots of similar comparator functions and prefer a coding style that results in shorter code. I guess it's largely a matter of taste. – jcsahnwaldt Reinstate Monica Nov 20 '19 at 13:58
25

As mdn docs says:

If a and b are two elements being compared, then:

If compareFunction(a, b) returns less than 0, sort a to an index lower than b (i.e. a comes first).

If compareFunction(a, b) returns 0, leave a and b unchanged with respect to each other, but sorted with respect to all different elements. Note: the ECMAscript standard does not guarantee this behavior, thus, not all browsers (e.g. Mozilla versions dating back to at least 2003) respect this.

If compareFunction(a, b) returns greater than 0, sort b to an index lower than a (i.e. b comes first).

compareFunction(a, b) must always return the same value when given a specific pair of elements a and b as its two arguments. If inconsistent results are returned, then the sort order is undefined.

So, the compare function has the following form:

function compare(a, b) {
  if (a is less than b by some ordering criterion) {
    return -1;
  }
  if (a is greater than b by the ordering criterion) {
    return 1;
  }
  // a must be equal to b
  return 0;
}

let arr = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5];

arr.sort((x, y) => {
    if (x > 0 && y > 0) {
        return y - x;
    }
    return x - y;
});

console.log(arr);
StepUp
  • 36,391
  • 15
  • 88
  • 148
  • 3
    +1 for giving what appears to be the only solution so far with a comparison function that's actually consistent (as defined in [the standard](https://www.ecma-international.org/ecma-262/6.0/#sec-array.prototype.sort)) for all inputs, including negative numbers. – Ilmari Karonen Nov 20 '19 at 02:58
  • 3
    @IlmariKaronen: Unfortunately, the comparison is *consistent* for negative numbers, but it's the *wrong comparison* for negative numbers. Negatives sort before 0 with this comparator, and they sort in ascending order. – user2357112 Nov 20 '19 at 08:37
  • 2
    @user2357112supportsMonica Nevertheless, OP has not specified the desired behaviour for negative numbers and, with this prototype, it is trivial to adjust the behaviour of negative numbers to fit whatever requirement OP has. What's good about this answer is that it is idiomatic and uses a standard comparison function to do the job. – J... Nov 20 '19 at 20:01
13

If you care about efficiency, it's probably fastest to filter out the zeros first. You don't want sort to waste time even looking at them, let alone adding extra work to your comparison callback to handle that special case.

Especially if you expect a significant number of zeros, one pass over the data to filter them out should be much better than doing a larger O(N log N) sort that will look at each zero multiple times.

You can efficiently prepend the right number of zeros after you're done.

It's also just as easy to read the resulting code. I used TypedArray because it's efficient and makes numeric sorting easy. But you can use this technique with regular Array, using the standard idiom of (a,b)=>a-b for .sort.

let arr = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5];

let nonzero_arr = Int32Array.from(arr.filter(n => n != 0));
let zcount = arr.length - nonzero_arr.length;
nonzero_arr.sort();      // numeric TypedArray sorts numerically, not alphabetically

// Reverse the sorted part before copying into the final array.
nonzero_arr.reverse();

 // efficient-ish TypedArray for main result
let revsorted = new Int32Array(arr.length);   // zero-filled full size
revsorted.set(nonzero_arr, zcount);           // copy after the right number of zeros

console.log(Array.from(revsorted));      // prints strangely for TypedArray, with invented "0", "1" keys

/*
   // regular Array result
let sorted = [...Array(zcount).fill(0), ...nonzero_arr]  // IDK if this is efficient
console.log(sorted);
*/

I don't know if TypedArray .sort() and then .reverse is faster than using a custom comparison function to sort in descending order. Or if we can copy-and-reverse on the fly with an iterator.


Also worth considering: only use one TypedArray of the full length.

Instead of using .filter, loop over it and swap the zeros to the front of the array as you go. This takes one pass over your data.

Then use .subarray() to get a new TypedArray view of the non-zero elements of the same underlying ArrayBuffer. Sorting that will leave you the full array with a zero start and a sorted tail, with the sort only ever looking at the non-zero elements.

I didn't see a partition function in the Array or TypedArray methods, but I barely know JavaScript. With good JIT, a loop shouldn't be too much worse than a built-in method. (Especially when that method involves a callback like .filter, and unless it uses realloc under the hood to shrink, it has to figure out how much memory to allocate before it actually filters).

I used regular-Array .filter() before converting to a TypedArray. If your input is already a TypedArray you don't have this problem, and this strategy gets even more attractive.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
  • This can probably be simpler, and/or more idiomatic, or at least more compact. IDK if JS engines manage to eliminate / optimize away a lot of the copying or zero-initializing, or if it would help to use loops instead of TypedArray methods e.g. to copy backwards instead of reverse + .set. I didn't get around to speed-testing it; if someone wants to do that I'd be interested. – Peter Cordes Nov 20 '19 at 10:37
  • According to [@Salman's testing](https://stackoverflow.com/questions/58933996/sorting-numbers-in-descending-order-but-with-0s-at-the-start/58949538#comment104192088_58933996) this answer performs like crap for smallish arrays (of ~1000 elements, 10% of which are 0). Presumably all the creation of new arrays hurts. Or maybe careless mixing of TypedArray and regular? In-place partition inside a TypedArray into all-zero and non-zero + subarray sort might be much better, as suggested in the 2nd section of my answer. – Peter Cordes Nov 23 '19 at 22:10
8

Just modify the condition of your compare function like this -

let arr = [-1, 0, 1, 0, 2, -2, 0, 3, -3, 0, 4, -4, 0, 5, -5];
arr.sort((a, b) => {
   if(a && b) return b-a;
   if(!a && !b) return 0;
   return !a ? -1 : 1;
});

console.log(arr);
Harun Or Rashid
  • 5,589
  • 1
  • 19
  • 21
7

Not playing code golf here:

let arr = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5, -1];
arr.sort(function(a, b) {
  if (a === 0 && b !== 0) {
    // a is zero b is nonzero, a goes first
    return -1;
  } else if (a !== 0 && b === 0) {
    // a is nonzero b is zero, b goes first
    return 1;
  } else {
    // both are zero or both are nonzero, sort descending
    return b - a;
  }
});
console.log(arr.toString());
Salman A
  • 262,204
  • 82
  • 430
  • 521
5

Don't write your own numeric sorting if it already exists. What you want to do is exactly what you say in the title; sort numbers in descending order except zeroes at the start.

const zeroSort = arr => [...arr.filter(n => n == 0),
                         ...new Float64Array(arr.filter(n => n != 0)).sort().reverse()];

console.log(zeroSort([0, 1, 0, 2, 0, 3, 0, 4, 0, 500]));

Don't write any code you don't need to; you might get it wrong.

Pick the TypedArray based on what Type of numbers you want the Array to handle. Float64 is a good default since it handles all normal JS numbers.

JollyJoker
  • 1,256
  • 8
  • 12
  • @IlmariKaronen Better? – JollyJoker Nov 20 '19 at 12:26
  • You don't need to filter twice; as my answer shows you can filter once and subtract lengths to get a number for `Array(n).fill(0)`. – Peter Cordes Nov 20 '19 at 14:14
  • @PeterCordes Although it could possibly be called simpler, I think the code gets long enough to be less easy to read – JollyJoker Nov 20 '19 at 14:41
  • Yes, efficiency would require a 1 extra line for a tmp var. My answer uses multiple extra lines because I'm used to C and C++ and assembly language, and having more separate lines makes it easier to trace compiler-generated asm back to the statement responsible for it when optimizing / profiling. Plus I don't normally do JS so I didn't feel any need to cram it all onto a couple lines. But you could do `let f64arr = new Float64Array(arr.filter(n => n != 0))`, then `[ ...Array(arr.length - f64arr.length).fill(0),` ... so it adds 1 extra line and simplifies the last line. – Peter Cordes Nov 20 '19 at 14:47
4

You can do this like this:

let arr = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5];

let result = arr.sort((a,b) => {
  if(a == 0 || b == 0)
    return a-b;
  return b-a;
})
console.log(result)

or you can do this:

let arr = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5];

let result = arr.sort().sort((a,b) => {
  if(a > 0 && b > 0)
    return b-a
  return 0
})

console.log(result)
S.S. Anne
  • 15,171
  • 8
  • 38
  • 76
noone
  • 6,168
  • 2
  • 42
  • 51
  • 4
    Your first solution has the same consistency issue with negative inputs as Harunur Rashid's answer. The second one _is_ consistent, but treats all negative numbers as equal to zero, leaving their mutual sort order undefined. – Ilmari Karonen Nov 20 '19 at 02:52
0

most of the answers above are not optimum keeping following real life scenario:

  1. not always 0 is to be filtered out. so all answers which use Only Sort function are not good enough per me
  2. inputs not always will be integers, but strings or even objects to be compared during sort. so answers using TypedArray for doing Sort and Reverse are not good enough

i would prefer something like following

function customSort( source, target )  { target - source } // a custom sorting depending on business logic required

function sortAndFilter(inputArray, candidateValue) { // a Candidate Value to be filtered out

// step 1: use optimized custom sort (think Merge Sort) to sort all items. You would like to pass candidateValue(s) to be Not considered for sorting and move them to separate list or at beginning/end of the array depending on business logic

const reversedArray = inputArray.sort( customSort);

// step 2: merge the sorted list(s) with custom logic. in this case its just moving the candidateValue to front and removing from elsewhere so following is good enough
reversedArray.forEach( (item, index) => {
    if (item === candidateValue) {
        reversedArray.splice(index, 1)
        reversedArray.unshift(item)
    }
});
return reversedArray
}

console.log(sortAndFilter(arr,0))
ajayrc
  • 1
  • 1
-2

const myArray = [0, 1, 0, 2, 0, 3, 0, 4, 0, 5];
const splitArray = myArray.reduce((output, item) => {
     if(!item) output[0].push(item);
    else output[1].push(item);
    return output;
}, [[], []]);
const result = [...splitArray[0], ...splitArray[1].reverse()]
console.log(result);
Max
  • 1,020
  • 7
  • 13