【数据结构-C语言】双向循环链表

1、概念

对链表而言,双向均可遍历是最方便的,另外首尾相连循环遍历也可打打增加链表操作的便捷性。因此,双向循环链表,是在实际运用中最常见的链表形态。

 2、基本操作

与普通的链表完全一致,双向循环链表虽然指针较多,但逻辑是完全一样。基本的操作包括“

1、结点设计

2、初始化空链表

3、增删结点

4、链表遍历

5、销毁链表

3、实现代码

双向链表的结点只是比单向链表多了一个前向指针

typedef struct node //取别名
{

    //数据域
    int data;

    //指向相邻结点的双向指针
    struct node* prev;    
    struct node* next;

}node;

设计完节点之后,就要初始化。所谓初始化,就是构建一条不含有效节点的空链表。

以带头节点的双线循环链表为例,初始化后,其状态如下图所示:

//初始化双向循环链表
node* init()
{
    //申请头节点
    node* head = malloc(sizeof(node));

    //让首尾互相指向
    //不需要对头结点的数据域data进行和操作
    if(head != NULL)
    {
        head->prev = head;
        head->next = head;
    }

    return head;
}

初始化链表之后,就可以新建节点,然后进行头插或者尾插(即将新节点插入到链表的首部或者尾部),示例代码为:

//创建新节点
node* newNode(node* head,int data)
{
    //开辟堆区空间
    node* new = malloc(sizeof(node));
    if(new != NULL)
    {
        new->data = data;
        new->prev = new;
        new->next = new;
    }

    return new;
}


//头插法:将新节点插入到链表的头部
void insertHead(node* head,node* new)
{
        new->next = head->next;
        new->prev = head;

        head->next = new;
        head->next->prev = new; 
}


//尾插法:将新节点插入到链表的尾部
void insertTail(node* head,node* new)
{
        new->next = head;
        new->prev = head->prev;


        head->prev->next = new;
        head->prev = new;
}

看着代码可能有些难理解,所以直接上图:

对初始化之后的循环双向链表执行过插入节点动作之后,我们还可以剔除掉循环双向链表中的一个节点,注意,从链表中将一个节点剔除出去,并不意味着要释放节点的内容。当然,我们经常在剔除了一个节点之后,紧接着的动作往往是释放它,但是将“剔除”与“释放”两个动作分开,是最基本的函数封装的原则,因为它们虽然常常连在一起使用,但它们之间并无必然联系,例如:当我们要移动一个节点的时候,实质上就是将“剔除”和“插入”的动作连起来,此时就不能释放该节点了。

//查找某数据是否存在在链表中
node* findNode(node* head, int data)
{
    node* p;
    for (p = head->next; p != head; p = p->next)
    {
        if (p->data == data)
        {
            return p;
        }
    }
}


// 将指定节点p从链表中剔除
void remove(node *head, node *p)
{
    p->prev->next = p->next;
    p->next->prev = p->prev;

    p->prev = NULL;
    p->next = NULL;
}

//判断双向循环链表是否为空
bool isEmpty(node* head)
{
    return head->next == head;    //最简便的方式,如果链表为空,则头结点的下一个节点依然是自身
}

然后需要封装一个遍历链表的函数来进行代码测试,遍历就存在向前遍历和向后遍历

// 向前遍历
void listForEachPrev(node * head)
{
    if(isEmpty(head))
        return;

    for(node * tmp=head->prev; tmp!=head; tmp=tmp->prev)
    {
        printf("%d\n", tmp->data);
    }
}

// 向后遍历
void listForEach(node * head)
{
    if(isEmpty(head))
        return;

    for(node * tmp=head->next; tmp!=head; tmp=tmp->next)
    {
        printf("%d\n", tmp->data);
    }
}

我们使用完循环双向链表之后,需要销毁链表。由于链表中的各个节点被离散地分布在各个随机的内存空间,因此销毁链表必须遍历每一个节点,释放每一个节点。注意:销毁链表时,遍历节点要注意不能弄丢相邻节点的指针。不销毁掉链表可能会造成内存泄漏,程序崩溃等情况。

//销毁链表
//第一种写法
void destroy(node * head)
{
    if(isEmpty(head))
        return;

    node *n;
    for(node *tmp = head->next; tmp!=NULL; tmp=n)
    {
        n = tmp->next;
        free(tmp);
    }
}

//第二种写法
node* destroy(node* head)
{
    node* p;
    for(p=head->next;p != head;p = head->next)
    {
        remove(p);
        free(p);
    }
    //释放头节点
    free(head);
    return NULL;
}

4、适用场合

经过单链表、双链表的学习,可以总结链表的适用场合:

  • 适合用于节点数目不固定,动态变化较大的场合
  • 适合用于节点需要频繁插入、删除的场合
  • 适合用于对节点查找效率不十分敏感的场合

5、最后附上我的主函数测试代码

int main()
{
	node* head = initList();
	if (head)
	{
		printf("初始化空链表成功!\n");
	}
	else
	{
		printf("初始化空链表失败!\n");
	}

	//在链表的头部插入一些节点
	for (int i = 1; i <= 5; i++)
	{
		node* new = newNode(i);
		insertHead(head, new);
	}
	show(head);

	在链表的尾部插入一些节点
	//for (int i = 1; i <= 5; i++)
	//{
	//	node* new = newNode(i);
	//	insertTail(head,new);
	//}
	
	//输入要删除的节点当中的数值
	int n;
	while (1)
	{
		scanf("%d", &n);
		if (n == 0)
		{
			break;
		}
		node* p = findNode(head, n);
		if (p != NULL)
		{
			removeNode(p);
			free(p);
		}
		else
		{
			printf("没有你想删除的节点!\n");
		}
		show(head);
	}

	//向前输出每个节点数据
	listForEachPrev(head);

	//向后输出每个节点数据
	listForEach(head);

	//销毁整条链表
	head = destroy(head);

	return 0;
}

  • 3
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值