实参形参与指针5——无头结点循环链表与带头结点循环链表

前言

虽然C语言的指针偏向底层,不利于上手和大型项目开发(某些),但是用C去实现链表基本操作是有利于我们的代码功底的。而且在实现的过程中,实现远比理解要复杂困难的多,尤其是循环链表,当你实现过就会发现,为啥你的大学老师总是劝你要带头结点了。

双链表与单链表

双链表的基本操作与单链表几乎相同,但是双链表除了能够指向后继结点以外,还额外增加了一个指针域指向前驱结点。也就是说,对于一个已知双链表,当我们顺序查找到第x个结点时,我们很容易就能获得它的前驱结点和后继结点,相当于前驱后继可知(红框为可知范围);
双链表
但如果是单链表,获取后继结点和双链表一样简单,但是想要获得前驱结点,是要从头(指针)开始的,即后继可知但前驱不可知(红框为可知范围)。
单链表
但是凡是都是有代价的,就像顺序表和单链表之间的优劣一样,双联便在方便了寻找前驱的同时,却为插入、删除、输出结点带来了不少麻烦事,在文章后面会讲到。

双链表的数据结构

其实就是比单链表的结构体多了一个自引用指针作为前驱结点指针域,直接上代码。

typedef struct Node{
	int data;//数据域
	struct Node* prior;//前驱结点指针域
	struct Node* next;//后继结点指针域
}DuNode,*DLinklist;
//DuNode用于指代结点本身
//*DLinklist用于指代头指针本身,为了简化二级指针声明以及二级指针的操作和传值

双链表与循环双链表

单链表与单循环链表

在单链表中,如果仅仅是针对基本操作的算法实现,其实带不带头结点都是一样的,加之其指针域结构是单向性的,所以想实现循环也很简单,只需增加代码让p->next == NULL的结点的指针域指向头指针的下一个结点就可以了,无论带不带头结点(因为头结点也是头指针指向的第一个结点),即:

if(p->next == NULL){
	p->next = L->next;
}

以下为图示理解:
对比图
但需要注意,涉及以结点指针域为条件的循环的基本操作都会有所改变。这里以printAll函数为例(顺序打印所有结点的数据域内容),单链表的实现不在赘述(百度有很多或者可以看我之前发的代码实现),重点看一下循环单链表的实现方法,实现代码如下:

Status printAll(Linklist L) {
	Linklist p = L;
	if (p->next == NULL) {
		printf_s("没有元素可以打印。\n");
		return ERROR;
	}
	p = p->next;//提前在while循环外顺序移动一次指针,由于链表本身是循环的,头指针会参与判断,所以需要避免功能指针与头指针重合
	printf_s("当前链表为:");
	while (p->next != L->next) {//注意此处,不再是检查p->next是否为NULL。
		printf_s("%d ", p->data);
		p = p->next;//由于最开始p=L,所以要先将p指向下一个有意义结点,也就是p->next
	}
	printf_s("%d ", p->data);
	printf_s("\n");
	return OK;
}

当不带头结点时,循环单链表实现遍历打印就会比单链表麻烦的多,体现在几个方面:

  1. 循环链表的while循环判断条件有了改变,需要判断指针域是否指向了头指针指向的结点。也就是说,循环的加入也让头指针参与了条件判断
  2. 基于第一条,对于不含头结点的链表,其头指针指向下一结点可能为NULL,所以初始需要让p = L对链表合法性进行判断;而之后的循环有了头指针的参与,需要让p结点指向第一个结点,所以在while循环前要加上一句p = p->next先让p指针进行一次移动,以避免p和L的后继结点相同,也因此在while循环内的两条语句要互换位置。
  3. 同时,不具有头结点的循环单链表,在执行完while循环后还要再执行一次结点元素打印,这是因为p移动到末端结点时就已经满足while的循环判断就跳出循环了,即末端结点元素还没打印,所以要单独执行一次。
  4. 基于第二、三条,在循环单链表中,具有头结点就会比不带头结点的实现方式更简单,因为头指针永远会指向头结点而不是NULL,所以p指针可以在一开始就初始化为p = L->next去指向头结点,也因此不需要再考虑while循环前的p指针移动问题。

当然,这段代码可以通过两种方式优化,一种时将判断链表合法性单独形成一个函数,另一个是将while循环条件改为p->next != L->next->next,但是无论哪种都比带头结点的实现更复杂。

双链表与双循环链表

这部分都会以下面这段代码中的内容去讲解记录:

#define OK 1
#define ERROR 0

typedef int Status;
typedef int Elemtype;

typedef struct DNode {
	Elemtype data;
	struct DNode* prior;
	struct DNode* next;
}DuNode, *DuLinklist;

/*
初始化头指针
*/
Status initLinklist(DuLinklist &L) {
	L = (DuLinklist)malloc(sizeof(DNode));
	L->next = NULL;
	return OK;
};

/*
尾插法建立双链表
*/
Status tailInitDuLinklist(DuLinklist &L) {
	DuLinklist p = L;
	DuLinklist s;
	Elemtype num = 0;
	Elemtype TempData = 0;
	Elemtype count = 0;
	printf_s("创建的链表需要几个结点?请输入:\n");
	scanf_s("%d", &num);
	if (num <= 0 || L->next != NULL) {
		printf_s("输入个数不合法或双链表已经创建。\n");
		return ERROR;
	}
	printf_s("请连续输入数据域元素:\n");
	for (count = 1; count <= num; count++) {
		s = (DuLinklist)malloc(sizeof(DNode));
		scanf_s("%d", &TempData);
		s->data = TempData;
		s->prior = p;
		p->next = s;
		s->next = L->next;
		L->next->prior = s;
		p = s;
	}
	return OK;
};

/*
顺序打印双链表所有结点的数据域元素
*/
Status printAll(DuLinklist L) {
	DuLinklist p = L;
	if (p->next == NULL) {
		printf_s("链表为空,程序结束。\n");
		return ERROR;
	}
	p = L->next;
	while (p->next != L->next) {
		printf_s("%d", p->data);
		p = p->next;
	}
	printf_s("%d ", p->data);
	printf_s("打印结束。\n");
	return OK;
};

这里的代码是针对最复杂的情况,也就是不带头结点的双循环链表
其中printAll和不带头结点的单循环链表一样,因为也不涉及使用前驱结点指针域。而尾插法建立双循环链表就比较麻烦了,这是由于初始化的链表仅有一个指向NULL的头指针,在插入第一个结点时会和尾插法插入新元素产生歧义,毕竟对于第一个结点,它前面是个只有指向功能的头指针
由于清华严蔚敏的书和《大话数据结构》没有提供实现代码,且网络上的代码风格迥异,甚至会出现malloc函数都用不对就贴出来水贴的情况,所以只能自行解决。
我的解决方式是,把头指针当成只有next域有意义的结点,这样的话直接循环使用尾插法插入元素方法就可以了,相当于把头指针当成头结点本身去思考。
至于头插法,不带头结点的就很麻烦了,还需要先遍历一遍找到末端结点,每插入一个新节点都要修改一次末端结点的next指针域。从复杂度来说,压根就没必要看不带头结点的实现方式了。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值