链表的定义
定义:线性表的链式存储称为链表,每个存储节点包含数据元素本身+元素之间逻辑关系的信息,分别称为数据域和指针域。
单链表:每个节点除了数据域外,只有一个指针指向后继结点。如下面图所示。
这里我们能看出顺序表和单链表的区别,顺序表逻辑相邻的两个元素,物理上也是相邻的,单链表逻辑相邻的物理上则不用相邻,它是通过一个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;
}
因为是指针,所以我们需要分配内存空间给它,头结点一开始的next
是NULL
。这个函数的时间复杂度是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
。所以后面我们进行了判断,如果p
是NULL
,那就说明没找到,不然就返回当前p
的data
。这里不太明白的可以自己画个图,然后来模拟下。
这里的时间复杂度同样是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=0
,j<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)
了。想要理解链表更好的是自己去模拟下这些操作,这样可以更快的了解链表。如果本文有什么问题,欢迎在评论区留言讨论。