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