目录
2.4 单向循环链表
单向循环链表的理解非常简单,操作跟普通链表操作基本上是一致的,只要针对循环特性稍作修改即可只是将单向链表的尾指针指向头结点即可为单向循环链表。
比如,单向链表变成循环链表的示意图如下所示:
在操作时,仅需将终止条件改成指向头结点即可
例如单向循环链表的初始化:
struct node *Init_node(){
list head = malloc(sizeof (struct node));
if(head == NULL) return NULL;
head->data = 0;
head->next = head;//指向头
return head;
}
head->next指向的结点不再是空,而是头结点,其他操作也是如此。
2.5 双向循环链表
双向循环链表逻辑上跟单向循环链表一致,但加入了前驱指针,使得对链表的操作更方便。因此,双向循环链表是在实际运用中是最常见的链表形态。
2.5 双向循环链表操作
双向循环链表的操作与基本的单向循环链表基本一致,逻辑上也基本一致。
- 节点设计
- 节点初始化
- 增删查改
- 链表遍历
- 销毁链表
2.5.1 双向循环链表结点设计
定义结构体:
在管理结构体中,多了前驱指针prev,可以指向上一个数据结点。
typedef struct node{
int data;//数据域
struct node *prev;//前驱指针
struct node *next;//后驱指针
}*list;
2.5.2 双向循环链表初始化
原理与单向循环链表类似,为头结点申请内存空间,将头结点的前驱和后继指针指向头结点,最后将头结点返回得到初始化的双向循环链表。
//初始化双向循环链表
list init_list(){
list head = malloc(sizeof (list));
head -> next = head;
head -> prev = head;
return head;
}
2.5.3 双向循环链表数据插入
双向循环链表的插入也分为头插法和尾插法,但多了对前驱指针的操作。
头插法:
//头插法
int insert_head(list head,int data){
//给插入的新结点申请内存
list p = malloc(sizeof (list));
if(p == NULL){
printf("malloc error!");
return -1;//申请内存失败
}
p -> data = data;
//联系新关系
p -> next = head -> next;
p -> prev = head;
//断开旧关系
head -> next = p;
head -> next -> prev = p;
return 1;//插入成功
}
注意:对链表进行插入操作时也要遵循上一篇操作单向链表的原则,即建立新关系,断开旧关系。
尾插法:
//尾插法
int insert_back(list head,int data){
//给插入的新结点申请内存
list p = malloc(sizeof (list));
if(p == NULL){
printf("malloc error!");
return -1;//申请内存失败
}
p -> data = data;
//联系新关系
p -> next = head -> next;
p -> prev = head;
//断开旧关系
head -> next = p;
head -> next -> prev = p;
return 1;
}
注意:尾插法进行插入数据操作时,不再需要循环遍历链表,因为加入了前驱指针,它可以指向尾结点,我们对尾结点插入数据即可。
2.5.4 双向循环链表删除操作
删除操作与单向链表类似,都需要循环遍历链表,只不过删除时多了对前驱指针的操作。
//删除数据结点
int del_node(list head,int data){
if(head == NULL){
printf("this list is null");
return -1;
}//判断链表是否为空
list pos = head;
while(pos ->next!= head && (pos ->next)-> data != data){
pos = pos -> next;
}//遍历查找需要删除的数据结点
if(pos ->next == head && (pos ->next)-> data != data){
printf("no this data\n");
return -1;
}//没找到数据结点则打印信息
else{
list temp = pos->next;
//找到数据结点后删除
pos->next = temp->next;
temp->next->prev = pos;
temp->next = NULL;
temp->prev = NULL;
//释放内存
free(temp);
return 1;
}
}
2.5.5 双向循环链表销毁链表
同理销毁链表也是遍历所有结点逐一删除即可。
//删除链表
void del_list(list head)
{
list cur = head->next;
list temp = NULL;
//遍历所有数据的节点,依次删除
while (cur!= head)
{
temp = cur;
cur = cur->next;
free(temp);
}
head->next = NULL;
head->prev = NULL;
//释放内存
free(head);
}
2.6 线性表总结
顺序表(Sequential List)
- 存储方式:顺序表通常使用数组来实现,元素在内存中连续存储。
- 访问时间:可以快速地通过索引访问任何元素,时间复杂度为O(1)。
- 插入和删除:在顺序表中插入或删除元素可能需要移动其他元素以维持连续性,平均时间复杂度为O(n)。
- 空间效率:顺序表需要预先分配固定大小的内存空间,可能存在空间浪费。
- 动态数组:为了解决固定大小的问题,动态数组可以自动调整大小,但调整大小时可能需要复制现有元素到新的内存位置。
链表(Linked List)
- 存储方式:链表由一系列节点组成,每个节点包含数据部分和指向下一个节点的指针。
- 访问时间:访问链表中的元素需要从头开始遍历,时间复杂度为O(n)。
- 插入和删除:在链表中插入或删除元素只需要改变相邻节点的指针,不需要移动其他元素,时间复杂度为O(1)。
- 空间效率:链表不需要预先分配固定大小的内存空间,可以更有效地利用内存。
- 类型:链表有多种类型,包括单链表、双向链表和循环链表等,每种类型有其特定的应用场景和优势。
比较
- 内存使用:顺序表通常需要更多的内存,因为它们需要连续的内存空间。链表则可以根据需要动态分配内存。
- 访问速度:顺序表提供更快的随机访问能力,而链表则需要顺序访问。
- 插入和删除操作:链表在插入和删除操作上通常更高效,因为不需要移动其他元素。
- 适用场景:顺序表适合随机访问频繁的场景,而链表适合插入和删除操作频繁的场景。