數據結構與算法 - 單鏈表

本文首發於 我的博客node

對於非空的線性表和線性結構,具備如下特色:數組

  • 存在惟一的一個被稱做 第一個 的數據元素
  • 存在惟一的一個被稱做 最後一個 的數據元素
  • 除了第一個元素之外,其餘每一個數據元素都有 一個前驅
  • 除了最後一個元素之外,其餘每一個數據元素都有 一個後繼

線性表是最基本也是最經常使用的一種線性結構,同時它也是其餘數據結構的基礎。尤爲是 單鏈表 ,這篇文章主要講述一下單鏈表的結構以及如何用 C語言 實現一個單鏈表。markdown

單鏈表的實現

單鏈表是一種線性數據結構,咱們考察的主要是它的初始化、添加、刪除、遍歷等方法數據結構

初始化

單向鏈表是由一個個節點組成的,每個節點都包含一個數據段和指針段,數據段主要保存節點的相關信息,指針段主要保存後繼節點的地址。函數

#define ERROR 0
#define TRUE 1
#define FAILURE 0
#define SUCCESS 1

typedef int Status; /* Status是函數的類型,其值是函數結果狀態代碼,如 SUCCESS、FAILURE等 */
typedef int ListData;/* ListData類型根據實際狀況而定,這裏假設爲int */

// 定義節點
typedef struct Node{
    ListData data;
    struct Node *next;
}Node;

typedef struct Node *LinkList;
// 初始化單鏈表
Status InitList(LinkList *L) {
    *L = (LinkList)malloc(sizeof(Node));
    if (*L == NULLreturn ERROR;
    (*L)->next = NULL;
    return SUCCESS;
}
複製代碼

添加節點

既然鏈表的節點已經定義並且鏈表的數據結構已經初始化,接下來咱們看看如何去添加元素,這裏咱們看兩種狀況:oop

  • 不帶頭節點的單鏈表post

如圖所示,咱們要對單鏈表進行插入操做的時候要進行額外判斷,若是要在首元節點以前添加節點,就要挪動List指針,若是其餘位置則不用。spa

  • 帶頭節點的單鏈表指針

這裏帶頭節點的插入就很簡單了,全部的地方一視同仁,而不須要額外去操做鏈表的指針。PS:其中頭節點中的信息咱們不須要關心,由於它至關於咱們默認放進去的一個節點。code

因此後續咱們使用的單鏈表默認添加頭節點。

咱們要插入節點,就要對鏈表進行修改,那麼咱們須要把鏈表的指針做爲參數傳入。

// location from 1...
Status InsertNode(LinkList *L,int location,ListData data) {
    // 找到須要location-1位置的節點
    Node *pre = *L;
    // 由於0位置被頭節點佔了,因此要從1位置開始
    int currentLocation = 1;
    while (pre && currentLocation < location) {
        pre = pre->next;
        currentLocation ++;
    }
    if (!pre || currentLocation < location) return ERROR;

    // 根據data生成一個新節點
    Node *insertNode = (Node *)malloc(sizeof(Node));
    insertNode->data = data;
    // 讓新節點的next 指向 pre->next
    insertNode->next = pre->next;
    // 讓前一個節點的next指向新節點
    pre->next = insertNode;
    return SUCCESS;
}
複製代碼

此處邏輯跟圖上一一對應,接下來咱們要驗證就要打印鏈表每一個位置的值,咱們添加一個打印的方法,咱們只是打印鏈表,不必把傳遞指針。

// 打印方法 咱們不用修改鏈表 無需傳指針
Status printList (LinkList L) {
    LinkList p = L->next;
    while (p) {
        printf("%d\n",p->data);
        p = p->next;
    }
    return SUCCESS;
}
複製代碼

咱們驗證一下:

int main(int argc, const char * argv[]) {
    LinkList L;
    Status status = InitList(&L);
    printf("s is %d\n",status);
    // 插入元素
    for (int i = 10; i >= 1; i --) {
        InsertNode(&L, 1, i);// 此處1表示,老是從頭節點後面插入新節點,也就是頭插法,比較簡單,由於尾插法還要保留鏈表長度
    }
    // 打印鏈表
    printList(L);
    return 0;
}
複製代碼

打印結果以下:

刪除節點

結果如咱們所願,鏈表建立以及插入讀取都正常,接下來咱們看看鏈表是如何刪除節點的:

  • 建立臨時變量指向咱們即將刪除的節點,一方面爲了找到下一個節點,另一個方面爲了釋放內存,不然就內存泄漏了。
  • 直接將臨時變量的上一個節點直接指向臨時變量的下一個節點
  • 釋放臨時變量
Status DeleteNode (LinkList *L ,int location,ListData *deleteData) {
    Node *pre = *L;
    int currentLocation = 1;
    // 仍是找到location-1位置的節點
    while (pre && currentLocation < location) {
        pre = pre->next;
        currentLocation++;
    }
    if (!pre || currentLocation < location) return ERROR;
    // 建立臨時變量 保存即將被刪除的節點
    Node *temp = pre->next;
    if (!temp) return ERROR;
    // 前驅節點指向後驅節點
    pre->next = temp->next;
    // 將咱們刪除的內容返回出去
    *deleteData = temp->data;
    // 釋放內存
    free(temp);
    return SUCCESS;
}
//在main方法中添加以下代碼驗證
// 刪除第五個節點
    ListData data;
    DeleteNode(&L, 5, &data);
    printf("刪除第五個元素後的鏈表是 :\n");
    printList(L);
    printf("被刪除的值是 %d\n",data);
複製代碼

清空單鏈表

  1. 指針指向首元節點 注意不是頭節點 ,並將該節點釋放
  2. 指針偏移到下一個節點 中間節點
  3. 釋放下一個節點 中間節點
  4. 指針以此類推到 尾節點
  5. 釋放 尾結點
  6. 頭節點指向 NULL 此處若是不處理,頭節點的 next就是 野指針
Status clearList(LinkList *L) {
    // 因爲第一個是頭節點,咱們從第二個節點開始刪除,這個地方能夠根據實際狀況來
    Node *pre = (*L)->next;
    Node *nextNode;
    while (pre) {
        // 用一個臨時變量保存當前要刪除的節點指向的下一個節點,有點像遞歸的意思
        nextNode = pre->next;
        // 釋放
        free(pre);
        // 將要刪除的指針偏移到下一個指針
        pre = nextNode;
    }
    // 此處將頭節點指向NULL ,不然就出現野指針了
    (*L)->next = NULL;
    return SUCCESS;
}
複製代碼

頭插法初始化

根據名字就知道了,從表頭處添加節點,以前一篇文章 @synchronized底層探索 裏的數據結構用的就是哈希表,內部就是經過頭插法進行操做的。

Status InitFromHead(LinkList *L,int n) {
    *L = (LinkList)malloc(sizeof(Node));
    if (*L == NULLreturn ERROR;
    (*L)->next = NULL;
    Node *pre = *L;
    for (int i = 1; i <= n; i ++) {
        Node *temp = (Node *)malloc(sizeof(Node));
        temp->data = i;
        temp->next = pre->next;
        pre->next = temp;
    }
    return SUCCESS;
}

// 在main中添加以下,會倒序打印30---1 就是頭插法
    clearList(&L);
    InitFromHead(&L, 30);
    printf("鏈表是 :\n");
    printList(L);
複製代碼

上述打印結果會從30倒序到1,足以證實是頭插法。

尾插法初始化

尾插法就是從鏈表尾部依次插入數據,這樣就跟咱們日常的數組的邏輯差很少了,至關於addObject,這裏跟頭插法不一樣的是,頭插法依賴頭節點,此處依賴尾節點,因此咱們要用一個臨時的指針指向尾結點並依次保存。

Status InitFromTail(LinkList *L,int n) {
    *L = (LinkList)malloc(sizeof(Node));
    if (*L == NULLreturn ERROR;
    // 初始化的時候尾結點就是頭節點
    Node *tail = *L;
    for (int i = 1; i <= n; i ++) {
        Node *temp = (Node *)malloc(sizeof(Node));
        temp->data = i;
        temp->next = NULL;
        tail->next = temp;
        // 尾節點偏移
        tail = tail->next;
    }
    return SUCCESS;
}
// 在main函數中添加以下代碼
    clearList(&L);
    InitFromTail(&L, 30);
    printf("鏈表是 :\n");
    printList(L);
複製代碼

如此尾插法,打印就會按照增序打印從1到30。

我又把方法稍微變化了一下,添加了一個從尾部添加節點的方法,這樣把元素的添加放到外圍,內部只關心須要從鏈表尾部添加什麼節點便可:

// 尾部添加節點
Status AddNodeToTheTail(LinkList *L,ListData data{
    Node *temp = *L;
    while (temp->next) {
        temp = temp->next;
    }
    if (!temp) return ERROR;
    Node *add = (Node *)malloc(sizeof(Node));
    add->data = data;
    add->next = NULL;
    temp->next = add;
    return SUCCESS;
}
// main函數中添加以下代碼
    clearList(&L);
    for (int i = 0; i < 20; i ++) {
        AddNodeToTheTail(&L, i);
    }
    printf("鏈表是 :\n");
    printList(L);
複製代碼

可是細心的觀察你會發現,這個往鏈表尾部添加節點的方法的關鍵點在於先要找到尾節點,無非是經過循環直到找到一個節點的 nextNULL ,對於這個方法若是要初始化一個包含100個數字的的鏈表就要循環1+2+3+....+100 = 5050次,而用它上面那個函數添加的話只用循環100次,這個函數的時間複雜度O(n^2) 而上面那個是 O(n),因此這個方法只能針對初始化以後須要從尾部額外添加一個節點使用。

單向循環鏈表

看到 循環 兩個字咱們就大概知道了,就是全部的節點組成一個 閉合的環,看起來應該是這樣:

由於上面的單鏈表咱們選用了使用頭節點的方式,下面我沒使用不帶頭節點的方式實現單向循環鏈表。具體以代碼體現,其原理跟單向鏈表差很少,有不清楚的結合單向鏈表的圖鏈接首位便可。

初始化

這裏咱們採用符合咱們正常邏輯的尾插法來實現單向循環鏈表的初始化,由於咱們使用不帶頭節點的方式,這裏咱們就要對鏈表的首元節點進行判斷:

  • 首元節點不存在,初始化並建立賦值給鏈表地址
  • 存在即找到當前鏈表的尾節點,依次插入後續節點
// 輸入的方式尾插法建立單向循環鏈表
Status InitList(LinkList *L) {
    int number;
    Node *tail = NULL;
    while (1) {
        scanf("%d",&number);
        // 輸入0結束建立
        if (number == 0break;
        if (*L == NULL) {
            *L = (LinkList)malloc(sizeof(Node));
            if (*L == NULL)return ERROR;
            (*L)->data = number;
            (*L)->next = *L;
            tail = *L;
        } else {
            //找尾結點  方法1
            for (tail = *L; tail->next != *L; tail = tail->next);
            Node *temp = (Node *)malloc(sizeof(Node));
            if (!temp) return ERROR;
            temp->data = number;
            temp->next = *L;
            tail->next = temp;
            if (tail == NULLreturn ERROR;
            //方法2
            Node *node = (Node *)malloc(sizeof(Node));
            if (!node) return ERROR;
            node->data = number;
            node->next = *L;
            tail->next = node;
            tail = tail->next;
        }
    }
    return SUCCESS;
}
複製代碼

上述咱們找尾節點展現了兩種方法:

  • 方法①:每次從首元節點依次循環直至找到 節點的next = 首元節點 便是當前的尾結點。

  • 方法②:用一個臨時變量指向尾節點(初始化的時候尾結點指向首元節點),每次插入新節點,臨時變量進行偏移繼續指向尾結點。

    顯然方法②的時間複雜度更小一點 O(n),方法①每插入一個新數據都要循環遍歷整個鏈表其時間複雜度是O(n^2)

插入節點

插入節點的邏輯其實跟單向鏈表差很少,無非就是指針的一些指向,可是這裏要注意一些細節點:

  • 插入新的首元節點,就要找到尾節點,而後尾節點指向新首元節點,新首元節點指向原首元節點,鏈表地址指向新首元節點。

  • 其餘地方同單向鏈表邏輯

void InsertNode(LinkList *List,int location, ListData data) {
    // 建立待插入節點
    Node *insertNode = (Node *)malloc(sizeof(Node));
    if (insertNode == NULLreturn;
    insertNode->data = data;
    insertNode->next = NULL;
    if (location == 1) {
        // 找到最後一個節點即尾結點
        Node *tail = NULL;
        for (tail = *List; tail->next != *List; tail = tail->next);
        insertNode->next = tail->next;
        tail->next = insertNode;
        *List = insertNode;
    } else {
        Node *preNode = *List;
        // 找到插入位置的的前一個節點
        for (int i = 1; preNode->next != *List && i != location-1; preNode = preNode->next,i++);
        insertNode->next = preNode->next;
        preNode->next = insertNode;
    }
}
複製代碼

刪除節點

刪除節點一樣咱們也要對首元節點的處理單獨拎出來:

Status DeleteNode(LinkList *List,int location,ListData *deleteData) {
    Node *temp = *List;
    if (temp == NULLreturn ERROR;
    Node *target;
    if (location == 1) {// 刪除首元節點
        // 找到尾節點
        for (target = *List; target->next != *List; target = target->next);
        if (target == *List) {
            target->next = NULL;
            *List = NULL;
            *deleteData = temp->data;
            free(target);
            return SUCCESS;
        }
        target->next = temp->next;
        *List = target->next;
        *deleteData = temp->data;
        free(temp);
    } else {
        // 找到待刪除節點的前一個節點
        target = *List;
        int i;
        for (i = 1,target = *List; i < location-1; i ++) {
            target = target->next;
        }
        Node *deleteNode = target->next;
        target->next = deleteNode->next;
        *deleteData = deleteNode->data;
        free(deleteNode);
    }
    return SUCCESS;
}
複製代碼

總結

至此咱們 單鏈表單向循環鏈表 的一系列方法都已實現,使用頭節點不使用頭節點 的方式都有,最後對比咱們發現使用頭節點讓咱們對於處理鏈表的插入數據以及刪除數據的處理會更簡單,由於沒有針對首節點的單獨處理,針對此你們能夠根據具體狀況自行斟酌。其實還有不少方法和實現等着你去發掘,但願這篇文章能將單鏈表的概念和實現講清楚。

相關文章
相關標籤/搜索