1.循环链表意义
为了使处理更加方便灵活,通过任意一个结点出发都可以找到链表中的其它结点,因此我们引入了循环链表。
2.定义
将单链表中终端结点的指针端由空指针改为头结点,就使整个单链表形成了一个环,这种头尾相接的单链表称为循环链表。
简单理解就是形成一个闭合的环,在环内寻找自己需要的结点。
在循环单链表中附设尾指针有时比附设头指针会使操作变得更简单。
3.图解
如在用头指针表示的循环单链表中,找开始结点a1的时间复杂度是O(1),然而要找到终端结点an,则需要从头指针开始遍历整个链表,其时间复杂度是O(n)。
如果用尾指针rear来表示循环单链表,则查找开始结点和终端结点都很方便,它们的存储位置分别是rear->next->next和rear,显然,查找时间复杂度都是O(1)。因此,实用中多采用尾指针表示循环单链表。
4.操作
带头结点的循环单链表的各种操作的实现算法与带头结点的单链表的实现算法类似,
差别仅在于算法中的循环条件不是p!=NULL或p->next!=NULL,而是p!=L或 p->next!=L。(L为头节点)
1.定义
typedef struct *Node{
int data;
struct Node* next;
} Node;
2.初始化
创建一个不带头节点的循环链表,并将头结点指向自己。
//初始化循环链表
Node *Initlist(){
Node* list=(Node*)malloc(sizeof(Node));
list->data=0;
list->next=list;//将末端指向开头
return list;
}
3.销毁
销毁整个循环链表
需要先销毁其他,最后特殊处理最后一个节点
void xiaohuiList(Node* L) {
Node* p = L->next; // 获取链表的第一个节点
while (p->next != L) { // 循环遍历链表,直到到达最后一个节点
Node* s = p->next; // 获取当前节点的下一个节点
p->next = s->next; // 将当前节点的 next 指针指向下一个节点的 next 指针,跳过当前节点
free(s); // 释放当前节点所占用的内存空间
}
// 最后一个节点特殊处理
Node* s = p->next; // 获取最后一个节点
p->next = L->next; // 将最后一个节点的 next 指针指向头结点的 next 指针,断开与头结点的连接
L = p; // 更新链表头指针为最后一个节点
free(s); // 释放最后一个节点所占用的内存空间
}
4.插入
1.头插法
首先使用malloc
函数动态分配内存空间来创建一个新的节点node
。如果内存分配失败(即node
为NULL
),则函数直接返回,不进行任何操作。
接下来,将传入的数据data
赋值给新节点的data
成员。然后将新节点的next
指针指向原链表的第一个节点(即list->next
)。接着将原链表的头结点list
的next
指针指向新节点,完成头部插入操作。
void HeadInsert(Node* list,int data){
Node* node=(Node*)malloc(sizeof(Node));
if (node == NULL) {
// 内存分配失败的处理,这里可以根据实际需求进行操作
return;
}
node->data=data;
node->next=list->next;
list->next=node;
}
2.尾插法
将传入的数据data
赋值给新节点的data
成员。然后通过一个循环找到链表的最后一个节点,将其next
指针指向头结点,完成尾部插入操作。最后,将原链表的尾节点的next
指针指向新节点,完成整个循环链表的尾部插入操作。
void TailInsert(Node* list,int data){
Node* node=(Node*)malloc(sizeof(Node));
if (node == NULL) {
// 内存分配失败,进行相应处理
return;
}
Node* head=list;
node->data=data;
while(list—>next!=head){
list=list->next;
}
node->next=head;
list->next=node;
}
5.遍历
//遍历
void Print_LinkList(Node* L)
{
Node *p=L->next;
printf("输出链表:");
while(p!=L)
{
printf("\t%d",p->data);
p=p->next;
}
printf("\n");
}
6.查找第i个节点
函数首先检查i是否大于链表的长度,如果是,则打印错误消息并返回NULL。然后,函数使用一个循环来遍历链表,直到找到第i-1个节点或到达链表的起始节点。如果找到了第i-1个节点,则返回该节点的指针;否则,打印错误消息并返回NULL。
int getLength(Node* head) {
int length = 0;
Node* current = head;
while (current != NULL) {
length++;
current = current->next;
}
return length;
}//链表长度
Node*FindINode(Node*L,int i){
if(i>LinkListLength(L)){
printf("没有相关的位置可插入!\n");
return NULL;
}
int j=1;
Node*p=L->next;
//这里使用j<i-1,主要是为了便于后面的插入操作,直接在p节点后面插入节点即可
while(p!=L&&j<i){
p=p->next;
j++;
}
if(p==L){
printf("插入的位置不合法!\n");
return NULL;
}
return p;
}
7.在第i个位置插入节点
调用FindINode函数来查找要插入新节点的前一个节点,并将其保存在变量p中。
使用malloc函数动态分配内存空间,创建一个新的节点s,并将数据成员data设置为参数e的值。
将新节点s的next指针指向前一个节点p的下一个节点。 将前一个节点p的next指针指向新节点s,从而将新节点插入到循环链表中。
打印一条消息"插入节点成功!",表示新节点已成功插入。
void InsertINode(Node*L,int i,int e){
Node*p=FindINode(L,i);
Node*s=(Node*)malloc(sizeof(Node));
s->data=e;
s->next=p->next;
p->next=s;
printf("插入节点成功!\n");
}
8.删除
void DeleteILNode(Node* L, int i, int e) {
Node* p = FindINode(L, i); // 找到要删除节点的前一个节点
Node* s = p->next; // 找到要删除节点
e = s->data; // 将要删除节点的数据保存到变量e中
p->next = s->next; // 将前一个节点的next指针指向要删除节点的下一个节点
free(s); // 释放要删除节点的内存空间
}
循环单链表有一些重要的性质:
1、从一个结点出发,无论这个结点位于链表的哪里,都可以找到其他任何一个结点,而单链表不行。
2、在一些情况下,我们需要频繁对链表的头部和尾部进行操作,此时使用循环单链表就很有用,
原因是对于单链表,已知头结点,想找到最后一个结点的话,时间复杂度是O(n);
但是对于循环单链表,我们可以一开始就把头指针指向尾结点,这样找到尾结点所需时间复杂度是O(1),因为尾结点的next指针总是指向头结点,所以找到头结点的时间复杂度也是O(1)。
因此,循环单链表在这种情况下的表现明显优于普通的单链表。