數據結構:鏈表

本文內容:
1、 什么是鏈表?
2、 鏈表共分幾類?
3、 鏈表的 C 實現!

總表:《數據結構?》

工程代碼 Github: Data_Structures_C_Implemention -- Link List


1、什么是鏈表?

鏈表 [Linked List]:鏈表是由一組不必相連【不必相連:可以連續也可以不連續】的內存結構 【節點】,按特定的順序鏈接在一起的抽象數據類型。

補充:
抽象數據類型(Abstract Data Type [ADT]):表示數學中抽象出來的一些操作的集合。
內存結構:內存中的結構,如:struct、特殊內存塊...等等之類;


2、鏈表共分幾類?

鏈表常用的有 3 類: 單鏈表、雙向鏈表、循環鏈表。
鏈表.png

鏈表的核心操作集有 3 種:插入、刪除、查找【遍歷】

單鏈表

單鏈表 [Linked List]:由各個內存結構通過一個 Next 指針鏈接在一起組成,每一個內存結構都存在后繼內存結構【鏈尾除外】,內存結構由數據域和 Next 指針域組成。

單鏈表實現圖示:

文字解析:

  • Data 數據 + Next 指針,組成一個單鏈表的內存結構 ;
  • 第一個內存結構稱為 鏈頭,最后一個內存結構稱為 鏈尾;
  • 鏈尾的 Next 指針設置為 NULL [指向空];
  • 單鏈表的遍歷方向單一【只能從鏈頭一直遍歷到鏈尾】

單鏈表操作集:
單向鏈表-操作.png
雙向鏈表

雙向鏈表 [Double Linked List]:由各個內存結構通過指針 Next 和指針 Prev 鏈接在一起組成,每一個內存結構都存在前驅內存結構和后繼內存結構【鏈頭沒有前驅,鏈尾沒有后繼】,內存結構由數據域、Prev 指針域和 Next 指針域組成。

雙向鏈表實現圖示:

文字解析:

  • Data 數據 + Next 指針 + Prev 指針,組成一個雙向鏈表的內存結構;
  • 第一個內存結構稱為 鏈頭,最后一個內存結構稱為 鏈尾;
  • 鏈頭的 Prev 指針設置為 NULL, 鏈尾的 Next 指針設置為 NULL;
  • Prev 指向的內存結構稱為 前驅, Next 指向的內存結構稱為 后繼;
  • 雙向鏈表的遍歷是雙向的,即如果把從鏈頭的 Next 一直到鏈尾的[NULL] 遍歷方向定義為正向,那么從鏈尾的 Prev 一直到鏈頭 [NULL ]遍歷方向就是反向;

雙向鏈表操作集:
雙向鏈表-操作.png
循環鏈表

單向循環鏈表 [Circular Linked List] : 由各個內存結構通過一個指針 Next 鏈接在一起組成,每一個內存結構都存在后繼內存結構,內存結構由數據域和 Next 指針域組成。

雙向循環鏈表 [Double Circular Linked List] : 由各個內存結構通過指針 Next 和指針 Prev 鏈接在一起組成,每一個內存結構都存在前驅內存結構和后繼內存結構,內存結構由數據域、Prev 指針域和 Next 指針域組成。

循環鏈表的單向與雙向實現圖示:

文字解析:

  • 循環鏈表分為單向、雙向兩種;
  • 單向的實現就是在單鏈表的基礎上,把鏈尾的 Next 指針直接指向鏈頭,形成一個閉環;
  • 雙向的實現就是在雙向鏈表的基礎上,把鏈尾的 Next 指針指向鏈頭,再把鏈頭的 Prev 指針指向鏈尾,形成一個閉環;
  • 循環鏈表沒有鏈頭和鏈尾的說法,因為是閉環的,所以每一個內存結構都可以充當鏈頭和鏈尾;

循環鏈表操作集:
循環鏈表-操作.png

3、 鏈表的 C 實現!

單鏈表
  • 節點[內存結構]與鏈表的定義:

節點[內存結構],

struct __ListNode {
    ElementTypePrt data; // 數據域
    ListNode next; // 指針域 [指向下一個節點]
};

解析:struct __ListNode 就是一個節點的內存結構,因為一個節點包含了兩個不同的信息,而且相互沒有共享內存,所以在 C 語言環境下選擇 struct 去表示;

1、ElementTypePrt 原型是 typedef void * ElementTypePrt;,使用 typedef 是方便后期進行修改;這里使用指針的目的是為了讓鏈表支持更多的數據類型,使代碼的可擴展性更強;【如: data 可以直接指向一個單純的 int 或者 一個 struct ,又或者是一個 list 等等】

2、ListNode 原型是

typedef struct __ListNode * _ListNode;
typedef _ListNode ListNode;

這里使用 typedef 是為了后面定義函數接口,以及具體代碼實現更為簡潔;

單鏈表【有時也簡稱,鏈表】,

struct __List {
    unsigned int size;  // 鏈表的長度
    MatchFunc matchFunc; // 兩個節點的數據匹配
    DestroyFunc destroyFunc; // 節點數據的釋放
    ListNode head; // 鏈表的鏈頭指針
    ListNode tail; // 鏈表的鏈尾指針
};

解析:
1、MatchFunc 原型是 typedef _BOOL(*MatchFunc) (const void *key1, const void *key2); 指向形如 _BOOL(*) (const void *key1, const void *key2); 的函數指針;原因,上面提到過 data 是可以指向任意類型的,也就是說兩個節點的 data 怎樣才算匹配,設計者是不知道的,只有使用者才知道,所以提供一個函數接口,讓使用者根據自身的情況進行數據匹配;也是為了代碼可擴展性;

2、_BOOL 原型是

typedef enum __BOOL {
    LINKEDLIST_TRUE  = 1,
    LINKEDLIST_FALSE = 0,
}_BOOL;

目的是為了提高代碼的可讀性,不建議在代碼中直接使用數字,因為誰也不知道那是個啥;【過個幾天,你自己估計也會忘掉,那個數字表示啥】

3、DestroyFunc 原型是 typedef void(*DestroyFunc) (void * data); 指向形如 void(*) (void * data); 的函數指針;data 如何進行釋放也由使用者決定;也是為了代碼可擴展性;

4、size + head + tail 是鏈表的基本要素,當然您也可以根據自身情況,增加個別要素;

  • 單鏈表的核心操作集:
/* Create */ 

List List_Create(DestroyFunc des);
void List_Init(List l, DestroyFunc des);
void List_Destroy(List l);

/* Operations */

Position List_Find(List l, MatchFunc mat, ElementTypePrt const x);
Position List_FindPrevious(List l, MatchFunc mat, ElementTypePrt const x);

_BOOL List_Insert(List l, Position p, ElementTypePrt const x);
_BOOL List_Remove(List l, Position deletePrev, ElementTypePrtPrt const x);
  • 單鏈表的創建與銷毀:

創建,

List List_Create(DestroyFunc des) {
    
    List l = (List)(malloc(sizeof(struct __List)));  // 1
    if (l == NULL) { printf("ERROR: Out Of Space !"); return NULL; } // 2

    List_Init(l, des); // 3

    return l;

}

解析:
1、List_Create 函數功能是創建并初始化一個空鏈表;
// 1 行 1 ,malloc 函數是 C 語言中進行內存創建的函數,需要提供的參數是 size_t ,就是空鏈表的要占的內存大小,所以使用 sizeof 來計算內存大小;

2、// 2 行 2 是防止,內存分配失敗;

3、// 3 請移步下面的 初始化 解析;

初始化,

void List_Init(List l, DestroyFunc des) {

    if (l == NULL) { printf("ERROR: Bad List !"); return; }

    l->size = LINKEDLIST_EMPTY; // LINKEDLIST_EMPTY 就是 0
    l->matchFunc = NULL;
    l->destroyFunc = des;
    l->head = NULL;
    l->tail = NULL;

}

解析:
1、List_Init 目的就是要把一個創建好的表初始化為需要的空表狀態;

2、LINKEDLIST_EMPTY 原型是 #define LINKEDLIST_EMPTY 0 提高代碼可讀性;

3、l->head = NULL; l->tail = NULL; 因為沒有節點,直接置空即可;

銷毀,

void List_Destroy(List l) {
    
    if (l == NULL) { printf("ERROR: Please Using List_Create(...) First !"); return; }

    ElementTypePrt data;

    while (!List_IsEmpty(l)) { // 1
        if ((List_Remove(l, NULL, (ElementTypePrtPrt)&data) == LINKEDLIST_TRUE) &&
            (l->destroyFunc != NULL)) { // 2
            
            l->destroyFunc(data);
        }
    }

    memset(l, 0, sizeof(struct __List)); // 3
    
}

解析:這個函數的功能就是,釋放鏈表中所有的節點,并把鏈表置成空鏈表;
1、 // 1 List_IsEmpty 原型是

_BOOL List_IsEmpty(List l) { return ((l->size == LINKEDLIST_EMPTY) ? LINKEDLIST_TRUE
                                                                   : LINKEDLIST_FALSE); }

2、// 2 請移步下面 刪除操作 的解析;

3、 // 3 memset 原型是 void* memset(void* _Dst, int _Val, size_t _Size); 功能是,設置內存塊的值, 這三個參數分別表示,內存塊個數、設置的內存單元的值、要設置的內存空間大小;

  • 插入操作:
_BOOL List_Insert(List l, Position p, ElementTypePrt const x) {

    if (l == NULL) { printf("ERROR: Bad List !"); return LINKEDLIST_FALSE; }

    /* Create New Node */
    ListNode lNew = ListNode_Create(x);  // 1

    int isLastNode = (p == NULL); // 2

    if (isLastNode) {
    
        if (List_IsEmpty(l)) { l->tail = lNew; }

        /* Insert Operations */ // 3
        lNew->next = l->head;
        l->head = lNew;

    } else {

        if (p->next == NULL) { l->tail = p; }

        /* Insert Operations */ // 4
        lNew->next = p->next;
        p->next = lNew;

    }

    /* Size ++ */
    l->size++;

    return LINKEDLIST_TRUE;

}

解析:函數的功能是在指定的節點后面插入一個新的節點;
鏈表中的插入,不外乎鏈頭后面、中間位置、鏈尾后面三個位置;

1、// 1 ListNode_Create 原型是

ListNode ListNode_Create(ElementTypePrt const x) {
    
    ListNode lNew = (ListNode)malloc(sizeof(struct __ListNode));
    if (lNew == NULL) { printf("ERROR: Out Of Space !"); return NULL; }

    lNew->data = x;
    lNew->next = NULL;

    return lNew;

}

此處不解析;

2、// 2 這里是為了區分插入發生在鏈頭與鏈尾,還是中間位置與鏈尾;

3、// 3// 4 就是鏈表插入的核心操作,

插入操作圖示,
// 對應的核心代碼
lNew->next = p->next;
p->next = lNew;
  • 刪除操作:
_BOOL List_Remove(List l, Position deletePrev, ElementTypePrtPrt const x) {
    
    if (l == NULL) { printf("ERROR: Bad List !"); return LINKEDLIST_FALSE; }
    if (List_IsEmpty(l)) { printf("ERROR: Empty List !"); return LINKEDLIST_FALSE; }

    int isHeadNode = (deletePrev == NULL);

    ListNode lDelete = NULL;
    if (isHeadNode) {
    
        /* Get The Deleted Data ! */
        *x = l->head->data;

        /* Delete Operations */
        lDelete = l->head;
        l->head = l->head->next;

        if (List_Size(l) == 1) { l->tail = NULL; }

    } else {

        /* Can`t Delete ... */
        if (deletePrev->next == NULL) { return LINKEDLIST_FALSE; }

        /* Get The Deleted Data ! */
        *x = deletePrev->next->data;

        /* Delete Operations */
        lDelete = deletePrev->next;
        deletePrev->next = deletePrev->next->next;

        if (deletePrev->next == NULL) { l->tail = deletePrev; }

    }

    /* Free The Deleted Node */
    free(lDelete);

    /* Size -- */
    l->size--;

    return LINKEDLIST_TRUE;
}

解析:函數功能是刪除鏈表節點;
刪除操作在單鏈表中比較特殊,因為鏈表是單向,即從鏈頭到鏈尾,而要刪除的節點并沒有指向前面節點的能力,所以要使用需刪除的節點的前一個節點進行刪除操作;

刪除操作圖示,
// 對應的核心代碼
lDelete = deletePrev->next;
deletePrev->next = deletePrev->next->next;

free(lDelete);
  • 遍歷操作:

上面的插入與刪除操作都要提供一個 Position 參數,不然兩個函數沒法用,而尋找這個行為就是遍歷操作;

這里提供兩個遍歷操作,

Position List_Find(List l, MatchFunc mat, ElementTypePrt const x);
Position List_FindPrevious(List l, MatchFunc mat, ElementTypePrt const x);

遍歷其實就是匹配內容,也就是要用到前面提到的 MatchFunc 函數,這個具體的匹配函數由用使用者實現;

Position List_Find(List l, MatchFunc mat, ElementTypePrt const x) 函數:

// Position 就是 ListNode 的別名
Position List_Find(List l, MatchFunc mat, ElementTypePrt const x) {

    if (l == NULL) { printf("ERROR: Bad List !"); return NULL; }
    if (List_IsEmpty(l)) { printf("ERROR: Empty List !"); return NULL; }

    if (mat == NULL) { printf("ERROR: Bad Match Function !"); return NULL; }
    l->matchFunc = mat;

    Position p = NULL;
    for (p = List_Head(l); p != NULL; p = List_NodeNext(p)) {  // 1
        if (mat(x, List_NodeData(p))) { return p; } // 2
    }

    return NULL;

}

解析:函數功能是遍歷鏈表,查找與當前節點內容匹配的節點;
1、// 1 從鏈頭開始,不斷地用 next 來進行指針偏移,一直到鏈尾才結束【因為鏈尾的 next 肯定是 NULL 所以使用它來結束循環】;

2、// 2 這一次的匹配都使用用戶實現的 MatchFunc 函數;

3、List_Head 就是 l->headList_NodeNext 就是 p->nextList_NodeData 就是 p->data

List_FindPrevious (...): 函數功能是遍歷鏈表,查找與當前節點內容匹配的節點的前一個節點;遍歷原理與 List_Find (...) 是一樣的;

雙向鏈表
  • 節點[內存結構]與雙向鏈表的定義:

節點[內存結構],

typedef struct __DoubleListNode * _DoubleListNode;
typedef _DoubleListNode DoubleListNode;
typedef _DoubleListNode DoublePosition;
struct __DoubleListNode {
    ElementTypePrt data;
    DoubleListNode prev;
    DoubleListNode next;
};

解析:
因為雙向鏈表有前驅節點和后繼節點,所以內存結構要在單鏈表的內存結構基礎上,增加一個 prev 指針指向節點的前驅節點;

雙向鏈表,

typedef struct __DoubleList * _DoubleList;
typedef _DoubleList DoubleList;
struct __DoubleList {
    unsigned int size;
    MatchFunc matchFunc;
    DestroyFunc destroyFunc;
    DoubleListNode head;
    DoubleListNode tail;
};

因為雙向鏈表只是在單鏈表的基礎上增加了一個遍歷方向,并沒有改變鏈表的其它內容,所以與單鏈表的定義一致,不再進行解析;

  • 雙向鏈表的核心操作集:
/* Create */

DoubleList DoubleList_Create(DestroyFunc des);
void DoubleList_Init(DoubleList l, DestroyFunc des);
void DoubleList_Destroy(DoubleList l);

/* Operations */

DoublePosition DoubleList_Find(DoubleList l, MatchFunc mat, ElementTypePrt const x);
DoublePosition DoubleList_Find_Reverse(DoubleList l, MatchFunc mat, ElementTypePrt const x);

_BOOL DoubleList_Insert_Prev(DoubleList l, DoublePosition p, ElementTypePrt const x);
_BOOL DoubleList_Insert_Next(DoubleList l, DoublePosition p, ElementTypePrt const x);
_BOOL DoubleList_Remove(DoubleList l, DoublePosition p, ElementTypePrtPrt const x);
  • 雙向鏈表的創建與銷毀:

創建、初始化、銷毀,這三個操作與單鏈表的實現基本一致,具體的可以查看工程代碼;

  • 插入操作 [分為兩種,在指定節點前插入和在指定節點后插入] :

在指定節點后插入

// 在指定節點后插入
_BOOL DoubleList_Insert_Next(DoubleList l, DoublePosition p, ElementTypePrt const x) {

    if (l == NULL) { printf("ERROR: Bad List !"); return LINKEDLIST_FALSE; }
    if (p == NULL) { printf("ERROR: Bad Position !"); return LINKEDLIST_FALSE; }

    /* Create New Node */
    DoubleListNode lNew = DoubleListNode_Create(x);

    if (DoubleList_IsEmpty(l)) {
    
        l->head = l->tail = lNew;  // 1

    } else {
    
        // 2
        lNew->prev = p;
        lNew->next = p->next;

        _BOOL isLastNode = (p->next == NULL);
        if (isLastNode) {
            l->tail = lNew;
        } else {
            p->next->prev = lNew;
        }

        p->next = lNew;

    }

    /* Size ++ */
    l->size++;

    return LINKEDLIST_TRUE;

}

解析:
1、當鏈表是空表的時候,鏈頭和鏈尾就要指向新創建的節點,這是要注意的;

2、雙向鏈表插入圖示,
// 對應代碼
// 2
lNew->prev = p;
lNew->next = p->next;
p->next->prev = lNew;
p->next = lNew;

在指定節點前插入

// 在指定節點前插入
_BOOL DoubleList_Insert_Prev(DoubleList l, DoublePosition p, ElementTypePrt const x) {

    if (l == NULL) { printf("ERROR: Bad List !"); return LINKEDLIST_FALSE; }
    if (p == NULL) { printf("ERROR: Bad Position !"); return LINKEDLIST_FALSE; }

    /* Create New Node */
    DoubleListNode lNew = DoubleListNode_Create(x);

    if (DoubleList_IsEmpty(l)) {

        l->head = l->tail = lNew;

    } else {

        lNew->next = p;
        lNew->prev = p->prev;

        _BOOL isFirstNode = (p->prev == NULL);
        if (isFirstNode) {
            l->head = lNew;
        } else {
            p->prev->next = lNew;
        }

        p->prev = lNew;

    }

    /* Size ++ */
    l->size++;

    return LINKEDLIST_TRUE;

}

解析:這個插入方法的核心代碼是,

lNew->next = p;
lNew->prev = p->prev;
p->prev->next = lNew;
p->prev = lNew;

其實原理是一樣的,只是插入的方向不同;你把上面的兩個插入方法的核心代碼對比一下就知道了,只是把 prevnext 的位置改了一下;而能輕松的原因是,鏈表的雙向遍歷只是單純的方向不同,其它沒有任何區別,非常像兩個單鏈表組合在一起;

  • 刪除操作:
_BOOL DoubleList_Remove(DoubleList l, DoublePosition p, ElementTypePrtPrt const x) {

    if (l == NULL) { printf("ERROR: Bad List !"); return LINKEDLIST_FALSE; }
    if (p == NULL) { printf("ERROR: Bad Position !"); return LINKEDLIST_FALSE; }
    if (DoubleList_IsEmpty(l)) { printf("ERROR: Empty List !"); return LINKEDLIST_FALSE; }

    /* Get Data */
    *x = p->data;

    _BOOL isHeadNode = ( p == l->head );

    if (isHeadNode) {
        
        l->head = p->next;

        _BOOL isEmpty = (l->head == NULL);
        if (isEmpty) {
            l->tail = NULL;
        } else {
            // p->next->prev = NULL;
            l->head->prev = NULL; 
        }

    } else {
    
        p->prev->next = p->next;

        _BOOL isLastNode = (p->next == NULL);
        if (isLastNode) {
            l->tail = p->prev;
        } else {
            p->next->prev = p->prev;
        }

    }

    /* Free The Deleted Node */
    free(p);

    /* Size -- */
    l->size--;

    return LINKEDLIST_TRUE;

}

解析:函數功能是刪除指定的節點;由于雙向鏈表有前驅節點,所以可以輕松地通過指定的節點的 prev 指針得到前一個節點,而不像單鏈表的刪除那樣要使用指定節點的前一個節點再進行刪除操作;

核心刪除操作圖示,
// 對應的核心代碼
p->prev->next = p->next;
p->next->prev = p->prev;

free(p);

與單鏈表的刪除相比,雙向鏈表的刪除操作更加簡單;

  • 遍歷操作:因為是雙向鏈表所以遍歷就有兩個方向;
DoublePosition DoubleList_Find(DoubleList l, MatchFunc mat, ElementTypePrt const x);
DoublePosition DoubleList_Find_Reverse(DoubleList l, MatchFunc mat, ElementTypePrt const x);

從鏈頭到鏈尾的遍歷 DoubleList_Find

DoublePosition DoubleList_Find(DoubleList l, MatchFunc mat, ElementTypePrt const x) {

    if (l == NULL) { printf("ERROR: Bad List !"); return NULL; }
    if (DoubleList_IsEmpty(l)) { printf("ERROR: Empty List !"); return NULL; }

    if (mat == NULL) { printf("ERROR: Bad Match Function !"); return NULL; }
    l->matchFunc = mat;

    DoublePosition p = NULL;
    for (p = DoubleList_Head(l); p != NULL; p = DoubleList_NodeNext(p)) {
        if (mat(x, DoubleList_NodeData(p))) { return p; }
    }

    return NULL;

}

它的實現與單鏈表的 List_Find 方法實現原理上完全一樣,不同的就是函數名和節點而已,所以你如果單鏈表的看懂了,那么這里也就懂了。

從鏈尾到鏈頭的遍歷 DoubleList_Find_Reverse

DoublePosition DoubleList_Find_Reverse(DoubleList l, MatchFunc mat, ElementTypePrt const x) {

    if (l == NULL) { printf("ERROR: Bad List !"); return NULL; }
    if (DoubleList_IsEmpty(l)) { printf("ERROR: Empty List !"); return NULL; }

    if (mat == NULL) { printf("ERROR: Bad Match Function !"); return NULL; }
    l->matchFunc = mat;

    DoublePosition p = NULL;
    for (p = DoubleList_Tail(l); p != NULL; p = DoubleList_NodePrev(p)) {
        if (mat(x, DoubleList_NodeData(p))) { return p; }
    }

    return NULL;

}

原理與上面的方法是一致的,它們不同的只是方向,前者是,
for (p = DoubleList_Head(l); p != NULL; p = DoubleList_NodeNext(p))
[ head --到-- Tail (next) ]
后者是,for (p = DoubleList_Tail(l); p != NULL; p = DoubleList_NodePrev(p))
[ Tail --到-- Next (prev) ] ;

循環鏈表

單向循環鏈表:

  • 單向循環鏈表的節點與鏈表:
/* struct */
struct __CircularListNode {
    ElementTypePrt data;
    CircularListNode next;
};

struct __CircularList {
    unsigned int size;
    MatchFunc matchFunc;
    DestroyFunc destroyFunc;
    CircularListNode head;
};

與單鏈表唯一的不同是,沒有 tail 指針,因為鏈表是循環的,所以不存在尾這個說法;

  • 單向循環鏈表的核心操作集:與單鏈表的操作集也是一樣的,這里就列出來一下;
/* Create */

CircularList CircularList_Create(DestroyFunc des);
void CircularList_Init(CircularList l, DestroyFunc des);
void CircularList_Destroy(CircularList l);

/* Operations */
CircularPosition CircularList_Find(CircularList l, MatchFunc mat, ElementTypePrt const x);
CircularPosition CircularList_FindPrevious(CircularList l, MatchFunc mat, ElementTypePrt const x);

_BOOL CircularList_Insert(CircularList l, CircularPosition p, ElementTypePrt const x);
_BOOL CircularList_Remove(CircularList l, CircularPosition deletePrev, ElementTypePrtPrt const x);
  • 單向循環鏈表的創建與銷毀:與單鏈表的實現完全一樣;

  • 插入操作:

_BOOL CircularList_Insert(CircularList l, CircularPosition p, ElementTypePrt const x) {
    
    if (l == NULL) { printf("ERROR: Bad List !"); return LINKEDLIST_FALSE; }
    if (p == NULL) { printf("ERROR: Bad Position !"); return LINKEDLIST_FALSE; }

    /* Create New Node */
    CircularListNode lNew = CircularListNode_Create(x);

    if (CircularList_IsEmpty(l)) {
        
        p->next = p;
        l->head = p;

    } else {
    
        /* Insert Operations */
        lNew->next = p->next;
        p->next = lNew;

    }

    /* Size ++ */
    l->size++;

    return LINKEDLIST_TRUE;

}

因為沒有 NULL 指針的存在,所以插入過程的出錯判斷就變得很少;

單向循環鏈表插入操作圖示,
// 對應的代碼
lNew->next = p->next;
p->next = lNew;

這里的核心代碼與單鏈表插入操作的核心代碼是一樣的;

  • 刪除操作:
_BOOL CircularList_Remove(CircularList l, CircularPosition deletePrev, ElementTypePrtPrt const x) {
    
    if (l == NULL) { printf("ERROR: Bad List !"); return LINKEDLIST_FALSE; }
    if (deletePrev == NULL) { printf("ERROR: Bad Position !"); return LINKEDLIST_FALSE; }
    if (CircularList_IsEmpty(l)) { printf("ERROR: Empty List !"); return LINKEDLIST_FALSE; }

    *x = deletePrev->next->data;

    CircularListNode lDelete = NULL;

    _BOOL isOnlyOneNode = (deletePrev->next == deletePrev);
    if (isOnlyOneNode) {
        
        lDelete = deletePrev->next;
        l->head = NULL;

    } else {
    
        /* Delete Operations */
        lDelete = deletePrev->next;
        deletePrev->next = deletePrev->next->next;

        if (lDelete == l->head) { l->head = lDelete->next; }

    }

    /* Free The Deleted Node */
    if (lDelete != NULL) { free(lDelete); }

    /* Size -- */
    l->size--;

    return LINKEDLIST_TRUE;

}

單向鏈表刪除操作圖示,
// 對應核心代碼
lDelete = p->next;
p->next = p->next->next;

free(lDelete);

這里的核心代碼與單鏈表刪除操作的核心代碼是一樣的;

  • 遍歷操作:與單鏈表的遍歷原理一樣,不同的是結束條件;

CircularPosition CircularList_Find(...) 函數:

CircularPosition CircularList_Find(CircularList l, MatchFunc mat, ElementTypePrt const x) {
    
    if (l == NULL) { printf("ERROR: Bad List !"); return NULL; }
    if (CircularList_IsEmpty(l)) { printf("ERROR: Empty List !"); return NULL; }

    if (mat == NULL) { printf("ERROR: Bad Match Function !"); return NULL; }
    l->matchFunc = mat;

    CircularPosition p = CircularList_Head(l);

    while ( ! mat(x, CircularList_NodeData(p))) {
        p = CircularList_NodeNext(p);
        if (p == CircularList_Head(l)) { p = NULL; break; }
    }

    return p;

}

解析,
這里主要是結束條件的選擇,循環鏈表的起點是 head 那么結束自然而然也是 head 了,所以 if (p == CircularList_Head(l)) { p = NULL; break; } 直接跳出循環就可以了;

CircularPosition CircularList_FindPrevious(...) 函數:

CircularPosition CircularList_FindPrevious(CircularList l, MatchFunc mat, ElementTypePrt const x) {

    if (l == NULL) { printf("ERROR: Bad List !"); return NULL; }
    if (CircularList_IsEmpty(l)) { printf("ERROR: Empty List !"); return NULL; }

    if (mat == NULL) { printf("ERROR: Bad Match Function !"); return NULL; }
    l->matchFunc = mat;

    CircularPosition p = CircularList_Head(l);

    while ( ! mat(x, CircularList_NodeData(CircularList_NodeNext(p)))) {

        CircularPosition nextP = CircularList_NodeNext(p);
        if (nextP == CircularList_Head(l)) {

            if ( ! mat(x, CircularList_NodeData(CircularList_NodeNext(p)))) { 
                p = NULL; 
                break;
            }

            break;
        }

        p = nextP;

    }

    return p;

}

解析,
這里的起點是 head->next ,但是由于循環與前一個節點的約束條件,結束點就是 headnext 節點,因為 head->next 要被訪問兩次,所以最后一次的訪問,就是鏈表遍歷結束的條件;

        CircularPosition nextP = CircularList_NodeNext(p);
        if (nextP == CircularList_Head(l)) {

            if ( ! mat(x, CircularList_NodeData(CircularList_NodeNext(p)))) { 
                p = NULL; 
                break;
            }

            break;
        }

這里是先判斷下一個節點是否是 head 已經結束了,因為 head 是最后一個要被訪問的 ;

雙向循環鏈表:

  • 雙向循環鏈表的節點與鏈表:
/* struct */
struct __DoubleCircularListNode {
    ElementTypePrt data;
    DoubleCircularListNode prev;
    DoubleCircularListNode next;
};

struct __DoubleCircularList {
    unsigned int size;
    MatchFunc matchFunc;
    DestroyFunc destroyFunc;
    DoubleCircularListNode head;
};

與雙向鏈表的實現區別就是,沒有 tail 指針;

  • 雙向循環鏈表的核心操作集:與雙向鏈表的操作集一致;
/* Create */

DoubleCircularList DoubleCircularList_Create(DestroyFunc des);
void DoubleCircularList_Init(DoubleCircularList l, DestroyFunc des);
void DoubleCircularList_Destroy(DoubleCircularList l);

/* Operations */
DoubleCircularPosition DoubleCircularList_Find(DoubleCircularList l, MatchFunc mat, ElementTypePrt const x);
DoubleCircularPosition DoubleCircularList_Find_Reverse(DoubleCircularList l, MatchFunc mat, ElementTypePrt const x);

_BOOL DoubleCircularList_InsertInTail(DoubleCircularList l, ElementTypePrt const x);
_BOOL DoubleCircularList_Insert_Prev(DoubleCircularList l, DoubleCircularPosition p, ElementTypePrt const x);
_BOOL DoubleCircularList_Insert_Next(DoubleCircularList l, DoubleCircularPosition p, ElementTypePrt const x);
_BOOL DoubleCircularList_Remove(DoubleCircularList l, DoubleCircularPosition p, ElementTypePrtPrt const x);
  • 雙向循環鏈表的創建與銷毀:與雙向鏈表的實現一致;

  • 插入操作:

指定節點前的插入,

_BOOL 
DoubleCircularList_Insert_Prev( DoubleCircularList l, 
                                DoubleCircularPosition p, 
                                ElementTypePrt const x ) {
    
    if (l == NULL) { printf("ERROR: Bad List !"); return LINKEDLIST_FALSE; }
    if (p == NULL) { printf("ERROR: Bad Position !"); return LINKEDLIST_FALSE; }

    /* Create New Node */
    DoubleCircularListNode lNew = DoubleCircularListNode_Create(x);

    if (DoubleCircularList_IsEmpty(l)) {

        l->head = lNew;
        l->head->prev = lNew;
        l->head->next = lNew;
    }
    else {

        lNew->next = p;
        lNew->prev = p->prev;

        p->prev->next = lNew;
        p->prev = lNew;

    }

    /* Size ++ */
    l->size++;

    return LINKEDLIST_TRUE;

}

指定節點后的插入,

_BOOL 
DoubleCircularList_Insert_Next( DoubleCircularList l,
                                DoubleCircularPosition p, 
                                ElementTypePrt const x ) {

    if (l == NULL) { printf("ERROR: Bad List !"); return LINKEDLIST_FALSE; }
    if (p == NULL) { printf("ERROR: Bad Position !"); return LINKEDLIST_FALSE; }

    /* Create New Node */
    DoubleCircularListNode lNew = DoubleCircularListNode_Create(x);

    if (DoubleCircularList_IsEmpty(l)) {

        l->head = lNew;
        l->head->prev = lNew;
        l->head->next = lNew;

    }
    else {

        lNew->prev = p;
        lNew->next = p->next;

        p->next->prev = lNew;
        p->next = lNew;

    }

    /* Size ++ */
    l->size++;

    return LINKEDLIST_TRUE;

}

解析,
上面的兩個插入操作與雙向鏈表的實現原理是一樣的,區別就是插入操作的錯誤處理更少;

雙向循環鏈表插入操作圖示,
// 對應核心代碼
lNew->prev = p;
lNew->next = p->next;
p->next->prev = lNew;
p->next = lNew;
  • 刪除操作:
_BOOL 
DoubleCircularList_Remove( DoubleCircularList l,
                           DoubleCircularPosition p,
                           ElementTypePrtPrt const x ) {

    if (l == NULL) { printf("ERROR: Bad List !"); return LINKEDLIST_FALSE; }
    if (p == NULL) { printf("ERROR: Bad Position !"); return LINKEDLIST_FALSE; }

    if (DoubleCircularList_IsEmpty(l)) { 
        printf("ERROR: Empty List !");
        return LINKEDLIST_FALSE; 
    }

    /* Get Data */
    *x = p->data;

    _BOOL isHeadNode = (p == l->head);
    if (isHeadNode) {

        l->head = p->next;
        if (l->size == 1) { l->head = NULL; }

    }

    p->next->prev = p->prev;
    p->prev->next = p->next;

    /* Free The Deleted Node */
    free(p);

    /* Size -- */
    l->size--;

    return LINKEDLIST_TRUE;

}

解析,
上面的刪除操作與雙向鏈表的實現原理一致,區別在于這里的錯誤處理更少;

雙向循環鏈表刪除操作圖示,
// 對應的核心代碼
p->next->prev = p->prev;
p->prev->next = p->next;

free(p);
  • 遍歷操作:與雙向鏈表的實現原理一致,區別在于結束條件的判斷;
DoubleCircularPosition DoubleCircularList_Find(DoubleCircularList l, MatchFunc mat, ElementTypePrt const x);
DoubleCircularPosition DoubleCircularList_Find_Reverse(DoubleCircularList l, MatchFunc mat, ElementTypePrt const x);

DoubleCircularList_Find(...) 函數:

DoubleCircularPosition 
DoubleCircularList_Find( DoubleCircularList l, 
                         MatchFunc mat,
                         ElementTypePrt const x ) {

    if (l == NULL) { printf("ERROR: Bad List !"); return NULL; }
    if (DoubleCircularList_IsEmpty(l)) { printf("ERROR: Empty List !"); return NULL; }

    DoubleCircularPosition p = DoubleCircularList_Head(l);

    while ( ! mat(x, DoubleCircularList_NodeData(p)) ) {
        p = DoubleCircularList_NodeNext(p);
        if (p == DoubleCircularList_Head(l)) { p = NULL; break; }
    }

    return p;

}

解析,
head 開始,通過 next 不斷地向后訪問后繼節點,到達 head 處結束 ,對應的結束代碼 if (p == DoubleCircularList_Head(l)) { p = NULL; break; } ;

DoubleCircularList_Find_Reverse(...) 函數:

DoubleCircularPosition
DoubleCircularList_Find_Reverse( DoubleCircularList l,
                                 MatchFunc mat,
                                 ElementTypePrt const x ) {
    
    if (l == NULL) { printf("ERROR: Bad List !"); return NULL; }
    if (DoubleCircularList_IsEmpty(l)) { printf("ERROR: Empty List !"); return NULL; }
    
    DoubleCircularPosition p = DoubleCircularList_Head(l);

    while ( ! mat(x, DoubleCircularList_NodeData(p))) {
        p = DoubleCircularList_NodePrev(p);
        if (p == DoubleCircularList_Head(l)) { p = NULL; break; }
    }

    return p;

}

解析,
head 開始,通過 prev 不斷地向后訪問前驅節點,到達 head 處結束 ,對應的結束代碼同樣是 if (p == DoubleCircularList_Head(l)) { p = NULL; break; } ;


參考書籍:
1、《算法精解_C語言描述(中文版)》
2、《數據結構與算法分析—C語言描述》

寫到這里,本文結束!下一篇,《數據結構:棧與隊列》

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容