Pre
前面我们实现了单向的链表的增删查改操作,可以发现单向链表是有弊端的:尾插需要遍历,找到尾结点,时间复杂度是O(N)。
今天来实现一个更完美的链式结构:带头双向循环链表 的增删查改。
如图所示
结构
typedef int LTDataType;
typedef struct LTNode{
struct LTNode* next; //指向后1个结点
struct LTNode* prev; //指向前1个结点
LTDataType value; //存放数据
}LTNode;
获得1个结点
LTNode* BuyLTNode(LTDataType x) {
LTNode* newNode = (LTNode*)malloc(sizeof(LTNode));
if (newNode == NULL) {
perror("BuyLTNode::malloc failed.");
exit(-1);
}
newNode->next = NULL;
newNode->prev = NULL;
newNode->value = x;
return newNode;
}
初始化
初始化时,我们Buy上1个结点,作为虚拟头结点:不存放有效数据,并且next和prev都指向自己。
LTNode* InitLT() {
LTNode* newNode = BuyLTNode(-1); //Buy 1个结点
newNode->next = newNode; //指向自己
newNode->prev = newNode; //指向自己
return newNode;
}
那么,用户调用时可以这样:
void test_1(){
LTNode* plist = InitLT(); //此时plist指向1个虚拟头结点
}
那么接下来所有的结点,都应该依附在plist
之后。
注:有些地方把这样的不存放有效数据的头结点叫做哨兵位头结点。
插入数据
尾插
直接看代码
void PushBackLT(LTNode* phead, LTDataType x) {
assert(phead); //阻止用户传入NULL
LTNode* newNode = BuyLTNode(x);
/* 下面仅仅需要改变指针指向就能完成插入 */
LTNode* tail = phead->prev; //仅通过phead即可找到尾结点
tail->next = newNode;
newNode->prev = tail;
newNode->next = phead;
phead->prev = newNode;
}
可以看到,虽然我们让链表结构复杂了一点,但可以提高效率,尾插时不再需要像单链表一样去遍历链表找到tail
了,只需要改一下指针指向。
头插
void PushFrontLT(LTNode* phead, LTDataType x) {
assert(phead);
LTNode* newNode = BuyLTNode(x);
newNode->next = phead->next;
phead->next->prev = newNode;
phead->next = newNode;
newNode->prev = phead;
}
pos位置前插入
void InsertLT(LTNode* pos, LTDataType x) {
assert(pos);
LTNode* newNode = BuyLTNode(x);
newNode->prev = pos->prev;
pos->prev->next = newNode;
newNode->next = pos;
pos->prev = newNode;
}
那么,实际实现的时候,可以先写完void InsertLT(LTNode* pos, LTDataType x)
,然后头插、尾插复用即可。
注:pos位置的结点如何获得?——》通过后面的查找数据接口
SearchLT()
获得。
尾插
void PushBackLT(LTNode* phead, LTDataType x) {
assert(phead);
InsertLT(phead, x); //复用
}
头插
void PushFrontLT(LTNode* phead, LTDataType x) {
assert(phead);
InsertLT(phead->next, x); //复用
}
删除数据
删除pos位置数据
void EraseLT(LTNode* pos) {
assert(pos);
LTNode* after = pos->next;
pos->prev->next = after;
after->prev = pos->prev;
free(pos);
}
尾删
void PopBackLT(LTNode* phead) {
assert(phead);
if (phead->next == phead) {
printf("链表已删完\n");
return;
}
EraseLT(phead->prev);
}
头删
void PopFrontLT(LTNode* phead) {
assert(phead);
if (phead->next == phead) {
printf("链表已删完\n");
return;
}
EraseLT(phead->next);
}
查改数据
查找数据
LTNode* SearchLT(LTNode* phead, LTDataType x) {
assert(phead);
LTNode* cur = phead->next;
while (cur != phead) {
if (cur->value == x) {
return cur;
}
cur = cur->next;
}
return NULL;
}
修改数据
可以通过SearchLT()
获得的pos进行修改。
例如,现在我们有一个链表,存放的数据是[ 1 , 2 , 3 , 4 , 5 ],我们希望把3改成30,代码实现如下
int main() {
LTNode* plist = InitLT();
PushFrontLT(plist, 5);
PushFrontLT(plist, 4);
PushFrontLT(plist, 3);
PushFrontLT(plist, 2);
PushFrontLT(plist, 1);
PrintLT(plist);// <= 1 <=> 2 <=> 3 <=> 4 <=> 5 =>
//修改数据
LTNode* pos = SearchLT(plist, 3);
if (pos != NULL) {
pos->value = 30;
}
PrintLT(plist);// <= 1 <=> 2 <=> 30 <=> 4 <=> 5 =>
return 0;
}
销毁链表
任何通过malloc出来的空间都要记得释放,防止内存泄漏。
void DestroyLT(LTNode* phead) {
assert(phead);
LTNode* cur = phead->next;
while (cur != phead) {
LTNode* after = cur->next;
free(cur);
cur = after;
}
free(phead);
}
其他接口
可视化
随便实现,可有可无,仅仅是为了方便看而已。
void PrintLT(LTNode* phead) {
assert(phead);
if (phead->next == phead) {
printf("链表无有效数据\n");
return;
}
LTNode* cur = phead->next;
printf("dummyHead<=");
while (cur->next != phead) {
printf(" %d ", cur->value);
printf("<=>");
cur = cur->next;
}
printf(" %d =>", cur->value);
printf("\n");
}
获得链表有效数据个数
size_t SizeLT(LTNode* phead) {
assert(phead);
LTNode* cur = phead->next;
size_t size = 0;
while (cur != phead) {
size++;
cur = cur->next;
}
return size;
}
判断链表是否空
// 记得需要引头文件
#include <stdbool.h>
bool IsEmptyLT(LTNode* phead) {
assert(phead);
return phead->next == phead;
}
End
谢谢观看~