线性表之单链表及C语言实现

链表的定义

定义:线性表的链式存储称为链表,每个存储节点包含数据元素本身+元素之间逻辑关系的信息,分别称为数据域指针域
单链表:每个节点除了数据域外,只有一个指针指向后继结点。如下面图所示。
在这里插入图片描述
这里我们能看出顺序表和单链表的区别,顺序表逻辑相邻的两个元素,物理上也是相邻的,单链表逻辑相邻的物理上则不用相邻,它是通过一个next指针指向它后面的结点。下面是单链表的结构体。

typedef struct LNode{
	ElemType data;
	struct LNode *next;
}LinkNode;

注:在本文中你可能会看到很多地方出现*&L这样的,你可以把&理解为如果我要改变L这个单链表,那么我就要加上&,这是语言里面的东西就不做深究了,*L就是L的指针。

建立单链表

单链表的建立有两种方法,一种是头插法,一种是尾插法。在介绍这两种插入方法前,我们先了解下带有头结点的单链表和不带头结点的单链表,如下图,左边是带有头结点的单链表,右边是不带头结点的单链表。
在这里插入图片描述
在这里,带头结点的中data的值是不写的,并且一开始头结点的next指向的就是NULL,插入的话,就改变头结点的next的值就可以。不带头结点的话,就让它指针指向你要插入的那个结点就可以。一般我们基本都是使用带头结点的链表。

头插法

从名字可以看出来,它就是插在头部,下面这个图可以让我们更好的理解插入的位置。
在这里插入图片描述
这就是头插法要插入的位置,是直接插在头结点的后面。
头插法思想:分配一个头结点head,插入的结点是s。那么我就需要让s->next指向本来头结点的next也就是s->next=head->next。然后再让头结点的next指向要插入的s即可,也就是head->next=s。实现代码如下:

void List_HeadInsert(LinkNode *&L,ElemType a[],int n){
	LinkNode *s;//要插入的结点
	int i;
	L=(LinkNode *)malloc(sizeof(LinkNode));
	L->next=NULL;
	for(int i=0;i<n;i++){
		s = (LinkNode *)malloc(sizeof(LinkNode));
		s->data=a[i];
		s->next=L->next;
		L->next = s;
	}
}

尾插法

与头插法的相反,尾插法就是把要插入的结点插在最后面。从下图可以看出来。
在这里插入图片描述
由于我们需要插入在最后一个后面,所以我们要另外声明一个r用来指向最后一个结点。
尾插法思想:先声明头结点,再声明一个r指向头结点,每插入一个都让r指向新的一个结点,并且新插入的那个结点的next要指向NULL,因为它是最后一个,就实现了尾插法。
实现代码如下:

void List_TailInsert(LinkNode *&L,ElemType a[],int n){
	LinkNode *s *r;
	int i;
	L=(LinkNode *)malloc(sizeof(LinkNode));
	r=L;
	for(i=0;i<n;i++){
		s = (LinkNode *)malloc(sizeof(LinkNode));
		s->data=a[i];
		r->next=s;
		r=s;
	}
	r->next=NULL;
}

单链表的基本操作

InitList(&L):构造空的单链表L
DestroyList(&L):销毁单链表

ListEmpty(L):判断单链表L是否为空。
ListLength(L):求单链表长度
DispList(L):输出单链表L。(依次访问每个结点,并显示各结点data值)

GetElem(L,i,&e):按位查找,用e返回L中第i个结点data域值
LocateElem(L,e):按值查找,返回L中第一个结点data域值与e相等的结点位序
ListInsert(&L,i,e):在L中第i个位置插入新的结点,值为e。
ListDelete(&L,i,&e):删除L的第i个结点,并用e返回data域值。

构造空的单链表

void InitList(LinkNode *&L){
	L=(LinkNode *)malloc(sizeof(LinkNode));//创建头结点
	L->next = NULL;
}

因为是指针,所以我们需要分配内存空间给它,头结点一开始的nextNULL。这个函数的时间复杂度是O(1)

销毁单链表

销毁单链表就是释放L占用的内存空间。

void DestroyList(LinkNode *&L){
	LinkNode *pre=L,*p = L->next;
	while(p!=NULL){
		free(pre);
		pre=p;
		p=pre->next;
	}
	free(pre);
}

这个思想就是声明两个指针,一个指向头结点,一个指向头结点的下一个结点,如果头结点的下一个不存在,那么就直接释放头结点,不然就先释放头结点,再让pre指向下一个结点,p指向pre的下一个结点,然后依次释放,直到全部释放结束。

判断是否为空链表

bool ListEmpty(LinkNode *L){
	return L->next==NULL; 
}

判断是否为空链表相对来说就简单了,只要看头结点后面有没有东西就行了。

求单链表长度

求单链表长度,那就是看头结点后面有几个结点,先声明一个n为0,依次遍历直到NULL即可。

int ListLength(LinkNode *L){
	int n=0;
	LinkNode *p = L;
	while(p->next!=null){
		n++;
		p=p->next;
	}
	return n;
}

这里要注意的是,我头结点是不可能移动的,不会出现L=L->next这种情况出现,这时候我们就需要声明一个p来指向头结点即可。时间复杂度可以马上得出是O(n)

输出单链表

输出单链表其实跟求单链表长度一样,在求长度的基础上把它输出就行,不过这时候我们就不要n来计数了。

void DispList(LinkNode *L){
	LinkNode *p = L->next;
	while(p!=null){
		printf("%d ",p->data);
		p=p->next;
	}
}

时间复杂度同样是O(n)

按位查找元素

按位查找元素,这里的位是位序。我们来看下它的代码。

bool GetElem(LinkNode *L,int i,ElemType &e){
	int j=0;
	LinkNode *p = L;
	while(j<i&&p!=NULL){
		j++;
		p=p->next;
	}
	if(p==NULL)
		return false;
	else{
		e=p->data;
		return true;
	}
}

先声明一个j用来判断与要找位序的大小关系,如果j<i表示可以继续往下找,否则就是找到了,那么还有一个条件我们要引起注意的是,你一定要有东西我们才能找,不然找什么去,所以这里要满足p!=NULL。所以后面我们进行了判断,如果pNULL,那就说明没找到,不然就返回当前pdata。这里不太明白的可以自己画个图,然后来模拟下。
这里的时间复杂度同样是O(n)

按值查找元素

其实按值和按位有点类似,基本要做个小变动就可以了。当它当前的数据不是我们要找的数据时往下找,是的话就返回。

int LocateElem(LinkNode *L,ElemType e){
	int i=1;
	LinkNode *p=L->next;
	while(p!=NULL&&p->data!=e){
		p=p->next;
		i++;
	}
	if(p==NULL)
		return 0;
	else
		return i;
}

这里我们同时也要注意,要p!=NULL,不然没有数据我们就无法寻找了。这个时间复杂度是O(n)

插入数据元素

插入数据元素与头插法有点类似,它首先要找到那个插入的位置,让插入的next指向后面一个结点,前面的next指向要插入的结点即可。用一张图来更直观的说明。
在这里插入图片描述
差不多就是如上图所示,这里插入的是位序,比如我要插入到第二位,那么首先我得找到第一位,这样我才能直到第一位的next。下面是实现代码。

bool ListInsert(LinkNode *&L,int i,ElemType e){
	int j=0;
	LinkNode *p=L,*s;
	if(i<=0) return false;
	while(j<i-1&&p!=NULL)
	{
		j++;
		p=p->next;
	}
	if(p==NULL) return false;
	else{
		s=(LinkNode *)malloc(sizeof(LinkNode));
		s->data = e;
		s->next = p->next;
		p->next = s;
		return true;
	}
}

来手工模拟一下,比如上面那张图,我要插入的i=2,那么我一开始p指向头结点,j=0j<i-1是成立的,那么p就指向了a1这个结点,j++后不满足j<-1这个条件了,所以现在我p指向的就是a1这个结点,后面我们就可以在a1结点后面插入了,因为找到a1也就是找到了a1->next
这个时间复杂度也是O(n)

删除数据元素

删除数据元素和插入有点类似,需要先找到删除的那个元素的前面一个元素,然后让它前面那个指向它后面那个就行,所以这里需要有两个指针来记录,一个记录前一个结点,一个记录当前结点。代码实现如下:

bool ListDelet(LinkNode *&L,int i,ElemType &e){
	int j=0;
	LinkNode *p=L,*q;
	if(i<=0) return false;
	while(j<i-1&&p!=NULL){
		j++;
		p=p->next;
	}
	if(p==NULL)
		return false;
	else{
		q=p->next;
		if(q==NULL) return false;
		e=q->data;
		p-next=q->next;
		free(q);
		return true;
	}
}

在这里补个图方便大家理解。
在这里插入图片描述
这个的时间复杂度同样是O(n)

总结

其实插入和删除那一步的时间复杂度是O(1),但是前面还有要查找这一步,所以总体时间复杂度下来就是O(n)了。想要理解链表更好的是自己去模拟下这些操作,这样可以更快的了解链表。如果本文有什么问题,欢迎在评论区留言讨论。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值