一、連結串列的原理與應用
對於順序表的資料增加和刪除是比較麻煩,因為都需要移動一片連續的記憶體。
順序表的優點是:由於順序表資料元素的記憶體地址都是連續的,所以可以實現隨機訪問,而且不需要多餘的資訊來描述相關的資料,所以儲存密度高。
順序表的缺點是:順序表的資料在進行增刪的時候,需要移動成片的記憶體,另外,當資料元素的數量較多的時候,需要申請一塊較大的連續的記憶體,同時當資料元素的數量的改變比較劇烈,順序表不靈活。
思考:既然順序表實現資料的增加和刪除比較麻煩,又佔用連續記憶體,請問有沒有更好方案?
回答:是有的,可以利用鏈式儲存的線性表實現,鏈式儲存指的是採用離散的記憶體單元來儲存資料元素,使用者需要使用某種方式把所有的資料元素連線起來,這樣就可以變為鏈式線性表,簡稱為連結串列,連結串列可以高效的使用碎片化記憶體。
可以看到:
順序表和鏈式表的區別:順序表使用連續的記憶體,鏈式表使用離散的記憶體空間。 |
思考:既然連結串列中的每個資料元素的地址都是不固定的,請問使用者如何訪問某個元素呢???
回答:由於連結串列中的每個資料元素的地址是不固定的,所以每個資料元素都應該使用一個指標指向直接後繼的記憶體地址,當然最後一個資料元素沒有直接後繼,所以最後一個資料元素指向NULL即可,作為使用者只需要知道第一個資料元素的記憶體地址,就可以訪問後繼元素了。
注意:如果採用鏈式儲存,則線性表中每一個資料元素除了儲存自身資料之外,還需要額外儲存直接後繼的地址,所以連結串列中的每一個資料元素都是由兩部分組成:儲存自身資料的部分被稱為資料域,儲存直接後繼地址的部分被稱為指標域,資料域和指標域組成的資料元素被稱為結點(Node)。
注意:連結串列的工作原理其實很簡單,只要大家搞清楚連結串列的使用流程就可以很輕鬆的理解,連結串列具體的操作步驟如下所示:
根據連結串列的結點的指標域的數量以及根據連結串列的首尾是否相連,把鏈式線性表分為以下幾種:單向連結串列、單向迴圈連結串列、雙向連結串列、雙向迴圈連結串列、核心連結串列。這幾種連結串列的使用規則差不多,只不過指標域數量不同。
上圖就是最簡單的單向連結串列的內部結構,可以看到每一個結點都儲存了一個地址,每個地址都是邏輯上相鄰的下一個結點的地址,只不過末尾結點的指標指向NULL。
另外注意:可以看到連結串列中是有一個頭指標的,頭指標只指向第一個元素的地址,想要訪問連結串列中的某個元素只需要透過頭指標即可。
思考:使用順序表的時候需要建立一個管理結構體來管理順序表,請問連結串列需不需要建立???
回答:可以根據使用者的需要來選擇,一般把連結串列分為兩種:一種是不帶頭結點的連結串列,一種是帶頭結點的連結串列,頭結點指的是管理結構體,只不過頭結點只儲存第一個元素的記憶體地址,頭結點並不儲存有效資料,頭結點的意義只是為了方便管理連結串列。
(1)不帶頭結點的連結串列
(2)附帶頭結點的連結串列
可以知道,頭指標是必須的,因為透過頭指標才可以訪問連結串列的元素, 頭結點是可選的,只是為了方便管理連結串列而已。
注意:在連結串列中,還有兩個專業名稱,一個是首結點,一個是尾結點,三者之前的區別如下:
A.頭結點:是不儲存有效資料的,只儲存第一個資料元素的地址,頭指標只指向頭結點。
B.首結點:是儲存有效資料的,也儲存直接後繼的記憶體地址,首結點就是第一個結點,首 結點是唯一一個只指向別的結點,不被別的結點指向的結點。
C.尾結點:是儲存有效資料的,尾結點就是連結串列的最後一個結點,所以尾結點中儲存的地 址一般指向NULL,尾結點是唯一一個只被別的結點指向,不能指向別的結點 的結點。
為了方便管理單向連結串列,所以需要構造 頭結點的資料型別以及構造有效結點的資料型別,如下:
(1)建立一個空連結串列,由於是使用頭結點,所以就需要申請頭結點的堆記憶體並初始化即可。
(2)建立一個新結點,併為新結點申請堆記憶體以及對新結點的資料域和指標域進行初始化。
(3)根據情況把新結點插入到連結串列中,此時可以分為尾部插入、頭部插入、指定位置插入。
(4)根據情況可以從連結串列中刪除某結點,此時可以分為尾部刪除、頭部刪除、指定元素刪除。
程式碼
/**
* @file name : 單連結串列
* @brief : 單連結串列的初始化、插入、刪除、修改、查詢列印
* @author : yfm3262@163.com
* @date : 2024/11/07
* @version : 1.1
* CopyRight (c) 2023-2024 yfm3262@163.com All Right Reseverd
*/
單連結串列公式
/*
單連結串列表總結成公式
struct xxx
{
//資料域(需要存放什麼型別的資料,你就定義對應的變數即可)
//指標域(存放下一個資料在記憶體中的地址)
};
單連結串列的基本操作:
初始化單連結串列 √
插入資料 √
刪除資料 √
修改資料
查詢列印資料
*/
初始化單連結串列
構建單連結串列結點
// DataType_t指的是單向連結串列中的結點有效資料型別,使用者可以根據需要進行修改
typedef int DataType_t;
typedef struct LinkedList
{
DataType_t data; // 結點的資料域
struct LinkedList *next; // 結點的指標域, 存放下一個結點的地址
} LList_t;
建立一個空連結串列(僅頭結點)
/**
* @name :LList_Create
* @brief :建立一個空連結串列,空連結串列應該有一個頭結點,對連結串列進行初始化
* @param :
* @retval :頭結點地址
* @date :2024年11月07日
* @version :1.0
* @note :
*/
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;
}
建立一個新結點
/**
* @name :LList_NewNode
* @brief :建立新的結點,並對新結點進行初始化(資料域 + 指標域)
* @param :
@data :要建立結點的元素
* @retval :NULL 申請堆記憶體失敗 ; New 新結點地址
* @date :2024/11/07
* @version :1.0
* @note :
*/
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;
}
插入資料
頭插
/**
* @name :LList_HeadInsert
* @brief :在單連結串列的頭結點後插入
* @param :
@Head :頭指標
@data :要建立結點的元素
* @retval :true 插入成功; false 插入失敗 / 申請記憶體失敗
* @date :2024/11/07
* @version :1.0
* @note :
*/
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;
}
中插
/**
* @name LList_DestInsert
* @brief 單連結串列中的指定元素後面插入新結點
* @param Head 頭指標
* @param dest 要查詢的結點
* @param data 要插入的資料
* @return 程式執行成功與否
* @retval false 插入失敗
* @retval true 插入成功
* @date 2024/11/07
* @version 1.0
* @note
*/
bool LList_DestInsert(LList_t *Head, DataType_t dest, DataType_t data)
{
// 操作指標指向首節點
LList_t *Phead = Head->next;
// 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.遍歷連結串列,目的是找到目標結點,比較結點的資料域
while (Phead != NULL && dest != Phead->data)
{
Phead = Phead->next;
}
if (NULL == Phead) // 若到了尾節點還未找到
{
return false;
}
// 4.說明找到目標結點,則把新結點加入到目標的後面
New->next = Phead->next;
Phead->next = New;
return true;
}
尾插
/**
* @name LList_TailInsert
* @brief 將新元素插入到尾結點後面
* @param Head 頭指標
* @param data 新元素
* @return 程式執行成功與否
* @retval false 插入失敗
* @retval true 插入成功
* @date 2024/11/07
* @version 1.0
* @note
*/
bool LList_TailInsert(LList_t *Head, DataType_t data)
{
// 複製頭指標
LList_t *Phead = Head;
// 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.如果連結串列為非空,則把新結點插入到連結串列的尾部
while (Phead->next)
{
Phead = Phead->next;
}
Phead->next = New;
return true;
}
刪除資料
頭刪
/**
* @name LList_HeadDel
* @brief 刪除頭結點後面的一個結點
* @param Head 頭指標
* @return 程式執行成功與否
* @retval false 刪除失敗
* @retval true 刪除成功
* @date 2024/11/07
* @version 1.0
* @note
*/
bool LList_HeadDel(LList_t *Head)
{
// 建立操作指標
LList_t *Phead = Head;
// 2.判斷連結串列是否為空,如果為空,則直接退出
if (NULL == Head->next)
{
return false;
}
// 3.連結串列是非空的,則直接刪除首結點
Head->next = Phead->next->next;
Phead->next->next = NULL;
free(Phead->next);
return true;
}
中刪
/**
* @name LList_DestDel
* @brief 中刪, 刪除某個元素結點
* @param Head 頭指標
* @param dest 要刪除的目標元素
* @return 程式執行成功與否
* @retval false 刪除失敗, 未找到目標元素結點
* @retval true 刪除成功
* @date 2024/11/07
* @version 1.0
* @note
*/
bool LList_DestDel(LList_t *Head, DataType_t dest)
{
// 操作指標, 指向當前結點的前一個結點, 初始化 指向頭指標
LList_t *prev = Head;
// 當前操作指標, 指向當前結點, 用於移動, 初始指向頭結點
LList_t *current = Head->next;
while (current != NULL) // 若連結串列不為空
{
if (current->data == dest) // 若找到, 此時 prev-->[dest結點直接前驅] ,cur-->[dest]
{
prev->next = current->next; // 連結刪除節點的直接前驅和直接後繼
current->next = NULL; // 防止記憶體洩漏
free(current); // 釋放記憶體
return true;
}
prev = current; // 若未找到, 兩個操作指標後移一個結點
current = current->next;
}
return false; // 沒有找到元素
}
尾刪
/**
* @name LList_TailDel
* @brief 刪除尾結點
* @param Head 頭指標
* @return 程式執行成功與否
* @retval false 刪除失敗, 連結串列為空
* @retval true 刪除成功
* @date 2024/11/07
* @version 1.0
* @note
*/
bool LList_TailDel(LList_t *Head)
{
// 檢查連結串列是否為空或只有頭結點
if (Head == NULL || Head->next == NULL)
{
return false;
}
LList_t *current = Head; // current 當前操作指標, 指向當前結點, 用於遍歷連結串列
LList_t *prev = NULL; // prev 用於記錄 current 的前一個結點
// 遍歷連結串列直到最後一個結點
while (current->next != NULL)
{
prev = current; // 若未找到, 兩個操作指標後移一個結點
current = current->next;
}
// 刪除尾結點
if (prev != NULL) // 到達尾結點的直接前驅
{
prev->next = NULL; // 直接前驅指標域為NULL
}
free(current); // 釋放尾結點的記憶體, 防止記憶體洩漏
return true;
}
刪除最小值結點
/**
* @name LList_DelMin
* @brief 刪除單連結串列中最小值結點
* @param Head 頭指標
* @return 無
* @date 2024/11/07
* @version 1.0
* @note
*/
void LList_DelMin(LList_t *Head)
{
LList_t *min_prev; // 記錄最小值結點的直接前驅地址
LList_t *min = Head->next; // 記錄最小值結點的地址
LList_t *phead = Head->next; // 記錄當前結點的地址
LList_t *phead_prev = Head; // 記錄當前結點的直接前驅地址
// 1.遍歷連結串列,目的是找到最小值結點
while (phead->next)
{
// 比較連結串列中結點的資料域的大小
if (min->data > phead->next->data)
{
min = phead->next;
min_prev = phead;
}
// 如果發現當前結點資料域不大於當前結點的直接後繼,則向後遍歷
phead_prev = phead;
phead = phead->next;
}
// 2.刪除當前的最小值結點,前提是讓最小值結點的直接前驅指向最小值結
min_prev->next = min->next;
// 3.釋放最小值結點的記憶體
min->next = NULL;
free(min);
}
其它
#include
#include
#include
// 主函式,用於演示
int main(int argc, char *argv[]) {
return 0;
}
題目:
/**
* @name LList_DelMin
* @brief 刪除單連結串列中最小值結點
* @param Head 頭指標
* @return 無
* @date 2024/11/07
* @version 1.0
* @note
*/
void LList_DelMin(LList_t *Head)
{
LList_t *min_prev; // 記錄最小值結點的直接前驅地址
LList_t *min = Head->next; // 記錄最小值結點的地址
LList_t *phead = Head->next; // 記錄當前結點的地址
LList_t *phead_prev = Head; // 記錄當前結點的直接前驅地址
// 1.遍歷連結串列,目的是找到最小值結點
while (phead->next)
{
// 比較連結串列中結點的資料域的大小
if (min->data > phead->next->data)
{
min = phead->next;
min_prev = phead;
}
// 如果發現當前結點資料域不大於當前結點的直接後繼,則向後遍歷
phead_prev = phead;
phead = phead->next;
}
// 2.刪除當前的最小值結點,前提是讓最小值結點的直接前驅指向最小值結
min_prev->next = min->next;
// 3.釋放最小值結點的記憶體
min->next = NULL;
free(min);
}