数据结构自学笔记(C语言)链表

从上一节内容里可以看出,线性表的顺序存储结构的特点是逻辑关系上相邻的两个数据元素在物理位置上也相邻,因此可以通过物理地址的关系来随机存取表中任意元素,它的存储位置可以用一个简单的直观的公式来表示。然而从另一方面来看,这个特点也导致了这种存储结构的弱点,在插入或者删除操作时需要移动大量元素。这节我们学习线性表的链式存储结构
线性表的链式存储结构其实就是我们学C语言时最难的链表,他的特点是可以用一组任意的存储单元来存储数据元素,单元可以是连续的也可以是不连续的。那么数据元素间的关系如何确定呢?用一个类似与链子的东西。链表中的每个数据元素我们称之为结点,每个结点包括两个域:数据域和指针域。数据域顾名思义,就是用来存储数据的,指针域是用来做什么的,它存储了下一个节点的地址,这个地址可以叫作指针也可以叫作链,也就很巧妙的将结点联系了起来。总结一下链表的优缺点,优点:它是一种动态结构,整个存储空间为多个链表公用;不需要预先分配存储空间。缺点:指针占用额外空间,不能随机存取,查找速度慢 ;在单链表的最后一个元素之后插入元素时需要遍历整个链表。下面我们对比一下线性表的顺序存储结构和链式存储结构。
在这里插入图片描述在这里插入图片描述
左边是链表的结构体,右边是顺序表的结构体。两个结构体中都有和数据存储有关的,但链表用基本数据类型就可以了,而顺序表要用指针。链表的另一个结构体成员是结构体指针,线性表的剩下两个成员是表长和元素数量。个人觉得对比学习的方法是有助于知识点理解的很好的一个方法。大家可以先思考一下为什么这两种线性表的结构体组成是这样的。
顺序表 :我相信大部分初学者思考完是一脸懵逼的,现在我们整理一下思路。先说顺序表。我们说了顺序表是通过物理地址的关系来确定数据元素之间的元素,所以在创建顺序表时我们要先申请出一片物理地址相连的空间,然后去对号入座,逐个存储数据。那我申请多大的空间呢,线性表表长(listsize)就可以告诉我们申请多大,通过初始化数据先申请一片空间,假如设定的listsize是100,那么哪怕我只想存一个元素,但我也需要申请出100个数据元素大小的空间,等到存满了之后,可以继续申请,但不会是一次申请一个,一般都是一次申请一片,存满了再申请一片。数据元素个数(length)是用来做什么的,顾名思义元素个数就是告诉我们在顺序表中存了多少个元素,知道了有多少元素才可以取元素,在范围外取元素时拒绝,不然会取出一些乱七八糟的数。比如我一共有8个数据元素,你非要问我第9个是什么,如果程序不够健壮的话,也会取出一个值,但这明显是不对的,有了数据元素个数我就可以拒绝你的无理要求了。length还有一个作用就是判断是否需要继续申请空间,在申请前先把length加一,看看是否超过了表长,超过了就继续申请存储空间,继续申请空间时用realloc函数,如果原先申请的地址还可以继续连续,那就继续申请,要是不够了,那就带上前面数据一起,再重新找一片连续的地方。还有一个结构体成员是数据指针,为什么是指针呢,因为我们是通过物理地址来存储啊,要取地址的值当然要用指针了。顺序表结构体中的结构体成员的作用就都用大白话解释完了,可以发现一个顺序表只需要一个结构体就好了。
链表 :链表的结构体中只有两个数据成员,对应我们的数据域和指针域,数据域用来存数据,用基本数据类型就好了,为什么不用指针呢?因为没必要。。指针域是一个结构体指针,用来指向下一个结点。也就是说我要存储多少数据,就要有多少结点,也就要有多少个结构体。在这里可能有人会问为什么不一次申请一大片,因为链表对物理地址没要求啊,存一个申请一次就好了,在哪都无所谓。也不知道我有没有说清楚,都是大白话,力求能讲明白,不然就跟看书没啥区别了。

顺序表的优势在于找,缺点在于插入和删除。而链表正好反过来了,插入删除非常容易,查找非常难。因为你只知道一个头节点,查找时要从头结点找下个节点,下个节点再找下个的下个,按最坏的情况来看,如果你要找第n个数据,那么你就要找n次,时间复杂度就是O(n)。插入和删除的时间复杂度也是O(n),因为时间都花在找节点上了,找完只要把前一个节点的指针指向要插的节点上,然后要插的节点的指针指向原先的下一个节点上就好了。链表这个名字还是很形象的,如果还是理解不了你就想一下,在一串链子中间加一个新的,你要怎么操作,和数据结构中的链表道理是一样的。

接下来也要开始上程序了,挑了几个:链表的正向创建、反向创建、获取特定节点数据、删除特定节点、两个从小到大排列的链表合成一个链表;在上程序前先说几个要注意的点。
typedef struct Linklist{
int data;
struct Linklist *next;
}LinkList,*LinkList1;
这种结构体建立的意思是LinkList可以用来创建结构体,struct Linklist也可以用来创建结构体,LinkList1用来创建结构体指针,LinkList也可以用来创建结构体指针,struct Linklist *也可以(一个星号,之所以写了两个是因为格式问题)。可能有点绕,在这就不说了,看不懂的话可以查查资料。
1.C语言要求在使用结构体指针的时候必须要初始化,不然要报错Run-Time Check Failure #3 - The variable ‘L2’ is being used without being initialized.所以创建结构体指针需要传到子函数中,后要在创建完申请一段空间让指针指过去。在上一节的程序中我是先创建了一个结构体,然后让指针指向这个结构体,一个道理。
2.链表中的结构体指针指向的是本身的结构体。typedef重命名后的结构体创建方式不能用,因为typedef语句还没执行完。所以只能用struct Linklist
,这个问题搞了很久。
3.因为结构体指针在传入子函数前必须初始化这个问题实在是太搞了,后来想了想为什么不在子函数中创建结构体指针然后把它返回呢,是可以的,在正向创建就是用的这种方法。但仔细想了想可能作者想让我们感受一下靠指针做形参来改变指针内容的感觉吧。

#include//printf函数
#include//malloc函数
//struct Linklist next;//在结构体还没定义完之前,编译器不知道该类型所需多大空间,报错
//LinkList *next;//typedef语句还没执行完,报错  
typedef struct Linklist{
	int data;
	struct Linklist *next;                    
}LinkList,*LinkList1;

LinkList1 ListCreat1(int n)//正向建立
{
	int	j;
	LinkList1 p,s,L;
	L = (LinkList*)malloc(sizeof(LinkList));
	L->next = NULL;//把头指针先搞出来
	p = L;
	for(j = 0; j < n; j++)
	{
		s = (LinkList1)malloc(sizeof(LinkList));
		s->data = j+1;
		s->next = NULL;
		p->next = s;
		p = p->next;
	}
	return L;
}

void ListCreat2(LinkList1 L,int n)//反向建立
{
	int	j;
	LinkList1 p;
	L->next = NULL;
	for(j=0;j<n;j++)
	{
		p = (LinkList1)malloc(sizeof(LinkList));
		p->data = j+1;
		p->next = L->next;
		L->next = p;
	}
	//return L;
}

int GetElem(LinkList1 L1,int n,int *e)//获取特定位置
{
	LinkList1 p;
	int j = 1;
	p = L1->next;
	while(p && j<n)
	{
		p = p->next;
		j++;
	}
	if(j>n||!p)
		return -1;
	else 
	{
		*e = p->data;
		return 1;
	}
}
int DelElem(LinkList1 L1,int n)//删除特定位置
{
	LinkList1 p,q;
	int j=1;
	p = L1->next;
	if(n==1)//删除第一个
	{
		L1->next = p->next;
		return 1;
	}
	else
	{
		while(j<n-1 && p->next)
		{
			p = p->next;
			j++;
		}
	}
	if(!p->next||n<j)return -1;
	q = p->next;
	p->next = q->next;
	return 1;

}

void ListMerge(LinkList1 L1,LinkList1 L2,LinkList1 L3)//合并链表
{
	LinkList1 p1,p2,p3;
	p1 = L1->next;
	p2 = L2->next;
	p3 = L3;
	while(p1 && p2)
	{
		if(p1->data>p2->data)
		{
			p3->next = p2;
			p3=p2;
			p2=p2->next;
		}
		else
		{
			p3->next = p1;
			p3=p1;
			p1=p1->next;
		}
	}
	if(p1)p3->next = p1;
	else p3->next = p2;
	free(L2);
	free(L1);
}
int main()
{
	int n = 5;
	int a =0;
	int *e = &a;
	LinkList1 L1,L2,L3,p;
	L2 = (LinkList1)malloc(sizeof(LinkList));
	L3 = (LinkList1)malloc(sizeof(LinkList));
	//L1 = ListCreat1(n);
	ListCreat2(L2,n);
	//L = ListCreat2(n);
	//GetElem(L1,2,e);
	//DelElem(L1,1);
	//ListMerge(L1,L2,L3);
	p = L2->next;
	while(p){ printf("%d ",p->data); p = p->next; }
	//printf("%d ",*e);
}

程序大概就是这样,main函数中的注释掉的都是用来测试的,大家可以自己取消注释。
这里要说的一点就是,在操作链表时要先想好自己需要多少指针。除了头指针外,正向建立需要两个额外指针,而反向建立只需要一个额外的指针。因为头指针是找到链表在哪的重要信息,不能动。如果是正向建立的话,需要一个指针用来指向插入的节点,另一个指针指向链尾。但反向建立的时候,只需要一个指针指向插入节点就好了,每次插在头指针后面,也就是尾巴在哪我们并不关心。最后友情提醒一下,写链表程序时在纸上画一画还是很有必要的。
链表还有两种,一种是循环链表,它的特点是表中最后一个节点的指针域指向头结点。由此,从表中任意节点触发均可找到其他节点,不一定非要从头结点开始,这种和单链表的区别就是在循环条件中结束条件是p指向头指针而不是空。另一种是双链表,由于单链表只能后趋不能前趋,所以寻找下一节点的时间为O(1),而寻找前一节点的时间就是O(n)了,因为要从头找。双链表的意思就是每个节点中不光有指向后节点的指针域,还要有指向前节点的指针域。其实单链表掌握好了,这两种链表写起来都一样的,有兴趣的同学可以自己写个试试,我就不上程序了,有问题的话可以在下面留言。下一节就要开始栈和队列的学习了,加油。

你可能感兴趣的:(数据结构自学笔记(C语言)链表)