單向循環鏈表的原理與應用
思考:對于單向鏈表而言,想要遍歷鏈表,則必須從鏈表的首結點開始進行遍歷,請問有沒有更簡單的方案實現鏈表中的數據的增刪改查?
回答:是有的,可以使用單向循環的鏈表進行設計,單向循環的鏈表的使用規則和普通的單向鏈表沒有較大的區別,需要注意:單向循環鏈表的尾結點的指針域中必須指向鏈表的首結點的地址,由于帶頭結點的單向循環鏈表更加容易進行管理,所以教學以帶頭結點的為例:
上圖所示的就是一個典型的單向循環鏈表的結構,可以發現單向循環鏈表的結構屬于環形結構,鏈表中的最后一個結點的指針域中存儲的是鏈表的第一個結點的地址。
為了管理單向循環鏈表,需要構造頭結點的數據類型以及構造有效結點的數據類型,如下:
//指的是單向循環鏈表中的結點有效數據類型,用戶可以根據需要進行修改
typedef int DataType_t;//構造單向循環鏈表的結點,鏈表中所有結點的數據類型應該是相同的
typedef struct CircularLinkedList
{DataType_t data; //結點的數據域struct LinkedList *next; //結點的指針域}CircLList_t;
創建一個空鏈表,由于是使用頭結點,所以就需要申請頭結點的堆內存并初始化即可!
//創建一個空單向循環鏈表,空鏈表應該有一個頭結點,對鏈表進行初始化
CircLList_t * CircLList_Create(void)
{//1.創建一個頭結點并對頭結點申請內存CircLList_t *Head = (CircLList_t *)calloc(1,sizeof(CircLList_t));if (NULL == Head){perror("Calloc memory for Head is Failed");exit(-1);}//2.對頭結點進行初始化,頭結點是不存儲數據域,指針域指向自身,體現“循環”思想Head->next = Head;//3.把頭結點的地址返回即可return Head;
}
?
創建新結點,為新結點申請堆內存并對新結點的數據域和指針域進行初始化,操作如下:
//創建新的結點,并對新結點進行初始化(數據域 + 指針域)
CircLList_t * CircLList_NewNode(DataType_t data)
{//1.創建一個新結點并對新結點申請內存CircLList_t *New = (CircLList_t *)calloc(1,sizeof(CircLList_t));if (NULL == New){perror("Calloc memory for NewNode is Failed");return NULL;}//2.對新結點的數據域和指針域進行初始化New->data = data;New->next = NULL;return New;
}
?遍歷整個鏈表
//遍歷鏈表
bool CircLList_Print(CircLList_t *Head)
{//對單向循環鏈表的頭結點的地址進行備份CircLList_t *Phead = Head;//判斷當前鏈表是否為空,為空則直接退出if (Head->next == Head){printf("current linkeflist is empty!\n");return false;}//從首結點開始遍歷while(Phead->next){//把頭結點的直接后繼作為新的頭結點Phead = Phead->next;//輸出頭結點的直接后繼的數據域printf("data = %d\n",Phead->data);//判斷是否到達尾結點,尾結點的next指針是指向首結點的地址if (Phead->next == Head->next){break;} }return true;
}
?根據情況把新結點插入到鏈表中,此時可以分為尾部插入、頭部插入、指定位置插入:
頭插
bool CircLList_HeadInsert(CircLList_t *Head, DataType_t data)
{CircLList_t *Phead = Head; //備份頭結點的地址//1.創建新的結點,并對新結點進行初始化CircLList_t *New = CircLList_NewNode(data);if (NULL == New){printf("can not insert new node\n");return false;}//2.判斷鏈表是否為空,如果為空,則把新結點作為首結點,體現“循環”if (Head->next == Head){Head->next = New;New->next = New;return true;}//3.如果鏈表是非空的,則需要對尾結點的next指針進行處理,指向首結點while(Phead->next){Phead = Phead->next;if ( Phead->next == Head->next ){break;}}Phead->next = New; //尾結點的next指針指向新的首結點New->next = Head->next; //新結點的next指針指向原本的首結點Head->next = New; //更新首結點地址,讓頭結點的next指針指向新結點return true;
}
?尾插
bool CircLList_TailInsert(CircLList_t *Head, DataType_t data)
{CircLList_t *Phead = Head; //備份頭結點的地址//1.創建新的結點,并對新結點進行初始化CircLList_t *New = CircLList_NewNode(data);if (NULL == New){printf("can not insert new node\n");return false;}//2.判斷鏈表是否為空,如果為空,則把新結點作為首結點,體現“循環”if (Head->next == Head){Head->next = New;New->next = New;return true;}//3.如果鏈表是非空的,則需要對尾結點的next指針進行處理,指向首結點while(Phead->next){Phead = Phead->next;if ( Phead->next == Head->next ){break;}}Phead->next = New; //舊的尾結點的next指針指向新結點New->next = Head->next; //新結點的next指針指向首結點地址return true;
}
指定位置后插入
bool CircLList_TailInsert(CircLList_t *Head, DataType_t data)
{CircLList_t *Phead = Head; //備份頭結點的地址//1.創建新的結點,并對新結點進行初始化CircLList_t *New = CircLList_NewNode(data);if (NULL == New){printf("can not insert new node\n");return false;}//2.判斷鏈表是否為空,如果為空,則把新結點作為首結點,體現“循環”if (Head->next == Head){Head->next = New;New->next = New;return true;}//3.如果鏈表是非空的,則需要對指定位置結點的指針進行處理while (Phead->next){if (Phead->data == Data) {New->next = Phead->next;Phead->next = New;return true;}Phead = Phead->nextif(Phead == Head->next)break;Phead = Phead->next;}
}
根據情況可以從鏈表中刪除某結點,此時可以分為尾部刪除、頭部刪除、指定元素刪除:
頭刪
//頭刪
bool LList_HeadDel(LList_t *Head)
{//對單向循環鏈表的頭結點的地址進行備份CircLList_t *Phead = Head;//對單向循環鏈表的首結點的地址進行備份CircLList_t *Temp = Head->next;//2.判斷鏈表是否為空,如果為空,則退出if (Head->next == Head){printf("Linkedlist is Empty\n");return false;}//3.判斷鏈表中是否只有首結點if ( Head->next == Head->next->next ){Temp->next = NULL; //首結點的next指針指向NULLHead->next = Head; //頭結點的next指針指向頭結點,體現“循環”free(Temp); //釋放結點內存return true;} //4.如果鏈表是非空的,則需要對尾結點的next指針進行處理,指向新的首結點while(Phead->next){Phead = Phead->next;if ( Phead->next == Temp ) //遍歷到尾結點{break;}}Phead->next = Temp->next ; //讓尾結點的next指針指向新的首結點Head->next = Phead->next; //更新首結點,讓頭結點的next指針指向新的首結點Temp->next = NULL; //舊的首結點的next指針指向NULL,從鏈表中斷開free(Temp); //釋放待刪除結點的內存return true;
}
尾刪
//頭刪
bool LList_HeadDel(LList_t *Head)
{//對單向循環鏈表的頭結點的地址進行備份CircLList_t *Phead = Head;//對單向循環鏈表的首結點的地址進行備份CircLList_t *Temp = Head->next;//2.判斷鏈表是否為空,如果為空,則退出if (Head->next == Head){printf("Linkedlist is Empty\n");return false;}//3.判斷鏈表中是否只有尾結點if ( Head->next == Head->next->next ){Temp->next = NULL; //首結點的next指針指向NULLHead->next = Head; //頭結點的next指針指向頭結點,體現“循環”free(Temp); //釋放結點內存return true;} //4.如果鏈表是非空的,則需要對尾結點的next指針進行處理,指向新的首結點while(Phead->next){Phead_prv = Phead //保存尾結點直接前驅的地址Phead = Phead->next;if ( Phead->next == Temp ) //遍歷到尾結點{break;}}Phead_prv->next = Temp ; //讓尾結點的直接前驅指向首節點Phead->next = NULL; //尾結點的next指針指向NULL,從鏈表中斷開free(Phead); //釋放待刪除結點的內存return true;
}
指定刪
雙向鏈表的原理與應用
如果想要提高單向鏈表或者單向循環鏈表的訪問速度,則可以在鏈表中的結點中再添加一個指針域,讓新添加的指針域指向當前結點的直接前驅的地址,也就意味著一個結點中有兩個指針域(prev + next),也被稱為雙向鏈表(Double Linked List)。?
由于帶頭結點更加方便用戶進行數據訪問,所以本次創建一條帶頭結點的雙向不循環的鏈表。
//指的是雙向鏈表中的結點有效數據類型,用戶可以根據需要進行修改
typedef int DataType_t;//構造雙向鏈表的結點,鏈表中所有結點的數據類型應該是相同的
typedef struct DoubleLinkedList
{DataType_t data; //結點的數據域struct DoubleLinkedList *prev; //直接前驅的指針域struct DoubleLinkedList *next; //直接后繼的指針域}DoubleLList_t;
創建一個空鏈表,由于是使用頭結點,所以就需要申請頭結點的堆內存并初始化即可!
//創建一個空雙向鏈表,空鏈表應該有一個頭結點,對鏈表進行初始化
DoubleLList_t * DoubleLList_Create(void)
{//1.創建一個頭結點并對頭結點申請內存DoubleLList_t *Head = (DoubleLList_t *)calloc(1,sizeof(DoubleLList_t));if (NULL == Head){perror("Calloc memory for Head is Failed");exit(-1);}//2.對頭結點進行初始化,頭結點是不存儲數據域,指針域指向NULLHead->prev = NULL;Head->next = NULL;//3.把頭結點的地址返回即可return Head;
}
創建新結點,為新結點申請堆內存并對新結點的數據域和指針域進行初始化,操作如下:
DoubleLList_t * DoubleLList_NewNode(DataType_t data)
{//1.創建一個新結點并對新結點申請內存DoubleLList_t *New = (DoubleLList_t *)calloc(1,sizeof(DoubleLList_t));if (NULL == New){perror("Calloc memory for NewNode is Failed");return NULL;}//2.對新結點的數據域和指針域(2個)進行初始化New->data = data;New->prev = NULL;New->next = NULL;return New;
}
根據情況可以從鏈表中插入新結點,此時可以分為尾部插入、頭部插入、指定位置插入:
頭插
//頭插
bool DoubleList_HeadInsert(DoubleList_t * Head, DataType_t data)
{//1.創建新結點并對新結點進行初始化DoubleList_t * New = DoubleList_NewNode(data);if (NULL == New){printf("can not insert new node\n");return false;}//2.判斷雙向鏈表是否為空,如果為空,則直接插入到頭結點之后if (NULL == Head->next){Head->next = New; //讓頭結點的next指針指向新結點return true;}//3.如果雙向鏈表為非空,則把新結點插入到鏈表的頭部New->next = Head->next; //新結點的next指針指向原本的首結點地址Head->next->prev = New; //原本的首結點的prev指針指向新結點的地址Head->next = New; //更新頭結點的next指針,讓next指針指向新結點的地址return true;
}
尾插
//尾插
bool DoubleList_TailInsert(DoubleList_t * Head, DataType_t data)
{DoubleList_t * Phead = Head; //備份頭結點地址,防止頭結點丟失//1.創建新結點并對新結點進行初始化DoubleList_t * New = DoubleList_NewNode(data);if (NULL == New){printf("can not insert new node\n");return false;}//2.判斷雙向鏈表是否為空,如果為空,則直接插入到頭結點之后if (NULL == Head->next){Head->next = New; //讓頭結點的next指針指向新結點return true;}//3.如果雙向鏈表為非空,則把新結點插入到鏈表的尾部while(Phead->next){Phead = Phead->next;}Phead->next = New; //尾結點的next指針指向新結點地址New->prev = Phead; //新結點的prev指針指向原本的尾結點地址return true;
}
指定插
根據情況可以從鏈表中刪除某結點,此時可以分為尾部刪除、頭部刪除、指定結點刪除:
//指定位置插入 插入目標結點之后
bool DoubleList_DestInsert(DoubleList_t *Head, DataType_t destval, DataType_t data)
{DoubleList_t * Phead = Head; //備份頭結點地址,防止頭結點丟失//1.創建新結點并對新結點進行初始化DoubleList_t * New = DoubleList_NewNode(data);if (NULL == New){printf("can not insert new node\n");return false;}//2.判斷雙向鏈表是否為空,如果為空,則直接插入到頭結點之后if (NULL == Head->next){Head->next = New; //讓頭結點的next指針指向新結點return true;}//3.如果雙向鏈表為非空,此時分為3種情況(尾部 or 中間)while(Phead->next){Phead = Phead->next;if (Phead->data == destval){break;}}//如果遍歷鏈表之后發現沒有目標結點,則退出即可if (Phead->next == NULL && Phead->data != destval){printf("dest node is not found\n");return false;}//如果遍歷鏈表找到目標結點,則分為(尾部 or 中間)if(Phead->next == NULL) //尾插{New->prev = Phead; //新結點的prev指針指向尾結點的地址Phead->next = New; //尾結點的next指針指向新結點}else //中間{New->next = Phead->next; // 新結點的next指針指向目標結點的直接后繼結點New->prev = Phead; // 新結點的prev指針指向目標結點的地址Phead->next->prev = New; // 目標結點的直接后繼結點的prev指針指向新結點Phead->next = New; // 目標結點的next指針指向新結點}return true;
}
頭刪
//頭刪
bool DoubleList_HeadDel(DoubleList_t *Head)
{//對雙向鏈表的首結點的地址進行備份DoubleList_t *Temp = Head->next;//2.判斷鏈表是否為空,如果為空,則退出if (Head->next == NULL){printf("DoubleList is Empty\n");return false;}//3.判斷鏈表中是否只有首結點if (Temp->next != NULL) // 如果鏈表不止一個節點{ Temp->next->prev = NULL; // 新首節點的prev置為NULL}//4.如果鏈表是非空的,則需要對頭結點的next指針進行處理,指向新的首結點Head->next = Temp->next; //頭結點的next指針進行處理,指向新的首結點Temp->next = NULL; //舊的首節點next指向NULLfree(Temp); //釋放待刪除結點的內存return true;
}
?如果沒有 if (Thead->next != NULL) 這個條件判斷,直接寫 Thead->next->prev = NULL ,會產生以下嚴重后果:當只有一個結點時, Thead->next 的值為 NULL 。因為尾節點的 next 指針指向 NULL ,表示鏈表結束。如果直接執行Thead->next->prev = NULL ,就相當于對一個空指針進行解引用操作,去訪問它的成員 prev 。在 C 語言中,這是不允許的,會導致程序崩潰,拋出類似 “Segmentation fault(段錯誤)” 這樣的運行時錯誤。?
尾刪
// 尾刪
bool DoubleList_TailDel(DoubleList_t *Head)
{// 備份頭結點的地址DoubleList_t *Phead = Head;// 判斷鏈表是否為空,如果為空則退出if (Head->next == NULL){printf("DoubleList is Empty\n");return false;}// 遍歷到尾結點while (Phead->next != NULL){Phead = Phead->next;}// 獲取尾結點的前驅結點DoubleList_t *Prev = Phead->prev;// 如果存在前驅結點(即鏈表不止一個節點)if (Prev != NULL){Prev->next = NULL; // 前驅結點的next指向NULL}else{Head->next = NULL; // 如果無前驅,說明刪除后鏈表為空}// 釋放尾結點內存free(Phead);return true;
}
指定刪
//指定刪除
bool DoubleList_DestDel(DoubleList_t * Head, DataType_t destval)
{DoubleList_t * Phead = Head; //備份頭結點地址,防止頭結點丟失//1.判斷雙向鏈表是否為空,如果為空,則刪除失敗if (NULL == Head->next){printf("linked list is empty\n");return false;}//2.如果雙向鏈表為非空,此時遍歷鏈表查找沒有目標結點while(Phead->next){Phead = Phead->next;if (Phead->data == destval){break;}}//如果鏈表中沒有目標結點,此時直接退出即可if (Phead->next == NULL && Phead->data != destval){printf("dest node is not found\n");return false;}//如果鏈表中發現目標結點,此時分為(頭部 or 尾部 or 中間)if(Phead == Head->next) //頭部{Head->next = Phead->next; //更新頭結點,讓頭結點的next指針指向首結點的直接后繼if (Phead->next != NULL) //如果待刪除節點的下一個節點存在,則將其 prev 置為 NULL,確保新頭節點的前驅正確。{Phead->next->prev = NULL;}Phead->next = NULL;free(Phead); //釋放待刪除結點內存}else if(Phead->next == NULL) //尾部{Phead->prev->next = NULL; //尾結點的直接前驅結點的next指針指向NULLPhead->prev = NULL; //尾結點的prev指針指向NULLfree(Phead); //釋放待刪除結點內存}else //中間{Phead->prev->next = Phead->next; //讓待刪除結點的直接前驅結點的next指針指向待刪除結點的直接后繼Phead->next->prev = Phead->prev; //讓待刪除結點的直接后繼結點的prev指針指向待刪除結點的直接前驅地址Phead->next = NULL; //讓待刪除結點的next指針指向NULLPhead->prev = NULL; //讓待刪除結點的prev指針指向NULLfree(Phead); //釋放待刪除結點內存}return true;
}
練習題
?
A :雙鏈表在插入和刪除時,需要同時調整前驅和后繼指針,操作并不比單鏈表簡單,所以 A 選項錯誤。
B:單鏈表和雙鏈表都不支持隨機訪問
C:雙鏈表無論是表頭指針還是表尾指針都很重要,都不能省略,因為雙鏈表的遍歷、插入和刪除等操作往往需要借助表頭指針或表尾指針來進行定位起始位置等操作\
D:單鏈表中每個結點只有一個指向后繼的指針,若要訪問前驅結點,只能從頭開始遍歷鏈表。而雙鏈表的每個結點既有指向后繼的指針,又有指向前驅的指針 ,可以方便靈活地訪問前后相鄰結點,所以 D 選項正確。
A:p->next=q;
?這一步執行后,p
?原來的后繼結點就丟失了,后續再執行?q->next=p->next;
?無法正確設置?q
?的后繼
B:操作順序符合上述在雙向鏈表中?p
?結點后插入?q
?結點的步驟,要記住插入動作要先連接在斷開
C:同 A
D:同 A
A:p->prior=q;
?這一步直接改變?p
?的前驅,后續?p->prior->next=q;
?操作時,由于?p
?的前驅已經改變,會導致邏輯錯誤
B:p->prior=q->next;
?這里邏輯混亂,q->next
?指向的是?p
?,將其賦給?p->prior
?不符合在?p
?前插入?q
?的指針調整邏輯
C:q->next=p; p->next=q;
?這兩步操作錯誤,在雙向鏈表中在?p
?前插入?q
?,不應該改變?p
?的后繼,且后續指針調整邏輯也混亂
D:對
A:對
B:p->prior = p->prior->prior;
?這一步將?p
?的前驅指向了?p
?前驅的前驅,邏輯混亂;p->prior->prior = p;
?也不符合雙向鏈表刪除結點的指針調整規則,會導致鏈表結構錯誤
C:p->next->prior = p;
?這一步錯誤地將?p
?后繼的前驅又指向了?p
?,沒有達到刪除?p
?的目的;p->next = p->next->next;
?也不能正確調整指針
D:p->next = p->prior->prior;
?和?p->prior = p->prior->prior;
?這兩步操作不符合雙向鏈表刪除結點時調整指針的邏輯,會使鏈表結構出錯
A:順序表:它可以通過數組下標直接隨機存取任一指定序號的元素,時間復雜度為O(1)?。在順序表最后進行插入和刪除運算時,若不涉及擴容等操作 ,插入和刪除的時間復雜度也較低,插入平均時間復雜度為 O(1)(不考慮擴容) ,刪除平均時間復雜度為 O(1)(不考慮元素移動后的整理等額外操作),所以順序表能滿足最常用操作的高效需求。
B:雙鏈表:雙鏈表不支持隨機存取,要存取指定序號的元素,需要從表頭或表尾開始遍歷,時間復雜度為?O(n)
C:雙循環鏈表:和雙鏈表類似,不支持隨機存取,存取指定序號元素需遍歷,時間復雜度為?O(n)?
D:單循環鏈表:同樣不支持隨機存取,存取指定序號元素要遍歷鏈表,時間復雜度為 O(n)
A:單鏈表:在單鏈表中,在最后一個元素之后插入元素,需要遍歷整個鏈表找到尾節點,時間復雜度為?O(n)?;刪除第一個元素,雖然時間復雜度為?O(1),但插入操作效率低,不滿足最常用操作高效的要求。
B:不帶頭結點的單循環鏈表:插入最后一個元素需要遍歷鏈表找尾節點,時間復雜度?O(n)?;刪除第一個元素時,因為沒有頭結點輔助,還需要先找到尾節點使其?next
?指向第二個節點,操作相對復雜,時間復雜度也較高,整體不滿足要求。
C:雙鏈表:在雙鏈表中,刪除第一個元素時間復雜度為?O(1),但在最后一個元素之后插入元素,需要遍歷鏈表找到尾節點(雖然可以從表頭或表尾雙向遍歷,但仍需一定時間),時間復雜度為?O(n)?,不是最節省時間的。
D:不帶頭結點且有尾指針的單循環鏈表:有尾指針,在最后一個元素之后插入元素時,可直接利用尾指針,時間復雜度為?O(1);刪除第一個元素時,可通過尾指針找到第一個元素(尾指針的?next
?指向第一個元素),然后調整指針完成刪除,時間復雜度也為?O(1)?,能高效滿足最常用的兩種操作,最節省運算時間。
A:在順序表中,由于元素是連續存儲的,可通過數組下標直接訪問第i個元素,時間復雜度為O(1)。
B:效率一樣
C:效率一樣
???????D:效率一樣