4

Example: I have an array like this: [0,22,56,74,89] and I want to find the closest number downward to a different number. Let's say that the number is 72, and in this case, the closest number down in the array is 56, so we return that. If the number is 100, then it's bigger than the biggest number in the array, so we return the biggest number. If the number is 22, then it's an exact match, just return that. The given number can never go under 0, and the array is always sorted.

I did see this question but it returns the closest number to whichever is closer either upward or downward. I must have the closest one downward returned, no matter what.

How do I start? What logic should I use?

Preferably without too much looping, since my code is run every second, and it's CPU intensive enough already.

Community
  • 1
  • 1
SeinopSys
  • 8,787
  • 10
  • 62
  • 110

7 Answers7

6

You can use a binary search for that value. Adapted from this answer:

function index(arr, compare) { // binary search, with custom compare function
    var l = 0,
        r = arr.length - 1;
    while (l <= r) {
        var m = l + ((r - l) >> 1);
        var comp = compare(arr[m]);
        if (comp < 0) // arr[m] comes before the element
            l = m + 1;
        else if (comp > 0) // arr[m] comes after the element
            r = m - 1;
        else // arr[m] equals the element
            return m;
    }
    return l-1; // return the index of the next left item
                // usually you would just return -1 in case nothing is found
}
var arr = [0,22,56,74,89];
var i=index(arr, function(x){return x-72;}); // compare against 72
console.log(arr[i]);

Btw: Here is a quick performance test (adapting the one from @Simon) which clearly shows the advantages of binary search.

Community
  • 1
  • 1
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • A binary search would be the best you could do. It would return your value in O(log n) although seeing as you didn't realize .grep is a loop in and of itself and that this problem can't be done without a loop of some sort then it is easy to see why you didn't realize this. – spartacus Mar 04 '13 at 15:46
  • Wow, even faster? Thanks! In this case performance matters a lot to me so the shorter it takes to run is better :) – SeinopSys Mar 04 '13 at 21:01
  • @DJDavid98: Of course, since it's `O(log n)` :-) Yet you might notice the effect only for big arrays, the one in the test has 1000 items. – Bergi Mar 05 '13 at 13:25
  • Nice solution :), but note that the binary search is faster as the array gets bigger (in your case size of 1000 random numbers), while a straight forward loop is better with smaller arrays like 10 or 20. See http://jsperf.com/test-a-closest-number-function/4. So the OP should evaluate which loop to take for his case. – Simon Mar 06 '13 at 07:48
  • Btw: should give aquinas some credit for he's already given the binary search solution and I think extending the prototype is a pretty good idea ;). – Simon Mar 06 '13 at 09:14
  • @Simon: If you had only 10 or 20 items in your array, you would not need to care about performance - the test runs still millions of times per second – Bergi Mar 06 '13 at 10:19
  • Of course, but if you take an array with a size of 5 like the OP's example, then the straight forward loop is 100% faster than a binary search after all (updated the jsperf #4) and it's still faster up to an array size of 20, beyond that the binary search starts to beat a straight forward loop. And as he pointed out: performance matters :). – Simon Mar 06 '13 at 10:34
5
var theArray = [0,22,56,74,89];
var goal = 56;
var closest = null;

$.each(theArray, function(){
  if (this <= goal && (closest == null || (goal - this) < (goal - closest))) {
    closest = this;
  }
});
alert(closest);

jsFiddle http://jsfiddle.net/UCUJY/1/

gregjer
  • 2,823
  • 2
  • 19
  • 18
2

Here's a solution without jQuery for more effiency. Works if the array is always sorted, which can easily be covered anyway:

var test = 72,
    arr = [0,56,22,89,74].sort(); // just sort it generally if not sure about input, not really time consuming

function getClosestDown(test, arr) {
  var num = result = 0;

  for(var i = 0; i < arr.length; i++) {
    num = arr[i];
    if(num <= test) { result = num; }
  }

  return result;
}

Logic: Start from the smallest number and just set result as long as the current number is smaller than or equal the testing unit.

Note: Just made a little performance test out of curiosity :). Trimmed my code down to the essential part without declaring a function.

Simon
  • 7,182
  • 2
  • 26
  • 42
2
Array.prototype.getClosestDown = function(find) {            
    function getMedian(low, high) {
       return (low + ((high - low) >> 1));
    }

    var low = 0, high = this.length - 1, i;  

    while (low <= high) {
     i = getMedian(low,high);
     if (this[i] == find) { 
         return this[i]; 
     }        
     if (this[i] > find)  { 
         high = i - 1;
     }
     else  { 
         low = i + 1;
     }
  }  
  return this[Math.max(0, low-1)];
}

alert([0,22,56,74,89].getClosestDown(75));
aquinas
  • 23,318
  • 5
  • 58
  • 81
1

Here's an ES6 version using reduce, which OP references. Inspired by this answer get closest number out of array

lookup array is always sorted so this works.

const nearestBelow = (input, lookup) => lookup.reduce((prev, curr) => input >= curr ? curr : prev);
const counts = [0,22,56,74,89];
const goal = 72;
nearestBelow(goal, counts); // result is 56.

Not as fast as binary search (by a long way) but better than both loop and jQuery grep https://jsperf.com/test-a-closest-number-function/7

Stevo
  • 2,601
  • 3
  • 24
  • 32
0

As we know the array is sorted, I'd push everything that asserts as less than our given value into a temporary array then return a pop of that.

var getClosest = function (num, array) {
    var temp = [],
        count = 0,
        length = a.length;

    for (count; count < length; count += 1) {

        if (a[count] <= num) {
            temp.push(a[count]);
        } else {
            break;
        } 
    }

    return temp.pop();
}

getClosest(23, [0,22,56,74,89]);
Mattyod
  • 1,349
  • 1
  • 9
  • 12
  • If you only care about the last value, why don't you just have a variable that holds that value? i.e., instead of `temp.push(a[count]);` just do: `currVal = a[count];` then `return currVal;`. – aquinas Mar 06 '13 at 13:56
0

Here is edited from @Simon. it compare closest number before and after it.

var test = 24,
arr = [76,56,22,89,74].sort(); // just sort it generally if not sure about input, not really time consuming

function getClosest(test, arr) {
  var num = result = 0;
  var flag = 0;
  for(var i = 0; i < arr.length; i++) {
    num = arr[i];
    if(num < test) {
      result = num;
      flag = 1;
    }else if (num == test) {
      result = num;
      break;
    }else if (flag == 1) {
      if ((num - test) < (Math.abs(arr[i-1] - test))){
        result = num;
      }
      break;
    }else{
      break;
    }
  }
  return result;
}