上一階段的字符串系列之后很長時間都沒有更新文章,現在接著來,本階段是鏈表系列的,鏈表系列的算法題我不會再用大量篇幅寫一個問題的解決方案,針對一個問題寫一個解決方案,所以篇幅會小很多。這篇文章會介紹和鏈表相關的簡單算法題,后面還會介紹復雜算法題。
- 添加/刪除結點
- 從尾到頭打印鏈表
- 翻轉單鏈表
- 在O(1)時間內刪除鏈表結點
1.添加/刪除結點
我們都知道,鏈表的添加和刪除操作相比數組是很方便的,因為鏈表不要求結點的物理順序和邏輯順序相同,所以添加刪除結點的時候不需要像數組一樣移動大量的結點,借助指針,修改指針指向,我們就可以很方便的實現添加和刪除結點的操作。
添加/刪除結點是鏈表類算法題中比較簡單的操作了,不會有太大問題。主要是在注意細節,添加/刪除的時候要注意判斷鏈表是否為空,如果鏈表為空,就要修改頭指針的指向,也就是修改頭指針指向的地址。
代碼
鏈表的數據結構的定義:
注意:這些聲明中包含了一個自引用結構的列子。在定義結構ListNode之前,已經定義了指向該結構的指針。C語言允許定義指向尚不存在的類型的指針。
typedef struct ListNode *list_pointer;
typedef struct ListNode
{
int value;
list_pointer link;
};
list_pointer pHead;
在鏈表尾添加節點
有在前端,中間插入結點的,也有在尾部,這里的例子是在鏈表尾插入結點。注意,如果鏈表為空,那么添加結點需要修改頭指針的指向,所以這里接收的參數是頭指針的地址。
//pHead是指向指針的指針 ListNode** p
void addToTail(list_pointer *pHead, int value){
list_pointer node = (list_pointer)malloc(sizeof(ListNode));
if (node == NULL)
{
fprintf(stderr, "Faile\n");
exit(1);
}
node->value = value;
node->link = NULL;
if (*pHead == NULL)
{
*pHead = node;
}
else{
list_pointer p = *pHead;
while (p->link != NULL){
p = p->link;
}
p->link = node;
}
}
刪除中間結點:
刪除指定值的結點,我們不知道該結點在什么位置,所以需要遍歷鏈表找到結點。
//如果刪除首節點,那么需要改變首節點指針的指向
bool deleteNode(list_pointer *pHead, int value){
if (*pHead == NULL)
{
fprintf(stderr, "The linklist is empty!\n");
exit(1);
}
ListNode *node = NULL;
if ((*pHead)->value == value){//刪除首節點
node = *pHead;
*pHead = (*pHead)->link;
free(node);
return true;
}
else{
list_pointer p = *pHead;
while (p->link != NULL && p->link->value != value){
p = p->link;
}
if (p->link != NULL && p->link->value == value)
{
node = p->link;
p->link = p->link->link;
free(node);
}
}
}
2.從尾到頭打印鏈表
看到這到道題的第一反應是從頭到尾打印鏈表會比較簡單,所以我們可以改變鏈表的指針指向,但是這樣會改變鏈表原來的結構,是否允許改變鏈表的結構,這個取決于面試官。這里的例子是不改變鏈表結構。
算法思想
從尾到頭打印鏈表也就是說先存入的元素后輸出,后存入的先輸出,和棧“后進先出”的思想一樣,所以我們可以在遍歷鏈表的時候先將元素存入棧中,再循環遍歷棧輸出元素。
想到了使用棧,而遞歸的本質就是使用棧存儲,那么我們使用遞歸也能實現同樣的效果,下面的例子就是用遞歸實現的。
代碼:
void PrintListReversingly(list_pointer pHead) {
list_pointer p = pHead;
if (p) {
if (p->link) {
PrintListReversingly(p->link);
}
printf("鏈表元素:%d\n", p->value);
}
}
使用遞歸代碼會簡潔很多,但是當鏈表非常長的時候,函數調用的層級會很深,可能會導致函數調用棧溢出,顯式的使用棧基于循環來遍歷的魯棒性會好一些。
3.翻轉單鏈表
題目:寫一個函數,輸入鏈表的頭結點,翻轉該鏈表,并返回翻轉后的鏈表的頭結點。
算法思想
看上面的圖,如果是對結點i翻轉鏈表,就是改變i的link的指向,將它指向前一個結點h,但是這樣會導致i指向j的鏈丟失,所以我們需要存儲下這個值,也就是說,在一次改變指針指向的操作中,我們需要存儲下前一個結點h,后一個結點j,和當前結點i。此外,我們還需要明確,在翻轉了鏈表之后,原來的頭結點變成了尾結點,而現在的尾節點呢,就是NULL。分析完這些之后很容易寫出代碼。
代碼
//翻轉鏈表
list_pointer Invert(list_pointer pHead)
{
list_pointer middle, trail;
middle = NULL;
//當pHead指向原鏈表的最后一個結點的link時,退出循環
//此時middle剛好指向原鏈表的最后一個結點
while (pHead) {
trail = middle;
middle = pHead;//此時middle和pHead指向的地址相同
pHead = pHead->link;
middle->link = trail;
}
return middle;
}
4.在O(1)時間內刪除鏈表結點
常規的思維,刪除鏈表結點需要知道它的前一個結點,簡單的三句代碼,就是判斷p->link->value == value,然后修改p->link = p->link->link,釋放p->link.這樣就需要遍歷鏈表,知道要刪除的結點的前一個,時間復雜度為O(n)。如果換一種思路呢?
算法思想
假設結點h,i,j是鏈表中相鄰的3個結點,現在要刪除i,可以進行下面3步:
- 將結點j的值復制到結點i上;
- 修改i的link;
- 釋放結點j。
現在需要考慮一下特殊情況下是否也滿足。當刪除的節點是頭結點時,需要修改頭結點的link指針。當刪除的是尾結點時,它沒有下一個結點,所以需要遍歷鏈表,得到它的前一個結點。當鏈表只有一個結點時,刪除的結點是頭結點(尾節點),需要將頭指針的指向置為NULL。
代碼
//在O(1)內刪除一個結點
void DeleteNode(list_pointer *pHead, ListNode *node)
{
if (!(*pHead) || !node)
return;
//要刪除的不是尾結點
if (node->link)
{
ListNode *pNext = node->link;
node->value = pNext->value;
node->link = pNext->link;
free(pNext);
}
//鏈表中只有一個結點刪除頭結點,也是尾結點
else if (*pHead == node)//node->link為NULL
{
free(node);
*pHead = NULL;
}
else//鏈表中有多個結點,刪除的是尾結點
{
list_pointer p = *pHead;
while (p->link != node)
{
p = p->link;
}
p->link = NULL;
free(node);
}
}
總結
這些題難度都不大,使用指針很靈活,但是也很容易出錯,主要是關注細節。這篇就到這里了,不足之處,還請多指教~