1.2 實現單鏈表
在上一篇文章中,單鏈表的實現只有一少部分,這一篇接著來了解單鏈表剩下的接口實現。
SList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>//定義單鏈表就是定義節點,因為單鏈表是由節點組成的,所以定義單鏈表就是定義節點
typedef int SLTDataType;
//定義鏈表節點的結構
typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SLTNode;//單鏈表的打印
void SLTPrint(SLTNode* phead);//單鏈表的尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);//單鏈表的頭插
void SLTPushFront(SLTNode** pphead, SLTDataType x);//單鏈表的尾刪
void SLTPopBack(SLTNode** pphead);//單鏈表的頭刪
void SLTPopFront(SLTNode** pphead);//單鏈表查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);//在指定位置之前插入數據
void* SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//在指定位置之后插入數據
void* SLTInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x);//刪除pos節點
void* SLTErase(SLTNode** pphead, SLTNode* pos);//刪除pos之后的節點
void SLTEraseAfter(SLTNode* pos);//銷毀鏈表
void SLTDesTroy(SLTNode** pphead);//申請新的節點
SLTNode* SLTBuyNode(SLTDataType x);
SList.c
#include"SList.h"//單鏈表的打印
void SLTPrint(SLTNode* phead) {SLTNode* pcur = phead;while (pcur != NULL){printf("%d -> ", pcur->data);pcur = pcur->next;}printf("NULL\n");
}//新節點的申請
SLTNode* SLTBuyNode(SLTDataType x)
{SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));//申請新節點這里傳的應該是節點類型而不是數據類型//單鏈表:每次插入一個節點時,需要分配一個節點(結構體)的內存,因此使用 `sizeof(節點類型)`。//順序表:在擴容時,需要為多個數據元素分配一塊連續的內存(即數組),因此使用 `元素個數 * sizeof(數據類型)`。if (node == NULL){perror("malloc fail!\n");exit(1);}node->data = x;node->next = NULL;return node;
}//單鏈表的尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x) {//單鏈表為空assert(pphead);//SLTNode* Tail = *pphead; 不能在這里定義Tail 因為這里的Tail為空,后面循環中的Tail->next 就會報錯,不會進入循環當中SLTNode* newnode = SLTBuyNode(x);if (*pphead == NULL){*pphead = newnode;}else {//單鏈表不為空,找尾節點,插入新節點SLTNode* Tail = *pphead;while (Tail->next != NULL){Tail = Tail->next;}//跳出循環,插入新節點Tail->next = newnode;//newnode->next = NULL; 不需要寫是因為,newnode在初始化的時候就已經置為NULL了}
}//單鏈表的頭插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead &&*pphead);//pphead 不能插入空的數據//*pphead 不能對空表進行插入SLTNode* newnode = SLTBuyNode(x);newnode->next = *pphead;*pphead = newnode;//將新的節點作為插入后鏈表的頭結點
}//單鏈表尾刪
void SLTPopBack(SLTNode** pphead)
{assert(pphead&&*pphead);//pphead--空鏈表不可以進行刪除操作// *pphead--不為空的鏈表--循環遍歷 --遍歷到尾節點的前一個節點,將此節點的next置為NULL;//只有一個節點的情況,是不能遍歷的,因為他的next->next為NULLif ((*pphead)->next == NULL) // -> 的優先級大于 * 的優先級{free(*pphead);*pphead = NULL;}else {SLTNode* Tail = *pphead;SLTNode* prev = NULL;while (Tail->next){prev = Tail;Tail = Tail->next;}//跳出循環prev->next = NULL;free(Tail);Tail = NULL;}}//單鏈表的頭刪
void SLTPopFront(SLTNode** pphead)
{assert(pphead&&*pphead);SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}//單鏈表查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* pcur = phead;while (pcur){if (pcur->data == x){return pcur;}pcur = pcur->next;}//跳出循環return NULL;}//在指定位置之前插入數據
void* SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {assert(pphead && pos);//下標pos從1開始的if (pos == *pphead){//頭插SLTPushFront(pphead,x);}else {SLTNode* newnode = SLTBuyNode(x);SLTNode* prev = *pphead;while (prev->next != pos){//找到prevprev=prev->next;}newnode->next = pos;prev->next = newnode;}}//在指定位置之后插入數據
void* SLTInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x) {assert(pos);SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next;pos->next = newnode;//不用考慮pos->next為不為空的情況,因為assert只限制了pos//所以也不用單獨考慮當pos->next=NULL時尾插的情況,上述的//代碼包含這種情況}//刪除pos節點
void* SLTErase(SLTNode** pphead, SLTNode* pos) {assert(pphead && pos);//pos為頭結點if (pos == *pphead){//頭刪操作SLTPopFront(pphead);}//pos非頭結點else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}}//刪除pos之后的節點
void SLTEraseAfter(SLTNode* pos)
{assert(pos && pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}//銷毀鏈表
void SLTDesTroy(SLTNode** pphead) {SLTNode* pcur = *pphead;while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}
1.3 鏈表的分類
鏈表的結構多樣,有8種鏈表結構:
鏈表的具體分類:
雖然鏈表的種類有很多,但是最常用的就兩種:單鏈表和雙鏈表。
單鏈表:單向不帶頭不循環的鏈表。結構簡單,一般不會單獨用來存儲數據;實際中更多的是作為其他數據結構的子結構。
雙鏈表:雙向帶頭循環的鏈表?。結構最復雜,一般用在單獨存儲數據;實際中使用的鏈表數據結構,都是帶頭雙向循環鏈表,雖然這個結構復雜,但是在寫代碼的時候會發現結構會帶來很多優勢,實現反而會變得更加簡單。
2??雙鏈表
2.1 概念與結構
雙鏈表全稱:帶頭雙向循環鏈表
注意:
我們這里提到的 "帶頭" 跟我們前面說的 "頭結點" 是兩個概念,前面在單鏈表中的稱呼是不嚴謹的,是為了更好的理解。
而在帶頭鏈表里的頭結點,實際是 "哨兵位",哨兵位節點不存儲任何的有效元素,只是站在這里 “放哨的”
2.2 實現雙向鏈表
List.c
#include "List.h"LTNode* buyNode(LTDataType x)
{LTNode* node=(LTNode*)malloc(sizeof(LTNode));node->data = x;node->next = node->prev = node;return node;
}//初始化雙向鏈表 --- 接口一致性
LTNode* LTInit()
{////創建頭結點//LTNode* phead = (LTNode*)malloc(sizeof(LTNode));//phead->data = -1; //頭結點是無效的數據,隨便存放一數值就ok//phead->next = phead->prev = phead; //指向自己代表是雙向的LTNode* phead = buyNode(-1);return phead;}//void LTInit(LTNode** pphead)
//{
// assert(pphead);
// *pphead = (LTNode*)malloc(sizeof(LTNode));
// (*pphead)->data = -1;
// (*pphead)->next = (*pphead)->prev = *pphead;
//}//銷毀
void LTDesTroy(LTNode* phead)
{LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}free(phead);phead = NULL;}//打印
void LTPrint(LTNode* phead)
{LTNode* pcur = phead->next;while (pcur != phead){printf("%d ->", pcur->data);pcur = pcur->next;}printf("\n");}//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = buyNode(x);//phead phead->prev newnodenewnode->prev = phead->prev;newnode->next = phead;phead->prev->next = newnode;phead->prev = newnode;
}//頭插
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);//phead newnode phead->nextLTNode* newnode = buyNode(x);phead->next->prev = newnode;newnode->next = phead->next;phead->next = newnode;newnode->prev = phead;}//判空
bool LTEmpty(LTNode* phead)
{assert(phead);return phead->next == phead;//等于 true 為空//不等于 false 不為空}//尾刪
void LTPopBack(LTNode* phead)
{assert(!LTEmpty(phead));//為空 true --- !true=false 報錯//不為空 false --- !false=true 繼續執行LTNode* del = phead->prev;//phead phead->prev->prev phead->prevdel->prev->next = phead;phead->prev = del->prev;free(del);del = NULL;
}
//頭刪 刪除的是頭結點的下一個結點
void LTPopFront(LTNode* phead)
{assert(!LTEmpty(phead));//phead phead->next phead->next->nextLTNode* next = phead->next;next->next->prev = phead;phead->next = next->next;free(next);next = NULL;}//查找指定元素
LTNode* LTFind(LTNode* phead, LTDataType x)
{LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}
//在pos之后插入數據
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);//pos為空就是在雙向鏈表中沒有找到x,就不能進行插入操作LTNode* newnode = buyNode(x);//pos newnode pos->nextpos->next->prev = newnode;newnode->next = pos->next;pos->next = newnode;newnode->prev = pos;}
//刪除在pos位置的數據
void LTErase(LTNode* pos)
{assert(pos);//pos 為空不可刪除 為空找不到該元素//pos->prev pos pos->nextpos->prev->next = pos->next;pos->next->prev = pos->prev;free(pos);pos = NULL;
}
3. 順序表與鏈表的分析
不同點 | 順序表 | 鏈表(單鏈表) |
存儲空間 | 物理上一定連續 | 邏輯上連續,但物理上不一定連續 |
隨機訪問 | 支持:O(1) | 不支持:O(N) |
任意位置插入或者刪除元素 | 可能需要搬移元素,效率低:O(N) | 只需要修改指針指向 |
插入 | 動態順序表,空間不夠是需要擴容和空間浪費 | 沒有容量的概念,按需申請釋放,不存在空間浪費 |
應用場景 | 元素高效存儲+頻繁訪問 | 任意位置高效插入和刪除 |