大话数据结构(四)线性表相关概念——(链式存储结构之单链表的读取、插入、删除以及整表创建、整表删除)

之前我们说过,顺序存储结构的缺点就是插入和删除操作需要移动的数据元素非常的多,显然需要消耗掉大量的时间,有没有什么办法可以解决这种问题呢?我们的前辈算法师就很好地解决了这个问题——链式存储结构。

目录

链式存储结构

单链表的定义

单链表的读取、插入和删除

单链表的读取

单链表的插入

单链表的删除

单链表的整表创建

单链表的整表删除


链式存储结构

我们可以脑补一下链子的形状,我们日常生活中的铁链是不是一环扣一环呢,两个环之间的链接方式并不是完全焊死在一起的,而是有空隙的,只需要保证不断就可以了。那么什么又是链式存储结构呢,线性表的链式存储结构说白了,就是线性表中相邻的两个数据元素并不需要放在连续的内存空间中,它们可以存放在内存中的任意位置,只要上一个数据元素知道它的下一个数据元素存放的地址,那么以此类推,第二个元素找到第三个元素,直到遍历完线性表中的所有数据元素。

链式存储结构的特点就是,线性表的数据元素所在的内存单元并不一定是连续的,这样就有非常高的灵活性,并且在内存紧张的时候,很好的利用了内存空间,但是可能会导致内存碎片化。

既然链式存储结构这么好用,那么它的示意图应该是什么样子的呢?

       

如图所示,我们可以看到一个链式存储结构的大概示意图,对于每个数据元素来说,我们不仅要知道他们本身包含的信息,还要知道,它的后继数据元素的地址指针。我们将包含数据信息的域称为数据域,包含后继元素的地址的域称为指针域,并且把这两部分合起来称为一个结点(Node)。

单链表的定义

n个结点(Node)链接成为一个链表,而如果每个结点(Node)只包含一个指针域——包含数据元素后继元素的地址,我们将这种结构的链表称为单链表。链表还有其他形式,我们后面会讲到。

另外我们可以从图中看到,在链表的"头部",有一个头指针,设置它的目的是为了存储链表中第一个结点的存储位置,我们有时候会在单链表的第一个数据结点前增设一个头结点,我们将其称之为头结点,那么这个时候头指针就是指向头结点的指针。简单点来说,头指针只是一个指针,他就是链表的名字,并且不管有没有头结点,它始终指向一个链表的"头部"数据域。

那么链表的“尾部”呢,当然对于单链表来说,最后一个数据元素不存在后继了,自然它指针域中的指针指向空NULL。

OK,描述清楚了单链表的存储结构,我们现在来编写代码实现。

typedef struct Node//定义一个单链表存储结构
{
	int data;
	struct Node *next;
}Node;
Node *LinkList//定义LinkList

如上代码所示,我们巧妙地利用结构体指针+递归的思想定义了一个单链表存储结构。假设我们设定p是指向单链表中第i个结点的指针,那么该结点数据域中的信息可以用p->data来表示,指针域中的信息可以用p->next来表示。那么我要访问第i+1个结点中的数据呢,我们可以利用p来访问,我们已知p->next指向的就是i+1结点的结构体地址,那么p->next->data就是第i+1结点的数据域中包含的数据元素,p->next->next就是第i+1结点指针域中包含的i+2结点的结构体地址。

至此,我们总算讲完了单链表的初始化操作,接下来我们继续总结单链表的读取、插入和删除操作。

单链表的读取、插入和删除

  • 单链表的读取

在进行读取操作代码实现之前,我们先来建立一个算法思路寻找链表中的第i个元素,

1.我们需要创建一个指针p来指向单链表的第一个结点;

2.我们需要定义一个j来充当计数器,当p指针向后移动一位时,j累加1,当j<i,p指针继续向后指向下一结点;

3.我们需要判断链表是否为空;

4.我们需要判断p是否已经遍历完整个链表,若p指向NULL,则说明链表中没有我们想要的第i个元素。

OK,理清了顺序,我们现在开始编写代码

int GetElem(LinkList L,int i,int *e)
{
	int j = 1;
	LinkList p;//定义一个结构体指针
/*L是链表名称,因此就是头指针,我们现在指向链表的第一个结点*/
	p = L->next;
	while(j<i && p)//当j<i并且p没有指向链尾时
	{
		p=p->next;让p指向链表L的下一结点
		j++;//累加j
	}
	if(!p || j>i)//如果p已经到了链尾
	{
		return 0;//链表中没有第i个数据元素,返回0
	}
	else
	{
		*e = p->data;//将想要的第i个结点中的数据元素取出
		return 1;//返回1
	}
}

哇,好多指针,还需要判断,看着好像比顺序存储结构稍微复杂一些,从代码我们可以看出来最坏的情况下,当想要的数据元素在链尾时,链式存储结构的读取操作需要循环n-1次,即时间复杂度为O(n),看起来不如顺序存储结构的读取操作效率高。但是凡事都有两面性,链式存储结构的插入和删除代码就非常“简洁”了。

  • 单链表的插入

同样的,我们先来观察一下链式存储结构的特点,相邻的两个结点指针i,j之间是通过指针来联系的,因此,如果我们想要在两个结点中插入一个结点k,只需要将前一个结点i的指针指向插入的结点k,将结点k的指针指向后一个结点j。具体实现如下所示:

k->next = i->next;//将i的后继给k"管理"
i->next= k;//将k给i"管理"

解读这段代码,也就是说让i的后继结点改为k的后继结点,再吧结点k变为结点i的后继,这样就很清楚了。需要注意的是,这两行代码不能交换,不然先将i的后继改为k,k的后继又回过头变为k。

好了,在理解了插入操作的核心思想后,我们开始编写我们的代码:

/*假设在单链表第i个结点的位置插入数据元素*e */
int ListInsert(LinkList L,int i,int *e)
{
	int j = 1;
	LinkList p,s;//定义指针p,s
	p = L->next;//假设有头结点
	while(p && j<i)
	{
		p = p->next;
		j++;
	}
	if(!p || j>i)
		return 0;//失败
	s = (LinkList)malloc(sizeof(Node))//开辟新的内存空间
	s->data = *e;
	s->next = p->next;
	p->next = s;
	return 1;//插入成功
}
  • 单链表的删除

在有了插入操作的代码基础上,我们要实现删除操作就很简单了,具体代码实现如下:

/*假设在单链表第i个结点的位置删除数据元素 */
int ListDelete(LinkList L,int i,int *e)
{
	int j = 1;
	LinkList p,s;//定义指针p,s
	p = L;
	while(p->next && j<i)
	{
		p = p->next;
		j++;
	}
	if(!(p->next) || j>i)
		return 0;失败
	s = p->next;
	p->next = s->next;
	*e = s->data; //将删除节点中的数据元素取出
	free(s);//释放内存空间
	return 1;//删除成功
}

我们所要做的,实际上就是以下这一个步骤:将要删除的结点的后继结点改为删除结点的前驱结点的后继结点。

	s = p->next;
	p->next = s->next;

好了,单链表的读取、插入和删除操作都总结完了。顺便我们来分析一下链式存储结构的插入和删除操作的时间复杂度,我们发现,它们的时间复杂度都是O(n),并且对比顺序存储结构,好像是没有太大优势的。但是,在插入和删除数据越频繁的情况下,单链表的效率优势就越明显。

单链表的整表创建

在有了之前单链表的单个结点的结构代码的基础上,我们来想办法实现单链表的整表创建,单链表整表创建的算法思路如下:

1.声明一个指针p和计数器变量i;

2.初始化一个空链表结点L;

3.让L的头结点的指针指向NULL,即建立一个带头结点的单链表;

4.循环生成结点。

代码实现如下:

void CreatListtHead(LinkList L,int n)
{
	LinkList P;
	int i;
	srand(time(0));//初始化随机数种子,需要头函数
	L=(LinkList)malloc(sizeof(Node));//为L结点开辟内存空间
	L->next = NULL;
	for(i = 0;i < n;i++)
	{
		p = (LinkList)malloc(sizeof(Node));//生成新结点
		p->data = rand()%100+1//随机生成
		p->next = L->next;
		L->next = p;
	}
}

我们称这种创建方法为头插法,因为新生成的结点始终成为单链表中的第一个结点(除头结点外)。有了头插法,我们对应的当然还有尾插法。

void CreatListtHead(LinkList L,int n)
{
	LinkList P,r;
	int i;
	srand(time(0));//初始化随机数种子,需要头函数
	L=(LinkList)malloc(sizeof(Node));//为L结点开辟内存空间
	r = L;
	for(i = 0;i < n;i++)
	{
		p = (LinkList)malloc(sizeof(Node));//生成新结点
		p->data = rand()%100+1//随机生成
		r->next = p;
		r = p;
	}
	r->next = NULL;
}

从代码可以看出,我们新生成的结点是依次向链表尾部插入的。注意最后一个结点的指针域中的的指针要指向NULL,不要忘记了。

单链表的整表删除

当我们不需要单链表了的时候,我们就需要将其删除以示范内存空间。具体的代码如下:

int ClearList(LinkList L)
{
	LinkList p,q;
	p = L->next;
	while(p)
	{
		q = p->next;//q指向p的后继结点
		free(p);//释放p指向结点
		p = q;//将p重新变为p的后继结点
	}
	L->next = NULL;
	return 1;//删除成功
}

在对单链表的相关操作进行复习过后,我们可以看到在需要对数据进行频繁操作的情况下,单链表比顺序存储结构更有优势,但是这种优势也不是绝对的,我们需要具体情况具体分析,综合评判找出关于实际问题的最优解

发布了22 篇原创文章 · 获赞 2 · 访问量 492

猜你喜欢

转载自blog.csdn.net/weixin_42709632/article/details/101222967