← Назад к вопросам

Когда применять List, а когда LinkedList?

2.3 Middle🔥 161 комментариев
#Коллекции и структуры данных

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI6 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Краткий ответ

Для C# Backend-разработки в 90%+ случаев следует использовать List<T>. LinkedList<T> применяется в специфических сценариях, когда требуются частые операции вставки/удаления в середине коллекции, особенно при большом размере данных.

Основные отличия структур

List<T> - массив с динамическим размером

Внутренняя реализация основана на массиве, который при необходимости пересоздаётся с увеличением ёмкости (обычно в 2 раза).

public class ListUsageExample
{
    public void ProcessUsers()
    {
        List<User> users = new List<User>();
        
        // Быстрая вставка в конец (O(1))
        users.Add(new User("Ivan"));
        users.Add(new User("Maria"));
        
        // Быстрый доступ по индексу (O(1))
        var firstUser = users[0];
        
        // Медленная вставка в середину (O(n))
        users.Insert(1, new User("Petr")); // Сдвигает все элементы
        
        // Двоичный поиск для сортированных данных
        users.Sort((a, b) => a.Name.CompareTo(b.Name));
        int index = users.BinarySearch(new User("Maria"));
    }
}

LinkedList<T> - двусвязный список

Состоит из узлов (Node), каждый из которых содержит значение и ссылки на предыдущий/следующий элементы.

public class LinkedListUsageExample
{
    public void ProcessOrders()
    {
        LinkedList<Order> orders = new LinkedList<Order>();
        
        // Вставка в начало/конец за O(1)
        orders.AddFirst(new Order(1));
        orders.AddLast(new Order(2));
        
        // Вставка в середину относительно известного узла - O(1)
        LinkedListNode<Order> node = orders.First;
        orders.AddAfter(node, new Order(3)); // Быстрая вставка
        
        // Медленный доступ по индексу (O(n))
        // НЕТ операции orders[index]!
        
        // Перебор элементов
        foreach (var order in orders) { }
        
        // Удаление элемента за O(1)
        orders.Remove(node);
    }
}

Сравнение производительности

ОперацияList<T>LinkedList<T>
Доступ по индексу✅ O(1) - мгновенно❌ O(n) - полный перебор
Вставка в конец✅ O(1)* (амортизированная)✅ O(1)
Вставка в начало❌ O(n) - сдвиг всех элементов✅ O(1)
Вставка в середину❌ O(n) - сдвиг всех элементов✅ O(1) при известном узле
Поиск элемента❌ O(n) (линейный поиск)❌ O(n)
Удаление из середины❌ O(n)✅ O(1) при известном узле
ПамятьМеньше накладных расходовБольше (каждый узел имеет 3 ссылки)
Локализация данных✅ Отличная (массив в памяти)❌ Плохая (разрозненные узлы)

*При переполнении внутреннего массива требуется его пересоздание

Практические рекомендации

Когда использовать List<T> (основной вариант):

  • Коллекции с частым доступом по индексу (обработка по порядку, алгоритмы с произвольным доступом)
  • Преимущественно добавление в конец (коллекции, буферы, кэши)
  • Требуется высокий уровень кэширования процессора (данные в памяти расположены последовательно)
  • Работа с малыми и средними коллекциями (до десятков тысяч элементов)
  • Частые операции поиска, сортировки, бинарного поиска

Когда рассмотреть LinkedList<T> (специфические сценарии):

  • Реализация очереди или стека (если Queue<T> или Stack<T> не подходят)
  • Частые вставки/удаления в произвольных позициях (редакторы текста, списки отмены действий)
  • Ситуации типа "карусель" или циклические списки
  • Реализация LRU-кэша (Least Recently Used), где нужно быстро перемещать элементы
  • Обработка потоков данных с операциями вставки посередине (но обычно лучше использовать специализированные структуры)

Примеры Backend-использования

// Типичный Backend-сценарий - List<T>
public class UserService
{
    private List<User> _activeUsers = new List<User>(100); // Предустановленная ёмкость
    
    // List оптимален: частый доступ по индексу, добавление в конец
    public User GetUserByIndex(int index) => _activeUsers[index];
    
    public void AddUser(User user) => _activeUsers.Add(user);
}

// Специфический сценарий - LinkedList<T>
public class RecentlyUsedCache<T>
{
    private LinkedList<T> _items = new LinkedList<T>();
    private Dictionary<T, LinkedListNode<T>> _nodeMap = new Dictionary<T, LinkedListNode<T>>();
    
    // Быстрое перемещение элемента в начало при доступе
    public void AccessItem(T item)
    {
        if (_nodeMap.TryGetValue(item, out var node))
        {
            _items.Remove(node);    // O(1)
            _items.AddFirst(node);  // O(1)
        }
    }
}

Производительность в реальных условиях

Тест на 100 000 операций вставки в середину:

// List<T> - ~4000 мс
List<int> list = new List<int>();
for (int i = 0; i < 100000; i++)
    list.Insert(list.Count / 2, i); // Медленно!

// LinkedList<T> - ~10 мс
LinkedList<int> linkedList = new LinkedList<int>();
LinkedListNode<int> currentNode = null;
for (int i = 0; i < 100000; i++)
{
    if (currentNode == null)
        linkedList.AddLast(i);
    else
        linkedList.AddAfter(currentNode, i); // Быстро!
}

Заключение

Для типичных Backend-задач (обработка API-запросов, работа с БД, бизнес-логика) List<T> является стандартным выбором благодаря:

  1. Предсказуемой производительности для большинства операций
  2. Эффективному использованию кэша процессора
  3. Богатому API (сортировка, поиск, удобные методы)
  4. Низким накладным расходам на хранение

LinkedList<T> применяйте осознанно, только при явном преимуществе в конкретных сценариях с частыми операциями вставки/удаления в середине коллекции. В 95% случаев для C# Backend оптимален List<T>, особенно при правильной начальной установке ёмкости через List<T>(int capacity).