1、概念
对链表而言,双向均可遍历是最方便的,另外首尾相连循环遍历也可打打增加链表操作的便捷性。因此,双向循环链表,是在实际运用中最常见的链表形态。
2、基本操作
与普通的链表完全一致,双向循环链表虽然指针较多,但逻辑是完全一样。基本的操作包括“
1、结点设计
2、初始化空链表
3、增删结点
4、链表遍历
5、销毁链表
3、实现代码
双向链表的结点只是比单向链表多了一个前向指针
typedef struct node //取别名
{
//数据域
int data;
//指向相邻结点的双向指针
struct node* prev;
struct node* next;
}node;
设计完节点之后,就要初始化。所谓初始化,就是构建一条不含有效节点的空链表。
以带头节点的双线循环链表为例,初始化后,其状态如下图所示:
//初始化双向循环链表
node* init()
{
//申请头节点
node* head = malloc(sizeof(node));
//让首尾互相指向
//不需要对头结点的数据域data进行和操作
if(head != NULL)
{
head->prev = head;
head->next = head;
}
return head;
}
初始化链表之后,就可以新建节点,然后进行头插或者尾插(即将新节点插入到链表的首部或者尾部),示例代码为:
//创建新节点
node* newNode(node* head,int data)
{
//开辟堆区空间
node* new = malloc(sizeof(node));
if(new != NULL)
{
new->data = data;
new->prev = new;
new->next = new;
}
return new;
}
//头插法:将新节点插入到链表的头部
void insertHead(node* head,node* new)
{
new->next = head->next;
new->prev = head;
head->next = new;
head->next->prev = new;
}
//尾插法:将新节点插入到链表的尾部
void insertTail(node* head,node* new)
{
new->next = head;
new->prev = head->prev;
head->prev->next = new;
head->prev = new;
}
看着代码可能有些难理解,所以直接上图:
对初始化之后的循环双向链表执行过插入节点动作之后,我们还可以剔除掉循环双向链表中的一个节点,注意,从链表中将一个节点剔除出去,并不意味着要释放节点的内容。当然,我们经常在剔除了一个节点之后,紧接着的动作往往是释放它,但是将“剔除”与“释放”两个动作分开,是最基本的函数封装的原则,因为它们虽然常常连在一起使用,但它们之间并无必然联系,例如:当我们要移动一个节点的时候,实质上就是将“剔除”和“插入”的动作连起来,此时就不能释放该节点了。
//查找某数据是否存在在链表中
node* findNode(node* head, int data)
{
node* p;
for (p = head->next; p != head; p = p->next)
{
if (p->data == data)
{
return p;
}
}
}
// 将指定节点p从链表中剔除
void remove(node *head, node *p)
{
p->prev->next = p->next;
p->next->prev = p->prev;
p->prev = NULL;
p->next = NULL;
}
//判断双向循环链表是否为空
bool isEmpty(node* head)
{
return head->next == head; //最简便的方式,如果链表为空,则头结点的下一个节点依然是自身
}
然后需要封装一个遍历链表的函数来进行代码测试,遍历就存在向前遍历和向后遍历
// 向前遍历
void listForEachPrev(node * head)
{
if(isEmpty(head))
return;
for(node * tmp=head->prev; tmp!=head; tmp=tmp->prev)
{
printf("%d\n", tmp->data);
}
}
// 向后遍历
void listForEach(node * head)
{
if(isEmpty(head))
return;
for(node * tmp=head->next; tmp!=head; tmp=tmp->next)
{
printf("%d\n", tmp->data);
}
}
我们使用完循环双向链表之后,需要销毁链表。由于链表中的各个节点被离散地分布在各个随机的内存空间,因此销毁链表必须遍历每一个节点,释放每一个节点。注意:销毁链表时,遍历节点要注意不能弄丢相邻节点的指针。不销毁掉链表可能会造成内存泄漏,程序崩溃等情况。
//销毁链表
//第一种写法
void destroy(node * head)
{
if(isEmpty(head))
return;
node *n;
for(node *tmp = head->next; tmp!=NULL; tmp=n)
{
n = tmp->next;
free(tmp);
}
}
//第二种写法
node* destroy(node* head)
{
node* p;
for(p=head->next;p != head;p = head->next)
{
remove(p);
free(p);
}
//释放头节点
free(head);
return NULL;
}
4、适用场合
经过单链表、双链表的学习,可以总结链表的适用场合:
- 适合用于节点数目不固定,动态变化较大的场合
- 适合用于节点需要频繁插入、删除的场合
- 适合用于对节点查找效率不十分敏感的场合
5、最后附上我的主函数测试代码
int main()
{
node* head = initList();
if (head)
{
printf("初始化空链表成功!\n");
}
else
{
printf("初始化空链表失败!\n");
}
//在链表的头部插入一些节点
for (int i = 1; i <= 5; i++)
{
node* new = newNode(i);
insertHead(head, new);
}
show(head);
在链表的尾部插入一些节点
//for (int i = 1; i <= 5; i++)
//{
// node* new = newNode(i);
// insertTail(head,new);
//}
//输入要删除的节点当中的数值
int n;
while (1)
{
scanf("%d", &n);
if (n == 0)
{
break;
}
node* p = findNode(head, n);
if (p != NULL)
{
removeNode(p);
free(p);
}
else
{
printf("没有你想删除的节点!\n");
}
show(head);
}
//向前输出每个节点数据
listForEachPrev(head);
//向后输出每个节点数据
listForEach(head);
//销毁整条链表
head = destroy(head);
return 0;
}