【資料結構】用C語言實現單連結串列及其常見操作

codels發表於2024-03-30

【資料結構】用C語言實現單連結串列及其常見操作

連結串列是一種常用的基礎資料結構,可以快速插入和刪除資料,但是不能隨機訪問。

那麼它在記憶體中是怎麼儲存的呢?它和陣列不同,陣列在記憶體中是連續儲存的,而連結串列不一定是連續的,它們之間是透過指標來連線的。

指標 是C語言中最重要的特性之一。那麼,什麼是指標?說白了就是資料在記憶體中存放的地址,可以理解為資料在記憶體中住的哪一棟,幾零幾。

連結串列中的每個元素都有資料域和指標域,資料域用於存放資料,指標域用於存放指向下一個元素的指標,即下一個元素在記憶體中的位置。

在連結串列中,指向第一個元素的指標,稱為頭指標。

連結串列的結束標記是空指標 NULL,當我們遍歷表時發現當前指標為 NULL,那就說明,這裡是連結串列的結尾。

好,接下來,我們來一步一步地實現連結串列。

結構

連結串列的資料結構如下:

typedef int SLDataType;
typedef struct LinkedList {
    SLDataType data;
    struct LinkedList * next;
}SL;

其中 struct LinkedList * next 這個套娃語句可能有點糊塗人。這就是上文中提到的指標域,如你所見,它指向了和自己相同的資料型別,請看下圖。

圖中的每一個小框框就是連結串列中的一個元素(常被叫結點,也有叫節點的),然後我們發現每個小框框裡面都有兩個部分,一個是 data,一個是 next,這就是資料域和指標域。它們對應我們在結構體中定義的成員變數。

圖中的每個 next 都指向了下一個元素,末尾是 NULL,這樣是不是就比較清楚了?

然後我們來看看這個 typedeftypedef 是C語言的關鍵字,用於給資料型別起別名,其語法如下:

typedef <資料型別> <別名>

例如:

typedef int mydata;

這樣操作下來就可以用mydata來代替int了。

mydata a = 1;int a = 1; 效果相同。

初始化

現在我們來把一個連結串列初始化一下,把它的頭指標置為空。

void SLInit(SL ** pphead)
{
    *pphead = NULL;
}

這個 ** 是什麼呢?這個叫做二級指標,是指向一級指標的指標,這麼說有點抽象,來看一個例項:

int a = 114514;
int *p = &a;
int ** pp = &p;

printf("a = %d, p = %p, pp = %p\n", a, p , pp);
printf("&a = %p, p = %p, &p = %p, pp = %p\n", &a, p, &p, pp);
a = 114514, p = 00000000005ffe84, pp = 00000000005ffe78
&a = 00000000005ffe84, p = 00000000005ffe84, &p = 00000000005ffe78, pp = 00000000005ffe78

其中 p 是指向整型變數 a 的一級指標,pp 是指向指標變數 p 的二級指標。

輸出的結果是多少不重要,重要的是我們發現,&a == p&p = pp,用文字描述一下大致就是:變數 p 中存放著變數 a 的地址,而變數 pp 存放著變數 p 的地址。這也就意味著 *pp == p

接下來說一下不用二級指標傳遞引數會怎麼樣:

void TestSLInit(SL * phead)
{
    phead = NULL;
}

表面上看起來沒有問題,可是實際上,當我呼叫 TestSLInit()函式的時候是這樣的:

呼叫:

TestSLInit(plist);

此時函式內部:

void TestSLInit(SL * phead)
{
    //等價於phead = plist; phead = NULL;
    phead = NULL;
}

由於C語言預設是使用 “值傳遞” 的。也就是說傳入引數的時候,C語言會在函式內部建立一個臨時變數來接收這個引數,既然是臨時變數,那麼它的作用域就只能在函式內部。

也就是上述的 phead 在程式執行完 TestSLInit()函式後就被銷燬了,所以,該函式並沒有對我的引數 plist 做出任何改變。我們來透過列印直觀感受一下。

void Test1()
{
    SL * plist;
    printf("plist = %p\n", plist);      //列印一個隨機值
    TestSLInit(plist);
    printf("plist = %p\n", plist);      //值不會變
}

輸出結果:

plist = 000001f928431350
plist = 000001f928431350

可以看出,此時的TestSLInit()函式確實沒有直到任何作用。

我們來換上 SLInit() 函式試試:

void Test1()
{
    SL * plist;
    printf("plist = %p\n", plist);      //列印一個隨機值
    SLInit(&plist);                     //注意,這裡要傳遞plist的指標
    printf("plist = %p\n", plist);      //成功置空
}

輸出:

plist = 000001418f2d1350
plist = 0000000000000000

此時,我們成功地把連結串列置空了。

尾插

尾插,顧名思義就是從連結串列的尾部插入資料,所以要在插入之前找到尾結點,然後再把元素接在尾結點的後面。(尾結點就是指標域指向空的那個結點。)

這裡我們需要分類討論,當連結串列沒有元素的時候,即 plist == NULL 此時我們要給它分配一個結點,當連結串列沒有元素的時候,我們透過遍歷找到它的尾結點,然後將要插入的結點接在後面。

那麼問題來了,怎麼分配結點呢?怎麼找到尾結點呢?

下面是分配結點的函式:

透過引數 x 來分配一個資料域為 x 指標域為 NULL 的結點。再透過 newNode 把新結點返回。

SL * SLBuyCapacity(SLDataType x)
{
    SL * newNode = (SL *)malloc(sizeof(SL));        //給新結點分配空間
    
    if (newNode == NULL)                            //判斷是否分配成功
    {
        printf("Malloc Failed!\n");
        exit(-1);
    }
    else
    {
        newNode->data = x;                         //給結點的資料域賦值
        newNode->next = NULL;                      //給結點的指標域賦值
    }

    return newNode;                                //返回該結點
}

尾插實現

void SLPushBack(SL ** pphead, SLDataType x)
{
    //沒有節點的情況
    if (*pphead == NULL)
    {
        *pphead = SLBuyCapacity(x);     //將頭指標指向當前分配的新結點
    }
    //其他情況
    else
    {
        SL * tail = *pphead;            //透過tail遍歷連結串列,找到尾結點
        SL * newNode = SLBuyCapacity(x);//分配一個新結點
        while (tail->next != NULL)      //遍歷
        {
            tail = tail->next;          //使tail指向下一個結點
        }
        tail->next = newNode;           //把新分配的結點接在表尾
    }
}

未完,待更新……

相關文章