循环链表
从单链表升级到双链表之后,很多操作一下子就变得简单容易了。
但有一个很麻烦的问题还是没有解决:如何从任意结点出发通过遍历访问所有结点。
下面的循环链表就是这个问题的解决方案。
循环单链表
将表中尾结点的指针由 NULL 改为指向头结点,整个链表形成一个环。从表中任一结点出发均可找到链表中其他结点。
所以,循环单链表与非循环单链表的差异就是:
- 循环单链表没有 NULL 指针
- p 所指节点为尾节点的条件:
p->next == L
例 1
某线性表最常用的操作是:在尾元素之后插入一个元素和删除第一个元素,采用下列哪种存储方式最节省运算时间
A. 单链表
B. 仅有头结点指针的循环单链表
C. 双链表
D. 仅有尾结点指针的循环单链表
四种结构删除第一个元素的操作的时间复杂度都是 O(1),现在仅需考虑在尾结点后插入新元素的操作。要插入元素首先要找到尾结点
- 单链表遍历整个链表找到尾结点的时间复杂度为 O(n)
- 仅有头结点的指针的循环单链表,尽管首尾相连,但只有一个指向头结点的指针,仍要通过遍历链表来找到尾结点,插入的时间复杂度为 O(n)
- 双链表,尽管可以反向遍历,但首尾没有相连,仍需要遍历链表来找到尾结点,插入的时间复杂度也为 O(n)
- 仅有尾结点指针的循环链表找到尾结点的时间复杂度为 O(1)
void
listInsert( node **l, int e )
{
node *s;
s = ( node* )malloc( sizeof( node ) );
s->data = e;
s->next = (*l)->next;
(*l)->next = s;
*l = s;
}
void
listDelete( node *l )
{
node *t;
t = l->next;
l->next = l->next->next;
free( t );
}
插入 | 删除 | |
---|---|---|
单链表 | O(n) | O(1) |
仅有头结点指针的循环单链表 | O(n) | O(1) |
双链表 | O(n) | O(1) |
仅有尾结点指针的循环单链表 | O(1) | O(1) |
循环双链表
循环双链表:形成两个环。
- 没有 NULL 指针域
- p 所指结点为尾结点的条件:
p->next == L
- 由L可以直接找到尾结点:
L->prior
例 2
如果含有 n( n > 1 )个元素的线性表的运算只有 4 种
- 删除第一个元素
- 删除尾元素
- 在第一个元素前面插入新元素
- 在尾结点的后面插入新元素
最好使用下列哪个存储方式
A. 只有尾结点指针没有头结点的循环单链表
B. 只有尾结点指针没有头结点的非循环双链表
C. 只有首结点指针没有尾结点指针的循环双链表
D. 既有头指针又有尾指针的循环单链表
和上面的例子类似,抓住这些操作的本质,分析它们在对应的存储结构下工作的时间复杂度
1 | 2 | 3 | 4 | |
---|---|---|---|---|
A | O(1) | O(n) | O(1) | O(1) |
B | O(n) | O(1) | O(n) | O(1) |
C | O(1) | O(1) | O(1) | O(1) |
D | O(1) | O(n) | O(1) | O(1) |
void
headDelete( node **l )
{
node *t = *l;
(*l)->prior->next = (*l)->next;
(*l)->next->prior = (*l)->prior;
*l = (*l)->next;
free( t );
}
void
tailDelete( node *l )
{
node *t = l->prior;
l->prior->prior->next = l;
l->prior = l->prior->prior;
free( t );
}
void
listInsert( node *l, int e )
{
node *s;
s = ( node* )malloc( sizeof( node ) );
s->data = e;
s->next = l;
l->prior->next = s;
s->prior = l->prior;
l->prior = s;
}