数据结构笔记(3)——循环链表与双向链表

循环链表

在介绍概念之前先说一下需求,假设你已经有了这么个链表:

数据结构笔记(3)——循环链表与双向链表_第1张图片
你现在的指针指向了3,但是你的任务是遍历一遍整个链表(例如要计算链表的长度),但是这是一个单项链表,所以只能够重新从头结点/第一个元素开始遍历。

这时候我们回想,如果能够直接从3开始,遍历到5,然后再遍历剩下的元素(1和2)就好了。

所以,我们需要一个能够指向回头结点的指针,构成一个循环链表:

数据结构笔记(3)——循环链表与双向链表_第2张图片
指回到头结点,那么当我们遍历到结点5的时候,就可以回到原点,遍历剩下的元素。

这种头尾相接的单链表就是单循环链表,简称循环链表。

循环链表通常都会有一个头结点,目的是为了使对第一个元素的处理和其他元素的操作保持一致(方便维护代码)。因此循环链表的结构如下:

数据结构笔记(3)——循环链表与双向链表_第3张图片
在变成循环链表之后,当遍历循环链表的结束调节就应该是看p->next是否等于头结点。同理,判断是否是空表的办法就是判断head->next==head,如果相等则表为空表。

指向头结点的指针叫头指针,相对的,就有指向最后一个元素的指针,叫做尾指针。

数据结构笔记(3)——循环链表与双向链表_第4张图片

尾指针的作用是方便查找第一个和第二个元素。我们用rear指代尾指针。

尾指针带来的另一个好处就是能够很方便地合并两个链表,例如:

数据结构笔记(3)——循环链表与双向链表_第5张图片
要合并上面两个链表很简单,只需要确保表B的next指向A的头结点,而A的最后一个元素的next指向B的第一个元素(没必要有两个头结点)。

数据结构笔记(3)——循环链表与双向链表_第6张图片

相当于代码:

p = rearA->next;
rearA->next = rearB->next->next;
rearB->next = p;
free(p);

简单地说,就是首尾相连:

数据结构笔记(3)——循环链表与双向链表_第7张图片
图片来源见图中网址

双向链表

需求:只能往一个方向遍历链表太不方便,所以需要多一个方向,能够反向遍历链表。

双向链表的定义:

typedef struct DulNode
{
    ElemType data;
    struct DulNode *prior;  // 指向前一个结点,前驱指针
    struct DulNode *next;   // 指向后一个结点,后继指针
}DulNode, *DuLinkList;

和单链表的区别只是多了个指向前驱结点的指针。

双向链表的空表:

数据结构笔记(3)——循环链表与双向链表_第8张图片
可以看到和单链表的空表还是很相似的。

如果是非空的:

数据结构笔记(3)——循环链表与双向链表_第9张图片
双向链表的前驱的后继等于后继的前驱,即

在这里插入图片描述

双向链表的插入操作

这里要注意顺序,首先上图:

数据结构笔记(3)——循环链表与双向链表_第10张图片

思路就是,先让待插入结点的前驱指针指向前一个元素,然后让待插入结点的后继指针指向后一个元素,然后才开始对原链表中的元素进行操作。首先让后继元素的前驱指针指向待插入元素s,然后让前驱元素的后继指针指向待插入元素s。大概是这样:

s->prior = p;			// 待插入结点的前驱指针指向前一个元素
s->next = p->next;		// 待插入结点的后继指针指向后一个元素
p->next->prior = s;		// 后继元素的前驱指针指向待插入元素s
p->next = s;			// 前驱元素的后继指针指向待插入元素s

p就是我们当前指针指向的位置。所以要先处理好p->next->prior,如果是先p->next = s那么就没办法改变s后继元素的前驱指针了。至于第一步和第二步其实顺序要求不是很大,可以交换顺序。

双向链表的删除操作

只需要两步:

  1. 前驱元素的后继指针指向后继元素
  2. 后继元素的前驱指针指向前驱元素

即:

数据结构笔记(3)——循环链表与双向链表_第11张图片

p->prior->next = p->next;
p->next->prior = p->prior;
free(p);

第一步和第二步的顺序无所谓,因为这期间p没变过。

总结

双向链表其实就是用空间换时间,因为要记录两个指针所以需要多一点空间,但是可以提高链表的时间性能。

参考

《大话数据结构》

你可能感兴趣的:(学习笔记,数据结构)