【資料結構】用C語言實現單連結串列及其常見操作
連結串列是一種常用的基礎資料結構,可以快速插入和刪除資料,但是不能隨機訪問。
那麼它在記憶體中是怎麼儲存的呢?它和陣列不同,陣列在記憶體中是連續儲存的,而連結串列不一定是連續的,它們之間是透過指標
來連線的。
指標
是C語言中最重要的特性之一。那麼,什麼是指標?說白了就是資料在記憶體中存放的地址
,可以理解為資料在記憶體中住的哪一棟,幾零幾。
連結串列中的每個元素都有資料域和指標域,資料域用於存放資料,指標域用於存放指向下一個元素的指標
,即下一個元素在記憶體中的位置。
在連結串列中,指向第一個元素的指標,稱為頭指標。
連結串列的結束標記是空指標 NULL
,當我們遍歷表時發現當前指標為 NULL
,那就說明,這裡是連結串列的結尾。
好,接下來,我們來一步一步地實現連結串列。
結構
連結串列的資料結構如下:
typedef int SLDataType;
typedef struct LinkedList {
SLDataType data;
struct LinkedList * next;
}SL;
其中 struct LinkedList * next
這個套娃語句可能有點糊塗人。這就是上文中提到的指標域,如你所見,它指向了和自己相同的資料型別,請看下圖。
圖中的每一個小框框就是連結串列中的一個元素(常被叫結點,也有叫節點的),然後我們發現每個小框框裡面都有兩個部分,一個是 data
,一個是 next
,這就是資料域和指標域。它們對應我們在結構體中定義的成員變數。
圖中的每個 next
都指向了下一個元素,末尾是 NULL
,這樣是不是就比較清楚了?
然後我們來看看這個 typedef
。 typedef
是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; //把新分配的結點接在表尾
}
}
未完,待更新……