单链表(线性表的链式存储)---C语言版

单链表(线性表的链式存储)—C语言版

一、相关说明

逻辑上相邻的元素在物理位置上不一定相邻。
优点:

没有了顺序存储所具有的弱点(也就是说:插入和删除不需要移动元素,而只需要修改指针。)
同时,由于不借助数组实现,在定义链表时,无需指定它的长度。

缺点:

也失去了顺序存储的优点

  1. 非随机存取(不能直接找到某个特定序号的结点,需要从表头开始遍历)
  2. 存储密度降低(除了存储本身信息外链表还要存储指针)

二、单链表的定义

定义

线性表的链式存储又称为单链表,它是指通过一组任意的存储单位来存储线性表中的元素。为了建立数据元素之间的线性关系,对每个链表结点,除存放元素的自身信息外,还需要存放一个指向其后继的指针。

仍旧使用该例子,我们要如何存储该表呢?

id name description
1 史强 最强辅助
2 章北海 我不需要钢印,我是自己思想的主人
3 罗辑 人类不感谢罗辑
4 维德 失去人性,失去很多。失去兽性,失去一切

步骤一:声明数据元素类型

首先我们使用结构体声明数据元素(对应表中的一行数据)的类型

typedef struct{
    
    
    int id;//对应表中的id
    char name[100];//对应表中的姓名
    char description[200];//对应表中的描述
}ElemType;//此处的ElemType是个类型名

步骤二:声明结点
由于逻辑上相邻的元素在物理位置上不一定相邻,那么我们如何确定下一个元素的存储位置呢?

解决办法,增加一个变量。用来存储一个指示其后继位置的指针。

我们把这两部分的信息组成数据元素a的存储映像,称为结点。它包含两个域,其中存储数据元素信息的域称为数据域,存储直接后继存储位置的域称为指针域

单链表中结点定义如下:

typedef struct LNode{
    
    
    ElemType data;//数据域
    struct LNode *next;//指针域
}LNode,*LinkList;

相关概念
1.头指针

整个链表的存储必须从头指针开始
头指针指示链表中第一个结点的存储位置
同时,由于最后一个数据元素没有直接后继,则单链表中最后一个一个结点的指针为NULL

//头指针定义1
LinkList L;
//头指针定义2
LNode *L;

如图所示:
在这里插入图片描述

2.头结点

在单链表的第一个结点之前附设一个结点,称为头结点。
头结点的数据域可以不存储任何信息,也可以存储如线性表的长度之类的附加信息。
头结点的指向第一个结点的指针(即第一个元素结点的存储位置)
此时,单链表的头指针指向头结点。

如图所示:
在这里插入图片描述
逻辑图:
在这里插入图片描述

关于引入头结点的优缺点,后续会根据有无头结点,创建链表操作(3.2)的具体实现来介绍。

空表
含头结点

LinkList L;
L->next==NULL时为空表

在这里插入图片描述

不含头结点

LinkList L;
L==NULL时为空表;

三、单链表上具体操作的实现和时间复杂度

1、初始化表。构造一个空表。

int InitList_Link(LinkList *L){
    
    
    *L=(LinkList)malloc(sizeof(LNode));//创建头结点,并让头指针指向头结点
    if (!(*L)) return FALSE;//分配失败
    (*L)->next=NULL;//初始化为空
    return TRUE;//初始化成功
}

时间复杂度:

时间复杂度为O(1)

2、根据数组创建单链表

分类一:含头结点
1.头插法

int create_HeadInsert(LinkList *L,ElemType a[],int n){
    
    
 // InitList_Link(L);
    LNode *s;//用来指示新创建的结点
    int i;
    for(i=n-1;i>=0;i--){
    
    //由于头插法是倒序插入,所以这里我们从数组最后开始遍历
        s=(LinkList)malloc(sizeof(LNode));//创建一个新结点
        s->data=a[i];//为结点赋值
        s->next=(*L)->next;//让该结点指向第一个结点
        (*L)->next=s;//让头结点指向该结点
    }
}

2.尾插法

int create_TailInsert(LinkList *L,ElemType a[],int n){
    
    
 // InitList_Link(L);
    LNode *s,*r=(*L); //s用来指示新创建的结点,r用来移动以连接链表
    int i;
    for(i=0;i<n;i++){
    
    //遍历数组
        s=(LinkList)malloc(sizeof(LNode));//创建一个新结点
        s->data=a[i];//为结点赋值
        r->next=s;//r始终指向链表的尾端,这里将新结点连接到r的后面
        r=s;//r移动到链表尾端
    }
    r->next=NULL;//结束时,最后一个结点的指针域赋值为空
}

分类二:不含头结点
1.头插法

int create_HeadInsert(LinkList *L,ElemType a[],int n){
    
    
    *L=NULL;
    LNode *s;//用来指示新创建的结点
    int i;
    for(i=n-1;i>=0;i--){
    
    //由于头插法是倒序插入,所以这里我们从数组最后开始遍历
        s=(LinkList)malloc(sizeof(LNode));//创建一个新结点
        s->data=a[i];//为结点赋值
        if((*L)==NULL){
    
    //对第一个结点需要特殊处理
            *L=s;//头指针指向插入结点
            s->next=NULL;//倒序插入,最后一个结点的指针域赋值为NULL
        }else{
    
    
            s->next=*(L);//新插入的结点指向第一个结点
            *(L)=s;//头指针指向新的插入节点
        }
    }
}

2.尾插法

int create_TailInsert(LinkList *L,ElemType a[],int n){
    
    
 // InitList_Link(L);
    (*L)=NULL;
    LNode *s,*r=(*L); //s用来指示新创建的结点,r用来移动以连接链表
    int i;
    for(i=0;i<n;i++){
    
    //遍历数组
        s=(LinkList)malloc(sizeof(LNode));//创建一个新结点
        s->data=a[i];//为结点赋值
        if((*L)==NULL){
    
    //对第一个结点需要特殊处理
            (*L)=s;//头指针指向第一个结点
            r=s;//尾指针移动到最后一个结点
        }else{
    
    
            r->next=s;//r始终指向链表的尾端,这里将新结点连接到r的后面
            r=s;//r移动到链表尾端
        }
    }
    r->next=NULL;//结束时,最后一个结点的指针域赋值为空
}

含头结点优点:

使得链表第一个结点的操作和其他位置一样,无需特殊处理。

含头结点缺点:

头结点的数据域一般不存储数据,该空间被浪费。

时间复杂度:

每个结点插入的时间为O(1),设表长为n
所有方式的时间复杂度均为O(n)

3、求表长

int Length_Link(LinkList L){
    
    
    int i=0;//空表返回0
    while (L->next!=NULL){
    
    //遍历,直到尾结点,空表不进入循环
        L=L->next;//向后移动一个
        i++;//表长加一
    }
    return i;//返回表长
}

时间复杂度:

设表长为n
时间复杂度为O(n)

4、插入操作。在表L中的第i个位置上插入指定元素e。

int ListInsert_Link(LinkList *L,int i,ElemType e){
    
    
    int j=0;//空表序号为0
    LNode *p=(*L);//p指向头结点
    LNode *s;//s用来指示新创建的结点
    while (p&&j<i-1){
    
    //找到被插入序号的前一个位置
       p=p->next;//向后移动一个
       j++; 
    }
    if(!p||i<1) return FALSE;//序号在无效范围内
    s=(LinkList)malloc(sizeof(LNode));//创建一个新结点
    s->data=e;//赋值
    s->next=p->next;//让该结点指向p(插入位置的前一个结点)的下一个结点
    p->next=s; //让p指向该结点  
}

时间复杂度:

在给定的结点后插入新结点的时间复杂度为O(1)
查找第i-1个节点的时间复杂度为O(n)
综上:时间复杂度为O(n)

在这里插入图片描述

5、删除操作。删除表中L中第i个位置的元素,并用e返回删除的元素。

int ListDelete_Link(LinkList *L,int i,ElemType *e){
    
    
    int j=0;//空表序号为0
    LNode *p=(*L);//p指向头结点
    LNode *q;//q用来指向被删除的结点
    while (p->next&&j<i-1){
    
    //找到要被删除结点的前一个结点
        p=p->next;//向后移动一个
        j++;
    }
    if (!(p->next)||i<1) return FALSE;//序号在无效范围内
    q=p->next;//让q指向被删除的结点
    p->next=q->next;//让p(被删除结点的前一个结点)指向被删除结点的下一个结点,q从链表中断开
    *e=q->data;//把被删除的结点的值返回
    free(q);//释放该结点
    return TRUE; //删除成功
}

在这里插入图片描述

时间复杂度:

在给定的结点后删除结点的时间复杂度为O(1)
查找第i-1个节点的时间复杂度为O(n)
综上:时间复杂度为O(n)

6、按位查找操作。获取表L中第i个位置的元素的值。

int GetElem_List(LinkList L,int i,ElemType *e){
    
    
    LNode *p=L; //p指向头结点
    int j=0;//空表序号为0
    while (p&&j<i){
    
    //找到第i个位置
        p=p->next;//向后移动一个
        ++j;
    }
    if(!(p)||i<1)  return FALSE; //i不在有效范围内
    *e=p->data; //把第i个结点的值用e返回
    return TRUE;
}

时间复杂度:

时间复杂度为O(n)

7、按值查找操作。在表L中查找具有给定关键字值的元素,返回序号

int LocateElem_List(LinkList L,ElemType e,int *i){
    
    
    int j=0;//空表序号为0
    while (L->next!=NULL){
    
    //遍历链表
        L=L->next;//向后移动
        j++;//序号加一
        if (L->data.id==e.id){
    
    //这里我们只用id来判断元素的值是否相等,假设id唯一
            *i=j;//如果相等,用i返回序号
            return TRUE;//查找成功
        }    
    }
    return FALSE;
}

时间复杂度:

时间复杂度为O(n)

8、输出操作。按前后顺序输出表L的所有元素值。

void PrintList_Link(LinkList L){
    
    
    int i=0;//空表序号为0
    while (L->next!=NULL)//遍历表
    {
    
    
        L=L->next;
        i++;
        printf("第%d行:id=%d,name=%s,description=%s\n",i,L->data.id,L->data.name,L->data.description);
    }
}

时间复杂度:

时间复杂度为O(n)

9、销毁表

void DestroyList(LinkList *L){
    
    
    while((*L)->next!=NULL) {
    
    //遍历链表
    	LNode *p=(*L)->next;//p指示头结点的下一个,要被删除的结点
    	(*L)->next=p->next;//让头结点指向p的下一个,把p从链表断开
    	free(p);//删除p
	}
}

时间复杂度:

时间复杂度为O(n)

四、完整代码实现

#include<stdio.h>
#include<stdlib.h>

#define TRUE 1
#define FALSE 0

typedef struct{
    
    
    int id;//对应表中的id
    char name[100];//对应表中的姓名
    char description[200];//对应表中的描述
}ElemType;//此处的ElemType是个类型名

typedef struct LNode{
    
    
    ElemType data;//数据域
    struct LNode *next;//指针域
}LNode,*LinkList;

/*初始化*/
int InitList_Link(LinkList *L){
    
    
    *L=(LinkList)malloc(sizeof(LNode));//创建头结点,并让头指针指向头结点
    if (!(*L)) return FALSE;//分配失败
    (*L)->next=NULL;//初始化为空
    return TRUE;//初始化成功
}

/*头插法创建链表*/
int create_HeadInsert(LinkList *L,ElemType a[],int n){
    
    
 // InitList_Link(L);
    LNode *s;//用来指示新创建的结点
    int i;
    for(i=n-1;i>=0;i--){
    
    //由于头插法是倒序插入,所以这里我们从数组最后开始遍历
        s=(LinkList)malloc(sizeof(LNode));//创建一个新结点
        s->data=a[i];//为结点赋值
        s->next=(*L)->next;//让该结点指向第一个结点
        (*L)->next=s;//让头结点指向该结点
    }
}

/*尾插法创建链表*/
int create_TailInsert(LinkList *L,ElemType a[],int n){
    
    
 // InitList_Link(L);
    LNode *s,*r=(*L); //s用来指示新创建的结点,r用来移动以连接链表
    int i;
    for(i=0;i<n;i++){
    
    //遍历数组
        s=(LinkList)malloc(sizeof(LNode));//创建一个新结点
        s->data=a[i];//为结点赋值
        r->next=s;//r始终指向链表的尾端,这里将新结点连接到r的后面
        r=s;//r移动到链表尾端
    }
    r->next=NULL;//结束时,最后一个结点的指针域赋值为空
}

/*求表长*/
int Length_Link(LinkList L){
    
    
    int i=0;//空表返回0
    while (L->next!=NULL){
    
    //遍历,直到尾结点,空表不进入循环
        L=L->next;//向后移动一个
        i++;//表长加一
    }
    return i;//返回表长
}

/*插入*/
int ListInsert_Link(LinkList *L,int i,ElemType e){
    
    
    int j=0;//空表序号为0
    LNode *p=(*L);//p指向头结点
    LNode *s;//s用来指示新创建的结点
    while (p&&j<i-1){
    
    //找到被插入序号的前一个位置
       p=p->next;//向后移动一个
       j++; 
    }
    if(!p||i<1) return FALSE;//序号在无效范围内
    s=(LinkList)malloc(sizeof(LNode));//创建一个新结点
    s->data=e;//赋值
    s->next=p->next;//让该结点指向p(插入位置的前一个结点)的下一个结点
    p->next=s; //让p指向该结点  
}

/*删除*/
int ListDelete_Link(LinkList *L,int i,ElemType *e){
    
    
    int j=0;//空表序号为0
    LNode *p=(*L);//p指向头结点
    LNode *q;//q用来指向被删除的结点
    while (p->next&&j<i-1){
    
    //找到要被删除结点的前一个结点
        p=p->next;//向后移动一个
        j++;
    }
    if (!(p->next)||i<1) return FALSE;//序号在无效范围内
    q=p->next;//让q指向被删除的结点
    p->next=q->next;//让p(被删除结点的前一个结点)指向被删除结点的下一个结点,q从链表中断开
    *e=q->data;//把被删除的结点的值返回
    free(q);//释放该结点
    return TRUE; //删除成功
}

/*按位查找*/
int GetElem_List(LinkList L,int i,ElemType *e){
    
    
    LNode *p=L; //p指向头结点
    int j=0;//空表序号为0
    while (p&&j<i){
    
    //找到第i个位置
        p=p->next;//向后移动一个
        ++j;
    }
    if(!(p)||i<1)  return FALSE; //i不在有效范围内
    *e=p->data; //把第i个结点的值用e返回
    return TRUE;
}

/*按值查找*/
int LocateElem_List(LinkList L,ElemType e,int *i){
    
    
    int j=0;//空表序号为0
    while (L->next!=NULL){
    
    //遍历链表
        L=L->next;//向后移动
        j++;//序号加一
        if (L->data.id==e.id){
    
    //这里我们只用id来判断元素的值是否相等,假设id唯一
            *i=j;//如果相等,用i返回序号
            return TRUE;//查找成功
        }    
    }
    return FALSE;
}

/*打印*/
void PrintList_Link(LinkList L){
    
    
    int i=0;//空表序号为0
    while (L->next!=NULL)//遍历表
    {
    
    
        L=L->next;
        i++;
        printf("第%d行:id=%d,name=%s,description=%s\n",i,L->data.id,L->data.name,L->data.description);
    }
}

/*销毁单链表*/
void DestroyList(LinkList *L){
    
    
    while((*L)->next!=NULL) {
    
    //遍历
    	LNode *p=(*L)->next;
    	(*L)->next=p->next;
    	free(p);
	}
}

int main(){
    
    
    LinkList L;
    int i=InitList_Link(&L);
    if(i==1){
    
    
        printf("初始化成功\n");
    }
    printf("\n");    
///
    ElemType a[4]={
    
    
        {
    
    1,"史强","最强辅助"},
        {
    
    2,"章北海","我不需要钢印,我是自己思想的主人"},
        {
    
    3,"罗辑","人类不感谢罗辑"},
        {
    
    4,"维德","失去人性,失去很多。失去兽性,失去一切"}
    };  
    create_HeadInsert(&L,a,4);
//    create_TailInsert(&L,a,4);  
    PrintList_Link(L);
    printf("\n");    
///
    ElemType e={
    
    5,"xxx","xxxxxxxxxxxxxxxxxxxx"};
    ListInsert_Link(&L,5,e); 
    PrintList_Link(L);
    printf("\n");    
///
    ListDelete_Link(&L,5,&e);
    PrintList_Link(L);
    printf("被删除的元素:id=%d,name=%s,description=%s\n",e.id,e.name,e.description);
    printf("\n");    
///
    GetElem_List(L,4,&e);
    printf("查询到的元素:id=%d,name=%s,description=%s\n",e.id,e.name,e.description);
    printf("\n");    
//
    LocateElem_List(L,a[2],&i);
    printf("查询到的位序为:%d\n",i);
    printf("\n");    
//
    i=Length_Link(L);
    printf("表长为:%d\n",i);
    printf("\n");    
//
    DestroyList(&L);
    PrintList_Link(L);
}

运行结果:

初始化成功

第1行:id=1,name=史强,description=最强辅助
第2行:id=2,name=章北海,description=我不需要钢印,我是自己思想的主人
第3行:id=3,name=罗辑,description=人类不感谢罗辑
第4行:id=4,name=维德,description=失去人性,失去很多。失去兽性,失去一切

第1行:id=1,name=史强,description=最强辅助
第2行:id=2,name=章北海,description=我不需要钢印,我是自己思想的主人
第3行:id=3,name=罗辑,description=人类不感谢罗辑
第4行:id=4,name=维德,description=失去人性,失去很多。失去兽性,失去一切
第5行:id=5,name=xxx,description=xxxxxxxxxxxxxxxxxxxx

第1行:id=1,name=史强,description=最强辅助
第2行:id=2,name=章北海,description=我不需要钢印,我是自己思想的主人
第3行:id=3,name=罗辑,description=人类不感谢罗辑
第4行:id=4,name=维德,description=失去人性,失去很多。失去兽性,失去一切
被删除的元素:id=5,name=xxx,description=xxxxxxxxxxxxxxxxxxxx

查询到的元素:id=4,name=维德,description=失去人性,失去很多。失去兽性,失去一切

查询到的位序为:3

表长为:4


猜你喜欢

转载自blog.csdn.net/weixin_47138646/article/details/121618597