鏈表,也稱線性表的鏈式存儲結構,其元素在內存空間上不是連續(xù)的,通過指針鏈接來決定元素的次序。
鏈表的結構多種多樣,以下的情況組合起來就有8種鏈表結構。
1.單向或者雙向
雖然有這么多種結構,但是我們最常用的只有兩種:
1.無頭單向非循環(huán)鏈表:結構簡單,一般不會單獨用來存儲數(shù)據(jù)。實際上更多是作為其他數(shù)據(jù)結構的子結構,如哈希桶,圖的鄰接表等。
2.帶頭雙向循環(huán)鏈表:結構最復雜,但實現(xiàn)起來比較簡單。
typedef int SLTDataType;
typedef struct SListNode
{SLTDataType data;
struct SListNode* next;
}SLTNode;
//動態(tài)申請一個節(jié)點
SLTNode* BuySListNode(SLTDataType x);
//單鏈表打印
void SLTPrint(SLTNode* phead);
//單鏈表尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//單鏈表尾刪
void SLTPopBack(SLTNode** pphead);
//單鏈表頭插
void SLTPushFount(SLTNode** pphead, SLTDataType x);
//單鏈表頭刪
void SLTPopFount(SLTNode** pphead);
//單鏈表查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//單鏈表pos之后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//單鏈表pos之前插入x
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//單鏈表刪除pos之后的結點
void SLTEraseAfter(SLTNode* pos);
//單鏈表刪除pos結點
void SLTErase(SLTNode** pphead, SLTNode* pos);
//單鏈表銷毀
void SLTDestroy(SLTNode** pphead);
動態(tài)申請一個節(jié)點SLTNode* BuySListNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (newnode == NULL)
{perror("malloc file:");
exit(-1);//退出程序
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
單鏈表打印void SLTPrint(SLTNode* phead)
{SLTNode* cur = phead;
while (cur != NULL)
{printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL\n");
}
單鏈表尾插void SLTPushBack(SLTNode** pphead, SLTDataType x)
{SLTNode* newnode = BuySListNode(x);
if (*pphead == NULL)
{*pphead = newnode;
}
else
{SLTNode* tail = *pphead;
while (tail->next)
{ tail = tail->next;
}
tail->next = newnode;
}
}
這里使用二級指針是因為當鏈表為空時,需要改變一級指針phead為newnode,想要改變一級指針的值要用二級。
單鏈表尾刪void SLTPopBack(SLTNode** pphead)
{assert(*pphead);
if ((*pphead)->next == NULL)
{free(*pphead);
*pphead = NULL;
}
else
{SLTNode* prev = NULL;
SLTNode* tail = *pphead;
while (tail->next)
{ prev = tail;
tail = tail->next;
}
free(tail);
prev->next = NULL;
}
}
單鏈表頭插void SLTPushFount(SLTNode** pphead, SLTDataType x)
{SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
單鏈表頭刪void SLTPopFount(SLTNode** pphead)
{assert(*pphead);
SLTNode* next = (*pphead)->next;
free(*pphead);
*pphead = next;
}
單鏈表查找SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* cur = phead;
while (cur)
{if (cur->data == x)
{ return cur;
}
cur = cur->next;
}
return NULL;
}
單鏈表pos之后插入xvoid SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);
SLTNode* newnode = BuySListNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
單鏈表pos之前插入xvoid SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pos);
if (*pphead == pos)
{SLTPushFount(pphead, x);
}
else
{SLTNode* prev = *pphead;
while (prev->next != pos)
{ prev = prev->next;
}
SLTNode* newnode = BuySListNode(x);
prev->next = newnode;
newnode->next = pos;
}
}
------為什么在pos之后插入x相比在之前插入要簡單呢?
因為想要在pos之前插入,必須要找到pos的前一個節(jié)點,只能通過遍歷鏈表來找到它。后面的刪除指定結點也是相同的道理。
void SLTEraseAfter(SLTNode* pos)
{assert(pos);
if (pos->next == NULL)
{return;
}
else
{SLTNode* nextNode = pos->next;
pos->next = pos->next->next;
free(nextNode);
}
}
單鏈表刪除pos節(jié)點void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pos);
if (*pphead == pos)
{SLTPopFount(pphead);
}
else
{SLTNode* prev = *pphead;
while (prev->next != pos)
{ prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
單鏈表銷毀void SLTDestroy(SLTNode** pphead)
{SLTNode* cur = *pphead;
while (cur)
{SLTNode* next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
通過以上代碼可以看出,相比順序表,鏈表的頭插和頭刪要簡便許多,而尾插和尾刪要繁瑣一些,這是因為鏈表不能隨機存取,想要找到最后一個結點必須從頭開始遍歷。
學會了單鏈表的實現(xiàn),那么有不少單鏈表的題就可以有思路了,許多題看似復雜,但實際上就是單鏈表的插入、刪除。
給你一個鏈表的頭節(jié)點 head 和一個整數(shù) val ,請你刪除鏈表中所有滿足 Node.val == val 的節(jié)點,并返回 新的頭節(jié)點 。
val = 5:
struct ListNode* removeElements(struct ListNode* head, int val)
{struct ListNode* cur = head, *p = head;
while (cur)
{if(cur == head) //當cur為頭時
{if (cur->val == val)
{cur = cur->next;
head = cur;
p = cur;
}
else
{cur = cur->next;
}
}
else if (cur->next == NULL) //當cur為尾時
{if (cur->val == val)
{cur = cur->next;
p->next = NULL;
}
else
{cur = cur->next;
}
}
else //當cur在中間
{if (cur->val == val)
{cur = cur->next;
p->next = p->next->next;
}
else
{p = cur;
cur = cur->next;
}
}
}
return head;
}
這是我做這道題時想出的第一個方法,這個方法非常繁瑣,要考慮三種情況,就是當cur在頭、尾、中間。這個方法很容易想到但難以用代碼去實現(xiàn),在寫代碼的過程中經常容易亂。
其實這個方法可以優(yōu)化一下,把“if (cur->val = val)”這個判斷拿到外層。代碼如下:
struct ListNode* removeElements(struct ListNode* head, int val)
{struct ListNode* cur = head, *prev = head;
while (cur)
{if (cur->val == val)
{if (cur == head) //當cur為頭時
{cur = cur->next;
free(head);
head = cur;
prev = cur;
}
else //當cur不為頭
{prev->next = cur->next;
free(cur);
cur = prev->next;
}
}
else
{prev = cur;
cur = cur->next;
}
}
return head;
}
這下看起來是不是簡單多了?
回到優(yōu)化前的代碼:在每一個判斷cur位置的條件語句中,都包含了對cur->val的判斷,于是我想到不妨把里面的這個條件語句拿出來,這樣外層就只需要if-else就好了,少了一層判斷。再看內部,我也只分了兩個條件,一個是cur為頭,另一個就是不為頭,當cur在中間和尾時的操作其實時相同的,所以把它們合并在一起。并且加上了free函數(shù),把要刪除的結點free掉,防止內存泄漏。
其實很多人第一次寫都會寫出像優(yōu)化前那樣的代碼,這很正常,因為初次接觸思路可能沒有那么清晰,多練練就好了。
與其像方法一那樣在原鏈表上進行操作,我們不妨創(chuàng)建一個新的鏈表,然后把符合條件的值尾插到新鏈表中。
struct ListNode* removeElements(struct ListNode* head, int val)
{struct ListNode* cur = head;
struct ListNode* newhead = NULL; //新鏈表的頭
struct ListNode* tail = NULL; //新鏈表的尾
while (cur)
{if (cur->val != val)
{if (tail == NULL) //判斷新鏈表是否為空
{newhead = tail = cur;
}
else
{tail->next = cur;
tail = tail->next;
}
cur = cur->next;
}
else
{struct ListNode* next = cur->next;
free(cur);
cur = next;
}
}
if (tail) //把新鏈表的尾結點的next置NULL
{tail->next = NULL;
}
return newhead;
}
這種方法想起來不難,實現(xiàn)起來也不難,寫完之后就會發(fā)現(xiàn)這不就是單鏈表的尾插嗎,只不過就是加了一個篩選條件而已。
方法三:創(chuàng)建帶頭鏈表第三種方法是創(chuàng)建一個帶頭鏈表。
我們知道,單鏈表的尾插相比頭插要復雜一些,于是我想到采用帶頭鏈表。
采用帶頭鏈表可以省去判斷新鏈表是否為空,因為它一定帶有頭節(jié)點不為空,使代碼更簡潔。
注意:本題要求返回的鏈表不能帶有頭節(jié)點,所以在最后應該返回頭節(jié)點的next。
struct ListNode* removeElements(struct ListNode* head, int val)
{struct ListNode* cur = head;
struct ListNode* guard, *tail;
guard = tail = (struct ListNode*)malloc(sizeof(struct ListNode)); //動態(tài)開辟一個頭結點
guard->next = NULL;
while (cur)
{if (cur->val != val)
{tail->next = cur;
tail = tail->next;
cur = cur->next;
}
else
{struct ListNode* next = cur->next;
free(cur);
cur = next;
}
}
tail->next = NULL;
struct ListNode* newhead = guard->next;
free(guard);
return newhead;
}
這種方法不用考慮多種情況,是我最喜歡的方法。
例題——反轉鏈表給你單鏈表的頭節(jié)點 head ,請你反轉鏈表,并返回反轉后的鏈表。
這題其實很容易,我們只需要創(chuàng)建一個新鏈表,然后依次把原鏈表中的元素頭插到新鏈表中即可。代碼如下:
struct ListNode* reverseList(struct ListNode* head)
{struct ListNode* cur = head;
struct ListNode* rhead = NULL;
while (cur)
{struct ListNode* next = cur->next;
cur->next = rhead;
rhead = cur;
cur = next;
}
return rhead;
}
方法二:遞歸法這道題也可以采用遞歸的方法,不過有些難以理解。
struct ListNode* reverseList(struct ListNode* head)
{if (head == NULL || head->next == NULL)
{return head;
}
struct ListNode* newhead = reverseList(head->next);
head->next->next = head;
head->next = NULL;
}
三、雙向帶頭循環(huán)鏈表的實現(xiàn)雙向帶頭循環(huán)鏈表雖然結構最復雜,但是實現(xiàn)起來可一點也不復雜,相信你通過前面對單鏈表的學習,一定能自己寫出雙向鏈表。
typedef int LTDataType;
typedef struct ListNode
{struct ListNode* next;
struct ListNode* prev;
LTDataType data;
}LTNode;
//動態(tài)申請一個結點
LTNode* BuyListNode(LTDataType x);
//初始化
LTNode* LTInit();
//打印
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//尾刪
void LTPopBack(LTNode* phead);
//頭插
void LTPushFront(LTNode* phead, LTDataType x);
//頭刪
void LTPopFront(LTNode* phead);
//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//插入
void LTInsert(LTNode* phead, LTDataType x);
//刪除
void LTErase(LTNode* phead);
//判斷鏈表是否為空
bool LTEmpty(LTNode* phead);
//返回鏈表長度
size_t LTSize(LTNode* phead);
//銷毀
void LTDestroy(LTNode* phead);
動態(tài)申請一個節(jié)點LTNode* BuyListNode(LTDataType x)
{LTNode* node = (LTNode*)malloc(sizeof(LTNode));
if (node == NULL)
{perror("malloc fail");
exit(-1);
}
node->data = x;
node->next = NULL;
node->prev = NULL;
return node;
}
初始化LTNode* LTInit()
{LTNode* phead = BuyListNode(-1);
phead->next = phead;
phead->prev = phead;
return phead;
}
打印void LTPrint(LTNode* phead)
{LTNode* cur = phead->next;
while (cur != phead)
{printf("%d ", cur->data);
cur = cur->next;
}
printf("\n");
}
尾插void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
//LTInsert(phead, x);
}
尾刪void LTPopBack(LTNode* phead)
{assert(phead);
assert(phead->next != phead);
LTNode* tail = phead->prev;
LTNode* tailPrev = tail->prev;
tailPrev->next = phead;
phead->prev = tailPrev;
free(tail);
//LTErase(phead->prev);
}
頭插void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);
LTNode* newnode = BuyListNode(x);
newnode->next = phead->next;
phead->next->prev = newnode;
phead->next = newnode;
newnode->prev = phead;
//LTInsert(phead->next, x);
}
頭刪void LTPopFront(LTNode* phead)
{assert(phead);
assert(phead->next != phead);
LTNode* first = phead->next;
LTNode* second = first->next;
free(first);
phead->next = second;
second->prev = phead;
//LTErase(phead->next);
}
查找LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);
LTNode* cur = phead->next;
while (cur != phead)
{if (cur->data == x)
{ return cur;
}
cur = cur->next;
}
return NULL;
}
插入void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);
LTNode* prev = pos->prev;
LTNode* newnode = BuyListNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
刪除void LTErase(LTNode* pos)
{assert(pos);
LTNode* prev = pos->prev;
LTNode* next = pos->next;
free(pos);
prev->next = next;
next->prev = prev;
}
在實現(xiàn)插入、刪除后,我們其實可以將其復用到尾插尾刪頭插頭刪中。
判斷鏈表是否為空bool LTEmpty(LTNode* phead)
{assert(phead);
return phead->next == phead;
}
返回鏈表長度size_t LTSize(LTNode* phead)
{assert(phead);
LTNode* cur = phead->next;
size_t size = 0;
while (cur != phead)
{++size;
cur = cur->next;
}
return size;
}
銷毀void LTDestroy(LTNode* phead)
{LTNode* cur = phead->next;
while (cur != phead)
{LTNode* next = cur->next;
free(cur);
cur = next;
}
free(phead);
}
四、總結本文主要講述了單鏈表和雙向帶頭循環(huán)鏈表的實現(xiàn),其中單鏈表尤為重要,在筆試和面試中經常會考到,建議各位讀完本文多去刷一些有關單鏈表的題,它還有許多奧秘等待大家去探索。
你是否還在尋找穩(wěn)定的海外服務器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準確流量調度確保服務器高可用性,企業(yè)級服務器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧