宣告: 本文以軒哥大佬所講述筆記為基礎並結合多方資料所寫
2024年4月22日 更新中
- 連結串列的原理與應用
- 順序表的優點:
- 順序表的缺點:
- 鏈式儲存的線性表
- 順序表和鏈式表的區別:
- 使用者如何訪問連結串列中的某個元素?
- 連結串列的使用流程
- 連結串列分類
- (1) 不帶頭結點的連結串列
- (2) 附帶頭結點的連結串列
- 連結串列結點
- 構造頭結點的資料型別以及構造有效結點
- 構造連結串列的結點
- 建立一個空連結串列(僅頭結點)
- 建立一個新結點
- 將新結點插入連結串列
- 頭插法
- 指定位置插入
連結串列的原理與應用
順序表的優點:
- 由於順序表資料元素的記憶體地址都是連續的,所以可以實現隨機訪問,而且不需要多餘的資訊來描述相關的資料,所以儲存密度高。
順序表的缺點:
- 順序表的資料在進行增刪的時候,需要移動成片的記憶體,另外,當資料元素的數量較多的時候,需要申請一塊較大的連續的記憶體,同時當資料元素的數量的改變比較劇烈,順序表不靈活。
鏈式儲存的線性表
鏈式儲存指的是採用離散的記憶體單元來儲存資料元素,使用者需要使用某種方式把所有的資料元素連線起來,這樣就可以變為鏈式線性表,簡稱為連結串列,連結串列可以高效的使用碎片化記憶體。
順序表和鏈式表的區別:
順序表使用連續的記憶體,鏈式表使用離散的記憶體空間。
使用者如何訪問連結串列中的某個元素?
由於連結串列中的每個資料元素的地址是不固定的,所以每個資料元素都應該使用一個指標指向直接後繼的記憶體地址,當然最後一個資料元素沒有直接後繼,所以最後一個資料元素指向NULL即可,作為使用者只需要知道第一個資料元素的記憶體地址,就可以訪問後繼元素了。
注意:如果採用鏈式儲存,則線性表中每一個資料元素除了儲存自身資料之外,還需要額外儲存直接後繼的地址,所以連結串列中的每一個資料元素都是由兩部分組成:儲存自身資料的部分被稱為資料域,儲存直接後繼地址的部分被稱為指標域,資料域和指標域組成的資料元素被稱為結點(Node)。
連結串列的使用流程
連結串列分類
根據連結串列的結點的指標域的數量以及根據連結串列的首尾是否相連,把鏈式線性表分為以下幾種:單向連結串列、單向迴圈連結串列、雙向連結串列、雙向迴圈連結串列、核心連結串列。這幾種連結串列的使用規則差不多,只不過指標域數量不同。
上圖就是最簡單的單向連結串列的內部結構,可以看到每一個結點都儲存了一個地址,每個地址都是邏輯上相鄰的下一個結點的地址,只不過末尾結點的指標指向NULL。
另外注意:可以看到連結串列中是有一個頭指標的,頭指標只指向第一個元素的地址,想要訪問連結串列中的某個元素只需要透過頭指標即可。
思考:使用順序表的時候需要建立一個管理結構體來管理順序表,請問連結串列需不需要建立???
回答:可以根據使用者的需要來選擇,一般把連結串列分為兩種:一種是不帶頭結點的連結串列,一種是帶頭結點的連結串列,頭結點指的是管理結構體,只不過頭結點只儲存第一個元素的記憶體地址,頭結點並不儲存有效資料,頭結點的意義只是為了方便管理連結串列。
(1) 不帶頭結點的連結串列
(2) 附帶頭結點的連結串列
可以知道,頭指標是必須的,因為透過頭指標才可以訪問連結串列的元素,頭結點是可選的,只是為了方便管理連結串列而已。
連結串列結點
注意:在連結串列中,還有兩個專業名稱,一個是首結點,一個是尾結點,三者之前的區別如下:
A. 頭結點:是不儲存有效資料的,只儲存第一個資料元素的地址,頭指標只指向頭結點。
B. 首結點:是儲存有效資料的,也儲存直接後繼的記憶體地址,首結點就是第一個結點,首結點是唯一一個只指向別的結點,不被別的結點指向的結點。
C. 尾結點:是儲存有效資料的,尾結點就是連結串列的最後一個結點,所以尾結點中儲存的地址一般指向NULL,尾結點是唯一一個只被別的結點指向,不能指向別的結點的結點。
構造頭結點的資料型別以及構造有效結點
為了方便管理單向連結串列,所以需要構造頭結點的資料型別以及構造有效結點的資料型別,如下:
構造連結串列的結點
//DataType_t指的是單向連結串列中的結點有效資料型別,使用者可以根據需要進行修改
typedef int DataType_t;
//構造連結串列的結點,連結串列中所有結點的資料型別應該是相同的
typedef struct LinkedList
{
DataType_t data; //結點的資料域
struct LinkedList *next; //結點的指標域, 存放下一個結點的地址
}LList_t;
建立一個空連結串列(僅頭結點)
(1) 建立一個空連結串列,由於是使用頭結點,所以就需要申請頭結點的堆記憶體並初始化即可。
//建立一個空連結串列,空連結串列應該有一個頭結點,對連結串列進行初始化
LList_t * LList_Create(void)
{
//1.建立一個頭結點並對頭結點申請記憶體, 只申請一個節點大小, calloc會初始化為0
LList_t *Head = (LList_t *)calloc(1,sizeof(LList_t));
//錯誤處理
if (NULL == Head)
{
perror("Calloc memory for Head is Failed");
exit(-1);
}
//2.對頭結點進行初始化,頭結點是不儲存有效內容的!!!
Head->next = NULL;
//3.把頭結點的地址返回即可
return Head;
}
建立一個新結點
(2) 建立一個新結點,併為新結點申請堆記憶體以及對新結點的資料域和指標域進行初始化。
//建立新的結點,並對新結點進行初始化(資料域 + 指標域)
LList_t * LList_NewNode(DataType_t data)
{
//1.建立一個新結點並對新結點申請記憶體
LList_t *New = (LList_t *)calloc(1,sizeof(LList_t));
if (NULL == New)
{
perror("Calloc memory for NewNode is Failed");
return NULL;
}
//2.對新結點的資料域和指標域進行初始化
New->data = data;
New->next = NULL;
return New;
}
將新結點插入連結串列
一定要先連結再 增加/刪除
(3) 根據情況把新結點插入到連結串列中,此時可以分為尾部插入、頭部插入、指定位置插入。
頭插法
/**
* @function name: LList_HeadInsert
* @brief 單連結串列中的頭插法
* @param @Head:頭指標
@data:要插入的資料
* @retval true插入成功 false插入失敗, 申請記憶體失敗
*/
bool LList_HeadInsert(LList_t *Head,DataType_t data)
{
//1.建立新的結點,並對新結點進行初始化
LList_t *New = LList_NewNode(data);
if (NULL == New)
{
printf("can not insert new node\n");
return false;
}
//2.判斷連結串列是否為空,如果為空,則直接插入即可
if (NULL == Head->next)
{
Head->next = New;
return true;
}
//3.如果連結串列為非空,則把新結點插入到連結串列的頭部
New->next = Head->next;
Head->next = New;
return true;
}
指定位置插入
/**
* @function name: LList_DestInsert
* @brief 單連結串列中的指定元素後插入
* @param @Head: 頭指標
@dest: 要查詢的結點
@data: 要插入的資料
* @retval true插入成功 false插入失敗, 申請記憶體失敗
*/
bool LList_DestInsert(LList_t *Head,DataType_t dest,DataType_t data)
{
//對連結串列的標頭檔案的地址進行備份
LList_t *Phead = Head;
//1.遍歷, 先找到目標結點
while(NULL != Phead && dest != Phead->data)//查詢data值為dest的結點
{
Phead = Phead->next;//把頭的直接後繼作為新的頭結點
}
if (NULL == Phead) //不存在元素為dest的結點, 返回false
return false;
//2.若目標結點存在, 建立新的結點,並對新結點進行初始化
LList_t *New = LList_NewNode(data);
if (NULL == New)
{
printf("can not insert new node\n");
return false;
}
New->data = data;
//3.把新結點插入到目標結點的後面
New->next = Phead->next;//新結點先連結後面的結點
Phead->next = New;//讓目標元素結點連結新結點
return true;
}
(4) 根據情況可以從連結串列中刪除某結點,此時可以分為尾部刪除、頭部刪除、指定元素刪除。