顺便提一下,之前说的顺序存储结构由于其两个元素在物理位置上相邻,故它的优点是很方便存取,但与此带来的缺点是如果进行插入或者删除操作的话,需要移动大量的元素,在工程量大的时候,很耗时间。链式存储结构不要求元素的物理位置相邻,不存在顺序存储的弱点,但也同时失去顺序表可随机存取的优点。
对于链表的数据元素,它除了要存储本身的信息外,还要存储一个能够指示其直接后继的信息(即指向后继的存储位置),逻辑上称这部分数据元素为结点,结点包括两个域:数据域,主要存储本身的数据元素信息。指针域,是用来存储其直接后继的存储位置,这部分信息称作指针。链表一般分为:单链表(结点只有一个指针域的链表)、双链表(有两个指针域的链表)、多链表(有多个指针域的链表)、循环链表(首尾相接的链表),一般都是说的单链表。
单链表的特点:
- 数据元素的逻辑顺序和物理顺序不一定相同。
- 在查找数据元素时,必须从头指针开始依次查找,表尾的指针域指向NULL。
- 在特定的数据元素之后插入或删除元素,不需要移动数据元素,因此时间复杂度为 O(1)。
- 存储空间不连续,数据元素之间使用指针相连,每个数据元素只能访问周围的一个元素。
- 长度不固定,可以任意增删。
单链表的存取必须从头指针开始进行,头指针表示指向链表中第一个结点(头结点或者首元结点)的指针,是一个具体的地址,同时,由于最后一个数据元素没有直接后继,链表最后一个结点的指针应设为“空”(NULL),其长度变化较大,主要用于插入和删除操作。
为了增加可读性,一般我们在单链表的第一个结点前附设一个结点,称作头结点,头结点的数据域可以不存任何信息,也可以存储如线性表的长度等类的附加信息,头结点的指针域存储指向第一个结点的指针(即第一个数据元素的存储位置)。首元结点是指链表中存储单链表第一个数据元素的结点。
其链表的存储结构为:
typedef int Status;
typedef int ElemType;
typedef struct LNode{
ElemType data ;
struct LNode *next ;
}LNode,*LinkList;
初始化,为头结点构造一个空间。
Status InitList(LinkList &L){
L = (LinkList)malloc(sizeof(LNode));
if(!L)
exit(OVERFLOW) ;
L->next=NULL ;
}
清空链表,前提是链表已存在(后续操作均在此基础上进行)。
Status ClearList(LinkList &L){
if(L==NULL){
cout<<"该链表不存在。"<<endl;
return TRUE;
}
LinkList p ,q ;
p=L->next;
while(p!=NULL){
q=p ;
p=p->next;
free(q);
}
L->next=NULL;
//cout<<"链表已被清空。"<<endl;
return TRUE ;
}
销毁链表,和清空不同的是注意要把头结点一块销毁。
Status DestroyList(LinkList &L){
LinkList p,q ;
p=L;
while(p){
q=p->next;
free(p);
p=q;
}
L=NULL ;
return TRUE ;
}
判断链表是否为空,是则TRUE,否便FALSE
Status ListEmpty(LinkList L){
if(L->next==NULL){
cout<<"链表是空的。"<<endl;
return TRUE ;
}
else{
cout<<"链表中尚有数据元素,不是空表。"<<endl;
return FALSE ;
}
}
获取长度:由于链表中两个相邻元素在物理位置上不相邻,在获取链表的长度时需要从头指针开始出发(包括后边的获取元素位置、插入和删除)
int ListLength(LinkList L){
if(L->next==NULL){
// cout<<"链表是空表。"<<endl;
return FALSE ;
}
LinkList p ;
int i=0 ;
p=L->next;
while(p!=NULL){
i++;
p=p->next ;
}
return i ;
}
按位置查找后返回其值
Status GetElem(LinkList L,int i,ElemType &e){
LinkList p ,q;
int j=1 ;
p=L->next;
while((p->next!=NULL)&&(j<i)){
p=p->next;
j++;
}
if(!p || j>i ){
cout<<"查找不符合要求"<<endl;
return FALSE ;
}
e = p->data ;
return TRUE ;
}
此处写的是返回链表中第一个和元素e相等元素的位置。完整的应该同之前顺序表中的那个LocateElem函数功能一样,形参多设有一比较参数,判断链表中第一个和所传入形参满足比较参数关系的数据元素的位置。
int LocateElem(LinkList L,ElemType e){
LinkList p ;
int i=1 ;
p=L->next ;
while((p->next!=NULL)&&(p->data!=e)){
p=p->next;
i++;
}
if(p->next==NULL){
cout<<"链表中未找到所查找的元素。"<<endl;
return FALSE;
}
return i;
}
找所查元素的前驱
Status PriorElem(LinkList L,ElemType cur_e,ElemType &pre_e){
LinkList p ,q;
p=L->next;
q=NULL;
while(p&&(p->data!=cur_e)){
q=p;
p=p->next;
}
if(q==NULL){
cout<<cur_e<<"的前驱不存在。"<<endl;
return FALSE ;
}
pre_e=q->data ;
cout<<"其前驱是: "<<pre_e <<endl;
return TRUE;
}
找所查元素的后继
Status NextElem(LinkList L,ElemType cur_e,ElemType &next_e){
LinkList p ;
p=L->next;
while(p&&(p->data!=cur_e))
p=p->next;
if(p->next==NULL){
cout<<cur_e<<"的后继不存在。"<<endl;
return FALSE ;
}
next_e=p->next->data ;
cout<<"其后继是: "<<next_e <<endl;
return TRUE ;
}
数据元素的插入
Status ListInsert(LinkList &L,int i,ElemType e){
LinkList p ;
LinkList s ;
int j=0;
p=L; //如果在链表第一个位置插入元素的话,p应该表示头结点。
while(p&&j<i-1){
p=p->next;
j++;
}
// q->next->data=e;
// q->next->next=p;
if(!p || j > i-1){
cout<<"链表中找不到所求的元素。"<<endl;
return FALSE;
}
s=(LinkList)malloc(sizeof(LNode));
if(!s)
exit(OVERFLOW);
s->data=e;
s->next=p->next;
p->next=s ;
return TRUE ;
}
数据元素的删除
Status ListDelete(LinkList L,int i,ElemType &e){
LinkList p ,q ;
int j=0;
p=L;
while(p&&j<i){
j++;
q=p;
p=p->next;
}
if(q->next==NULL||j>i){
cout<<"表中不存在。"<<endl;
return FALSE;
}
q->next=p->next;
e=p->data;
free(p);
return TRUE ;
}
在主函数中的实现,一般来说,写完一段程序后,编译没毛病不表示写的就是对的,或许是你的算法出了问题,所以在主函数中将一些具体值实现是一个非常有必要的过程,我写这个链表的时候就是一下全把功能写完了,然后中间某些算法出了问题,改起来得一步一步再看,算法出错需要重新再考虑自己之前想的情况是不是漏了什么,这个编程思想是很重要的。我有个朋友,他是喜欢写一小部分就在主函数中运行看结果,虽然很慢,但这样不容易出错,或者就算出错也方便修改,这也是一种比较好的习惯,但如果对自己编程能力自信的话,那按照自己习惯来。下面是我的一些实现。
int main(){
int i ,l ,j,k;
ElemType e ,cur_e , pre_e , next_e ;
LinkList L ;
InitList(L);
cout<<"当前长度值:"<<ListLength(L)<<endl;
ListEmpty(L);
cout<<"设置链表的长度值:";
cin>>i;
cout<<"依次输入需要的链表值:"<<endl;
// 1 2 3 4 5
for(int j=1;j<=i;j++){
cin>>l;
ListInsert(L,j,l);
}
// ListInsert(L,1,1);
// ListInsert(L,2,2);
// ListInsert(L,3,3);
// ListInsert(L,4,4);
// ListInsert(L,5,5);
cout<<"将链表输出显示: ";
ListTraverse(L);cout<<endl;
cout<<"当前长度值:"<<ListLength(L)<<endl;
// ClearList(L);
// ListEmpty(L);
cout<<"输入一个查找元素: ";
cin>>cur_e ;
PriorElem(L,cur_e,pre_e);
cout<<"输入一个查找元素: ";
cin>>cur_e ;
NextElem(L,cur_e,next_e);
//cout<<"其后继是: "<<next_e <<endl;
//在写的过程中发现这一处加错位置了,在实现的结果中是有问题的 ,上边的前驱同样
cout<<"输入一个查找位置i:";
cin>>i ;
cout<<"获取链表中第i个位置的数据元素,用e将其输出: ";
GetElem(L,i,e);
cout<<"e=" << e <<endl;
cout<<"输入一个插入位置i:";
cin>>i ;
cout<<"对链表这个位置进行插入一个元素:";
cin>>j;
ListInsert(L,i,j);
cout<<"将链表再次输出显示: ";
ListTraverse(L);cout<<endl;
cout<<"输入一个删除位置i:";
cin>>i ;
ListDelete(L,i,k);
cout<<"将链表再次输出显示: ";
ListTraverse(L);cout<<endl;
cout<<"输出链表中和3相等元素的位置:"<<LocateElem(L,3)<<endl;
}
一般来说,这几个功能比较常用,也易容易实现,学习数据结构的话最好能自己基本上完全写出来,当然有实在不懂的可以去看一下别人是怎么写的,毕竟每个人的思维都不一样,多一种思路也就多一点见识,挺好。
至于下面有另外两个关于链表的功能函数,逆位序输入和链表的合并。
逆位序输入是一个从表尾到表头逆向建立单链表的算法。
void CreateList(LinkList &L,int n){ //逆位序输入。
LinkList p;
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;
for(int i=n ; i>0 ; i--){
p = (LinkList)malloc(sizeof(LNode)) ; //新建一个结点。
cin>>p->data ;
p->next=L->next ;
L->next=p; // 插入到表头
}
}
两个链表的合并。需注意的前提是这两个链表内的元素都按照元素值非递减的顺序排列。对于合并后的表Lc也是按照值得非递减顺序排列。
void MergerList(LinkList &La,LinkList &Lb,LinkList &Lc){
LinkList pa , pb , pc ;
pa=La->next;
pb=Lb->next;
pc=Lc; // 这一处可以用两个表中任一头结点作为表Lc的头结点 Lc=pc=La
while(pa&&pb){
if(pa->data<=pb->data){
pc->next=pa;
pc=pa; //pc永远指向Lc链表当前最后一个结点。
pa=pa->next;
}
else{
pc->next=pb;
pc=pb;
pb=pb->next;
}
}
if(pa)
pc->next=pa;
else if(pb)
pc->next=pb;
free(La);
free(Lb);
//有的教材上可以这样表示:
//pc->next = pa?pb:pb ; 用来插入剩余段。
}
时间效率分析:
- 查找:查找时要从头指针找起,查找的时间复杂度为 O(n)。
- 插入和删除 因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为 O(1)。但是,如果要在单链表中进行前插或删除操作,由要从头查找前驱结点,所耗时间复杂度为 O(n)。
空间效率分析:
链表中每个结点都要增加一个指针空间,相当于总共增加了n 个整型变量,空间复杂度为 O(n)。