数据结构:链表(C语言实现)

本文介绍了线性表、数组与链表的关系,线性表分为顺序表和链表。着重讲解了链表,它在逻辑上连续,物理上不一定连续,通过指针连接。详细阐述了单向链表和双向链表的结构定义、初始化、插入、删除等基本操作,还提及判断循环链表、偶数结点反转等特殊操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

介绍链表之前,我们还需要了解一下线性表、数组与链表的关系。

线性表是一种线性结构,它的特点是在数据元素的非空有限集中:

  • 存在唯一的一个被称作“第一个”的数据元素
  • 存在唯一的一个被称作“最后一个”的数据元素
  • 除第一个外,集合中的每个数据元素均只有一个前驱
  • 除最后一个外,集合中每个数据元素均只有一个后继

简单来谈就是数据元素“一个接一个的排列”,且由同一种类型的数据元素构成的线性结构就是线性表。而线性表又分为两种,顺序表和链表。

顺序表正如其名,是线性表的顺序存储结构,又被常称为向量(vector)如下图: 线性表的顺序储存结构
数据元素在内存的位置是相邻的,而顺序表的其中一种表达形式便是数组

线性表的链式存储结构——链表

不同于顺序表,链表在逻辑上是连续的,但在物理上不一定是连续的,而是通过指针完成每个内存空间的连接。在这里插入图片描述
链表在内存空间上不连续,为反映出各元素在线性表中的前后关系,除了存储元素本身的信息外,还需添加一个或多个指针域(如上图的next域)。指针域的值叫指针,又称作,它用来指示数据元素的存储首址。这两部分信息一起组成一个数据元素的存储映像,称存储映像为结点。上图中,head叫这个链表的头指针,指向链表的第一个结点,头指针是链表的必要元素,无论链表是否为空,头指针都不为空。

当链表的第一个节点的数据域无意义时,则这个结点叫做头节点,根据头节点存在与否,可将链表分为有头链表无头链表,一般情况下,有头链表的数据域可存放链表的长度。上图为有头链表。

而链表还有另一种分法,将链表分为单向链表(上图)、双向链表与循环链表。

单向链表

单向链表是最简单的链表形式,只能单向遍历,下面我们用C语言来实现无头结点单向链表的基本操作

结构定义

我们将结点定义成一个结构来表示,其中里面的数据类型用ElemType表示

// define element type
typedef int ElemType; 

// define struct of linked list
typedef struct LNode { 
	ElemType data; 
  	struct LNode *next; 
} LNode, *LinkedList;

// define Status
typedef enum Status { 
	ERROR,
	SUCCESS
} Status;
初始化

传入初始化接口的参数是链表的头指针(二级指针),事实上形参和实参分配的内存空间并不相同,如果说在这里传入的是一个一级指针,那么当调用这个接口时,会另外申请一个空间做指针,就不是拿原来的实参做操作了。使用二级指针能保证传入接口的是我们要操作的指针。

/**
 *  @name        : Status InitList(LinkList *L);
 *	@description : initialize an empty linked list with only the head node without value
 *	@param		 : L(the head node)
 *	@return		 : Status
 *  @notice      : None
 */
Status InitList(LinkedList *L)
{
	*L = (LinkedList)malloc(sizeof(LinkedList));		//给第一个结点申请空间
	if(!(*L))											//若申请失败返回错误
		return ERROR;
	(*L)->next = NULL;
	return SUCCESS;
}
摧毁链表
/**
 -  @name        : void DestroyList(LinkedList *L)
 - @description : destroy a linked list, free all the nodes
 - @param		 : L(the head node)
 - @return		 : None
 -  @notice      : None
 */
void DestroyList(LinkedList *L)
{
	LinkedList p;
	while(*L){
		p = (*L)->next;
		free(*L);
		*L = p;
	}
}
插入

插入方式分为前插法和后插法(下图),后插法更符合平常思维方式在这里插入图片描述

/**
 -  @name        : Status InsertList(LNode *p, LNode *q)
 - @description : insert node q after node p
 - @param		 : p, q
 - @return		 : Status
 -  @notice      : None
 */
Status InsertList(LNode *p, LNode *q)
{
	if(!p)					//如果p结点不存在,返回错误
		return ERROR;
	q->next = p->next;
	p->next = q;
	return SUCCESS;
}
删除

这里我们要把即将删除的结点数据储存到指针e所指向的内存空间,需要注意的是,在调用这个接口前,要给指针e赋予一个指向的内存空间,这也是新手们(包括我)经常犯的错误,导致q->data的数据没有内存空间存放而报错。
解决办法有:

  • 在传入指针之前,malloc一块内存空间
  • 传入ElemType变量的地址
/**
 *  @name        : Status DeleteList(LNode *p, ElemType *e)
 *	@description : delete the first node after the node p and assign its value to e
 *	@param		 : p, e
 *	@return		 : Status
 *  @notice      : None
 */
Status DeleteList(LNode *p, ElemType *e)
{
	if(!p && !p->next)
		return ERROR;
	LNode *q;
	q = p->next;
	*e = q->data;		//记得调用前检查e是否有指向一个已知的内存空间
	p->next = q->next;
	free(q);
	return SUCCESS;
}
遍历

遍历就没什么要讲的了,循环一次print一次数据数据

/**
 *  @name        : void TraverseList(LinkedList L, void (*visit)(ElemType e))
 *	@description : traverse the linked list and call the funtion visit
 *	@param		 : L(the head node), visit
 *	@return		 : None
 *  @notice      : None
 */
void TraverseList(LinkedList L, void (*visit)(ElemType e))
{
	LNode *p;
	p = L;
	while(p){
		visit(p->data);
		p = p->next;
	}
}

/**
 *  @name        : void visit(ElemType e)
 *	@description : print e
 *	@param		 : e
 *	@return		 : None
 *  @notice      : None
 */
void visit(ElemType e)
{
	printf("%d ",e);
}
搜索

其实和遍历的方法接近,经过遍历去比较数据中是否有e的值

/**
 *  @name        : Status SearchList(LinkedList L, ElemType e)
 *	@description : find the first node in the linked list according to e
 *	@param		 : L(the head node), e
 *	@return		 : Status
 *  @notice      : None
 */
Status SearchList(LinkedList L, ElemType e)
{
	LNode *p;
	p = L;
	while(p){
		if(p->data == e)
			return SUCCESS;
		else
			p = p->next;
	}
	return ERROR;
}
反转

反转的话比较有意思,它是通过定义三个指针——前中后,实现反转。中指针将指针指向前一个结点,前指针表示中指针的前一个结点,后指针原在中指针的下一个结点,用于控制前中指针往后移动。

/**
 *  @name        : Status ReverseList(LinkedList *L)
 *	@description : reverse the linked list
 *	@param		 : L(the head node)
 *	@return		 : Status
 *  @notice      : None
 */
Status ReverseList(LinkedList *L)
{
	if(!(*L) && !(*L)->next)
		return ERROR;
	LNode *pre, *cur, *Next;		//定义前中后三个指针
	pre = *L;
	cur = (*L)->next;				//先将第一个结点的指针域指向NULL
	pre->next = NULL;
	while(cur){
		Next = cur->next;			//后指针后移
		cur->next = pre;			//将中间的指针指向前一个结点
		pre = cur;					//前中指针后移
		cur = Next;
	}
	*L = pre;						//头指针指向“原尾结点”
	return SUCCESS;
}
判断是否为循环链表

单向链表和双向链表的尾结点指针域都指向了NULL,而循环链表的尾结点指针域却指向了第一个结点,如果对循环链表遍历的话如果没有限制次数,遍历不会停止。在这里插入图片描述
判断一个链表是否为循环链表也很简单,可以运用初高中“环形操场追及问题”的方法来解决,定义一个快指针和一个慢指针,通过循环,让快指针每次走两个结点,慢指针每次走一个结点,如果是循环链表,那总有一个时间会让两个指针出现在相同的位置(除开始点外的第一次相等)

/**
 *  @name        : Status IsLoopList(LinkedList L)
 *	@description : judge whether the linked list is looped
 *	@param		 : L(the head node)
 *	@return		 : Status
 *  @notice      : None
 */
Status IsLoopList(LinkedList L)
{
	LNode *fast, *slow;
	fast = slow = L;				//快慢指针在相同起点
	while(fast && fast->next){
		slow = slow->next;			//slow走一个结点
		fast = fast->next->next;	//fast走两个结点
		if(slow == fast)
			return SUCCESS;
	}
	return ERROR;
}
偶数结点反转

eg:1->2->3->4->5 变成 2->1->4->3->5
类似的反转就叫偶数结点反转,解决这个问题关键在分两类讨论:结点个数为奇数、结点个数为偶数。

/**
 *  @name        : LNode* ReverseEvenList(LinkedList *L)
 *	@description : reverse the nodes which value is an even number in the linked list, input: 1 -> 2 -> 3 -> 4  output: 2 -> 1 -> 4 -> 3
 *	@param		 : L(the head node)
 *	@return		 : LNode(the new head node)
 *  @notice      : choose to finish
 */
LNode* ReverseEvenList(LinkedList *L)
{
	LNode *pre, *cur, *Next;		//定义前中后指针,用途和反转相似
	pre = *L;
	*L = pre->next;
	while(pre && pre->next){
		cur = pre->next;
		Next = cur->next;
		if(cur->next && cur->next->next){			//如果cur->next不存在,结点个数为偶数
			pre->next = cur->next->next;			//如果cur->next->next不存在,结点个数为奇数
		}else{										//如果都存在,遍历未结束
			pre->next = cur->next;
		}
		cur->next = pre;							//将偶数结点反转
		pre = Next;
	}
	return *L;
}
搜索中间结点

搜索中间结点就是遍历两次,第一次计算结点个数,找出第几个结点是中间结点,第二次就是抓出中间结点的位置了

/**
 *  @name        : LNode* FindMidNode(LinkedList *L)
 *	@description : find the middle node in the linked list
 *	@param		 : L(the head node)
 *	@return		 : LNode
 *  @notice      : choose to finish
 */
LNode* FindMidNode(LinkedList *L)
{
	LNode *mid = *L;
	int cnt;
	for(cnt=0; mid; cnt++)
		mid = mid->next;
	mid = *L;
	for(int i=0; i<cnt/2; i++)
		mid = mid->next;
	return mid;
}

双向链表

双向链表区别于单向链表的地方在于一个结点有两个指针域,一个指向后节点,一个指向前结点,优势也比较明显,比如要计算链表中最后一次出现数字1的位置,单向链表需要先遍历一次找到第X个结点出现最后一次数字1,再遍历找到该结点位置,而双向链表可以直接从末尾开始遍历,可以大大节省搜索时间,然而缺点就是比单向链表占内存。
在这里插入图片描述

结构定义

和单向链表没什么差别,只是在结构中多出来一个指向前结点的指针

// define element type
typedef int ElemType;

// define struct of linked list
typedef struct DuLNode {
	ElemType data;
  	struct DuLNode *prior,  *next;
} DuLNode, *DuLinkedList;

// define status
typedef enum Status {
	ERROR,
	SUCCESS,
} Status;

双向链表的基本操作与单向链表相差不大,这里我就将代码一起发上来。

基本操作
/**
 *  @name        : Status InitList_DuL(DuLinkedList *L)
 *	@description : initialize an empty linked list with only the head node
 *	@param		 : L(the head node)
 *	@return		 : Status
 *  @notice      : None
 */
Status InitList_DuL(DuLinkedList *L)
{
	*L = (DuLinkedList)malloc(sizeof(DuLinkedList));
	if(!(*L))
		return ERROR;
	(*L)->next = NULL;
	(*L)->prior = NULL;
	return SUCCESS;
}

/**
 *  @name        : void DestroyList_DuL(DuLinkedList *L)
 *	@description : destroy a linked list
 *	@param		 : L(the head node)
 *	@return		 : status
 *  @notice      : None
 */
void DestroyList_DuL(DuLinkedList *L)
{
	DuLinkedList p;
	while(*L){
		p = (*L)->next;
		free(*L);
		*L = p;
		if(p)
			p->prior = NULL;
	}
}

/**
 *  @name        : Status InsertBeforeList_DuL(DuLNode *p, LNode *q)
 *	@description : insert node q before node p
 *	@param		 : p, q
 *	@return		 : status
 *  @notice      : None
 */
Status InsertBeforeList_DuL(DuLNode *p, DuLNode *q)
{
	if(!p)
		return ERROR;
	if(p->prior){
		p->prior->next = q;
		q->prior = p->prior;
	}
	q->next = p;
	p->prior = q;
	return SUCCESS;
}

/**
 *  @name        : Status InsertAfterList_DuL(DuLNode *p, DuLNode *q)
 *	@description : insert node q after node p
 *	@param		 : p, q
 *	@return		 : status
 *  @notice      : None
 */
Status InsertAfterList_DuL(DuLNode *p, DuLNode *q)
{
	if(!p)
		return ERROR;
	if(p->next){
		p->next->prior = q;
		q->next = p->next;
	}
	q->prior = p;
	p->next = q;
	return SUCCESS;
}

/**
 *  @name        : Status DeleteList_DuL(DuLNode *p, ElemType *e)
 *	@description : delete the first node after the node p and assign its value to e
 *	@param		 : p, e
 *	@return		 : status
 *  @notice      : None
 */
Status DeleteList_DuL(DuLNode *p, ElemType *e) {
	if(!p && !p->next)
		return ERROR;
	DuLNode *q;
	q = p->next;
	*e = q->data;
	q->next->prior = p;
	p->next = q->next;
	free(q);
	return SUCCESS;
}

/**
 *  @name        : void TraverseList_DuL(DuLinkedList L, void (*visit)(ElemType e))
 *	@description : traverse the linked list and call the funtion visit
 *	@param		 : L(the head node), visit
 *	@return		 : Status
 *  @notice      : None
 */
void TraverseList_DuL(DuLinkedList L, void (*visit)(ElemType e)) {
	DuLNode *p;
	p = L;
	while(p){
		(*visit)(p->data);
		p = p->next;
	}
}

/**
 *  @name        : void visit(ElemType e)
 *	@description : print e
 *	@param		 : e
 *	@return		 : None
 *  @notice      : None
 */
void visit(ElemType e)
{
	printf("%d ",e);
}

双向循环链表只是第一个节点的prior指针指向最后一个结点,最后一个结点的next指针指向第一个结点,其它与上述差别不大。这里就不展开叙述了。

参考:

  1. 算法与数据结构(C语言版) 主编·邓玉洁
  2. 数据结构:链表https://blog.youkuaiyun.com/juanqinyang/article/details/51351619
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值