数据结构-单链表(3)


前言

今天写一点关于单链表的整表操作过程中自己的理解,本来的打算写在上一篇博客的,但是篇幅太长了,但是太多了读着很辛苦,就分成两个部分了,如果对单链表的概念和基本操作不熟悉的同学可以看第二篇博客数据结构-单链表(2)


一、单链表的整表创建

之前在第二篇博客讲过单链表的插入操作,但是插入的只是一个或多个连续的结点,针对于已经存在的单链表而言的,但是如何创建一个链表却没有讲解。
创建思路是:通过对空表的循环插入结点,达到插入节点个数后跳出循环的这个方法去创建整表而这个过程对于插入的方式分成了头插法和尾插法

1. 头插法

顾名思义,头插法就是每次插入的地方是第一个结点,原来的第一个结点变成了现第二个节点。注意区分头结点和第一个结点的概念
头插法描述
前面的省略号就是头结点,当然也可以在指定的的单链表结点插入。
算法思路:

  1. 声明一个结构体指针(结点)p和计数器变量i ;
  2. 初始化空链表;
  3. 创建一个只含有头结点的空链表;
  4. 循环;
    a. 申请一块内存,将其地址赋值给p结点;
    b. 生成随机数或者自定义一个数据赋值给该内存(也就是p结点)的数据域p->data
    c. 把申请的内存(新的结点)插入头结点和第一个结点之间。
    伪代码:
void CreateListH(LinkList *L, int n)
{
	LinkList p; /*定义一个结点*/
	int i;
	*L = (LinkList)malloc(sizeof(Node));
	(*L) -> next = NULL;
	for i to n 
		p = (LinkList)malloc(sizeof(Node));
		p->data = rand();		/*将随机数赋值给现在生成结点的数据域*/
		p->next = (*L)->next;	/*把原第一个结点的地址赋值给现在插入结点的指针域使得
									现在插入的结点指向原第一个结点*/
		(*L)->next = p; /*让头结点的指针域指向现在插入的结点*/
}

再次说明:
申请的内存(LinkList)malloc(sizeof(Node))一定有地址。
p =(LinkList)malloc(sizeof(Node));这句就是把申请的内存的地址赋值给p。但是p是怎么定义的呢?它是LinkList类型的结构体指针,其本质是指针,但是它又具有先前定义的结构体那样的性质,我们可以操作它。因此我们就是通过操作p达到操作申请的内存。而p你可以认为是一个结点也可以看作是一个结构体指针,该指针指向申请的内存。

2.尾插法

尾插法就是再单链表的末尾进行插入;
算法思路:

  1. 声明两个结构体指针(结点)p,r和计数器变量i ;
  2. 初始化空链表;
  3. 创建一个只含有头结点的空链表;
  4. 循环;
    a. 申请一块内存,将其地址赋值给p结点;
    b. 生成随机数或者自定义一个数据赋值给该内存(也就是p结点)的数据域p->data
    c. 把申请的内存(新的结点)插入尾结点后面;
    d. 将最后一个结点的地址赋值给结构体指针r,即认为r是一个尾结点。
  5. 退出循环后将尾结点的指针域置空r->next = NULL
    伪代码:
void CreateListH(LinkList *L, int n)
{
	LinkList p,r; /*定义一个结点*/
	int i;
	*L = (LinkList)malloc(sizeof(Node));
	r = *L;
	for i to n 
		p = (LinkList)malloc(sizeof(Node));
		p->data = rand();		/*将随机数赋值给现在生成结点的数据域*/
		r->next = p;			/*把新开辟的内存地址赋值给
								  尾结点的指针域,使其指向新结点*/
		r = p; 					/*更新尾结点*/
	 r->next = NULL;            /*尾结点指针域置空*/
}

尾插法有点难理解,但是对于能够分清指针和内存的人而言其实也能厘清其中的关系。现举一个例子,希望能够将明白这个关系。
内存个指针的关系理解
图片的左边申请了一个int类型的内存,大小是sizeof(int)(操作系统位数决定大小),右边申请了一块LinkList类型的指针大小是sizeof(Node),这两块内存都有自己的地址,如0x123和0x127而p和r是申明的结构体指针,如前面所说的一样它就是一个指针,p指针和r指针都可以指向内存地址为0x127的内存,通过操作指针达到操作该指针指向的内存(这里操作r和操作p都可以操作他们指向的内存)。这就是指针和内存的关系,并且可以定义不同类型和结构的内存。
那么对于上面的尾插操作中:
第五行代码:
*L = (LinkList)malloc(sizeof(Node));
就是申请了一块内存并把内存地址赋值给形参 *L
第六行代码:
r = *L;
就是把内存地址传递给指针r,这里可以从两个方面理解这个指针r:

  1. 把指针r就定义为一个尾指针,不管是空链表还是真正插入新的结点,它永远是链表的尾结点,比如形参n=0时程序指令不进入循环,此时直接把结点r的指针域置空,这些操作是对结点r操作,但是结点r是指针,对它的操作就变相等于对申请的内存操作(映射)此时链表是一个空表。又比如n=3则进入循环
    第八行代码:
    p = (LinkList)malloc(sizeof(Node));
    申请一块新的内存,并把地址赋值给结点p,此时结点p就是表示这块内存;
    第九行代码:
    p->data = rand();
    把随机产生的数据赋值给结点p的数据域
    第十行代码:
    r->next = p;
    把结点p的地址赋值给结点r的指针域,使得结点r的指针域指向结点p
    第十一行:
    r = p
    把结点p的地址赋值给结点r,使得结点r永远是尾结点。就这样循环,最后一步就是尾结点置空操作尾插法
  2. 第二种理解就是把两个结点单纯的认为是两个临时的指针,这两个指针交替出现,但是在循环过程中r始终在最后。
    用图行表示就是这样:
    第二种理解
    其实在循环体中r=p这句代码执行以后就应该是r指针和p指针指向同一个结点,并非r指针指向两个结点(内存),但是为了显式的说明r和p是交替出现的,且r指针在循环中每次都是指向最后一个结点,才没有画成下面这种图解:

在执行到r=p时就是将r指针指针指向第一个结点,那么在进行第二次循环时,r结点代表的就是尾结点。

二、单链表的整表删除

在不需要单链表时需要销毁这个链表,如果不销毁则会出现内存的浪费或者其它问题出现,这个时候的思路则是:

  1. 声明两个结点(这里理解为结构体指针好一点)p,q;
  2. 将第头结点(没有头结点时就是第一个结点)地址赋值给p;
  3. 循环:
    a. 将下一个结点地址赋值给指针q;
    b. 释放p指针指向的内存(一个结点);
    c. 将q的地址赋值给p
  4. 将最后剩余的一个结点的指针域置空
    伪代码:
void ClearList(LinkList *L)
{
	LinkList p,q; /*定义一个结点*/
	p = *L ->next;
	while p 
		q = p ->next;
		free(p);		/*释放指针p指向的内存*/
		p = q; 					/*更新p指针指向的内存*/
	 *L->next = NULL;            /*头结点指针域置空*/
}

删除是交替删除的,q指针作为中间变量,因为free(p)后p中的指针域也被销毁了,如果不事先确定好要销毁的下一个结点(内存),则系统不知道下一个结点(内存)的地址在哪里。


总结

在这个章节中学习了单链表的基本操作,相比顺序存储结构的线性表,单链表的优势在于添加和删除,但是查找的时间复杂度(时间复杂度为 O ( n ) O(n) O(n))还不如顺序存储结构的线性表(时间复杂度为 O ( 1 ) O(1) O(1))。但是单链表的空间结构和对内存的分配上比顺序存储结构的线性表更加灵活和合理。
因此:
频繁查找,顺序存储结构的线性表;
频繁添删,链式存储结构的单链表;
下一讲将对静态表做出自己的理解。
2021-8-18

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

gccRobot

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

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

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

打赏作者

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

抵扣说明:

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

余额充值