02-2.3.2_1 单链表的插入和删除

喜欢《数据结构》部分笔记的小伙伴可以订阅专栏,今后还会不断更新。

此外,《程序员必备技能》专栏和《程序员必备工具》专栏(该专栏暂未开设)日后会逐步更新,

插入

按位序插入

(1)带头结点

ListInsert(&L, i, e)插入 操作------在表L中的i个位置上 插入指定元素e

找到第i-1个结点,将新结点插入其后
具体代码实现:

c 复制代码
//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, ElemType e){
	if(i<1)
		return false;
	LNode *p;//指针p指向当前扫描到的结点
	int j = 0;//当前p指向的是第几个结点
	p = L;//L指向头结点,头结点是第0个结点(不存数据)
	while(p != NULL && j < i-1){//循环找到i-1个结点
		p = p->next;
		j++;
	}
	if(p == NULL)//i值不合法
		return false;
	LNode *s = (LNode *) malloc(sizeof(LNode));
	s->data = e;
	s->next = p->next;
	p->next = s;//将结点s连到p之后
	return true;//插入成功
}

代码分析:

  • 如果i=1(插在表头)
  • 如果i=3,那么i-1=2,那么j=0p会指向下个结点,所以j=1,因为j=1 < 2,所以j会再+1变成2
  • 如果i=5(插在表尾),这种情况是最坏的,最坏时间复杂度 = O ( n ) O(n) O(n)
  • 如果i=6i>length),执行之后发现p == NULL,返回false
    需要注意的是:
    s->next = p->next;p->next = s;两句顺序不能颠倒

(2)不带头结点

ListInsert(&L, i, e)插入 操作------在表L中的i个位置 上插入指定元素e

找到第i-1个结点,将新结点插入其后

但是因为不存在所谓的"第0个"结点,因此当i=1时需要进行特殊处理
具体代码实现:

c 复制代码
bool ListInsert(LNode &L, int i, ElemType e){
	if(i>1)
		return false;
	if(i==1){//插入第一个结点的操作和其他结点的操作不同
		LNode *s = (LNode *) malloc (sizeof(LNode));
		s->data = e;
		s->next = L;
		L = s;//头指针指向新结点
		return true;
	}
	LNode *p;//指针p指向当前扫描到的结点
	int j = 1;//当前p指向的是第几个结点
	p = L;//p指向第1个结点(注意:不是头结点)
	while(p! = NULL && j < i-1){//循环找到第i-1个结点
		p = p->next;
		j++;
	}
	if(p == NULL)//i值不合法
		return false;
	LNode *s = (LNode *) malloc (sizeof(LNode));
	s->data = e;
	s->next = p->next;
	p->next = s;
	return true;//插入成功
}

代码分析:

  • i=1时,如果单链表不带头结点,则插入、删除第1个元素时,需要修改头指针L
  • i>1时,这种情况和带头结点 的情况是一样的,唯一需要注意的是:我们修改了int j = 1

指定结点的后插操作

c 复制代码
//后插:在p结点之后插入元素e
bool InsertNextNode(LNode *p, ElemType e){
	if(p == NULL)
		return false;
	LNode *s = (LNode *) malloc (sizeof(LNode));
	if(s == NULL)
		return false;
	s->data = e;//用结点s保存元素e
	s->next = p->next;
	p->next = s;//将结点s连到p之后
	return true;
}

指定结点的前插操作

c 复制代码
//前插:在p结点之前插入元素e
bool InsertPriorNode(LNode *p, ElemType e)

前插的困难在于,单链表只能往后查找结点,对于给定的结点p,之前有哪些结点都是未知的

那么如何操作呢?我们可以传入头指针

c 复制代码
bool InsertPriorNode(LinkList L, LNode *p, ElemType e)

传入头指针之后,通过循环的方式查找p的前驱结点q ,然后再对q进行后插操作 ,并且通过这种方式操作时,时间复杂度为 O ( n ) O(n) O(n)


但是还有另一种实现方式:

c 复制代码
bool InsertPriorNode(LNode *p, ElemType e){
	if(p == NULL)
		return false;
	LNode *s = (LNode *) malloc (sizeof(LNode));
	if(s == NULL)
		return false;
	s->next = p->next;
	p->next = s;//新结点s连接到p之后
	s->data = p->data;//将p中的元素复制到s中
	p->data = e;//p中的元素覆盖为e
	return true;
}

通过这种方式的时间复杂度为 O ( 1 ) O(1) O(1)


王道书版本:

c 复制代码
bool InsertPriorNode(LNode *p, LNode *s){
	if(p == NULL || s == NULL)
		return false;
	s->next = p->next;
	p->next = s;               //s连到p之后
	ElemType temp = p->data;   //交换数据部分
	p->data = s->data;
	s->data = temp;
	return true;
}

代码分析:

  1. p->data : 访问指针p所指向的结点的data成员
  2. ElemType temp : 声明一个类型为ElemType的局部变量tempElemType通常是一个自定义的类型(例如intfloat结构体等),它表示结点中存储的数据类型
  3. temp = p->data : 将p->data的值赋值给temp
c 复制代码
ElemType temp = p->data; // 将 p 结点的 data 值存储到 temp 变量中
p->data = s->data;       // 将 s 结点的 data 值赋值给 p 结点的 data
s->data = temp;          // 将 temp 中保存的原 p 结点的 data 值赋值给 s 结点的 data

通过以上步骤,p和s之间的data值就被交换了,但结点本身的位置没有改变。这样做的目的是为了在逻辑上实现插入前驱结点的效果。

删除

按位序删除

只探讨带头结点的情况:
ListDelete(&L, i, &e)删除 操作------删除表L中第i个位置 的元素,并用e返回删除元素的值

找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点

头结点可以看做"第0个"结点
具体代码实现:

c 复制代码
bool ListDelete(LinkList &L, int i, ElemType &e){
	if(i<1)
		return false;
	LNode *p;                     //指针p指向当前扫描到的结点
	int j = 0;                    //当前p指向的是第几个结点
	p = L;                        //L指向头结点,头结点是第0个结点(不存数据)
	while(p != NULL && j < i-1){  //循环找到第 i-1 个结点
		p = p->next;
		j++;
	}
	if(p == NULL)                 //i值不合法
		return false;
	if(p->next == NULL)           //第 i-1 个结点后,已经没有其他结点
		return false;
	LNode *q = p->next;           //令q指向被删除的结点
	e = q->data;                  //用e返回元素的值
	p->next = q->next;            //将 *q 结点从链中断开
	free(q);                      //释放结点的存储空间
	return true;
}

代码分析:

  • 在这部分代码中:
    • LNode *q = p->next;: q 指向要删除的第 i 个结点
    • e = q->data;: 将被删除结点的数据存储在 e 中
    • p->next = q->next;: 这是关键的一步,将 pnext 指针指向 q 的后继结点,从而将 q 结点从链表中断开。具体来说,p->next 本来指向 q,现在改为指向 q 的后继结点 q->next
    • free(q);: 释放 q 结点的内存。
  • 我们来更详细地解释 p->next = q->next; 的意义:
    • 在这条语句之前,链表的链接是这样的:
      p --> q --> q->next
    • 执行这条语句后,链表的链接变成了这样:
      p -> q->next
    • 这样,q 结点就从链表中被移除了
  • 平均、最坏 时间复杂度: O ( n ) O(n) O(n),最好 时间复杂度: O ( 1 ) O(1) O(1)

指定结点的删除

c 复制代码
//删除指定结点p
bool DeleteNode(LNode *p)

删除结点p,需要修改其前驱结点的next指针

解决方法:

  1. 传入头指针,循环寻找 p 的前驱结点
  2. 偷天换日(类似于结点前插的实现)
c 复制代码
bool Delete(LNode *p){
	if(p == NULL)
		return false;
	LNode *q = p->next;       //令q指向*p的后继结点
	p->data = p->next->data;  //和后继结点交换数据域
	p->next = q->next;        //将*q结点从链中断开
	free(q);                  //释放后继结点的存储空间
	return true;
}

p->next指向 q 结点的后继结点(可能是NULL),这个时候 q 结点就被断开了,释放存储空间即可

时间复杂度: O ( 1 ) O(1) O(1)
但是 ,如果 p 是最后一个结点,这一段代码是有 bug 的。这种情况下,我们只能从头开始依次寻找 p 的前驱

时间复杂度: O ( 1 ) O(1) O(1)

单链表的局限性

无法逆向检索,有时候不太方便。如果有指向前面的指针,情况就不一样了

这种表就是双链表

相关推荐
随意起个昵称2 小时前
线性dp-计数类题目10(ZBRKA)
算法·动态规划
Navigator_Z7 小时前
LeetCode //C - 1089. Duplicate Zeros
c语言·算法·leetcode
sulikey9 小时前
个人Linux操作系统学习笔记6 - 操作系统与进程初识
linux·笔记·学习·操作系统·进程
云泽80810 小时前
C++ 可调用对象通关指南:深度解析 Lambda 表达式、function 包装器与 bind 绑定器
开发语言·c++·算法
XGeFei10 小时前
【Fastapi学习笔记(3)】——资源的层级关系、安全性-幂等性、Field、工厂函数
笔记·学习·fastapi
wlsh1510 小时前
Go 迭代器
算法
语戚11 小时前
力扣 3161. 块放置查询:线段树解法(Java 实现)
java·算法·leetcode·面试·线段树·力扣·
luweis11 小时前
企智孪生 ETA(3.3 认知算法层:ETA 的思维内核 3.4 基础架构:算力与弹性)【浙江联保网络 卢伟舜】
大数据·运维·线性代数·ai·矩阵·学习方法
CS创新实验室11 小时前
从顺序表到动态数组:数据结构的永恒基石与现代语言的优雅封装
数据结构·算法
星恒随风12 小时前
Python 基础语法详解(一):从表达式、变量到数据类型
开发语言·笔记·python·学习