Ordenar y buscar en listas en C#: Sort, OrderBy y BinarySearch

Ordenar una lista en C# es una línea, pero detrás hay decisiones que cambian el resultado: si ordenas la lista o creas otra, en qué idioma comparas el texto, qué pasa cuando buscas algo que no está. Esta página las repasa con ejemplos que puedes copiar.

Ordenar números

int[] notas = [7, 3, 9, 3, 10, 5];
Array.Sort(notas);
Console.WriteLine(string.Join(", ", notas));

var lista = new List<int> { 7, 3, 9, 3, 10, 5 };
lista.Sort((a, b) => b.CompareTo(a));          // de mayor a menor
Console.WriteLine(string.Join(", ", lista));

lista.Reverse();
Console.WriteLine(string.Join(", ", lista));
salida3, 3, 5, 7, 9, 10
10, 9, 7, 5, 3, 3
3, 3, 5, 7, 9, 10

Array.Sort y List.Sort ordenan la propia colección, sin crear otra. Para otro orden se les pasa una comparación: una función que recibe dos elementos y devuelve un número negativo, cero o positivo. b.CompareTo(a), con los argumentos al revés, da el orden de mayor a menor.

Ordenar texto: depende del idioma

using System.Globalization;

string[] palabras = ["ñu", "nube", "Zorro", "árbol", "abeja", "oso"];

var ordinal = palabras.Order(StringComparer.Ordinal);
Console.WriteLine("Ordinal: " + string.Join(", ", ordinal));

var espanol = palabras.Order(StringComparer.Create(new CultureInfo("es-ES"), ignoreCase: true));
Console.WriteLine("Español: " + string.Join(", ", espanol));
salidaOrdinal: Zorro, abeja, nube, oso, árbol, ñu
Español: abeja, árbol, nube, ñu, oso, Zorro

La comparación ordinal compara los códigos de los caracteres: todas las mayúsculas van antes que las minúsculas, y las letras con tilde y la ñ, al final. Para un orden que entienda una persona hay que decir el idioma: con la cultura española, «árbol» va con la a, la ñ va después de la n y, con ignoreCase, «Zorro» va donde le toca. Order() es de .NET 7; en versiones anteriores, OrderBy(p => p, comparador).

Sin indicar nada, depende del ordenador. List<string>.Sort() sin argumentos usa la cultura del sistema, así que el mismo programa puede ordenar distinto en dos máquinas. Si el orden importa (un archivo, una prueba automática), indica siempre el comparador.

Ordenar objetos por varios campos

var productos = new List<Producto>
{
    new("Café molido", "bebidas", 4.50m),
    new("Galletas", "dulces", 2.10m),
    new("Té verde", "bebidas", 3.20m),
    new("Chocolate", "dulces", 2.10m),
    new("Cacao puro", "bebidas", 6.75m),
};

var ordenados = productos
    .OrderBy(p => p.Categoria)
    .ThenByDescending(p => p.Precio)
    .ThenBy(p => p.Nombre);

foreach (var p in ordenados)
{
    Console.WriteLine($"{p.Categoria,-8} {p.Precio,5}  {p.Nombre}");
}

record Producto(string Nombre, string Categoria, decimal Precio);
salidabebidas   6.75  Cacao puro
bebidas   4.50  Café molido
bebidas   3.20  Té verde
dulces    2.10  Chocolate
dulces    2.10  Galletas

OrderBy, ThenBy y sus versiones Descending son de LINQ y, al contrario que Sort, no tocan la lista original: devuelven una secuencia nueva. Además el orden es estable: los elementos que empatan en todos los criterios conservan su orden de partida. Aquí, a igual categoría y precio, desempata el nombre.

Buscar

var productos = new List<Producto>
{
    new("Café molido", 4.50m), new("Té verde", 3.20m), new("Cacao puro", 6.75m),
};

Console.WriteLine(productos.Find(p => p.Precio < 4)?.Nombre);
Console.WriteLine(productos.FindIndex(p => p.Nombre.StartsWith("Cacao")));
Console.WriteLine(productos.Any(p => p.Precio > 10));
Console.WriteLine(productos.FirstOrDefault(p => p.Precio > 10)?.Nombre ?? "(ninguno)");
Console.WriteLine(productos.Where(p => p.Precio > 4).Count());

var caro = productos.First(p => p.Precio > 10);

record Producto(string Nombre, decimal Precio);
salida con errorTé verde
2
False
(ninguno)
2
Unhandled exception. System.InvalidOperationException: Sequence contains no matching element

Find y FirstOrDefault devuelven null si no hay ninguno; FindIndex devuelve -1. Pero First lanza una excepción si nada cumple la condición, que es lo que pasa en la última línea. Usa First solo cuando que no exista sea un error de verdad, y FirstOrDefault cuando sea una posibilidad normal.

Búsqueda binaria

Buscar en una lista recorriéndola es lento si la lista es enorme. Si está ordenada, la búsqueda binaria mira el elemento del centro y descarta la mitad en la que no puede estar, una y otra vez:

Búsqueda binaria del número 31 en la lista ordenada 3, 8, 12, 19, 23, 31, 40, 52: primero mira el centro, 19; como 31 es mayor, descarta la mitad izquierda; luego mira 31 en el índice 5 y lo encuentraBúsqueda binaria de 31: cada paso descarta la mitadpaso 13812192331405231 > 19: busca a la derechapaso 23812192331405231 = 31: encontrado en el índice 5Con 1,000,000 de elementos bastan 20 pasos; recorrerlos uno a uno puede costar un millón.

Cada comparación con el elemento central descarta la mitad de lo que queda. Esquema: decodigo.com.
int[] ordenados = [3, 8, 12, 19, 23, 31, 40, 52];

Console.WriteLine(Array.BinarySearch(ordenados, 31));

int i = Array.BinarySearch(ordenados, 20);
Console.WriteLine($"{i} -> iría en la posición {~i}");

int[] desordenados = [40, 31, 3, 8, 52, 19];
Console.WriteLine(Array.BinarySearch(desordenados, 31));
salida5
-5 -> iría en la posición 4
-5

Si lo encuentra, devuelve el índice. Si no, devuelve un número negativo: el complemento (~i) de la posición en la que habría que insertarlo para mantener el orden, que es muy útil. La última línea es la trampa: con una lista desordenada no da error, da una respuesta equivocada: un número negativo, «no está», aunque el 31 está en la posición 1. Ordena antes, o usa un HashSet<T> si solo necesitas saber si algo está.

Todo el código de esta página se compiló con el .NET SDK 10.0.403 (C# 14) y se ejecutó con .NET 10.0.14 en un contenedor limpio antes de publicarla; las salidas y los mensajes del compilador están copiados de esa ejecución.