数据结构 链表

目录

一.链表的概念

二.单链表

1.基础知识

2.代码实现

三.单循环链表

1.基础知识

2.代码实现

四.思考问题


一.链表的概念

顺序表的顺序存储结构的特点是逻辑关系上相邻的两个元素在物理位置上也相邻,因此可以随机存取表中的任一元素。但是,在做插入与删除操作时,需要移动大量元素,效率并不高。然而,链式存储结构只要求逻辑上相邻而物理上可以不相邻,因此也没有顺序存储的弱点,但同时也失去了顺序表可以随机访问的优点。

线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)因此,为了表示每个数据元素a(i)与其直接后继的数据元素a(i+1)之间的逻辑关系,对数据元素a(i)来说,除了存储其本身的信息之外,还需要一个指示其直接后继的信息(即直接后继的存储位置)。这两部分信息组成数据元素a(i)的存储映像,成为结点(node)。它包括两个域:其中存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称做指针或链。n个结点链结成一个链表,即为线性表(a1,a2,a3....,an)的链式存储结构。

二.单链表

1.基础知识

1.1是线性表的另一种表现形式:链式存储。

1.2特点:逻辑相邻,但是物理上不一定相邻。

1.3单链表一般分为两种实现方式:带头结点的和不带头结点的

1.3.1带头结点的单链表:只需要使用一级指针

1.3.2不带头结点的单链表:需要使用到二级指针

 

 带头结点的单链表的处理方式:额外申请一个辅助头结点,这时就变成了带头结点,按照后面所写代码的方式处理,最后释放掉这个辅助头结点即可。

2.代码实现

2.1头文件的设计

 单链表的结构体设计:

struct Node

{

ELEM_TYPE data;  //数据域

struct Node* next;  //指针域

}

#pragma once
//带头结点的单链表的结构体设计
typedef int ELEM_TYPE;

typedef struct Node {
	ELEM_TYPE data;   //数据域(保存数据的有效值)
	struct Node* next;//指针域(保存着下一个有效节点的地址)
}Node,*PNode;

//可实现的操作:
//初始化
void Init_list(PNode plist);

//头插
bool Insert_head(PNode plist,ELEM_TYPE val);

//尾插
bool Insert_tail(PNode plist, ELEM_TYPE val);

//按位置插
bool Insert_pos(PNode plist,int pos, ELEM_TYPE val);

//头删
bool Del_head(PNode plist);

//尾删
bool Del_tail(PNode plist);

//按位置删
bool Del_pos(PNode plist,int pos);

//按值删
bool Del_val(PNode plist, ELEM_TYPE val);

//查找  //查找到返回的是这个节点的地址
struct Node* Search(PNode plist, ELEM_TYPE val);

//判空
bool IsEmpty(PNode plist);

//清除
void Clear(PNode plist);

//销毁
void Destory1(PNode plist);

void Destory2(PNode plist);

//打印
void Show(PNode plist);

//获取有效值个数
int GetLength(PNode plist);

2.2初始化

//初始化
void Init_list(PNode plist) {
	//1.判断plist是否为NULL地址
	assert(plist!=NULL);
	//2.对plist指向的头结点里面的每一个成员变量进行赋值

	//3.因为头结点直接借用的是有效节点的结构体设计,省事但是多了一个数据域用不到,
	//既然头结点的数据域用不到,那就浪费掉,只用指针域即可
	plist->next = NULL;
}

2.3头插,尾插,按位置插

头插:先修改pnewnode的next域,在修改待插入结点的next域

//头插
bool Insert_head(PNode plist, ELEM_TYPE val) {
	//1.安全性处理  断言
	assert(plist!=NULL);
	//2.购买新节点
	struct Node* pnewnode = (struct Node*)malloc(sizeof(struct Node));
	assert(pnewnode!=NULL);
	pnewnode->data = val;
	//3.找到合适的插入位置(其实就是用柱子很指向插入位置上一个节点)
	//因为是头插,永远都是插在头结点后面,所以不用找,直接用plist即可

	//4.插入
	pnewnode->next = plist->next;
	plist->next=pnewnode;

	return true;
}

 尾插:用p->next是否为NULL 判断p的下一个结点是否存在(当p->next==NULL时,也就是p的下一个结点不存在,反过来解释就是,当前p指向的尾结点)

//尾插
bool Insert_tail(PNode plist, ELEM_TYPE val) {
	//1.安全性处理  断言
	assert(plist!=NULL);//判断plist指向的单链表是否存在

	//2.购买新节点
	struct Node* pnewnode = (struct Node*)malloc(sizeof(struct Node));
	assert(pnewnode!=NULL);
	pnewnode->data = val;

	//2.找到合适的插入位置(找到在哪一个结点插入,并用指针p指向这个结点)
	struct Node* p = plist;
	for (; plist->next != NULL; p = p->next);
	//3.插入
	pnewnode->next = p->next;
	p->next = pnewnode;

	return true;
	
}

 //按位置插:pos默认为0时,为头插

//按位置插
bool Insert_pos(PNode plist, int pos, ELEM_TYPE val) {
	//0.安全性处理  断言
	assert(plist != NULL);
	//1.购买新节点
	struct Node* pnewnode = (struct Node*)malloc(sizeof(struct Node));
	assert(pnewnode != NULL);
	pnewnode->data = val;
	//2.找到合适的插入位置(找到在哪一个结点插入,并用指针p指向这个结点)
	//  发现规律:pos=几  则让指针p从头结点开始向后走,走pos步即可
	struct Node* p = plist;
	for (int i = 0; i < pos;i++) {
		p = p->next;
	}

	//3.插入
	pnewnode->next = p->next;
	p->next = pnewnode;

	return true;
}

 重点:

这里for循环分为两种:

第一种:指针p指向头结点

struct Node *p=plist;

for(;p->next!=NULL;p=p->next);

第二种:指针p指向第一个有效结点

struct Node *p=plist->next;

for(;p!=NULL;p=p->next);

总结:

需要前驱的函数,用第一种for循环,例如插入,删除等;

不需要前驱的函数,用第二种for循环,例如判断有效值个数等;

4.头删,尾删,按位置删

删除结点:让待删除结点上一个结点的next域保存待删除结点的下一个结点的地址,将待删除结点跨越,然后释放待删除结点


//头删
bool Del_head(PNode plist) {
	//0.安全性处理,不仅需要判断头结点是否存在,还需要判断是否为空链表
	assert(plist!=NULL);
	if (IsEmpty(plist)) {
		return false;
	}
	//如果不是空链表,则代表至少有一个有效节点

	//1.申请一个临时指针p指向待删除结点
	struct Node* p = plist->next;//头删比较特殊,待删除结点就是第一个有效结点

	//2.在申请一个临时指针q指向待删除结点的上一个结点(前驱)
	
	//头删比较特殊,待删除结点的上一个结点为头结点,所以q不需要定义,直接使用plist即可

	//3.跨越指向
	plist->next = p->next;
	//4.释放临时指针p(待删除结点)
	free(p);

	return true;

}

//尾删
bool Del_tail(PNode plist) {
	//0.安全性处理  不仅仅判断头结点是否存在,还需要判断是否空链表
	//如果不是空链表,则代表至少有一个有效节点
	assert(plist->next!=NULL);
	//1.申请一个临时指针p指向待删除结点  p指向倒数第一个结点(尾结点)
	struct Node* p = plist;
	for (;p->next!=NULL;p=p->next);
	//2.申请一个临时指针q指向待删结点的上一个结点(前驱) q指向倒数第二个结点
	struct Node* q = plist;
	for (;q->next!=p;q=q->next);
	//3.跨越指向
	q->next = p->next;
	//4.释放待删除结点
	free(p);

	return true;
}

//按位置删
bool Del_pos(PNode plist, int pos) {
	//0.安全性处理
	assert(plist!=NULL);
	assert(pos>=0&&pos<GetLength(plist));

	//这里先找q,再找p
    //1.申请一个临时指针q指向待删除结点的上一个结点(前驱)
	struct Node* q = plist;
	for (int i = 0; i < pos;i++) {
		q = q->next;
	}
	//2.申请一个临时指针p指向待删除结点,将q的next给p
	struct Node* p = q->next;
	//3.跨越指向
	q->next = p->next;
	//4.释放
	free(p);

	return true;
}

2.5按值删,查找

//按值删
bool Del_val(PNode plist, ELEM_TYPE val) {
    //0.安全性处理
	assert(plist!=NULL);

	//1.先判断这个值是否存在与单链表之中
	struct Node* p = Search(plist,val);
	if (p == NULL) {
		return false;
	}
	//此时,p!=NULL,则代表val这个结点存在,且现在还不能被指针p指向
    //这时待删除结点找到了,则接下来需要找到待删除结点的上一个结点,用q指向
	struct Node* q = plist;
	for (;q->next!=p;q=q->next);
	//此时,指针q也找到了,则接下来需要找到待删除结点的上一个结点,用q指向

	//p和q现在都找到了,则跨越指向+释放
	q->next = p->next;

	free(p);

	return true;
}

//查找  //查找到返回的是这个节点的地址
struct Node* Search(PNode plist, ELEM_TYPE val) {
	//判断使用不需要前驱的for循环,只要将单链表遍历一遍即可
	
	for (struct Node* p = plist->next; p != NULL;p=p->next) {
		if (p->data == val) {
			return p;
		}
	}
	return NULL;
}

 2.6判空,清除,销毁,打印,统计字符个数

//判空
bool IsEmpty(PNode plist) {
	return plist->next == NULL;
}

//清除
void Clear(PNode plist) {
	//单链表里面的结点,用的时候买一个,不用的时候释放掉
	//所以清空函数,其实就是销毁
	Destory1(plist);
	//或者Destory2(plist);
}

//销毁

//销毁1  无限头删(只要单链表不空,则头删一个结点,无限循环,直到单链表空了)
void Destory1(PNode plist) {
	while (plist->next!=NULL) {
		struct Node* p = plist->next;
		plist->next = p->next;
		free(p);
	}
}

//销毁2
void Destory2(PNode plist) {
	//0.安全性处理
	assert(plist!=NULL);
	//1.定义两个指针p和q,p指向第一个有效节点,q先不要赋值
	struct Node* p = plist->next;
	struct Node* q = NULL;

	//2.断开头结点 因为不借助头结点,所以一开始就将头结点变成销毁完成的样子 
	plist->next = NULL;
	//3.两个指针合作,循环释放后续结点
	while (p != NULL) {
		q = p->next;
		free(p);
		p = q;
	}
}

//打印
void Show(PNode plist) {
	assert(plist != NULL);
	for (struct Node* p = plist->next; p != NULL; p = p->next) {
		printf("%d",p->data);
	}
	printf("\n");
}


//获取有效值个数
int GetLength(PNode plist){
	//0.安全性处理
	assert(plist != NULL);
	//1.先判断这个函数使用的那种for循环
	//判断得到,使用第二种for循环  也就是让指针p指向第一个有效节点
	int count = 0;
	for (struct Node* p = plist->next; p != NULL;p=p->next) {
		count++;
	}
	return count;
}

3.单链表和顺序表的比较

顺序表链表
底层实现连续存储的容器,在堆上分配内存动态内存,在堆上分配空间
空间利用率提前购买空间,空间大概率放不满,空间效率低插入一个数据,购买一个结点,不会造成浪费,空间效率高
查找元素

find  O(1)

单独的随机访问空间,因为可以利用到下标,所以是O(1)

find  O(n)
插入和删除

因为顺序表插入和删除需要挪动元素,所以只有尾部操(插入和删除)作才是O(1),其他都是O(n)

单链表的结点通过指针链接,不需要挪动元素,所以插入和删除的时间复杂度都是O(1)

 

 顺序表和单链表的适用场景分析:

1.结点个数大概能预估出来,就使用顺序表,预估不出来就使用单链表

2.如果经常要使用插入和删除操作,就使用单链表,因为不需要挪动元素

3.如果只使用尾插和尾删,也可以考虑顺序表 

4.如果经常要访问元素,可以考虑使用顺序表,因为顺序表可以通过下标进行随机访问

三.单循环链表

1.基础知识

      循环链表(circular linked list)是另一种形式的链表存储结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。由此,从表中任一结点出发均可找到表中其他的结点。

       循环链表的操作和线性链表基本一致,差别仅在于循环条件不是p或者p->next是否为NULL,而是他们是否为头指针。

 

2.代码实现

2.1头文件的设计

单循环链表的结构体设计:

struct CNode

{

ELEM_TYPE data;//数据域

struct CNode *next;//指针域

}

#pragma once

typedef int ELEM_TYPE;

typedef struct CNode  {
	ELEM_TYPE data;
	struct CNode* next;
}CNode,*PCNode;

//初始化
void Init_clist(PCNode pclist );

//头插
bool Insert_head(PCNode pclist,ELEM_TYPE val);

//尾插
bool Insert_tail(PCNode pclist, ELEM_TYPE val);

//按位置插
bool Insert_pos(PCNode pclist,int pos, ELEM_TYPE val);

//头删
bool Del_head(PCNode pclist);

//尾删
bool Del_tail(PCNode pclist);

//按位置删
bool Del_pos(PCNode pclist,int pos);

//获取有效值个数
int Get_length(PCNode pclist);

//按值删
bool Del_val(PCNode pclist,ELEM_TYPE val);

//查找
struct CNode *Search(PCNode pclist,ELEM_TYPE val);

//判空
bool IsEmpty(PCNode pclist);

//清除
void Clear(PCNode pclist);

//销毁
void Destroy1(PCNode pclist);

void Destroy2(PCNode pclist);

//打印
void Show(PCNode pclist);

2.2初始化

如果没有一个有效结点,那么头结点的next域应该指向自己

//初始化
void Init_clist(PCNode pclist) {
	//判断pclist是否为空链表
	assert(pclist!=NULL);
	//对pclist指向的头结点里面的每一个成员进行赋值
	//头结点的数据域不使用,只需要对指针域赋值即可,赋值为自身地址
	pclist->next = pclist;
}

2.3 尾插

通过判断,需要使用需要前驱的for循环,也就是说,申请一个临时指针p,指向头结点

//尾插
bool Insert_tail(PCNode pclist, ELEM_TYPE val) {
	//安全性处理
	assert(pclist!=NULL);
	//购买新结点
	struct CNode* pnewcnode = (struct CNode*)malloc(sizeof(struct CNode));
	assert(pnewcnode!=NULL);
	pnewcnode->data = val;
	//找到插入位置  (也就是说找到在哪一个结点后面插入) 
	//尾插,找到尾结点,用指针p指向
	//通过判断,确实使用需要前驱的for循环,也就是说,申请一个临时指针p,执行头结点
	struct CNode* p= pclist;
	for (;pclist->next!=pclist;p=p->next);

	//直接插入
	pnewcnode->next = p->next;
	p->next = pnewcnode;

	return true;
}

2.5销毁

//销毁2  不借助头结点,有两个辅助指针
void Destroy2(PCNode pclist) {
	assert(pclist!=NULL);
	if (IsEmpty(pclist)) {
		return;
	}
	//如果在定义指针q的时候,直接赋值为p->next  则一定要保证指针p存在
	struct CNode* p = pclist->next;
	struct CNode* q = pclist;
	//将头结点断开
	pclist->next = pclist;

	while (p!=pclist) {
		q = p->next;
		free(p);
		p = q;
	}
}

 由于,单循环链表和单链表代码实现几乎一样,这里就只写出几个有区别的例子。

四.思考问题

1.单链表如何逆置?(单链表如何获取到着打印的数据)

如果需要逆置,至少存在两个有效结点

两种方式:

1)无线头插   需要使用两个指针

//逆置单链表 方法1:无限头插,较为简单,需要使用两个指针,所有同学必须掌握的方法
void Reverse(struct Node *plist)
{
	assert(plist != NULL);
	int length = GetLength(plist);//保存最少有两个有效节点,才需要去逆置
	if(length < 2)
	{
		return;
	}

	//1.申请两个指针p和q,分别指向第一个有效节点和第二个有效节点
	struct Node *p = plist->next;
	struct Node *q = p->next;

	//2.断开头结点
	plist->next = NULL;

	//3,通过p和q配合,将所有节点依次头插进来
	while(p != NULL)
	{
		q = p->next;
		p->next = plist->next;
		plist->next = p;
		p = q;
	}

}

2)不借助头结点,需要使用三个指针,互相搭配处理

//逆置单链表 方法2:不借助头结点,需要使用三个指针,较难
void Reverse2(struct Node *plist)
{
	assert(plist != NULL);
	int length = GetLength(plist);//保存最少有两个有效节点,才需要去逆置
	if(length < 2)
	{
		return;
	}

	//1.申请三个指针p和q和r,分别指向第一个有效节点和第二个有效节点以及第三个有效节点
	struct Node *p = plist->next;
	struct Node *q = p->next;
	struct Node *r = NULL;


	//2.将p指向的节点的next域提前置为NULL(p指向的第一个节点,一旦逆置结束,就是尾结点,所以next域提前置为NULL)
	p->next = NULL;

	//3.在不使用头结点的情况下,将所有节点的next域反转
	while(q != NULL) //只能用q
	{
		r = q->next;
		q->next = p;
		p = q;
		q = r;
	}

	//4.将已经逆置结束的单链表的有效节点,重新让头结点指向回来
	plist->next = p;

}

2.如何判断两个单链表是否存在交点?如果存在交点,则找到第一个相交点

问题:如何判断两个单链表是否存在交点

解决方法:

分别申请指针p和指针q,让指针p跑到单链表1的尾结点处,让指针q跑到单链表2的尾结点处,然后判断p和q指向的尾结点是否用一个结点即可。

问题:如果存在交点,则找到第一个相交点

解决方法:

先获取两条单链表的各自长度,用len1和len2保存。然后,将指针p指向较长的单链表的头结点;将指针q指向较短的单链表的头结点。然后,让指针p提前出发,向后走|len1-len2|步,则这时,指针p和指针q分别对于尾结点的距离相等,则这时,p和q同步向后走,每走一步,判断一次,是否指向同一个结点,如果是,则找到了第一个相交点

//判断两条单链表是否相交,如若相交,则找到第一个相交点
struct Node* Is_intersect(struct Node *plist1, struct Node *plist2)
{
	//assert plist1 plist2
	struct Node *p = plist1;
	struct Node *q = plist2;
	for(; p->next!=NULL; p=p->next);
	for(; q->next!=NULL; q=p->next);

	if(p != q)
	{
		return NULL;
	}

	//反之,存在相交点
	int len1 = GetLength(plist1);
	int len2 = GetLength(plist2);
	p = len1>len2 ? plist1 : plist2;
	q = len1>len2 ? plist2 : plist1;

	for(int i=0; i<abs(len1-len2); i++)
	{
		p = p->next;
	}
	//此时,p和q,相较于尾结点的长度是一致的,这时,依次判断即可

	while(p != q)//p和q同步向后走,如果p和q还没相遇,同时向后走一步,直到相遇,相遇点就是第一个相交点
	{
		p=p->next;
		q=q->next;
	}

	return p;//return q;
}

3.判断一个单链表是否存在一个环?如果存在则找到入环点

经典方法:用到快慢指针,即两个指针,一个走得快一个走得慢 

验证方式:让快慢指针同时指向头结点,然后同步向后走,快指针一次走两步,满指针一次走一步;

当单链表不存在环时,则快指针肯定不会和慢指针二次相遇,且快指针一定在慢指针之前先指向NULL;

当单链表存在环时,则快指针会先于慢指针进入环内,但是由于快指针在环内出不来,一直打转,则当慢指针也进入到环内的时候,在环内会与慢指针二次相遇

 

 找到入环点:

 申请两个指针p和q

让p指向头结点,让q指向快慢指针相交点

接下来,如果p和q相遇,则相遇点就是入环点

//3.2 若存在环,则找到入环点
struct Node* Is_Circle_FirstNode(struct Node *plist)
{
	//0.安全性处理
	assert(plist != NULL);
	if(IsEmpty(plist))
	{
		return NULL;
	}

	//1.申请两个指针fast 和 slow
	struct Node *fast = plist;
	struct Node *slow = plist;

	slow = slow->next;
	fast = fast->next;
	if(fast != NULL)
	{
		fast = fast->next;
	}

	//2.同步向后走
	while(fast!=slow && fast!=NULL)//快慢指针还没有二次相遇且快指针也没有指向NULL
	{
		//快指针一次走两步,慢指针一次走一步
		slow = slow->next;
		//fast = fast->next->next; //error 
		//注意:1.快指针不要一次将两步走完,因为第一步能不能走踏实还是个问题
		//      2.两步可以分开走,先走一步,走踏实了,再走一步
		fast = fast->next;
		if(fast != NULL)
		{
			fast = fast->next;
		}
	}

	//3.while循环退出,注意退出条件:fast!=slow && fast!=NULL
	//      如果是因为快慢指针相遇,则代表存在环,则接着要判断入环点
	//      如果是快指针先指向了NULL,则代表不存在环,则直接退出函数
	if(fast == NULL)
	{
		return NULL;
	}

	//如果上面if没有进去,则代表有环,接下来需要找入环点
	//4.找入环点,申请p和q,p指向头结点,q指向快慢指针相遇点
	struct Node *p = plist;
	struct Node *q = fast; //q=slow

	while(p != q)//如果p和q还未相遇,则保持同样速度,向后走
	{
		p = p->next;
		q = q->next;
	}

	//当while循环退出的时候,则p和q相遇,且相遇点就是我们要找的入环点
	return p; //return q;
}

4.单链表,给一个随机结点的地址,如何在O(1)时间内,删除这个结点(这个随机结点不能是尾结点)?

方法:狸猫换太子/借尸还魂

操作:删除下一个结点代替待删除结点,将待删除结点的值修改为下一个结点的值

优点:速度快,不需要头结点

局限性:尾结点不行,因为尾结点后面没有结点代替待删除结点被删除

//删除地址为del_node的结点
bool Del_Rand_Node(struct Node *del_node)
{
	//assert

	//狸猫换太子
	if(del_node->next == NULL)//狸猫得存在
	{
		return false;
	}

	//狸猫换太子
	del_node->data = del_node->next->data;//将狸猫化妆为太子
	struct Node *p = del_node->next;//让p指向狸猫
	del_node->next = p->next;//跨越指向
	free(p);//释放待删除节点

	return true;
}
  • 12
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值