16

Suppose I have this number list:

List<int> = new List<int>(){3,5,8,11,12,13,14,21}

Suppose that I want to get the closest number that is less than 11, it would be 8 Suppose that I want to get the closest number that is greater than 13 that would be 14.

The numbers in list can't be duplicated and are always ordered. How can I write Linq for this?

Sarawut Positwinyu
  • 4,974
  • 15
  • 54
  • 80

8 Answers8

22

with Linq assuming that the list is ordered I would do it like this:

var l = new List<int>() { 3, 5, 8, 11, 12, 13, 14, 21 };
var lessThan11 = l.TakeWhile(p => p < 11).Last();
var greaterThan13 = l.SkipWhile(p => p <= 13).First();

EDIT:

As I have received negative feedback about this answer and for the sake of people that may see this answer and while it's accepted don't go further, I explored the other comments regarding BinarySearch and decided to add the second option in here (with some minor change).

This is the not sufficient way presented somewhere else:

var l = new List<int>() { 3, 5, 8, 11, 12, 13, 14, 21 };
var indexLessThan11 = ~l.BinarySearch(10) -1;
var value = l[indexLessThan11];

Now the code above doesn't cope with the fact that the value 10 might actually be in the list (in which case one shouldn't invert the index)! so the good way is to do it:

var l = new List<int>() { 3, 5, 8, 11, 12, 13, 14, 21 };
var indexLessThan11 = l.BinarySearch(10);
if (indexLessThan11 < 0) // the value 10 wasn't found
{    
    indexLessThan11 = ~indexLessThan11;
    indexLessThan11 -= 1;
}
var value = l[indexLessThan11];

I simply want to note that:

l.BinarySearch(11) == 3
//and
l.BinarySearch(10) == -4;
ub1k
  • 1,674
  • 10
  • 14
  • 1
    It's nice how intent is clear with this solution. I'd write the 2nd one as `l.Reverse().TakeWhile(p => p > 13).Last()` just to make it even more obvious (and more consistent with *less than* case). – dbkk Jun 22 '11 at 07:33
  • actually from what I know, TakeWhile and SkipWhile is quite efficient.. btw. you could always reinvent the wheel once again but what's the point? – ub1k Jun 22 '11 at 07:35
  • @dbkk you're right. It would be even more "good-looking" but as some could point out, not that efficient :) – ub1k Jun 22 '11 at 07:36
  • 1
    @Vince @ub1k -- agreed about inefficiency. However, in most cases, readability is more desirable than efficiency (at micro level, not when it comes to architectural decision). If after profiling the app you figure this piece of LINQ is the bottleneck, it's well-contained and easy to optimize. – dbkk Jun 22 '11 at 07:43
  • 1
    Should it be var greaterThan13 = l.SkipWhile(p => p <= 13).First(); ? – Sarawut Positwinyu Jun 22 '11 at 08:40
  • 1
    I've done a simple test (just a loop) and TakeWhile() is slower than the binary search by a factor of 4. You should consider this if your application will be served by thousands of requests per second as it can add up to several seconds in CPU time. – Razor Jun 22 '11 at 11:16
  • -1 I disagree with this implementation. If you want to use the fact that it's ordered, it's insane not using a O(log N) algorithm. I agree that a default, naive, but more readable implementation would be better, but if you want to do that, don't go against that by iontroducing the complexity of TakeWhile and all that stuff. (I like the observation that TakeWhile/SkipWhile is idiomatic LINQ for searching in general but I disagree with the usage of LINQ for the sake of it when it's making things more complex and dramatically inefficient for no good reason in the face of a more correct approach) – Ruben Bartelink Jun 22 '11 at 16:50
  • +0 (-1 undone). Havent analysed your edit in full, but my main objection was the recommendation of something other than a binary search which is no longer [completely] the case – Ruben Bartelink Jul 01 '11 at 16:14
11

Use Array.BinarySearch - no need for LINQ or visiting on average half the elements to find your target.

There are also a variety of SortedXXX classes that may be suitable for what you're doing [that will have such efficient O(log N) searches built-in]

Ruben Bartelink
  • 59,778
  • 26
  • 187
  • 249
  • So after find the target just use the index-1 or index +1 right, Thanks :D – Sarawut Positwinyu Jun 22 '11 at 07:31
  • More or less - have a look at the examples in the linked docs. They deal with large dinosaurs, so you need to do a little mapping but I didn't see anything that's likely to definitively answer your precise need :P – Ruben Bartelink Jun 22 '11 at 07:58
6

You can do this using a binary search. If your searching for 11, well obviously you'll get the index your after. If you search for 10 and use the bitwise complement of the result, you'll get the closest match.

   List<int> list = new List<int>(){3,5,8,11,12,13,14,21};

   list.Sort();

   int index = list.BinarySearch(10);

   int found =  (~index)-1;

   Console.WriteLine (list[found]); // Outputs 8

The same goes searching in the other direction

int index = list.BinarySearch(15);

Console.WriteLine("Closest match : " + list[+~index]); // Outputs 21

Binary searches are also extremely fast.

wesm
  • 451
  • 6
  • 14
Razor
  • 17,271
  • 25
  • 91
  • 138
5

closest number below 11:

        int someNumber = 11;
        List<int> list = new List<int> { 3, 5, 8, 11, 12, 13, 14, 21 };

        var intermediate = from i in list
                     where i < someNumber
                     orderby i descending
                     select i;

        var result = intermediate.FirstOrDefault();

closest number above 13:

        int someNumber = 13;
        List<int> list = new List<int> { 3, 5, 8, 11, 12, 13, 14, 21 };

        var intermediate = from i in list
                     where i > someNumber
                     orderby i
                     select i;

        var result = intermediate.FirstOrDefault();
yas4891
  • 4,774
  • 3
  • 34
  • 55
1
var list = new List<int> {14,2,13,11,5,8,21,12,3};
var tested = 11;

var closestGreater = list.OrderBy(n => n)
                         .FirstOrDefault(n => tested < n); // = 12

var closestLess = list.OrderByDescending(n => n)
                      .FirstOrDefault(n => tested > n); // = 8

if (closestGreater == 0)
    System.Diagnostics.Debug.WriteLine(
        string.Format("No number greater then {0} exists in the list", tested));

if (closestLess == 0)
    System.Diagnostics.Debug.WriteLine(
        string.Format("No number smaler then {0} exists in the list", tested));
Daniel Dušek
  • 13,683
  • 5
  • 36
  • 51
1

Here is my way hope this helps somebody!

List<float> list = new List<float> { 4.0f, 5.0f, 6.0f, 10.0f, 4.5f,  4.0f, 5.0f, 6.0f, 10.0f, 4.5f, 4.0f, 5.0f, 6.0f, 10.0f };
float num = 4.7f;

float closestAbove = list.Aggregate((x , y) => (x < num ? y : y < num ? x : (Math.Abs(x - num)) < Math.Abs(y - num) ? x : y));
float closestBelow = list.Aggregate((x , y) => (x > num ? y : y > num ? x : (Math.Abs(x - num)) < Math.Abs(y - num) ? x : y));

Console.WriteLine(closestAbove);
Console.WriteLine(closestBelow);

This means you dont have to order the list

Credit: addapted from here: How to get the closest number from a List<int> with LINQ?

The Expanded Code

float closestAboveExplained = list.Aggregate((closestAbove , next) => {
    if(next < num){
        return closestAbove;
    }

    if(closestAbove < num){
        return next;
    }

    else{
        if(Math.Abs(closestAbove - num) < Math.Abs(next - num)){
            return closestAbove;
        }
    }
    return next;
});

Jonese1234
  • 193
  • 8
1

This is my answer

List<int> myList = new List<int>() { 3, 5, 8, 11, 12, 13, 14, 21 };
    int n = 11;
    int? smallerNumberCloseToInput = (from n1 in myList
                                    where n1 < n
                                    orderby n1 descending
                                    select n1).First();

    int? largerNumberCloseToInput = (from n1 in myList
                                    where n1 > n
                                    orderby n1 ascending
                                    select n1).First();
dotcoder
  • 2,828
  • 10
  • 34
  • 50
0

You can use a query for this such as:

List<int> numbers = new List<int>() { 3, 5, 8, 11, 12, 13, 14, 21 };
List<int> output = (from n in numbers
                            where n > 13 // or whatever
                            orderby n ascending //or descending
                            select n).ToList();
Bas
  • 26,772
  • 8
  • 53
  • 86