數據結構之單鏈表的實現


數據結構之單鏈表的實現

 

  在上一節 :數據結構之順序表

  我們提到了順序表的一些缺陷,那有沒有什么數據結構可以減少這些問題呢?

  答案自然就是今天我們所要說的鏈表。

 

本節大綱:

  • 鏈表的概念與結構
  • 單鏈表的實現
  • 完整代碼展示

 

 

一.鏈表的概念與結構

 

  1.概念

  鏈表是一種物理存儲結構上非連續、非順序的存儲結構,數據元素的邏輯順序是通過鏈表中的指針鏈接次序實現的 。

   2.結構及種類

  鏈表有好多種:單雙向鏈表,帶頭不帶頭鏈表,循環非循環鏈表 ;它們兩兩組合可以組成 8 種鏈表結構,如下圖所示:

  鏈表又包括 數據域 和 指針域

 

 

 

 

 

 

  雖然有這么多的鏈表的結構,但是我們實際中最常用還是兩種結構:

 

 

   1. 無頭單向非循環鏈表:

    結構簡單,一般不會單獨用來存數據。實際中更多是作為其他數據結構的子結構,如哈希桶、圖的鄰接表等等。

    另外這種結構在筆試面試中出現很多。

  2. 帶頭雙向循環鏈表:

    結構最復雜,一般用在單獨存儲數據。實際中使用的鏈表數據結構,都是帶頭雙向循環鏈表。

    另外這個結構雖然結構復雜,但是使用代碼實現以后會發現結構會帶來很多優勢,實現反而簡單了。

 

  所以我們今天從 無頭單向非循環鏈表 開始:

  

三.單鏈表的實現

   1.自定義數據類型

  首先我們肯定要先創建一個自定義數據類型:

//進行自定義類型,方便修改
typedef int DataType;

//進行自定義數據類型
typedef struct SLTNode
{
    DataType data;//存放數據
    struct SLTNode* next;//指向下一塊空間
}SLTNode;

 

   2.打印函數

  假設我們現在有一個數據完全的單鏈表,但是我們想在看到它的值,這時就需要一個打印函數了。

  現在我們來想一想我們剛剛所提到的單鏈表結構,它是由數據域和指針域組合而成,而它中的指針指向的是下一塊儲存空間,如果該指針為NULL,就說明該鏈表已結束。

//單鏈表打印函數
void SLTPrint(SLTNode *p)
{
    if (p == NULL)//如果該鏈表為空
    {
        printf("該鏈表中並無值可供打印\n");
        return;//直接退出
    }
    //正常情況下
    
    //cur指向當前位置
    SLTNode *cur = p;
    while (cur)//如果cur是NULL,循環就停止
    {
        printf("%d -> ", cur->data);
        cur = cur->next;//cur指向下一塊空間
    }
    printf("NULL\n");
}

  在這會有一些人問 :while 中的判斷條件是否可以寫為 cur->next ,我們來看一下:

 

   從上圖中,我們可以明顯看出:若循環條件為  cur->next ,這樣循環到達最后一個節點就退出了,最后一個節點內的值並未打印,當然要是你在循環結束后,再加一句printf,那自然也沒錯

   

   3.創建節點函數:

  因后面的頭插、尾插、任意位置插入,都需要去創建節點,所以在這就把它單獨列一個模塊:

//創建節點函數
SLTNode *BuySLTNode(DataType data)
{
    //動態開辟一塊空間
    SLTNode *new_node = (SLTNode *) malloc(sizeof(SLTNode));
    if (new_node == NULL)//如果未開辟成功
    {
        perror("BuyNode Error!");//打印錯誤信息
        exit(-1);
    }
    else
    {
        //新結點的值為傳進來的值
        new_node->data = data;
        new_node->next = NULL;
    }
    //返回新建立的節點
    return new_node;
}

 

   4.單鏈表尾插

  尾插,我們肯定要先找到該鏈表的尾,然后將尾的next指向我們新建立的結點,將新節點的next指向NULL;

//單鏈表尾插函數
void SLTPushBack(SLTNode *p, DataType InsertData)
{
    //創建一個新節點
    SLTNode *new_node = BuySLTNode(InsertData);
    if (p == NULL)//說明該鏈表為空
    {
        p = new_node;
    }
    else//非空就找尾
    {
        SLTNode *cur = p;
        while (cur->next)//循環遍歷找尾,注意在這,我們的循環條件為cur->next,而非cur
            // 因為我們要用最后一個節點的next
            // 否則,若是cur的話,就直接走到NULL了,我們無法給將一個節點加在它上面
        {
            cur = cur->next;//指向下一個
        }
        cur->next = new_node;//尾插
    }
}

  可是,上面這段代碼有沒有問題呢? 我們來運行一下:

void test1()
{
    SLTNode *p = NULL;
    SLTPushBack(p,0);
    SLTPushBack(p,1);
    SLTPushBack(p,2);
    SLTPushBack(p,3);
    SLTPrint(p);
}
測試代碼

 

   我們明明尾插了4次,最后為什么還是無值可供打印呢? 我們再來調試一下:

 

   我們發現,它在函數里都換過了,卻出了函數又變為了NULL。我們思來想去總覺得這種情形好像在哪見過 --- 哦,原來曾在這個例子中見過:

#include<stdio.h>

void swap(int a, int b)
{
    int c = a;
    a = b;
    b = c;
}

int main()
{
    int a = 2, b = 3;
    printf("before:\na=%d, b=%d\n", a, b);
    swap(a, b);
    printf("after:\na=%d, b=%d\n", a, b);
    return 0;
}

 

   當時,我們便已提到,改變其值時,要傳值,但要改變其指向,要傳址;

  在舉一個例子:

void change(char *p)
{
    p[0] = '!';
}

void change_2(char **p)
{
    static char *a = "12334";
    *p = a;
}

int main()
{
    char a[] = "dadasfsfa";
    char *p = a;

    //改變指針所指向的值
    change(p);
    printf("%s\n", p);//!adasfsfa

    //改變指針的指向
    change_2(&p);
    printf("%s\n", p);//12334
    return 0;
}
示例

  所以正確的尾插怎么寫呢:

    1.函數參數為STLNode** ,即改為址傳遞,因為當 p為空的時候,我們需要改變其指向,指向我們新建的new_node

//單鏈表尾插函數
void SLTPushBack(SLTNode **p, DataType InsertData)
{
    //創建一個新節點
    SLTNode *new_node = BuySLTNode(InsertData);
    if (*p == NULL)//說明該鏈表為空
    {
        *p = new_node;//改變p的指向
    }
    else//非空就找尾
    {
        SLTNode *cur = *p;
        while (cur->next)//循環遍歷找尾,注意在這,我們的循環條件為cur->next,而非cur
            // 因為我們要用最后一個節點的next
            // 否則,若是cur的話,就直接走到NULL了,我們無法給將一個節點加在它上面
        {
            cur = cur->next;//指向下一個
        }
        cur->next = new_node;//尾插
    }
}

    5.單鏈表頭插

  現在有了之前的錯誤經驗,是不是一下就知道這個肯定是傳址啊:

//單鏈表頭插函數
void SLTPushFront(SLTNode **p, DataType InsertData)
{
    SLTNode *new_node = BuySLTNode(InsertData);//創建新節點

    //在頭插中,並不關心*p為不為空,反正最后它們的處理都是一樣的

    //將原鏈表內容連接在new_node上
    new_node->next = *p;
    //改變指向
    *p = new_node;
}

   6.單鏈表尾刪

  循環遍歷找尾,釋放,置NULL

//單鏈表尾刪函數
void SLTPopBack(SLTNode **p)
{
    if (*p == NULL)//表示該鏈表為空
    {
        printf("該鏈表中並無值可供刪除!\n");
        return;
    }
    else if ((*p)->next == NULL)//表示該鏈表中只有一個值,刪除之后就為空
    {
        free(*p);//釋放空間
        *p = NULL;//置為NULL;
    }
    else
    {
        //循環遍歷找尾
        SLTNode *cur = *p;//指向當前位置
        SLTNode *prev = NULL;//指向前一個位置
        while (cur->next)
        {
            prev = cur;
            cur = cur->next;
        }
        free(cur);//釋放空間
        cur = NULL;//及時置NULL
        //刪除之后prev便是最后一個節點了
        prev->next = NULL;
    }
}

   7.單鏈表頭刪

  釋放改指向

//單鏈表頭刪函數
void SLTPopFront(SLTNode **p)
{
    if (*p == NULL)//表示該鏈表為空
    {
        printf("該鏈表中並無值可供刪除!\n");
        return;
    }
    else
    {
        SLTNode* pop_node=*p;//指向頭節點
        *p=(*p)->next;//指向下一個元素
        free(pop_node);//釋放空間
        pop_node=NULL;//及時置NULL
    }
}

   8.單鏈表查找函數

  循環遍歷

//單鏈表查找函數
SLTNode *SLTFindNode(SLTNode *p, DataType FindData)
{
    assert(p);//確保傳進來的指針不為空
    SLTNode *cur = p;
    while (cur)
    {
        if (cur->data == FindData)
        {
            return cur;//找到就返回該節點的地址
        }
        cur = cur->next;//循環遍歷
    }
    return NULL;//找不到就返回NULL
}

   9.單鏈表修改函數

//單鏈表修改函數
void SLTModify(SLTNode *pos, DataType ModifyData)
{
    assert(pos);//確保pos不為空
    pos->data = ModifyData;//修改
}

   10.任意位置插入

  1.單鏈表在pos位置之后插入insertdata

//單鏈表在pos位置之后插入InsertData
void SLTInsertAfter(SLTNode* pos,DataType InsertData)
{
    assert(pos);//保證pos不為NULL
    SLTNode* new_node=BuySLTNode(InsertData);//創建一個新節點
    //該節點的下一個指向原本該位置的下一個
    new_node->next=pos->next;
    //該位置的下一個就是new_node
    pos->next=new_node;
}

  2.單鏈表在pos位置之前插入insertdata

    這時就要考慮到若位置是第一個位置時,那就相當於頭插了

    此外,我們還得找到它的前一個位置

 

//單鏈表在pos位置之前插入InsertData
void SLTInsertBefore(SLTNode **p, SLTNode *pos, DataType InsertData)
{
    assert(pos);
    SLTNode *new_node = BuySLTNode(InsertData);
    if (pos == *p)//這就說明是第一個位置前插入
    {
        //頭插
        new_node->next = *p;
        *p = new_node;
    }
    else
    {
        SLTNode *cur = *p;
        SLTNode *prev = NULL;
        //新節點的下一個指向這個位置
        new_node->next = pos;
        //循環遍歷找到它的前一個位置
        while (cur != pos)
        {
            prev = cur;
            cur = cur->next;
        }
        //前一個位置指向新節點
        prev->next = new_node;
    }
}

    除此之外,還有哦另外一種方法,可以不用去知道它鏈表的第一個位置

    我們在傳過去的那個位置,來進行一個后插,再交換這兩個節點的數據 ,這也是一種方法:

//單鏈表在pos位置之前插入InsertData
void SLTInsertBefore_2(SLTNode *pos, DataType InsertData)
{
    assert(pos);

    SLTNode *new_node = BuySLTNode(InsertData);
    new_node->next = pos->next;
    pos->next = new_node;
    //進行了一個后插

    //交換這兩個變量的值
    DataType tmp = pos->data;
    pos->data = new_node->data;
    new_node->data = tmp;
}

  如圖:

 

    11.任意位置刪除

   1.刪除pos位后的數據

//刪除pos位后的數據
void SLTEraseAfter(SLTNode *pos)
{
    assert(pos);
    if (pos->next == NULL)
    {
        //說明后面並沒有元素了
        printf("該位置后無數據可以刪除\n");
    }
    else
    {
        //創建一個指針指向下一個位置
        SLTNode *next = pos->next;
        //原位置的下一個位置,指向 下一個位置 的下一個位置
        pos->next = next->next;
        //釋放內存
        free(next);
        next = NULL;
    }
}

    2.刪除pos位的數據 --- 這就包含了頭刪

//刪除pos位的數據 --- 含有頭刪
void SLTEraseCur(SLTNode **p, SLTNode *pos)
{
    assert(pos);
    if (*p == pos)
    {
        //說明是頭刪
        *p = pos->next;
        free(pos);
        pos = NULL;
    }
    else
    {
        //普通位置
        SLTNode *prev = *p;
        SLTNode *cur = *p;
        //找到頭一個位置
        while (cur != pos)
        {
            prev = cur;
            cur = cur->next;
        }
        //頭一個位置指向下下個位置
        prev->next = pos->next;
        free(pos);
        pos = NULL;
    }
}

    3.同樣可以用之前所提到的方法進行帶有頭刪的任意位置刪除

      1.把該位置 后一個節點的數據存下;

      2.刪除該位置后的節點

      3.將頭節點的值改為之前存下的數據

  

   單鏈表的實現到這便就結束了

 

三.完整代碼展示

//確保頭文件只包含一次
#ifndef FINASLT_SLT_H
#define FINASLT_SLT_H

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <assert.h>


//進行自定義類型,方便修改
typedef int DataType;

//進行自定義數據類型
typedef struct SLTNode
{
    DataType data;//存放數據
    struct SLTNode *next;//指向下一塊空間
} SLTNode;

//單鏈表打印函數
void SLTPrint(SLTNode *p);

//創建節點函數
SLTNode *BuySLTNode(DataType data);

//單鏈表尾插函數
void SLTPushBack(SLTNode **p, DataType InsertData);

//單鏈表頭插函數
void SLTPushFront(SLTNode **p, DataType InsertData);

//單鏈表尾刪函數
void SLTPopBack(SLTNode **p);

//單鏈表頭刪函數
void SLTPopFront(SLTNode **p);

//單鏈表查找函數
SLTNode *SLTFindNode(SLTNode *p, DataType FindData);

//單鏈表修改函數
void SLTModify(SLTNode *pos, DataType ModifyData);

//單鏈表在pos位置之后插入InsertData
void SLTInsertAfter(SLTNode *pos, DataType InsertData);

//單鏈表在pos位置之前插入InsertData
void SLTInsertBefore(SLTNode **p, SLTNode *pos, DataType InsertData);


//單鏈表在pos位置之前插入InsertData
void SLTInsertBefore_2( SLTNode *pos, DataType InsertData);

//刪除pos位后的數據
void SLTEraseAfter(SLTNode *pos);

//刪除pos位的數據 --- 含有頭刪
void SLTEraseCur(SLTNode **p, SLTNode *pos);

#endif //FINASLT_SLT_H
SLT.h
#include "SLT.h"

//單鏈表打印函數
void SLTPrint(SLTNode *p)
{
    if (p == NULL)//如果該鏈表為空
    {
        printf("該鏈表中並無值可供打印\n");
        return;//直接退出
    }
    //正常情況下

    //cur指向當前位置
    SLTNode *cur = p;
    while (cur)//如果cur是NULL,循環就停止
    {
        printf("%d -> ", cur->data);
        cur = cur->next;//cur指向下一塊空間
    }
    printf("NULL\n");
}

//創建節點函數
SLTNode *BuySLTNode(DataType data)
{
    //動態開辟一塊空間
    SLTNode *new_node = (SLTNode *) malloc(sizeof(SLTNode));
    if (new_node == NULL)//如果未開辟成功
    {
        perror("BuyNode Error!");//打印錯誤信息
        exit(-1);
    }
    else
    {
        //新結點的值為傳進來的值
        new_node->data = data;
        new_node->next = NULL;
    }
    //返回新建立的節點
    return new_node;
}

////單鏈表尾插函數
//void SLTPushBack(SLTNode *p, DataType InsertData)
//{
//    //創建一個新節點
//    SLTNode *new_node = BuySLTNode(InsertData);
//    if (p == NULL)//說明該鏈表為空
//    {
//        p = new_node;
//    }
//    else//非空就找尾
//    {
//        SLTNode *cur = p;
//        while (cur->next)//循環遍歷找尾,注意在這,我們的循環條件為cur->next,而非cur
//            // 因為我們要用最后一個節點的next
//            // 否則,若是cur的話,就直接走到NULL了,我們無法給將一個節點加在它上面
//        {
//            cur = cur->next;//指向下一個
//        }
//        cur->next = new_node;//尾插
//    }
//}

//單鏈表尾插函數
void SLTPushBack(SLTNode **p, DataType InsertData)
{
    //創建一個新節點
    SLTNode *new_node = BuySLTNode(InsertData);
    if (*p == NULL)//說明該鏈表為空
    {
        *p = new_node;//改變p的指向
    }
    else//非空就找尾
    {
        SLTNode *cur = *p;
        while (cur->next)//循環遍歷找尾,注意在這,我們的循環條件為cur->next,而非cur
            // 因為我們要用最后一個節點的next
            // 否則,若是cur的話,就直接走到NULL了,我們無法給將一個節點加在它上面
        {
            cur = cur->next;//指向下一個
        }
        cur->next = new_node;//尾插
    }
}

//單鏈表頭插函數
void SLTPushFront(SLTNode **p, DataType InsertData)
{
    SLTNode *new_node = BuySLTNode(InsertData);//創建新節點

    //在頭插中,並不關心*p為不為空,反正最后它們的處理都是一樣的

    //將原鏈表內容連接在new_node上
    new_node->next = *p;
    //改變指向
    *p = new_node;
}

//單鏈表尾刪函數
void SLTPopBack(SLTNode **p)
{
    if (*p == NULL)//表示該鏈表為空
    {
        printf("該鏈表中並無值可供刪除!\n");
        return;
    }
    else if ((*p)->next == NULL)//表示該鏈表中只有一個值,刪除之后就為空
    {
        free(*p);//釋放空間
        *p = NULL;//置為NULL;
    }
    else
    {
        //循環遍歷找尾
        SLTNode *cur = *p;//指向當前位置
        SLTNode *prev = NULL;//指向前一個位置
        while (cur->next)
        {
            prev = cur;
            cur = cur->next;
        }
        free(cur);//釋放空間
        cur = NULL;//及時置NULL
        //刪除之后prev便是最后一個節點了
        prev->next = NULL;
    }
}

//單鏈表頭刪函數
void SLTPopFront(SLTNode **p)
{
    if (*p == NULL)//表示該鏈表為空
    {
        printf("該鏈表中並無值可供刪除!\n");
        return;
    }
    else
    {
        SLTNode *pop_node = *p;//指向頭節點
        *p = (*p)->next;//指向下一個元素
        free(pop_node);//釋放空間
        pop_node = NULL;//及時置NULL
    }
}

//單鏈表查找函數
SLTNode *SLTFindNode(SLTNode *p, DataType FindData)
{
    assert(p);//確保傳進來的指針不為空
    SLTNode *cur = p;
    while (cur)
    {
        if (cur->data == FindData)
        {
            return cur;//找到就返回該節點的地址
        }
        cur = cur->next;//循環遍歷
    }
    return NULL;//找不到就返回NULL
}

//單鏈表修改函數
void SLTModify(SLTNode *pos, DataType ModifyData)
{
    assert(pos);//確保pos不為空
    pos->data = ModifyData;//修改
}

//單鏈表在pos位置之后插入InsertData
void SLTInsertAfter(SLTNode *pos, DataType InsertData)
{
    assert(pos);//保證pos不為NULL
    SLTNode *new_node = BuySLTNode(InsertData);//創建一個新節點
    //該節點的下一個指向原本該位置的下一個
    new_node->next = pos->next;
    //該位置的下一個就是new_node
    pos->next = new_node;
}

//單鏈表在pos位置之前插入InsertData
void SLTInsertBefore(SLTNode **p, SLTNode *pos, DataType InsertData)
{
    assert(pos);
    SLTNode *new_node = BuySLTNode(InsertData);
    if (pos == *p)//這就說明是第一個位置前插入
    {
        //頭插
        new_node->next = *p;
        *p = new_node;
    }
    else
    {
        SLTNode *cur = *p;
        SLTNode *prev = NULL;
        //新節點的下一個指向這個位置
        new_node->next = pos;
        //循環遍歷找到它的前一個位置
        while (cur != pos)
        {
            prev = cur;
            cur = cur->next;
        }
        //前一個位置指向新節點
        prev->next = new_node;
    }
}

//單鏈表在pos位置之前插入InsertData
void SLTInsertBefore_2(SLTNode *pos, DataType InsertData)
{
    assert(pos);

    SLTNode *new_node = BuySLTNode(InsertData);
    new_node->next = pos->next;
    pos->next = new_node;
    //進行了一個后插

    //交換這兩個變量的值
    DataType tmp = pos->data;
    pos->data = new_node->data;
    new_node->data = tmp;
}

//刪除pos位后的數據
void SLTEraseAfter(SLTNode *pos)
{
    assert(pos);
    if (pos->next == NULL)
    {
        //說明后面並沒有元素了
        printf("該位置后無數據可以刪除\n");
    }
    else
    {
        //創建一個指針指向下一個位置
        SLTNode *next = pos->next;
        //原位置的下一個位置,指向 下一個位置 的下一個位置
        pos->next = next->next;
        //釋放內存
        free(next);
        next = NULL;
    }
}

//刪除pos位的數據 --- 含有頭刪
void SLTEraseCur(SLTNode **p, SLTNode *pos)
{
    assert(pos);
    if (*p == pos)
    {
        //說明是頭刪
        *p = pos->next;
        free(pos);
        pos = NULL;
    }
    else
    {
        //普通位置
        SLTNode *prev = *p;
        SLTNode *cur = *p;
        //找到頭一個位置
        while (cur != pos)
        {
            prev = cur;
            cur = cur->next;
        }
        //頭一個位置指向下下個位置
        prev->next = pos->next;
        free(pos);
        pos = NULL;
    }

}
SLT.c
#include "SLT.h"

void test1()
{
    SLTNode *p = NULL;
    SLTPushBack(&p, 0);
    SLTPushBack(&p, 1);
    SLTPushBack(&p, 2);
    SLTPrint(p);

    SLTPushFront(&p, 0);
    SLTPushFront(&p, 1);
    SLTPushFront(&p, 2);
    SLTPrint(p);

    SLTPopBack(&p);
    SLTPrint(p);

    SLTPopFront(&p);
    SLTPrint(p);

    SLTNode *pos = SLTFindNode(p, 1);
    if (pos == NULL)
    {
        printf("該鏈表中無所查找的值!\n");
    }
    else
    {
        SLTModify(pos,123);
    }
    SLTPrint(p);

    SLTInsertAfter(pos,321);
    SLTPrint(p);

    SLTInsertBefore(&p,pos,000);
    SLTPrint(p);

    SLTInsertBefore_2(SLTFindNode(p,1),666);

    SLTPrint(p);

    SLTEraseAfter(SLTFindNode(p,1));
    SLTEraseAfter(SLTFindNode(p,666));
    SLTEraseCur(&p,SLTFindNode(p,0));
    SLTEraseCur(&p,SLTFindNode(p,123));
    SLTEraseCur(&p,SLTFindNode(p,321));
    SLTEraseCur(&p,SLTFindNode(p,0));
    SLTEraseCur(&p,SLTFindNode(p,0));
    SLTEraseCur(&p,SLTFindNode(p,666));
    SLTPrint(p);

}

int main()
{
    setbuf(stdout, NULL);
    test1();
    return 0;
}
main.c

 

 

 


|--------------------------------------------------------

關於單鏈表的知識到這便結束了

因筆者水平有限,若有錯誤,還望指正!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM