Круговий пов’язаний список: переваги та недоліки

⚡ Розумний підсумок

Циклічні зв'язані списки розташовують вузли таким чином, щоб останній вузол повертався до першого, забезпечуючи безперервну структуру без NULL, яка підходить для циклічного планування, токенів та будь-якого робочого процесу, що потребує безперебійного проходження.

  • 📚 Визначення: Кожен вузол містить значення та наступний вказівник, а наступний вказівник останнього вузла посилається назад на перший, створюючи замкнутий цикл.
  • 📌 Core Operaтиони: Вставка, видалення та обхід обертаються навколо оновлення одного або двох наступних вказівників зі збереженням циклу.
  • 🛠️ Впровадження на C: Вузли на основі структур зі вставками, підкріпленими malloc, та видаленнями, підкріпленими вільними, охоплюють як випадки поточної позиції, так і випадки після вузла.
  • переваги: Відсутність розіменування NULL, безшовні переходи від початку до кінця та подвійно циклічні варіанти, які вдвічі зменшують кількість пошуків у найгіршому випадку.
  • ⚠️ Недоліки: Складніше керування циклами, вища складність, ніж у однозв'язаних списків, та нескінченні цикли, якщо завершення написано неправильно.
  • 🎯 Область застосування: Циклічне планування процесора, мережі Token-Ring, циклічні буфери, списки відтворення медіа та блоки безперервного відображення.

Круговий зв’язаний список

Що таке циклічний пов’язаний список?

Циркулярний зв'язаний список - це послідовність вузлів, розташованих таким чином, що кожен вузол можна перебудувати.tracсам по собі. Кожен «вузол» є самопосилальним елементом з вказівниками на один або два вузли в безпосередній близькості від нього.

Нижче наведено зображення кругового пов’язаного списку з 3 вузлами.

Круговий зв’язаний список

Тут ви можете бачити, що кожен вузол повторноtracсам по собі. Наведений вище приклад — це циклічний однозв'язний список.

Примітка: Найпростіший циклічний зв'язаний список — це один вузол, наступний покажчик якого tracповертається до себе, як показано нижче.

Круговий зв’язаний список

Базовий Operaції в циклічних зв'язаних списках

Три основні операції над циклічним зв'язаним списком:

  1. вставка
  2. Видалення і
  3. Обхід
  • Вставка — це процес розміщення вузла у вказаній позиції в циклічному зв’язаному списку.
  • Видалення — це процес видалення існуючого вузла зі зв’язаного списку. Вузол можна ідентифікувати за його значенням або за його положенням.
  • Обхід циклічного зв'язаного списку — це процес відображення всього вмісту зв'язаного списку та його повторного перегляду.tracповернення до вузла-джерела.

У наступному розділі пояснюється, як працює вставка, і два можливі типи вставки в циклічному однозв'язному списку.

вставка Operaції

Спочатку створюється один вузол, наступний вказівник якого вказує на себе, як показано нижче. Без цього початкового вузла перша вставка стає першим вузлом у списку.

вставка Operaції

Далі є дві можливості:

  • Вставка в поточну позицію циклічного зв'язаного списку. Це відповідає вставці на початку або в кінці звичайного однозв'язного списку — у циклічному зв'язаному списку початок і кінець знаходяться в одній точці.
  • Вставка після індексованого вузла. Вузол має бути ідентифікований номером індексу, що відповідає значенню його елемента.

Щоб вставити на початок або кінець циклічного зв'язаного списку, тобто в позицію, де було додано перший вузол, виконайте наведені нижче дії.

  • Вам доведеться розірвати існуюче власне посилання на існуючий вузол
  • Наступний покажчик нового вузла посилатиметься на існуючий вузол.
  • Наступний покажчик останнього вузла вказуватиме на вставлений вузол.

ПРИМІТКА. Вказівник, який позначає початок або кінець кола, можна перепризначити будь-якому вузлу. Обхід все одно повернеться до того самого вузла, як обговорюється далі в цій статті.

Етапи (a) i-iii показані нижче:

вставка Operaції

(Існуючий вузол)

вставка Operaції

Крок 1) Розірвати існуюче посилання

вставка Operaції

Крок 2) Створення прямого посилання (від нового вузла до існуючого вузла)

вставка Operaції

Крок 3) Створіть зв'язок циклу з першим вузлом

Далі ви спробуєте вставити після вузла.

Наприклад, вставте «VALUE2» після вузла, що містить «VALUE0», вважаючи, що початковою точкою є вузол із «VALUE0».

  • Розірвіть зв'язок між першим і другим вузлами та помістіть вузол з "VALUE2" між ними.
  • Наступний вказівник першого вузла посилається на новий вузол, а наступний вказівник нового вузла посилається на те, що раніше було другим вузлом.
  • Решта розташування залишається незмінною. Усі вузли переробленіtracдоступні самим собі.

ПРИМІТКА. Оскільки розташування є циклічним, процедура вставки вузла ідентична незалежно від обраної позиції. Вказівник, який замикає цикл, поводиться як будь-який інший вказівник у списку.

Це показано нижче:

вставка Operaції

(Припустимо, є лише два вузли. Це тривіальний випадок)

вставка Operaції

Крок 1) Видаліть внутрішню зв'язок між підключеними вузлами

вставка Operaції

Крок 2) З’єднайте вузол лівої сторони з новим вузлом

вставка Operaції

Крок 3) Підключіть новий вузол до правого вузла.

видалення Operaції

Припустимо, що це циклічний зв'язаний список із 3 вузлів. Два випадки видалення:

  • Видалення поточного елемента
  • Видалення після елемента.

Видалення на початку/кінці:

  1. Перейдіть до першого вузла від останнього вузла.
  2. Видалення з кінця вимагає лише одного кроку обходу, від останнього вузла до першого вузла.
  3. Видалити зв'язок між останнім вузлом та першим вузлом.
  4. Зв’яжіть останній вузол з наступним елементом першого вузла.
  5. Звільніть перший вузол.

видалення Operaції

(Існуюче налаштування)

видалення Operaції

Крок 1) Видалити кругове посилання

видалення Operaції

Крок 2) Видаліть зв’язок між першим і наступним, пов’яжіть останній вузол із вузлом, наступним за першим

видалення Operaції

Крок 3) Звільнити / звільнити перший вузол

Видалення після вузла:

  1. Переміщайтеся, доки наступний вузол не буде вузлом, який потрібно видалити.
  2. Перейдіть до наступного вузла, навівши покажчик на попередній вузол.
  3. З’єднайте попередній вузол із вузлом після поточного вузла, використовуючи його наступний покажчик.
  4. Звільніть поточний (від’єднаний) вузол.

видалення Operaції

Крок 1) Скажімо, нам потрібно видалити вузол із «VALUE1».

видалення Operaції

Крок 2) Видалити зв'язок між попереднім вузлом та поточним вузлом, а потім безпосередньо зв'язати попередній вузол з вузлом, на який вказує наступний вказівник поточного вузла (вузол після VALUE1).

видалення Operaції

Крок 3) Звільнити або відмінити поточний вузол.

Обхід циклічного пов’язаного списку

Щоб пройтися по циклічному зв'язаному списку від останнього вказівника, спочатку перевірте, чи останній вказівник має значення NULL. Якщо воно не дорівнює NULL, перевірте, чи список містить лише один елемент. В іншому випадку пройдіться по списку з тимчасовим вказівником, доки знову не досягнете останнього вказівника, як показано на анімації нижче.

Обхід циклічного пов’язаного списку

Переваги циклічного пов’язаного списку

Деякі з переваг циклічних пов’язаних списків:

  1. Немає вимог щодо призначення NULL у коді. Циркулярний список ніколи не вказує на покажчик NULL, якщо він не повністю звільнений.
  2. Циклічні зв'язані списки є вигідними для операцій з кінцем списку, оскільки початок і кінець збігаються. Algorithms такі як циклічне планування, можуть чітко проходити через процеси в черзі, не зустрічаючи завислих або NULL-вказівників.
  3. Циркулярний зв'язаний список все ще підтримує всі звичайні операції однозв'язного списку. Циркулярний двозв'язаний список може навіть усунути необхідність повного обходу для пошуку елемента — у гіршому випадку ціль знаходиться навпроти початкового вказівника, тому потрібно пройти щонайбільше половину списку.

Недоліки циклічного пов'язаного списку

Нижче наведено недоліки використання циклічного пов’язаного списку:

  1. Кругові списки складніші, ніж однозв'язані списки.
  2. RevСкасування циклічного списку є складнішим, ніж зворотне виконання одно- або двозв'язного списку.
  3. Якщо завершення циклу не обробляється обережно, код обходу може увійти в нескінченний цикл.
  4. Важче знайти кінець списку та записати правильні умови керування циклом.
  5. Вставка на початку вимагає проходження всього списку, щоб досягти останнього вузла (з точки зору реалізації).

Однозв’язаний список як циклічний зв’язаний список

Рекомендується прочитати та реалізувати наведений нижче код на C. Він ілюструє арифметику вказівників, пов'язану з циклічним однозв'язним списком.

#include<stdio.h>
#include<stdlib.h>

struct node
{
    int item;
    struct node *next;
};

struct node* addToEmpty(struct node*,int);
struct node *insertCurrent(struct node *, int);
struct node *insertAfter(struct node *, int, int);
struct node *removeAfter(struct node *, int);
struct node *removeCurrent(struct node *);

void peek(struct node *);

int main()
{
...

Однозв'язаний список

Пояснення коду:

  1. Перші два рядки коду є необхідними включеними файлами заголовків.
  2. У наступному розділі визначено структуру кожного самопосилального вузла. Вона містить значення та вказівник того ж типу, що й структура.
  3. Кожен екземпляр структури пов'язаний з іншими об'єктами структури того ж типу.
  4. Існують різні прототипи функцій для:
    1. Додавання елемента до порожнього пов’язаного списку
    2. Вставляючи в наразі вказано позиція циклічного пов’язаного списку.
    3. Вставка після конкретного індексований значення у пов’язаному списку.
    4. Видалення/видалення після певного індексований значення у пов’язаному списку.
    5. Видалення в поточній позиції кругового пов’язаного списку
  5. Остання функція друкує кожен елемент через циклічний обхід у будь-якому стані зв’язаного списку.
int main()
{
    struct node *last = NULL;
    last = insertCurrent(last,4);
    last = removeAfter(last, 4);
    peek(last);
    return 0;
}

struct node* addToEmpty(struct node*last, int data)
{
    struct node *temp = (struct node *)malloc(sizeof( struct node));
    temp->item = data;
    last = temp;
    last->next = last;
    return last;
}
  
struct node *insertCurrent(struct node *last, int data)

Однозв'язаний список

Пояснення коду:

  1. Для коду addToEmpty виділіть порожній вузол за допомогою функції malloc().
  2. Помістіть вхідні дані у тимчасовий вузол.
  3. Призначте тимчасовий вузол останньому та встановіть його наступний вказівник на себе, щоб єдиний вузол вказував назад на себе.
  4. Повернути останній вказівник назад до контексту main() / програми.
struct node *insertCurrent(struct node *last, int data)
{
    if(last == NULL)
    {
       return    addToEmpty(last, data);
    }
    struct node *temp = (struct node *)malloc(sizeof( struct node));
    temp -> item = data;
    temp->next = last->next;
    last->next = temp;
    return last;
}
struct node *insertAfter(struct node *last, int data, int item)
{
    struct node *temp = last->next, *prev = temp, *newnode =NULL;
&#8230;

Однозв'язаний список

Пояснення коду

  1. Якщо список порожній, передайте керування addToEmpty().
  2. Створіть тимчасовий вузол, який буде розміщено після поточного вузла.
  3. З'єднайте покажчики, як показано на діаграмі вище.
  4. Повертає останній вказівник, що відповідає шаблону, використаному в попередній функції.
...
struct node *insertAfter(struct node *last, int data, int item)
{
    struct node *temp = last->next, *prev = temp, *newnode =NULL;
    if (last == NULL)
    {
       return addToEmpty(last, item);
    }
    do
    {
        prev = temp;
        temp = temp->next;
    } while (temp->next != last && temp->item != data );

    if(temp->item != data)
    {
       printf("Element not found. Please try again");
...

Однозв'язаний список

Пояснення коду:

  1. Якщо список порожній, ігнорувати ключ пошуку, додати поточний елемент як єдиний вузол у списку та повернути керування.
  2. У кожній ітерації циклу do-while попередній вказівник містить результат останнього проходження.
  3. Тільки після цього відбувається наступний крок обходу.
  4. Виконання операції do-while завершується, коли цільові дані знайдено або коли temp знову досягає останнього вказівника. Наступний блок коду вирішує, що робити з знайденим елементом.
...
    if(temp->item != data)
    {
       printf("Element not found. Please try again");
       return last;
    }
    else
    {
   	 newnode = (struct node *)malloc(sizeof(struct node));
             newnode->item = item;
             prev->next = newnode;
             newnode->next = temp;
    }
    return last;
}

struct node *removeCurrent(struct node *last)
...

Однозв'язаний список

Пояснення коду:

  1. Якщо весь список переглянуто, але елемент не знайдено, вивести повідомлення «Елемент не знайдено» та повернути керування викликаючій службі.
  2. Якщо цільовий вузол знайдено, виділіть новий вузол для значення, яке потрібно вставити.
  3. посилання попередній вузол до нового вузла та зв'язати наступний вказівник нового вузла з temp (змінною обходу).
  4. Це розміщує новий елемент одразу після цільового вузла в циклічному зв'язаному списку. Потім керування повертається до викликаючої сторони.
struct node *removeCurrent(struct node *last)
{
    if(last == NULL)
    {
        printf("Element Not Found");
        return NULL;
    }
    struct node *temp = last->next;
    last->next = temp->next;
    free(temp);
    return last;
}

struct node *removeAfter(struct node *last, int data)

Однозв'язаний список

Пояснення коду

  1. Щоб видалити останній (поточний) вузол, спочатку перевірте, чи список порожній. Якщо так, то жоден елемент видалити неможливо.
  2. Змінна temp переміщує на одне посилання вперед.
  3. Зв'яжіть останній вказівник з вузлом після першого вузла.
  4. Звільніть тимчасовий вказівник, щоб звільнити незв'язаний вузол.
struct node *removeAfter(struct node *last,int data)
{
    struct node *temp = NULL,*prev = NULL;
    if (last == NULL)
    {
   	 printf("Linked list empty. Cannot remove any element\n");
   	 return NULL;
    }
    temp = last->next;
    prev = temp;
    do
    {
        prev = temp;
        temp = temp->next;
    } while (temp->next != last && temp->item != data );

    if(temp->item != data)
    {
      printf("Element not found");
...

Однозв'язаний список

Пояснення коду

  1. Як і у випадку з попередньою функцією видалення, спочатку перевірте, чи список порожній. Якщо так, то жоден елемент не може бути видалений.
  2. Два pointers призначаються певні позиції, щоб знайти елемент, який потрібно видалити.
  3. Вказівники просуваються один за одним (темп. попередніх трас).
  4. Обхід продовжується доти, доки не буде знайдено цільовий елемент або наступний вказівник знову не досягне останнього вузла.
    if(temp->item != data)
    {
        printf("Element not found");
        return last;
    }
    else
    {
        prev->next = temp->next;
        free(temp);
    }
    return last;
}

void peek(struct node * last)
{
    struct node *temp = last;
    if (last == NULL)
    {
   return;

Однозв'язаний список

Пояснення програми

  1. Якщо весь зв'язаний список переглянуто без знаходження цілі, відображається повідомлення «Елемент не знайдено».
  2. В іншому випадку елемент буде від’єднано та звільнено на кроках 3 та 4.
  3. Попередній вказівник пов'язаний з вузлом, на який вказує наступний вказівник temp (вузол після того, що видаляється).
  4. Потім тимчасовий покажчик звільняється.
...
void peek(struct node * last)
{
    struct node *temp = last;
    if (last == NULL)
    {
         return;  
    }
    if(last -> next == last)
    {
        printf("%d-", temp->item);
    }
    while (temp != last)
    {
       printf("%d-", temp->item);
       temp = temp->next;
    }
}

Однозв'язаний список

Пояснення коду

  1. Перегляд за допомогою попереднього перегляду неможливий, якщо вузлів немає — користувач повинен спочатку виділити або вставити вузол.
  2. Якщо є лише один вузол, обхід не потрібен — вміст вузла виводиться безпосередньо, і цикл while не виконується.
  3. Якщо вузлів більше одного, temp виводить кожен елемент до останнього.
  4. У момент досягнення останнього елемента цикл завершується, і функція повертає керування до main().

Застосування Циркулярного пов’язаного списку

  • Реалізація циклічного планування в системних процесах і циклічного планування у високошвидкісній графіці.
  • Планування Token-Ring у комп'ютерних мережах.
  • Використовується у дисплеях, таких як цифрові дошки магазинів, що потребують безперервного проходження даних.

Поширені запитання

Помічники штучного інтелекту, такі як структури вузлів scaffold на GitHub Copilot та ChatGPT, вставки на основі malloc та циклічні цикли обходу. Розробники перевіряють згенерований код на наявність правильних умов завершення та очищення пам'яті перед його об'єднанням у структури даних робочого середовища.

Конвеєри машинного навчання використовують циклічні буфери, побудовані на циклічних зв'язаних списках, для зберігання вікон потокових даних, що змінюються, зразків буферів відтворення для агентів навчання з підкріпленням та циклічних черг для працівників типу "виробник-споживач", які живлять навчальні пакети.

Однозв'язаний список закінчується покажчиком NULL, тоді як останній вузол циклічного зв'язаного списку вказує назад на перший вузол. Цей замкнутий цикл усуває перевірки NULL у кінці та підтримує безперервний обхід в одному циклі.

Круговий двозв'язаний список має два вказівники на кожен вузол — next та prev — і обидва кінці повертаються один до одного. Ця структура підтримує двонаправлений обхід та пошук у найгіршому випадку не більше ніж половини довжини списку.

Алгоритм Флойда «черепаха-заєць» використовує два покажчики, що рухаються з різною швидкістю. Якщо вони зустрічаються, цикл існує. Він виконується за час O(n) та має O(1) додаткового простору і є стандартним рішенням для виявлення циклів на основі інтерв'ю.

Вставка або видалення в поточній позиції циклічного зв'язаного списку виконується за O(1). OperaФункції, що орієнтовані на певне значення або індекс, виконуються за O(n), оскільки для знаходження цільового вузла необхідно пройтися по списку.

OperaПланувальники систем ting використовують їх для циклічного планування процесора, мережі Token Ring передають керування між станціями, медіаплеєри циклічно перемикаються між списками відтворення, а вбудовані системи використовують циклічні буфери, підкріплені циклічними списками для потоків датчиків.

До поширених помилок належать забуття оновлення обох покажчиків кінцевих точок після вставки або видалення, пропуск умови завершення та loo.ping назавжди, звільняючи вузол без повторного зв'язування його сусідів та витікаючи пам'ять, коли список відкидається.

Підсумуйте цей пост за допомогою: