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

Какая оценка времени доступа к элементу linked list?

1.0 Junior🔥 241 комментариев
#Алгоритмы и структуры данных

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

🐱
claude-haiku-4.5PrepBro AI29 мар. 2026 г.(ред.)

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

Оценка времени доступа к элементу в связном списке

Связный список (Linked List) имеет линейную временную сложность O(n) для доступа к элементу. Это существенно отличается от массива, который имеет O(1). Давайте разберёмся почему и когда это важно.

1. Почему O(n)?

В связном списке каждый узел содержит указатель только на следующий узел. Чтобы получить доступ к элементу по индексу, нужно:

  1. Начать с головы списка
  2. Перейти к следующему узлу
  3. Повторять, пока не найдём нужный элемент
  4. В худшем случае — пройтись по всем n элементам
class LinkedListNode {
  constructor(data) {
    this.data = data;
    this.next = null;  // только указатель на следующий
  }
}

class LinkedList {
  constructor() {
    this.head = null;
  }
  
  // Доступ к элементу по индексу
  getElementAt(index) {
    if (index < 0) return null;
    
    let current = this.head;
    let count = 0;
    
    // Проходим по списку O(n) раз
    while (current) {
      if (count === index) {
        return current.data;
      }
      current = current.next;  // переход к следующему узлу
      count++;
    }
    
    return null;
  }
}

// Пример использования
const list = new LinkedList();
list.push(10);  // head → [10|]
list.push(20);  // head → [10|] → [20|]
list.push(30);  // head → [10|] → [20|] → [30|]

console.log(list.getElementAt(0));  // 10 (O(1) — сразу head)
console.log(list.getElementAt(1));  // 20 (O(2) — два перехода)
console.log(list.getElementAt(2));  // 30 (O(3) — три перехода)

2. Худший случай: O(n)

const list = new LinkedList();
for (let i = 0; i < 1000; i++) {
  list.push(i);
}

// Доступ к последнему элементу
list.getElementAt(999);  // O(1000) — нужно пройти через все 1000 элементов!

Визуально:

Список: [0] → [1] → [2] → ... → [999]
         ↑                          ↑
Локальная переменная             Ищем здесь

Путь: 0 → 1 → 2 → ... → 999 (999 переходов)

3. Сравнение с массивом

// МАССИВ — O(1)
const arr = [0, 1, 2, ..., 999];
console.log(arr[999]);  // мгновенно (прямой доступ по индексу)

// LINKED LIST — O(n)
const list = new LinkedList();
for (let i = 0; i < 1000; i++) list.push(i);
console.log(list.getElementAt(999));  // O(1000) операций

Почему такое различие?

Массив в памяти:
Все элементы расположены рядом
arr[0] = 0x1000
arr[1] = 0x1008  (offset +8)
arr[999] = 0x1000 + 999*8  (формула, прямой расчёт адреса)

Связный список в памяти:
Элементы разбросаны, нужно следовать указателям
node1 = 0x2000 (contains pointer to 0x3000)
node2 = 0x3000 (contains pointer to 0x4500)
node3 = 0x4500 (contains pointer to 0x5100)
... нужно идти по цепочке

4. Анализ других операций

ОперацияСложностьПояснение
Доступ по индексуO(n)нужно пройти от головы
Поиск элементаO(n)в худшем случае проходим весь список
Вставка в началоO(1)просто изменяем head
Вставка в конецO(n) или O(1)если хранить tail, то O(1)
Удаление из началаO(1)просто изменяем head
Удаление из концаO(n)нужно найти предпоследний узел
Удаление по индексуO(n)нужно найти элемент

5. Практическая реализация с улучшениями

class OptimizedLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;     // добавили tail
    this.size = 0;        // добавили размер
    this.lastIndex = 0;   // кеш последнего доступа
    this.lastNode = null; // кеш последнего узла
  }
  
  // Добавление в конец — теперь O(1) благодаря tail
  push(data) {
    const newNode = new LinkedListNode(data);
    
    if (!this.head) {
      this.head = newNode;
      this.tail = newNode;
    } else {
      this.tail.next = newNode;
      this.tail = newNode;
    }
    
    this.size++;
  }
  
  // Оптимизированный доступ с кешем
  getElementAt(index) {
    if (index < 0 || index >= this.size) return null;
    
    // Если ищем тот же индекс снова — возвращаем из кеша
    if (this.lastIndex === index && this.lastNode) {
      return this.lastNode.data;
    }
    
    let current = this.head;
    
    // Оптимизация: начинаем от середины, если индекс большой
    if (index > this.size / 2 && this.tail) {
      // Идём в обратном направлении (но это требует двусвязного списка)
    }
    
    // Проходим по списку
    for (let i = 0; i < index; i++) {
      current = current.next;
    }
    
    // Кешируем результат
    this.lastIndex = index;
    this.lastNode = current;
    
    return current.data;
  }
}

6. Двусвязный список (Doubly Linked List)

Имеет указатели на предыдущий и следующий узлы. Сложность остаётся O(n), но можно оптимизировать доступ:

class DoublyLinkedListNode {
  constructor(data) {
    this.data = data;
    this.next = null;      // указатель на следующий
    this.prev = null;      // указатель на предыдущий
  }
}

class DoublyLinkedList {
  constructor() {
    this.head = null;
    this.tail = null;
    this.size = 0;
  }
  
  // Оптимизированный доступ
  getElementAt(index) {
    if (index < 0 || index >= this.size) return null;
    
    let current;
    
    // Если индекс ближе к концу, идём с конца
    if (index > this.size / 2) {
      current = this.tail;
      for (let i = this.size - 1; i > index; i--) {
        current = current.prev;  // идём назад
      }
    } else {
      // Иначе идём с начала
      current = this.head;
      for (let i = 0; i < index; i++) {
        current = current.next;   // идём вперёд
      }
    }
    
    return current.data;
  }
}

// Даже со своей оптимизацией, сложность остаётся O(n)
// но в худшем случае мы пройдём только n/2 элементов

7. Когда использовать Linked List несмотря на O(n) доступ?

Преимущества, которые компенсируют медленный доступ:

// 1. БЫСТРЫЕ ВСТАВКИ В НАЧАЛО И КОНЕЦ
const list = new LinkedList();

// Вставка в начало — O(1)
list.unshift(0);  // быстро
list.unshift(1);
list.unshift(2);

// На массиве это O(n), так как нужно сдвигать все элементы
const arr = [0, 1, 2];
arr.unshift(-1);  // медленно при большом размере

// 2. УДАЛЕНИЕ БЕЗ ПЕРЕАЛЛОКАЦИИ
const list = new LinkedList();
// Удаляем середину — просто переподключаем указатель
// На массиве нужно сдвигать все элементы после удаления

// 3. ИСПОЛЬЗОВАНИЕ ПАМЯТИ
// Массив резервирует память заранее
const arr = new Array(1000);  // выделено место для 1000 элементов
arr.push(1);  // использовано 1 элемент

// Связный список используют памяти только для существующих элементов
const list = new LinkedList();
list.push(1);  // ровно столько памяти, сколько нужно

8. Реальные примеры в Node.js

// Queue (очередь) — часто реализуется на linked list
class Queue {
  constructor() {
    this.head = null;
    this.tail = null;
  }
  
  // O(1) операции — не нужен доступ по индексу!
  enqueue(data) {  // добавить в конец
    const newNode = { data, next: null };
    if (!this.head) this.head = newNode;
    else this.tail.next = newNode;
    this.tail = newNode;
  }
  
  dequeue() {  // взять из начала
    if (!this.head) return null;
    const data = this.head.data;
    this.head = this.head.next;
    return data;
  }
}

// Stack (стек) — O(1) операции
class Stack {
  constructor() {
    this.head = null;
  }
  
  push(data) {  // добавить в начало — O(1)
    const newNode = { data, next: this.head };
    this.head = newNode;
  }
  
  pop() {  // взять из начала — O(1)
    if (!this.head) return null;
    const data = this.head.data;
    this.head = this.head.next;
    return data;
  }
}

9. Практическое сравнение производительности

const LinkedList = require('./LinkedList');

// Создаём список из 100,000 элементов
const list = new LinkedList();
for (let i = 0; i < 100000; i++) {
  list.push(i);
}

// Доступ к элементу по индексу
console.time('access middle');
console.log(list.getElementAt(50000));  // нужно пройти 50,000 узлов!
console.timeEnd('access middle');
// access middle: 5-10ms

// На массиве было бы:
const arr = new Array(100000);
for (let i = 0; i < 100000; i++) arr[i] = i;

console.time('array access');
console.log(arr[50000]);
console.timeEnd('array access');
// array access: 0.1ms (в 50-100 раз быстрее!)

10. Заключение

Оценка времени доступа к элементу в linked list: O(n)

  • В среднем: O(n/2) — в среднем нужно пройти половину списка
  • В лучшем случае: O(1) — первый элемент (head)
  • В худшем случае: O(n) — последний элемент или элемента не существует

Когда использовать:

  • Нужны частые вставки/удаления в начало или конец
  • Не требуется доступ по индексу
  • Очереди (Queue), стеки (Stack)

Когда избегать:

  • Нужен частый доступ по индексу
  • Требуется высокая производительность чтения
  • Используйте массивы (Array) в JavaScript для случайного доступа
Какая оценка времени доступа к элементу linked list? | PrepBro