C语言单链表(超级无敌详细,408考研同样适用)

单链表

顺序表的存储位置可以用一个简单直观的公式表示,它可以随机存取表中任意一个元素,但插入和删除操作需要移动大量元素。链式存储线性表时,不需要使用地址连续的存储单元,即不要求逻辑上相邻的元素在物理位置上也相邻,它通过“链”建立起元素之间的逻辑关系,因此插入和删除操作不需要移动元素,而只需修改指针,但也会失去顺序表可随机存取的优点。

请添加图片描述
请添加图片描述

1 定义链表结构

线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表结点,除存放元素自身的信息外,还需要存放一个指向其后继的指针。单链表结点结构如下代码所示,其中 data 为数据域,存放数据元素;next为指针域,存放其后继结点的地址

定义一个结构体来实现单链表
typedef 关键字的作用相当于起别名

有关结构体的定义,可以看看我的这篇博客c++结构体的定义

c语言也适用

typedef struct LNode {
	int data;               //单链表节点类型,即存放的数据类型
	struct LNode* next;     //指针指向下一个节点
}LNode,*LinkList;
//等价于
//struct LNode
//{
//	int data;
//	struct LNode* next;
//};
//typedef struct LNode LNode;    LNode是struct LNode的别名
//typedef struct LNode* LinkLIst;  LinkLIst 是struct LNode*的别名
//别名与本名功能一模一样,都可以用来定义变量,效果一样
//这里注意 Lnode* 与LinkList是等价的

别名与本名功能一模一样,都可以用来定义变量,效果一样
这里注意 Lnode* 与LinkList是等价的

2 链表的初始化

bool InitalList(LinkList& L){
	L = (LNode*)malloc(sizeof(LNode));
	if (L == NULL)return false;  //内存不足,申请失败

	L->next = NULL;//头结点之后还没有新的结点,所以传空
                  //会有人疑问为什么没有给头结点的data赋值
	    //因为头结点不参与保存数据,只是为了方便操作链表额外添加           的虚拟头结点

	return true;

} 

3 头插法创建单链表

因为每次创建新结点的时间复杂度是O(1),所以总时间复杂度为O(n)

看图

数字代表代码运行的先后步骤
请添加图片描述

大致思路就是,每一次新创建的结点,都让他指向上一次的结点,即每次创建的结点都是最新的头指针,最终发现是一个反向的链表,先存入的数据会在末尾,最晚存储的数据是头指针

因为L->next实际指向的就是上一次的结点,所以我们让s->next指向L->next

达到了让新结点指向上一次结点的功能

然后将L->next指向s 达到刷新的效果,让L->next始终指向的是上一次的结点

到最后因为没有新结点,L->next指向的就是头指针,L自己就是头结点

整个过程中L是没有变化的,发生变化的一直是L->next;

void BuildListHead(LinkList& L) {
	LNode* s=L;  
	
	int x;   //输入的值 规定当输入-1时结束创建
	scanf_s("%d", &x);
	while (x != -1) {
		 s = (LNode*)malloc(sizeof(LNode));  //为链表申请新空间
		 s->data = x;
		 s->next = L->next;
		 L->next = s;
		 scanf_s("%d", &x);

	}
}

注意头插法是有缺陷的,它的存入顺序和链表节点是相反的,我们使用时可能会不方便,所以我们通常使用接下来介绍的尾插法

4 尾插法创建单链表

在这特意感谢王学姐对尾插法的指点

同头插法因为每次创建新结点的时间复杂度是O(1),所以总时间复杂度为O(n)

看图

请添加图片描述

思路是,我们选择s为新来的结点,让r作为链表每次的尾节点,新创建一个结点,就让 r->next=s 来指向新结点,然后让 r=s 使新节点成为尾节点

r->next=s 的功能是实现了链表在地址上的链接指向,使每次的尾节点都指向新的结点

r=s 的功能实现了,让每次的新节点都作为新的尾节点

void BuildListTail(LinkList& L) {
	LNode* s, * r = L;
	int x;
	scanf_s("%d", &x);
	while (x != -1) {
		s = (LNode*)malloc(sizeof(LNode));

		s->data = x;
		r->next = s;
		r = s;
		scanf_s("%d", &x);
	}
	r->next = NULL;   //将尾节点指向空结点
}

5 链表的查找

LinkList 强调这是一个链表
LNode* 强调返回一个节点
实际上这两种方式完全等价

代码功能很简单,就是遍历输出
但是要注意边界问题以及空指针的问题,增强代码的健壮性

LNode* GetElem(LinkList L, int i) {  //i是要找的第i个节点

	int j = 1;  //从第一个节点开始遍历

	LNode* p = L->next;

	if (i == 0)  return L;
	if (i < 0) {

		printf("第%d个节点不存在\n",i);  //说明i不合法
		return NULL;
	}  
	while (p != NULL && j < i) {   //当i==j时跳出循环
		p = p->next;
		j++;
	}
	if(i==j) return p;
	else {
		printf("第%d个节点不存在\n", i);  
		return NULL;
	}
}

6 插入操作

1 按位序插入 带头结点

​ 在第i个节点插入元素e

请添加图片描述

以图示为例,在第二个结点插入e

先遍历找到第i-1个结点,也就是第一个结点,先将e->next=a2,然后将i-1处的结点a1->next=e;,实现了链表的连接插入,注意这两步是不能颠倒顺序的

bool ListInsert(LinkList& L, int i, int e) {
	 
	if (i < 1) {
		printf("插入位置不合法");  //增强代码的健壮性
		return false;
	 }
	              //指针p指向当前扫描到的节点
	LNode* p=L;  // 现在指针p指向头结点,头结点是不保存数据的,即第0个节点
	             
	int j = 0;  //j表示p指针当前指向的是第几个节点

	while (p != NULL && j < i - 1) {   //循环找到第i-1个节点
		p = p->next;
		j++;
	}

	if (p == NULL)
		return false;  //i不合法  //增强代码的健壮性

	LNode* s = (LNode*)malloc(sizeof(LNode));
	s->data = e;
	s->next = p->next;  //这两句不能颠倒顺序
	p->next = s;       //若是颠倒顺序,p->next还没有使用就被更新了
	return true;

}


2 不带头结点的插入方法

他与带头结点的区别在于在第一个位置插入时需要更头结点
其他功能与带头结点的相同,为了节省代码量,这里不在展出,只展示核心代码

只展示有区别的在第一个节点位置的插入

 bool ListInsert(LinkList& L, int i, int e) {
  //在第一个位置插入e
	if (i == 1) {
		LNode* s = (LNode*)malloc(sizeof(LNode));
		s->data = e;

		s->next = L;  //让s的next指向头指针
		L = s;       //然后将头指针改为s
		             //这样就实现了将s插入到第一个位置
		return true;
	}

}

7 指定结点的前插操作

你会发现,你是没办法找到给定节点的前驱节点
你可以选择从头结点挨个开始遍历,但是效率很低,而且若是不给头结点,则直接无法实现功能
所以,你可以采取下面的新方法
复制法 ----- 偷天换日大法
将给定结点的数据赋值给你新申请的节点,将要插入的节点复制给给定结点,实现数据错位的效果

即结点没有移动,但是结点内保存的数据发生了移动

bool InsertPriorNode(LNode* p, int e) {
	if (p == NULL)
		return false;

	LNode* s = (LNode*)malloc(sizeof(LNode));
	s->data = p->data;
	s->next = p->next;  //将p的所有信息复制给s  

	p->data = e;
	p->next = s;   //将要插入的节点复制给p
                   //俗称偷天换日大法
	return true;
}

8 按位序删除(带头结点)

代码的逻辑与 6 插入操作很相似

bool DeleteList(LinkList& L, int i,int& e) {

	if (i < 1) {
		printf("删除的位置不合法");  //增强代码的健壮性
		return false;
	}
	//指针p指向当前扫描到的节点
	LNode* p = L;  // 现在指针p指向头结点,头结点是不保存数据的,即第0个节点

	int j = 0;  //j表示p指针当前指向的是第几个节点

	while (p != NULL && j < i - 1) {   //循环找到第i-1个节点
		p = p->next;
		j++;
	}

	if (p == NULL||p->next==NULL)
		return false;  //增强代码的健壮性

	 LNode* q = p->next; //q结点创建的作用是为了能释放掉这一段内存空间

	   e = p->next->data;  //用e可以记录下被删除的结点保存的值,不过我觉着这一步没什么用

	   p->next = p->next->next ;// 覆盖掉p->next达到实现删除的效果,虽然有点掩耳盗铃,但是效果有了

	
	
	 free(q);//释放掉没用的空间

	return true;

}

9 删除给定结点

时间复杂度O(1)

思路同偷天换日大法
但是有如果要删除最后一个结点,这个代码是实现不了的,只能从表头开 始循环遍历
我们将给定节点的后继结点的信息复制给给定结点,然后释放掉后继节点,这样就实现了给定结点的删除

实际上是保护了后继结点的数据,覆盖了当前结点的信息,来达到实现删除的效果

如果是删除给定结点的后继,传参数的时候可以传给定结点的下一个结点,即传p->next

bool DeleteNode(LNode*p){
	if (p == NULL||p->next)return false;
	//如果p是最后一个结点,那么p->next==NULL ,p->next->next是不存在的
	LNode* q = p->next;  //q结点创建的作用是为了能释放掉这一段内存空间
	 p->data = p->next->data;
	p->next = p->next->next;
	  free(q);
	return true;
 }

10 完整代码

#include<stdio.h>
#include<stdlib.h>

// 本博客不特殊指出,默认带头结点

// 1 定义一个结构体来实现单链表
//   typedef 关键字的作用相当于起别名
typedef struct LNode {
	int data;               //单链表节点类型,即存放的数据类型
	struct LNode* next;     //指针指向下一个节点
}LNode,*LinkList;
//等价于
//struct LNode
//{
//	int data;
//	struct LNode* next;
//};
//typedef struct LNode LNode;    LNode是struct LNode的别名
//typedef struct LNode* LinkLIst;  LinkLIst 是struct LNode*的别名
//别名与本名功能一模一样,都可以用来定义变量,效果一样
//这里注意 Lnode* 与LinkList是等价的


// 2 链表的初始化
bool InitalList(LinkList& L){
	L = (LNode*)malloc(sizeof(LNode));
	if (L == NULL)return false;  //内存不足,申请失败

	L->next = NULL;//头结点之后还没有新的结点,所以传空
                  //会有人疑问为什么没有给头结点的data赋值
	    //因为头结点不参与保存数据,只是为了方便操作链表额外添加的虚拟头结点

	return true;

} 

// 3 头插法创建单链表
void BuildListHead(LinkList& L) {
	LNode* s=L;  
	
	int x;   //输入的值 规定当输入-1时结束创建
	scanf_s("%d", &x);
	while (x != -1) {
		 s = (LNode*)malloc(sizeof(LNode));  //为链表申请新空间
		 s->data = x;
		 s->next = L->next;
		 L->next = s;
		 scanf_s("%d", &x);

	}

}

// 4 尾插法创建单链表
void BuildListTail(LinkList& L) {
	LNode* s, * r = L;
	int x;
	scanf_s("%d", &x);
	while (x != -1) {
		s = (LNode*)malloc(sizeof(LNode));

		s->data = x;
		r->next = s;
		r = s;
		scanf_s("%d", &x);
	}
	r->next = NULL;   //将尾节点指向空结点
}


 
// 5 链表的查找
// LinkList 强调这是一个链表
// LNode* 强调返回一个节点
// 实际上这两种方式完全等价
LNode* GetElem(LinkList L, int i) {  //i是要找的第i个节点

	int j = 1;  //从第一个节点开始遍历

	LNode* p = L->next;

	if (i == 0)  return L;
	if (i < 0) {

		printf("第%d个节点不存在\n",i);  //说明i不合法
		return NULL;
	}  
	while (p != NULL && j < i) {   //当i==j时跳出循环
		p = p->next;
		j++;
	}
	if(i==j) return p;
	else {
		printf("第%d个节点不存在\n", i);  
		return NULL;
	}
}


// 6 插入操作 按位序插入  带头结点
//   在第i个节点插入元素e
bool ListInsert(LinkList& L, int i, int e) {
	 
	if (i < 1) {
		printf("插入位置不合法");  //增强代码的健壮性
		return false;
	 }
	              //指针p指向当前扫描到的节点
	LNode* p=L;  // 现在指针p指向头结点,头结点是不保存数据的,即第0个节点
	             
	int j = 0;  //j表示p指针当前指向的是第几个节点

	while (p != NULL && j < i - 1) {   //循环找到第i-1个节点
		p = p->next;
		j++;
	}

	if (p == NULL)
		return false;  //i不合法  //增强代码的健壮性

	LNode* s = (LNode*)malloc(sizeof(LNode));
	s->data = e;
	s->next = p->next;  //这两句不能颠倒顺序
	p->next = s;       //若是颠倒顺序,p->next还没有使用就被更新了
	return true;

}

// 这个是不带头结点的插入方法
// 他与带头结点的区别在于在第一个位置插入时需要更头结点
// 其他功能与带头结点的相同,为了节省代码量,这里不在展出,只展示核心代码 
// bool ListInsert(LinkList& L, int i, int e) {
//  //在第一个位置插入e
//	if (i == 1) {
//		LNode* s = (LNode*)malloc(sizeof(LNode));
//		s->data = e;
//
//		s->next = L;  //让s的next指向头指针
//		L = s;       //然后将头指针改为s
//		             //这样就实现了将s插入到第一个位置
//		return true;
//	}
//
//}

// 7 指定结点的前插操作
//   你会发现,你是没办法找到给定节点的前驱节点
//   你可以选择从头结点挨个开始遍历,但是效率很低,而且若是不给头结点,则无法实现
//   所以,你可以采取下面的新方法
//   复制法  -----  偷天换日大法
//   将给定结点的数据赋值给你新申请的节点,将要插入的节点复制给给定结点
bool InsertPriorNode(LNode* p, int e) {
	if (p == NULL)
		return false;

	LNode* s = (LNode*)malloc(sizeof(LNode));
	s->data = p->data;
	s->next = p->next;  //将p的所有信息复制给s  

	p->data = e;
	p->next = s;   //将要插入的节点复制给p
                   //俗称偷天换日大法
	return true;
}

// 8 按位序删除(带头结点)
bool DeleteList(LinkList& L, int i,int& e) {

	if (i < 1) {
		printf("删除的位置不合法");  //增强代码的健壮性
		return false;
	}
	//指针p指向当前扫描到的节点
	LNode* p = L;  // 现在指针p指向头结点,头结点是不保存数据的,即第0个节点

	int j = 0;  //j表示p指针当前指向的是第几个节点

	while (p != NULL && j < i - 1) {   //循环找到第i-1个节点
		p = p->next;
		j++;
	}

	if (p == NULL||p->next==NULL)
		return false;  //增强代码的健壮性

	 LNode* q = p->next; //q结点创建的作用是为了能释放掉这一段内存空间

	   e = p->next->data;  //用e可以记录下被删除的结点保存的值,不过我觉着这一步没什么用

	   p->next = p->next->next ;// 覆盖掉p->next达到实现删除的效果,虽然有点掩耳盗铃,但是效果有了

	
	
	 free(q);//释放掉没用的空间

	return true;

}


// 9 删除给定结点
//   思路同偷天换日大法
//   但是有如果要删除最后一个结点,这个代码是实现不了的,只能从表头开始循环遍历
//   我们将给定节点的后继结点的信息复制给给定结点,然后释放掉后继节点,这样就实现了给定结点的删除
//   如果是删除给定结点的后继,传参数的时候可以传给定结点的下一个结点,即传p->next
bool DeleteNode(LNode*p){
	if (p == NULL||p->next)return false;
	//如果p是最后一个结点,那么p->next==NULL ,p->next->next是不存在的
	LNode* q = p->next;  //q结点创建的作用是为了能释放掉这一段内存空间
	 p->data = p->next->data;
	p->next = p->next->next;
	  free(q);
	return true;
 }

int main() {

	 LinkList L;        // 1 声明一个指向单链表的指针
	 InitalList(L);    // 2 初始化链表,为链表添加头结点(即第0个结点)
	// BuildListHead(L);    // 3 头插法创建单链表     本博客使用尾插法
	 
	  BuildListTail(L); // 4 尾插法创建链表
	  //   注意1和3或者1和4可以合并在一起,方便新手学习理解,本博客分开处理

       LinkList se=GetElem(L, 2);   // 5 查找单链表元素
	   printf("查找到的数据为%d\n", se->data);

	   ListInsert(L, 2, 66);                 // 6 按位插入
	   InsertPriorNode(L->next->next, 99);  // 7 指定节点的前插操作

	   int e;
	   DeleteList(L, 5, e);         // 8 按位序删除(带头结点)
	   DeleteNode(L->next->next);  // 9  删除给定结点

	 LinkList p = L->next;         // 10 打印链表 没有再单独封装函数
	  while (p) {
		 printf("%d,", p->data);
		 p = p->next;
	 }

	return 0;
}
//单链表的局限性,不能逆向检索
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

海风许愿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值