数据结构链表详解(c语言实现)

绪论

线性表是数据结构中比较常见也比较重要的一种线性结构。简言之,线性表是n个数据元素的有限序列。而线性表又有两种常见的实现方式,其中比较常考的就是链表了。
链表实现顺序表的时候,与顺序表最大不同之处在于,链表中两个逻辑位置在一起的元素,物理地址并不连续,因为链表的每个会有一个指向下一项的指针,指针所指地址对应的值才是与该元素逻辑上相邻的值。本文我们会从单链表出发,循序渐进带大家了解双链表和循环链表。

单链表的实现

上文我们说到,链表里最典型的就是单链表了,什么是单链表,顾名思义,就是指只有一个指针用来与表中其他元素相对应。

第一个结点
第二个结点
第三个结点
.....
最后一个结点

单链表的定义

我们可以很清晰的看到,每一项都会一个指针来指向下一项,但是我们也能发现,我们的最后一项好像没有指,其实最后一项指针是指向了NULL,也就是我们的空。
结合上图我们可以很清楚地看到,对于一个链表,它一共需要两种类型地变量,一个数据项,一个指向下一项的指针。

下面展示一些 内联代码片

// 定义一个单链表   用typedef将struct list重命名为List,将List *命名为ListNode
typedef struct list{
	int data;
	struct list *next;
}List,*ListNode;

单链表的初始化

对于任何一个数据结构,我们对它的操作无非就是创建,销毁,增删改查这几个最基本的操作,只有能掌握这几个最基本的操作,我们才能扩展更多功能
对于一个单链表,其实也是可以分为两种,一种是带有头结点的,一种是不带头结点的,这两种的区别是什么呢?

0
1
2
.....
n

这个例子就是个不带头结点
而下面这种就是带头结点的

头结点
1
2
3
.....
n

我们可以清楚的感觉到,两者最大的区别就在于带头结点的链表更符合人的计数方式,对于我们而言,更好维护和操作,并且能够看出来,带头结点的单链表,他的头结点是没有data值的,因为我们并不需要它帮我们存取值,只是为了方便计数,所以他的data为缺省类型。接下来我们来尝试初始化一个单链表,这里我讲的所有方法都是基于带头结点的,有兴趣的朋友们可以自己尝试下不带头结点该怎么定义

// 初始化一个单链表,因为有头结点,所以头结点的下一项才是真正的数据第1项
//为此我们就得给头结点的指针所指的值来表示真实的第一项
void init(ListNode &L)
{
	//之所以用&是因为我们需要把处理之后的值返回到我们的主函数里,而不是复制一个新的值仅仅在函数栈里处理
	//参数我们传入进来一个指针,指向的是一个List元素
	L=(List *)malloc(sizeof(List));//在内存中申请一个空间存放头结点
	L->next=NULL;
	//一个没有任何元素的链表就初始化结束了
}

单链表的插入删除

对于单链表,我们要是想删除某一位上的元素,很明显,第一步我们肯定是先找到这个位置的前一位,然后再将此位置前一位元素的next指针指向我们要插入的部分,而我们要插入的部分的next指针指向原本在这个位置的元素
比如我们在第三个位置插入P结点

头结点
a
b
c
.....
n
p

然后我们先找到第二个位置b,找到之后我们把a->next指向p,p->next指向b

头结点
a
b
c
.....
n
p
头结点
a
b
c
.....
n
p

用代码实现时,我们把其封装到一个insert函数里面,这里我们应该看出来,我们应该有三个参数,分别是List * &L一个链表,int n,第n个位置,int e 需要插入的元素

// 一定要判断插入位置是否正确
bool insert(List * &L,int n,int e)
{
	if(n<1)//检测位置是否合法
	return false;
	List *p=L;
	int j=0
	while(p!=NULL&&j<n-1)//找到插入元素之前的位置
	{
		p=p->next;
		j++;
	}
	if(p==NULL)
	return false;//确认插入有无超出链表范围
	List *s=(List *malloc(sizeof(List));//存放新元素的data
	s->data=e;
	s->next=p->next;
	p->next=s;
	return true;
}

单链表的删除

其实删除的原理和增加也很想,第一步就是找到要删除的位置的前一位p,然后让p直接指向p->next->next即可,然后清理内存中的p就行了
比如删除第三位的c

头结点
a
b
c
.....
n
头结点
a
c
b
.....
n
bool remove(List * &L,int n)
{
	if(n<1)//检测位置是否合法
	return false;
	List *p=L;
	int j=0
	while(p!=NULL&&j<n-1)//找到删除元素之前的位置
	{
		p=p->next;
		j++;
	}
	if(p==NULL)
	return false;//确认插入有无超出链表范围
	List *q=p->next;
	p->next=q->next;
	free(q);
	return true;
}

单链表的查找

写到这里,估计有好多细心的朋友们发现了,我们之前的一大串寻找第n-1个位置和插入操作基本没变,变的只是后面的逻辑上的操作,所以,我们可以在用一个方法把之前那一大块封装起来,以方便我们维护

List * getNode(List * &L,int n)
{
	if(n<1)//检测位置是否合法
	return NULL;
	List *p=L;
	int j=0
	while(p!=NULL&&j<n)//找到元素的位置
	{
		p=p->next;
		j++;
	}
	return p;//如果p是NULL,他也会返回NULL
]

单链表的修改

其实只要能写出来查找,修改只不过是在查找的基础上,把data项修改了而已

void changeNode(List * &L,int n,int e)
{
	List *p=getNode(List * &L,int n);//直接调用我们写好的查找函数
	if(p!=NULL)
	p->data=e;
]

单链表的建立

单链表的建立分为两种模式,一种是头插法,一种是尾插法
头插法

头结点
a
b
c
p

比如我们要往这个链表中加入新的元素p,头插法的做法是每次就加入到头结点的后面

头结点
p
a
b
c

尾插法

头结点
a
b
c
p

比如我们要往这个链表中加入新的元素p,尾插法的做法是每次就加入到最后一个结点的后面

头结点
a
b
c
p

对于头插法

List * createbyhead(List * &L)//一个初始化过的新表
{
	List *p=L;
	int n;
	do{
		scanf("%d",&n);
		List *s=(List *)malloc(sizeof(List));
		s->next=p->next;//新结点的下一节点指向头结点的下一节点
		p->next=s;//新结点插入到表最头
		s->data=n;
	}while(getchar()='\n')//输入回车表示输入完了
	return p;
}

对于尾插法

List * createbytail(List * &L)//一个初始化过的新表
{
	List *tail=L;//创建一个尾指针tail
	int n;
	do{
		scanf("%d",&n);
		List *p=(List *)malloc(sizeof(List));
		p->data=n;//p指针就是每次我们想插入的新项
		tail->next=p;///把新项插入到表尾
		tail=p;//尾指针再次向后移动
	}while(getchar()='\n')//输入回车表示输入完了
	tail->next=NULL;//保证每次尾指针都指向NULL
	return p;
}

双向链表

双向列表也可以简称为双链表,什么是双链表,类比于单链表,则双链表就是有两个指针的链表,一个指针指向后继节点,一个指针指向前驱节点,那我们结合上面介绍的单链表,是不会发现,双链表无非是多了个指针,定义如下:

typedef struct Doublelist{
	int data;
	struct Doublelist *next;
	struct Doublelist *prior;
}Dlist;

所以我们用双向链表进行增删查改的时候,只需要每次兼顾它的前驱指针就是了

头结点
a
b
c
d

虽然双链表要比单链表更难一点,但是其实如果我们理解了的话,增删只不过是对这三个结点的操作罢了,比如初始化init()

void init(Dlist * &L)
{
	L=(Dlist *)malloc(sizeof(Dlist));
	L->next=NULL;
	L->prior=NULL;
}

对于双链表来说,插入是要比单链表难一点的,因为对于单链表,每次插入都是仅仅限定于插入位置前面的一项和新插入一项的两个指针,而双链表则需要考虑三个指针(前一项的next指针和新插入一项的prior指针以及next指针),但是如果理解的话也不是很难

bool insertDlist(Dlist * &L,int n,int e)
{
	if(n<1)
	return false;
	Dlist *p=L;
	int i =0;
	while(p!=NULL&&i<n-1)//先找要插入位置前一个结点
	{	
		p=p->next;
		i++;
	}
	if(p==NULL)
	return false;
	Dlist *q=p-next;
	Dlist *s=(Dlist *)malloc(Dlist);
	s->data=e;
	q->prior=s;
	s->next=q;
	s->prior=p;//不要忘了指向前面的指针
	p->next=s;
	return true
}

而除了增加操作,还有查找,删除操做等等,这里我就不在给大家用代码展示了,希望大家能自己私下用代码实现

循环链表

循环链表可以分为单循环链表和双循环链表,循环链表与普通链表不同的地方就是,循环链表的尾项指回了首项
我想经过上面对单链表的详细的介绍,到这里大家应该都能熟练的理解链表的工作原理了,所以我这里只帮大家粗略介绍下循环链表,具体的细节就不再多说了
循环链表的定义

typedef struct Clist{
	int data;
	struct Clist *next;
};
void init(struct Clist * &L)//循环链表里没有任何一项会指向NULL,每一项都有所指的东西
{
	L=(struct Clist *)malloc(sizeof(struct Clist));
	L->next=L;
}

至于接下来的一些接口如insert(),remove()我在这里就不再赘述了,因为大家只要掌握基本原理,写出来代码还是相对比较容易的

链表与顺序表的区别

顺序表存储数据,需预先申请一整块足够大的存储空间,然后将数据按照次序逐一存储,优点是存储密度高,因为不需要像链表一样存储指针,但是因为它的长度是限定的,所以不利于动态调整,但是因为顺序表基于数组下标直接寻址,所以查找的速度比链表快,同时删除增加时,就没有链表那么简单的只需要对当前位置的两个项进行操作简单了
在这里插入图片描述
链表和顺序表如何选择?
基于存储的考虑:
顺序表的存储空间是分配好不变的,在程序执行之前必须明确规定它的存储空间大小,即事先对”maxsize”要有合适的设定,过大造成浪费,过小造成溢出。在对线性表的长度或存储规模难以估计时,不宜采用顺序表;链表不用事先估计存储规模。
2.基于运算的考虑:
如果经常做的运算是按序号访问数据元素,显然顺序表优于链表;在顺序表中做插入,删除时,平均移动表中一半的元素,当数据元素的信息量较大且表较长时,这一点是不应该忽视的;在链表中做插入,删除,虽然也要找插入位置,但操作主要是比较操作,从这个角度考虑显然链表优于顺序表。
3.基于环境的考虑:
顺序表容易实现,任何高级语言中都有数组类型,链表的操作是基于指针的,相对来讲前者简单些

总结

对于学习数据结构,线性表只是最基础的,希望我能再接再励,能搞懂其原理,最后希望大家可以点赞关注支持,谢谢~~~~

  • 38
    点赞
  • 212
    收藏
    觉得还不错? 一键收藏
  • 16
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值