kz-maxx

Двусвязный список

Объяснение, пример задачи и строгая реализация на массивах

Объяснение

Двусвязный список — это список, где каждый элемент знает не только следующий элемент, но и предыдущий.

начало ⇄ 10 ⇄ 20 ⇄ 30 ⇄ конец

В односвязном списке у элемента есть только ссылка вперед. В двусвязном списке у элемента есть две связи:

  • pre[i] — предыдущий элемент
  • nxt[i] — следующий элемент

Благодаря этому можно быстро вставлять элемент слева или справа от известного элемента. Также можно быстро удалять известный элемент.

Иногда его случайно называют двумерным списком. Но правильное название в алгоритмах — двусвязный список.

Что храним в массивах?

  • val[i] — значение элемента с индексом i
  • pre[i] — индекс предыдущего элемента
  • nxt[i] — индекс следующего элемента
  • idx — индекс свободной ячейки для нового элемента

Фиктивные вершины

Чтобы код был проще, мы создаем две специальные вершины:

  • 0 — левая граница списка
  • 1 — правая граница списка
0 ⇄ элементы списка ⇄ 1

Настоящие элементы начинаются с индекса 2.

Если список пустой, то сразу после 0 идет 1. То есть nxt[0] = 1 и pre[1] = 0.

Пример хранения

Пусть список такой:

10 ⇄ 20 ⇄ 30

В массивах это может выглядеть так:

i val[i] pre[i] nxt[i]
0 - - 2
2 10 0 3
3 20 2 4
4 30 3 1
1 - 4 -

Пример задачи для реализации

Дан пустой двусвязный список. Нужно обработать несколько команд.

Команда Что делает
L x Вставить число x в начало списка
R x Вставить число x в конец списка
D k Удалить k-й добавленный элемент
IL k x Вставить x слева от k-го добавленного элемента
IR k x Вставить x справа от k-го добавленного элемента

Вход

7
L 10
R 20
IR 1 15
IL 2 17
D 3
L 5
R 30

Выход

5 10 17 20 30
Первый настоящий элемент получает индекс 2. Поэтому k-й добавленный элемент в массиве имеет индекс k + 1.

Реализация

#include <bits/stdc++.h>
using namespace std;

const int N = 100010;

int val[N];
int pre[N];
int nxt[N];
int idx;

void init() {
    nxt[0] = 1;
    pre[1] = 0;
    idx = 2;
}

void addAfter(int pos, int x) {
    val[idx] = x;

    pre[idx] = pos;
    nxt[idx] = nxt[pos];

    pre[nxt[pos]] = idx;
    nxt[pos] = idx;

    idx++;
}

void addBefore(int pos, int x) {
    addAfter(pre[pos], x);
}

void delAt(int pos) {
    nxt[pre[pos]] = nxt[pos];
    pre[nxt[pos]] = pre[pos];
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    init();

    int m;
    cin >> m;

    while (m--) {
        string op;
        cin >> op;

        if (op == "L") {
            int x;
            cin >> x;

            addAfter(0, x);
        } else if (op == "R") {
            int x;
            cin >> x;

            addBefore(1, x);
        } else if (op == "D") {
            int k;
            cin >> k;

            delAt(k + 1);
        } else if (op == "IL") {
            int k, x;
            cin >> k >> x;

            addBefore(k + 1, x);
        } else if (op == "IR") {
            int k, x;
            cin >> k >> x;

            addAfter(k + 1, x);
        }
    }

    for (int i = nxt[0]; i != 1; i = nxt[i]) {
        cout << val[i] << ' ';
    }

    return 0;
}

Объяснение переменных

  • val — значения элементов
  • pre — предыдущий элемент
  • nxt — следующий элемент
  • idx — свободная ячейка для нового элемента
  • pos — позиция элемента

Инициализация

void init() {
    nxt[0] = 1;
    pre[1] = 0;
    idx = 2;
}

В начале список пустой. После левой границы 0 сразу идет правая граница 1. Новые элементы начинаются с индекса 2.

Вставка справа от элемента

void addAfter(int pos, int x) {
    val[idx] = x;

    pre[idx] = pos;
    nxt[idx] = nxt[pos];

    pre[nxt[pos]] = idx;
    nxt[pos] = idx;

    idx++;
}

Новый элемент вставляется после элемента pos. Он становится между pos и старым следующим элементом.

было: pos ⇄ oldNext
стало: pos ⇄ new ⇄ oldNext

Вставка слева от элемента

void addBefore(int pos, int x) {
    addAfter(pre[pos], x);
}

Чтобы вставить слева от pos, можно вставить новый элемент справа от предыдущего элемента.

было: oldPrev ⇄ pos
стало: oldPrev ⇄ new ⇄ pos

Удаление элемента

void delAt(int pos) {
    nxt[pre[pos]] = nxt[pos];
    pre[nxt[pos]] = pre[pos];
}

Чтобы удалить элемент, нужно соединить его предыдущий элемент со следующим элементом.

было: left ⇄ pos ⇄ right
стало: left ⇄ right