目录
1. 双向链表的结构
2. 双向链表的实现
初始化哨兵位:
打印链表:
尾插:
头插:
尾删:
头删:
查找:
在指定位置之后插入数据:
删除目标位置的数据:
销毁链表:
1. 双向链表的结构
双向链表也叫双链表,是链表的一种,它的每个数据节点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。
带头双向循环链表:
注意:这里的“带头”指的是“哨兵位”,哨兵位不存储任何的有效元素。
哨兵位的意义在于,遍历循环链表避免出现死循环。
当链表中只有哨兵位时,称该链表为空链表。
2. 双向链表的实现
根据上图,可以写出双向链表的结构:
typedef int LTDataType;
typedef struct ListNode
{struct ListNode* next;//后继struct ListNode* prev;//前驱LTDataType data;
}LTNode;
初始化哨兵位:
老样子使用 malloc() 开辟空间,哨兵位的初始值可以是随便的数据。
双向链表在初始的时候只有哨兵位 phead 一个节点,哨兵位也有它的前驱指针和后继指针。因为双向链表是循环的,所以有:
使用一级指针,形参的改变不能影响实参。使用二级指针的目的就是对实参产生影响。
为了将哨兵位的初始值定义为-1,所以使用二级指针。
具体代码为:
//List.h//定义双向链表中节点的结构
typedef int LTDataType;
typedef struct ListNode
{struct ListNode* next;struct ListNode* prev;LTDataType data;
}LTNode;//注意,双向链表是带有哨兵位的,使用之前必须初始化一个哨兵位
void LTInit(LTNode** pphead);
//List.cvoid LTInit(LTNode** pphead)
{*pphead = (LTNode*)malloc(sizeof(LTNode));if (*pphead == NULL){perror("malloc fail");exit(1);}(*pphead)->data = -1;//初始化一个随便的值(*pphead)->next = (*pphead)->prev = *pphead;
}
测试初始化:
这是传参数的写法,如果不传参数呢?
环形链表的约瑟夫问题 中,通过返回值的形式将链表的头/尾返回。
同样的,在进行双向链表的初始化时,就可以不传参数,通过返回值的形式把创建好的哨兵位返回。
//List.hLTNode* LTInit();
//List.c//创建新节点
LTNode* LTBuyNode(LTDataType x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL) {perror("malloc fail!");exit(1);}newnode->data = x;newnode->next = newnode->prev = newnode;return newnode;
}//创建哨兵位
LTNode* LTInit()
{LTNode* phead = LTBuyNode(-1);return phead;
}
打印链表:
//List.cvoid LTPrint(LTNode* phead)
{assert(phead);//遍历链表LTNode* pcur = phead->next;while (pcur != phead){printf("%d->", pcur->data);pcur = pcur->next;}printf("\n");
}
注意:这里pcur 不指向哨兵位是为了避免死循环,所以选择指向哨兵位的下一位。
尾插:
首先要注意,不管插入还是删除都不能影响到哨兵位。
所以,这里将使用一级指针。
//List.h//不需要改变哨兵位,则不需要传二级指针
//如果需要修改哨兵位的话,则传二级指针
void LTPushBack(LTNode* phead, LTDataType x);
尾插示意图:
很显然,尾插影响到的节点有:newnode、ptail、哨兵位。
从图中可知,双向链表的尾节点 ptail 就是 head->prev 。
所以,和单链表不同的是,双向链表找尾节点不需要遍历链表!
示例代码:
//List.c//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);//顺序为 phead phead->prev(ptail) newnodenewnode->next = phead;newnode->prev = phead->prev;phead->prev->next = newnode;phead->prev = newnode;
}
测试尾插:
头插:
注意:头插指的是在第一个有效节点之前插入新节点!
如果在哨兵位之前插入,就相当于尾插!
头插示意图:
很显然,头插影响到的节点有:哨兵位、newnode、head->next。
先将 newnode->next 指向 head->next,newnode->prev 指向 head;
然后再改变 head->next 指向 newnode,head->next->prev 指向 newnode。
让 newnode 成为新的 head->next。
//List.h//头插
void LTPushFront(LTNode* phead, LTDataType x);
//List.c//头插
void LTPushFront(LTNode* phead, LTDataType x)
{assert(phead);LTNode* newnode = LTBuyNode(x);//phead newnode phead->nextnewnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;phead->next = newnode;
}
测试头插:
尾删:
尾删示意图:
很显然,尾删影响到的节点有:ptail(head->prev)、ptail->prev、哨兵位。
先将 head->prev->prev(图中就是d2)->next 指向 head;
然后将 head->prev 指向 head->prev->prev;
最后将原来的尾节点(d3)释放。
//List.h//尾删
void LTPopBack(LTNode* phead);
//List.c//尾删
void LTPopBack(LTNode* phead)
{assert(phead);//链表为空:只有一个哨兵位节点assert(phead->next != phead);LTNode* del = phead->prev;LTNode* prev = del->prev;prev->next = phead;phead->prev = prev;free(del);del = NULL;
}
测试尾删:
头删:
一样的,头删删除的是第一个有效的节点!
头删示意图:
很显然,头删影响到的节点有:head->next->next、head->next、哨兵位。
先将 head->next->next(就是d2)->prev 指向 head;
然后将 head->next 指向 head->next->next;
最后将原来的第一个节点(d1)释放。
//List.h//头删
void LTPopFront(LTNode* phead);
//List.c//头删
void LTPopFront(LTNode* phead)
{assert(phead);assert(phead->next != phead);LTNode* del = phead->next;LTNode* next = del->next;next->prev = phead;phead->next = next;free(del);del = NULL;
}
测试头删:
查找:
很简单,遍历链表,有则返回,无则NULL。
//List.h//查找
LTNode* LTFind(LTNode* phead, LTDataType x);
//List.c//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x) {return pcur;}pcur = pcur->next;}return NULL;
}
测试查找:
在指定位置之后插入数据:
插入示意图:
很显然,插入影响到的节点有:pos、pos->next。
但是要注意,如果先将 pos->next 指向 newnode,然后 pos->next->prev 指向 newnode,这个顺序是错误的,会导致找不到原来的 pos->next。
示例代码:
//List.h//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x);
//List.c//在pos位置之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = LTBuyNode(x);newnode->next = pos->next;newnode->prev = pos;pos->next->prev = newnode;pos->next = newnode;
}
测试插入:
删除目标位置的数据:
删除示意图:
很显然,删除影响到的节点有:pos、pos->next、pos->prev。
示例代码:
//List.h//删除pos位置的数据
void LTErase(LTNode* pos);
//List.c//删除pos位置的数据
void LTErase(LTNode* pos)
{assert(pos);pos->next->prev = pos->prev;pos->prev->next = pos->next;free(pos);pos = NULL;
}
测试删除:
销毁链表:
链表的销毁其实就是把链表里的数据一个个销毁。
多了要将哨兵位销毁的操作。
示例代码:
//List.h//销毁
void LTDesTroy(LTNode* phead);
//List.cvoid LTDesTroy(LTNode* phead)
{//哨兵位不能为空assert(phead);LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}//链表中只有一个哨兵位free(phead);phead = NULL;
}
自行调试。