本文基于链表的规定实现了对应的增删查改,同时实现了很多其它接口,这些接口都是有广泛应用的。链表的接口实现约在十个左右。
本文结构是先将链表的每一部分进行分析,最后给出完整代码,完整代码中只包含链表的实现。想要调用链表需要自己写main函数。
带头双向循环链表是链表结构中功能最强大、最好实现同时也是未来最常使用的一种结构。
本文中使用头结点来命名只起到头作用并不存储数据的那个结点,用首结点来命名第一个存储数据的结点,也就是头结点之后的那个结点。
1、定义结点的结构:
定义结点结构的代码如下:
typedef int DLDataType;
typedef struct DLNode
{
DLDataType data;
struct DLNode* next;
struct DLNode* prev;
}DLNode;
分析:①带头双向循环链表,每个结点中有两个指针,一个指针指向前一个结点,一个指针指向后一个结点。这种结点的优势在于我们如果删除或者唯一位置之前插入时都不必用头指针找前一个结点,而是可以直接获得前一个结点的地址。
2、链表初始化
链表初始化的代码如下:
void DListInit(DLNode** phead)
{
DLNode* newnode = BuyDListNode(0);
newnode->prev = newnode;
newnode->next = newnode;
*phead = newnode;
}
分析:①我通过写完了好几遍所有结构的链表,发现,只有带头结点的链表需要初始化一下,也就是创建一个头结点,这个头结点只起到一个头的作用,并不存储数据(任何使用头结点存储数据的结构都是公认的垃圾代码,因为很容易就会出现bug),创建头结点的方式我归结为三种方式:1°使用二级指针初始化。2°使用返回值初始化。3°创建一个链表结构,将头指针放在链表结构中。我在本文中使用的是第一种方式,使用二级指针初始化。
②初始化部分,因为我们写的是循环的链表,所以创建了头结点之后还需要做一些连接操作。因为刚开始链表只有头结点,所以我们让头结点的prev指针指向链表的最后一个结点(头结点),然后让头结点的next指针指向头结点的下一个结点,也是头结点,然后让头指针指向头结点,完成初始化。
3、头插
头插代码如下:
void DListPushFront(DLNode* phead, DLDataType x)
{
assert(phead);
DLNode* newnode = BuyDListNode(x);
DLNode* first = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
}
分析:①因为我们有了头结点,所以我们不用再改变头指针了,所以我们只需要传一级指针就可以了。我们还是插入分两种情况讨论,1°链表为空(只有头结点),2°链表不为空(有除头结点外的其它结点)。
②我们还是按照老规矩,先写普遍情况的代码,然后带入特殊情况通读代码,跑不过再特殊处理。对于我们分的两种情况,第二种很显然是普遍的情况,然后我们进行头插,思路:保存首结点的地址,然后申请一个新的结点,然后让头结点的next指针指向新结点,新结点的prev指针指向头结点,新结点的next指针指向首结点,首结点的prev指针指向新结点。根据思路,读完代码之后发现代码可以处理第二种情况,所以不用做特殊处理。
4、尾插
尾插代码如下:
void DListPushBack(DLNode* phead, DLDataType x)
{
assert(phead);
DLNode* newnode = BuyDListNode(x);
DLNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
分析:①还是因为有了头结点,所以不用传二级指针了,只需要传一级指针就可以了。因为我们这个链表是双向循环的,所以要找尾指针不需要再遍历链表了,头指针的prev就指向的尾结点。尾插我们还是要考虑两种情况:1°链表只有头结点,2°链表有除头结点之外的其它结点。
②我们来讨论上述提出的两种情况,我们发现情况2是比较普遍的一种,所以我们先写情况2的代码,思路:我们保存下尾结点的地址,然后申请新的结点,然后让尾结点的next指针指向新结点,新结点的prev指针指向原本的尾结点,新结点的next指针指向头结点,头结点的prev指针指向新结点。写完代码之后通读代码,发现可以处理情况1。
5、头删
头删代码如下:
void DListPopFront(DLNode* phead)
{
assert(phead);
assert(phead->next!=phead);
DLNode* first = phead->next;
DLNode* second = first->next;
free(first);
first = NULL;
phead->next = second;
second->prev = phead;
}
分析:①在头删时,因为有头结点,所以也不会改变头指针,因此还是只需要传一级指针。删除结点我们需要考虑三种情况,1°链表只有头结点,2°链表有头结点和另一个结点两个结点,3°链表有头结点和另外多个结点。
②因为链表为空时我们不能删除,所以我们使用assert断言限定死。所以还有情况2和情况3,很显然情况3是普遍的一种情况,情况2是一种特殊的情况。所以我们先来写情况3的代码,思路:保存首结点和首结点后的结点,释放掉首结点,并将首结点置空。然后让头结点的next指针指向首结点后的结点,让首结点后的结点的prev指针指向头结点。通读代码发现情况2可以处理。
6、尾删
尾删代码如下:
void DListPopBack(DLNode* phead)
{
assert(phead);
assert(phead->prev!=phead);
DLNode* tail = phead->prev;
DLNode* tailprev = tail->prev;
free(tail);
tail = NULL;
phead->prev = tailprev;
tailprev->next = phead;
}
分析:①因为头结点的存在,所以还是不需要传二级指针,只需要传一级指针。这种情况还是需要考虑三种情况:1°链表只有头结点,2°链表只有头结点和首结点,3°链表有头结点和其余的多个结点。
②我们来讨论这三种情况,当链表只有头结点时没有结点可删,所以我们可以使用assert断言限定死。对于情况2和情况3,情况3明显是普遍的情况,情况2是特殊的那种情况。所以我们来先写情况3的代码,思路:保存好尾结点和尾结点前面那个结点,然后释放掉尾结点,让头结点的prev指针指向尾结点前面那个结点,尾结点前面那个结点的next指针指向prev。然后我们通读代码,发现可以解决情况2。
7、打印链表(为了链表更好的测试)
链表打印代码如下:
void DListPrint(DLNode* phead)
{
assert(phead);
DLNode* cur = phead->next;
while (cur != phead)
{
printf("%d ",cur->data);
cur = cur->next;
}
printf("phead\n");
}
分析:①打印链表不需要修改链表,更不需要修改头指针,所以传的一级指针。我们遍历链表,打印每个结点的值。遍历完的条件是指针指到头结点。
8、查找
查找代码如下:
DLNode* DListFind(DLNode* phead, DLDataType x)
{
assert(phead);
DLNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
分析:①链表的查找和链表的打印几乎是异曲同工,本质上都是遍历链表,只不过遍历每个结点时都要进行一下判断,判断这个结点是不是我要找的那个结点,如果是那就返回这个结点的地址,如果不是就返回空指针。
9、任意位置之后插入
任意位置之后插入代码如下:
void DListInsertAfter(DLNode* pos, DLDataType x)
{
assert(pos);
DLNode* next = pos->next;
DLNode* newnode = BuyDListNode(x);
pos->next = newnode;
newnode->prev = pos;
next->prev = newnode;
newnode->next = next;
}
分析:①在任意位置之后插入结点,只需要在哪一个结点之后插入结点,只需要pos地址就可以。思路:保存pos地址处那个结点后面的结点的地址,然后申请一个新的结点,让pos的next指针指向新结点,新结点的prev指针指向pos,新结点的next指针指向pos后的结点,pos后的结点的prev指向新结点。
10、任意位置之前插入
任意位置之前插入代码如下:
void DListInsertFore(DLNode* pos, DLDataType x)
{
assert(pos);
DLNode* prev = pos->prev;
DLNode* newnode = BuyDListNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
分析:①在任意位置之前插入结点,因为我们这个链表是双向的,所以找前面那个结点时不用从头开始遍历了。所以只需要传pos结点就可以了。因为pos结点肯定不是头结点,所以说,pos结点之前必有结点。所以就不用考虑pos前面为空的情况了。可以直接放心的插入一个结点就行了。思路:保存pos结点前面的那个结点,创建一个新的结点,让pos结点前面的那个结点的next指针指向新结点,然后让新结点的prev指针指向pos结点前面的那个结点,新结点的next指针指向pos结点,pos结点的prev指针指向新结点。
11、任意位置删除
任意位置删除代码如下:
void DListErase(DLNode* pos)
{
assert(pos);
DLNode* prev = pos->prev;
DLNode* next = pos->next;
free(pos);
pos = NULL;
prev->next = next;
next->prev = prev;
}
分析:①在任意位置删除结点,pos结点肯定不是头结点,所以pos结点之前肯定有结点。所以思路是:保存pos前的结点和pos后的结点,释放掉pos结点,并将pos结点置空。然后将pos前结点的next指针指向pos后的结点,pos后结点的prev指针指向pos前的结点。
12、链表销毁
链表销毁代码如下:
void DListDestroy(DLNode** phead)
{
assert(phead);
DLNode* cur = (*phead)->next;
while (cur != (*phead))
{
DLNode* next = cur->next;
free(cur);
cur = next;
}
free(*phead);
*phead = NULL;
}
分析:①链表的销毁本质上也是一种遍历,只不过在遍历的过程中释放掉每个结点的空间。但是最后一定要检查头指针是否释放,以及头指针是否置空的问题。
总结:带头双向循环链表是一个很优的链表结构,链表写的过程中几乎没有坑,而且功能很强大的。
完整代码如下:
DList.h
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int DLDataType;
typedef struct DLNode
{
DLDataType data;
struct DLNode* next;
struct DLNode* prev;
}DLNode;
void DListInit(DLNode** phead);
void DListPushFront(DLNode* phead,DLDataType x);
void DListPushBack(DLNode* phead,DLDataType x);
void DListPopFront(DLNode* phead);
void DListPopBack(DLNode* phead);
void DListPrint(DLNode* phead);
DLNode* DListFind(DLNode* phead,DLDataType x);
void DListInsertAfter(DLNode* pos,DLDataType x);
void DListInsertFore(DLNode* pos,DLDataType x);
void DListErase(DLNode* pos);
void DListDestroy(DLNode** phead);
DList.c
#include"DList.h"
DLNode* BuyDListNode(DLDataType x)
{
DLNode* newnode = (DLNode*)malloc(sizeof(DLNode));
if (newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->prev = NULL;
newnode->next = NULL;
return newnode;
}
void DListInit(DLNode** phead)
{
DLNode* newnode = BuyDListNode(0);
newnode->prev = newnode;
newnode->next = newnode;
*phead = newnode;
}
void DListPushFront(DLNode* phead, DLDataType x)
{
assert(phead);
DLNode* newnode = BuyDListNode(x);
DLNode* first = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = first;
first->prev = newnode;
}
void DListPushBack(DLNode* phead, DLDataType x)
{
assert(phead);
DLNode* newnode = BuyDListNode(x);
DLNode* tail = phead->prev;
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
void DListPopFront(DLNode* phead)
{
assert(phead);
assert(phead->next!=phead);
DLNode* first = phead->next;
DLNode* second = first->next;
free(first);
first = NULL;
phead->next = second;
second->prev = phead;
}
void DListPopBack(DLNode* phead)
{
assert(phead);
assert(phead->prev!=phead);
DLNode* tail = phead->prev;
DLNode* tailprev = tail->prev;
free(tail);
tail = NULL;
phead->prev = tailprev;
tailprev->next = phead;
}
void DListPrint(DLNode* phead)
{
assert(phead);
DLNode* cur = phead->next;
while (cur != phead)
{
printf("%d ",cur->data);
cur = cur->next;
}
printf("phead\n");
}
DLNode* DListFind(DLNode* phead, DLDataType x)
{
assert(phead);
DLNode* cur = phead->next;
while (cur != phead)
{
if (cur->data == x)
return cur;
cur = cur->next;
}
return NULL;
}
void DListInsertAfter(DLNode* pos, DLDataType x)
{
assert(pos);
DLNode* next = pos->next;
DLNode* newnode = BuyDListNode(x);
pos->next = newnode;
newnode->prev = pos;
next->prev = newnode;
newnode->next = next;
}
void DListInsertFore(DLNode* pos, DLDataType x)
{
assert(pos);
DLNode* prev = pos->prev;
DLNode* newnode = BuyDListNode(x);
prev->next = newnode;
newnode->prev = prev;
newnode->next = pos;
pos->prev = newnode;
}
void DListErase(DLNode* pos)
{
assert(pos);
DLNode* prev = pos->prev;
DLNode* next = pos->next;
free(pos);
pos = NULL;
prev->next = next;
next->prev = prev;
}
void DListDestroy(DLNode** phead)
{
assert(phead);
DLNode* cur = (*phead)->next;
while (cur != (*phead))
{
DLNode* next = cur->next;
free(cur);
cur = next;
}
free(*phead);
*phead = NULL;
}