C 語言鏈結串列完整教學:從基礎概念到實作與應用範例

1. 前言

C 語言是一種廣泛應用於系統程式設計與嵌入式系統開發的程式語言。其中,「鏈結串列結構」(List Structure)是一種在資料管理與操作上非常便利的工具。本文將深入介紹 C 語言中的鏈結串列結構,從基本概念到具體的實作範例,為讀者提供完整的解說。

鏈結串列結構的重要性

鏈結串列結構是一種能夠保留資料順序並加以管理的資料結構,特別適用於以下情況:

  • 管理有順序的資料
  • 需要動態新增或刪除資料時
  • 有效利用記憶體資源

例如,在任務管理應用程式中,由於經常需要新增與刪除任務,相較於陣列,更靈活的鏈結串列結構會更適合。

本文目的

本篇文章將示範如何在 C 語言中實作鏈結串列結構,並透過具體的程式碼範例介紹操作方法。最終,讀者將能理解鏈結串列的基本概念與實際應用方式,並能在自己的程式中加以運用。

學習目標

  1. 理解鏈結串列結構的基本概念。
  2. 學會在 C 語言中實作鏈結串列。
  3. 參考實用的程式碼範例,培養應用能力。

2. 什麼是鏈結串列結構

鏈結串列結構是一種用於有序管理資料的資料結構。本節將介紹鏈結串列的基本概念與類型。

鏈結串列的基本概念

鏈結串列由多個節點(Node)串接而成,每個節點包含以下兩部分:

  1. 資料區塊:用於儲存實際的值。
  2. 鏈結區塊:用於儲存指向下一個節點的參考(指標)。

透過這種結構,可以動態新增或刪除資料,適合處理長度可變的資料。

陣列與鏈結串列的差異

鏈結串列與陣列在資料管理方式上有以下不同:

項目陣列鏈結串列
大小固定(宣告時決定)可變(可動態調整)
資料存取可透過索引直接存取需從開頭依序搜尋
記憶體管理使用連續記憶體空間使用分散記憶體空間
新增/刪除資料成本高(需移動其他元素)成本低(僅需調整指標)

總結來說,鏈結串列適合需要高度靈活性的情境,而陣列則適合需要快速存取的場合。

鏈結串列概述與特點

鏈結串列的代表性例子是「單向鏈結串列」,其特點包括:

  1. 可動態調整大小
    可依需求新增或刪除節點,有效利用記憶體。
  2. 節點插入與刪除容易
    僅需調整指標即可操作,即使在中間或末端也可輕鬆處理。
  3. 搜尋速度較慢
    存取資料時需從頭依序搜尋,定位特定元素耗時。

鏈結串列的種類

鏈結串列主要分為三種:

  1. 單向鏈結串列
    每個節點僅指向下一個節點。
  • 記憶體消耗少,結構簡單。
  • 只能向後移動,不能向前。
  1. 雙向鏈結串列
    每個節點同時指向前後節點。
  • 可前後移動,操作靈活。
  • 記憶體使用量較多。
  1. 循環鏈結串列
    最後一個節點指回第一個節點。
  • 可無限循環,適合迴圈處理。
  • 適用於特殊用途。

下一節將介紹「C 語言中鏈結串列的實作方法」,並附上完整程式碼範例。

年収訴求

3. 使用 C 語言實作鏈結串列

本節將示範如何使用 C 語言實作鏈結串列,並提供完整的程式碼範例與說明。

鏈結串列的基本結構

鏈結串列由以下要素構成:

  1. 節點定義
  • 每個節點包含資料與指向下一個節點的指標。
  1. 頭指標(Head Pointer)
  • 指向鏈結串列開頭的指標,用來管理整個串列。

程式碼範例:鏈結串列的基本實作

以下為一個簡單的鏈結串列操作範例:

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

// 節點定義
typedef struct Node {
    int data;                 // 資料區塊
    struct Node* next;        // 指向下一個節點的指標
} Node;

// 建立新節點
Node* createNode(int data) {
    Node* newNode = (Node*)malloc(sizeof(Node)); // 動態分配記憶體
    if (newNode == NULL) {
        printf("記憶體分配失敗。\n");
        exit(1);
    }
    newNode->data = data;
    newNode->next = NULL; // 預設下一個節點為 NULL
    return newNode;
}

// 在串列頭部插入節點
void insertAtHead(Node** head, int data) {
    Node* newNode = createNode(data);
    newNode->next = *head;
    *head = newNode;
}

// 列印串列內容
void printList(Node* head) {
    Node* temp = head;
    while (temp != NULL) {
        printf("%d -> ", temp->data);
        temp = temp->next;
    }
    printf("NULL\n");
}

// 釋放串列記憶體
void freeList(Node* head) {
    Node* temp;
    while (head != NULL) {
        temp = head;
        head = head->next;
        free(temp);
    }
}

int main() {
    Node* head = NULL; // 初始化空串列

    // 新增資料
    insertAtHead(&head, 3);
    insertAtHead(&head, 2);
    insertAtHead(&head, 1);

    // 列印串列
    printList(head);

    // 釋放記憶體
    freeList(head);

    return 0;
}

程式碼解說

  1. 建立節點
  • 透過 malloc() 進行動態記憶體分配建立新節點。
  1. 插入節點
  • 可在頭部或尾部插入資料,擴展串列內容。
  1. 刪除節點
  • 移除符合條件的節點,並釋放其記憶體。
  1. 記憶體管理
  • 透過 free() 釋放已使用的記憶體,避免記憶體洩漏。

4. 鏈結串列的種類

本節將介紹鏈結串列的主要類型與各自的特點,並分析其優缺點,幫助您根據需求選擇合適的結構。

1. 單向鏈結串列

每個節點僅指向下一個節點。

特點:

  • 節點只能單方向移動。
  • 記憶體消耗少,結構簡單。

優點:

  • 可動態調整大小,靈活性高。
  • 新增與刪除操作效率佳。

缺點:

  • 無法反向遍歷,逆向搜尋效率低。

2. 雙向鏈結串列

每個節點同時指向前一個與下一個節點。

特點:

  • 具備前後兩個指標。
  • 可雙向移動,操作靈活。

優點:

  • 雙向移動方便,刪除與搜尋容易。
  • 可直接反向操作。

缺點:

  • 比單向串列佔用更多記憶體。
  • 實作較複雜。

3. 循環鏈結串列

最後一個節點指向第一個節點,形成循環結構。

特點:

  • 無終點,可不斷循環。
  • 可為單向或雙向形式。

優點:

  • 可方便地在首尾之間切換,適合迴圈處理。
  • 適用於佇列與緩衝區實作。

缺點:

  • 由於無終點,刪除與搜尋需格外小心。

5. 鏈結串列的操作

本節將詳細介紹鏈結串列的基本操作,包括插入刪除搜尋,並附上程式碼範例。

1. 插入元素

在鏈結串列中插入元素主要有三種方式:

(1) 在串列開頭插入

在串列的開頭新增節點。

void insertAtHead(Node** head, int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->next = *head; // 新節點指向原本的頭節點
    *head = newNode;       // 更新頭節點
}

(2) 在串列尾端插入

在串列末尾新增節點。

void insertAtTail(Node** head, int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->next = NULL;

    if (*head == NULL) { // 串列為空
        *head = newNode;
        return;
    }

    Node* temp = *head;
    while (temp->next != NULL) { // 移動到最後一個節點
        temp = temp->next;
    }
    temp->next = newNode; // 新節點接到尾端
}

2. 刪除元素

刪除鏈結串列中的元素。

(1) 刪除開頭元素

void deleteAtHead(Node** head) {
    if (*head == NULL) {
        printf("串列為空。\n");
        return;
    }
    Node* temp = *head;
    *head = (*head)->next;
    free(temp); // 釋放記憶體
}

(2) 刪除特定值的節點

void deleteNode(Node** head, int key) {
    Node* temp = *head;
    Node* prev = NULL;

    if (temp != NULL && temp->data == key) {
        *head = temp->next;
        free(temp);
        return;
    }

    while (temp != NULL && temp->data != key) {
        prev = temp;
        temp = temp->next;
    }

    if (temp == NULL) {
        printf("找不到指定值。\n");
        return;
    }

    prev->next = temp->next;
    free(temp);
}

3. 搜尋元素

搜尋鏈結串列中是否存在指定值。

int search(Node* head, int key) {
    Node* temp = head;
    int position = 0;

    while (temp != NULL) {
        if (temp->data == key) {
            return position; // 回傳位置
        }
        temp = temp->next;
        position++;
    }
    return -1; // 未找到
}

4. 顯示串列內容

void printList(Node* head) {
    Node* temp = head;
    while (temp != NULL) {
        printf("%d -> ", temp->data);
        temp = temp->next;
    }
    printf("NULL\n");
}

小結

  • 插入操作可在開頭、尾端或任意位置新增元素。
  • 刪除操作可移除開頭元素或特定值的節點。
  • 搜尋操作可找到元素位置並回傳。

6. 記憶體管理注意事項

在鏈結串列實作中,記憶體管理至關重要。本節將說明動態記憶體分配避免記憶體洩漏的方法。

1. 什麼是動態記憶體分配?

在 C 語言中,建立鏈結串列節點時通常需要使用 malloc() 動態分配記憶體,確保執行階段可靈活配置所需的空間。

Node* createNode(int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    if (newNode == NULL) {
        printf("記憶體分配失敗。\n");
        exit(1);
    }
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

2. 記憶體釋放的重要性

如果在刪除節點後沒有適時釋放記憶體,就會造成記憶體洩漏

void freeList(Node* head) {
    Node* temp;
    while (head != NULL) {
        temp = head;
        head = head->next;
        free(temp);
    }
}

3. 避免記憶體洩漏的最佳實踐

  1. 務必釋放不再使用的記憶體
  2. 使用偵錯工具(如 Valgrind、AddressSanitizer)檢測洩漏位置
  3. 明確管理指標,使用後將其設為 NULL 防止誤用
valgrind --leak-check=full ./a.out

小結

  • 動態記憶體分配是鏈結串列靈活性的基礎。
  • 忘記釋放記憶體會造成程式不穩定。
  • 善用工具檢查可避免潛在問題。

7. 應用範例與實務活用

本節將介紹鏈結串列在實務上的應用,包括堆疊(Stack)佇列(Queue)的實作,展示其靈活性與實用性。

1. 堆疊實作範例

堆疊是一種後進先出(LIFO)的資料結構,最後加入的元素會最先被取出。

程式碼範例:使用鏈結串列實作堆疊

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

typedef struct Node {
    int data;
    struct Node* next;
} Node;

Node* createNode(int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->next = NULL;
    return newNode;
}

void push(Node** top, int data) {
    Node* newNode = createNode(data);
    newNode->next = *top;
    *top = newNode;
}

int pop(Node** top) {
    if (*top == NULL) {
        printf("堆疊為空。\n");
        exit(1);
    }
    Node* temp = *top;
    int poppedData = temp->data;
    *top = (*top)->next;
    free(temp);
    return poppedData;
}

void printStack(Node* top) {
    while (top != NULL) {
        printf("%d -> ", top->data);
        top = top->next;
    }
    printf("NULL\n");
}

int main() {
    Node* stack = NULL;

    push(&stack, 10);
    push(&stack, 20);
    push(&stack, 30);

    printf("堆疊內容: ");
    printStack(stack);

    printf("彈出值: %d\n", pop(&stack));
    printf("彈出值: %d\n", pop(&stack));

    printf("堆疊內容: ");
    printStack(stack);

    return 0;
}

2. 佇列實作範例

佇列是一種先進先出(FIFO)的資料結構,最先加入的元素會最先被取出。

程式碼範例:使用鏈結串列實作佇列

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

typedef struct Node {
    int data;
    struct Node* next;
} Node;

typedef struct Queue {
    Node* front;
    Node* rear;
} Queue;

Queue* createQueue() {
    Queue* q = (Queue*)malloc(sizeof(Queue));
    q->front = q->rear = NULL;
    return q;
}

void enqueue(Queue* q, int data) {
    Node* newNode = (Node*)malloc(sizeof(Node));
    newNode->data = data;
    newNode->next = NULL;
    if (q->rear == NULL) {
        q->front = q->rear = newNode;
        return;
    }
    q->rear->next = newNode;
    q->rear = newNode;
}

int dequeue(Queue* q) {
    if (q->front == NULL) {
        printf("佇列為空。\n");
        exit(1);
    }
    Node* temp = q->front;
    int data = temp->data;
    q->front = q->front->next;

    if (q->front == NULL) {
        q->rear = NULL;
    }

    free(temp);
    return data;
}

void printQueue(Queue* q) {
    Node* temp = q->front;
    while (temp != NULL) {
        printf("%d -> ", temp->data);
        temp = temp->next;
    }
    printf("NULL\n");
}

int main() {
    Queue* q = createQueue();

    enqueue(q, 1);
    enqueue(q, 2);
    enqueue(q, 3);

    printf("佇列內容: ");
    printQueue(q);

    printf("取出值: %d\n", dequeue(q));
    printf("取出值: %d\n", dequeue(q));

    printf("佇列內容: ");
    printQueue(q);

    return 0;
}

小結

  • 堆疊:適合遞迴運算與表達式求值等 LIFO 場景。
  • 佇列:適合緩衝區、任務排程等 FIFO 場景。

8. 總結

本文詳細介紹了 C 語言中的鏈結串列,從基本概念、實作範例到應用案例,幫助讀者建立完整的理解與實作能力。

1. 重點回顧

鏈結串列基礎

  • 鏈結串列可動態調整大小,靈活性高。
  • 與陣列相比,新增與刪除更高效,但搜尋速度較慢。

實作與操作

  • 示範了節點建立、插入、刪除、搜尋等操作。
  • 強調了正確的記憶體管理以防止記憶體洩漏。

種類與用途

  • 單向鏈結串列:結構簡單,適合輕量處理。
  • 雙向鏈結串列:適合需要前後移動的情境。
  • 循環鏈結串列:適合重複處理與迴圈操作。

應用範例

  • 使用鏈結串列實作堆疊與佇列,展示其靈活應用能力。

2. 學習成果與應用能力

  • 理解資料結構原理
  • 提升 C 語言程式設計能力
  • 增強解決問題的能力

3. 後續學習方向

  1. 資料結構進階
  • 樹狀結構(例如二元樹、AVL 樹)
  • 圖形演算法(BFS、DFS)
  1. 進階資料管理
  • 雜湊表(Hash Table)與映射結構
  • 動態陣列與鏈結串列混合設計
  1. 演算法最佳化
  • 排序與搜尋演算法的效能優化
  • 時間與空間複雜度分析

4. 實務應用建議

  • 任務管理應用程式
  • 模擬系統
  • 資料分析工具

5. 結語

鏈結串列雖屬於基礎資料結構,但應用範圍廣泛,對複雜系統的開發也極具價值。透過本文的內容,相信讀者已能從基礎到進階全面掌握鏈結串列,並靈活運用於實務專案中。