线性表_动态分配一维数组、单链表、循环链表、双向链表

线性表

线性表的定义:

由零个或多个数据元素组成的有限序列

  • 若元素存在多个,则第一个元素无前驱,而最后一个元素无后继,其他元素都有且只有一个前驱和后继。
  • 线性表强调是有限的,事实上无论计算机发展到多强大,他所能处理的元素都是有限的。

线性表的分类

线性表有两种物理存储结构:顺序存储结构和链式存储结构。

《线性表_动态分配一维数组、单链表、循环链表、双向链表》

线性表的顺序存储结构

线性表的顺序存储结构,指的是用一段地址连续的存储单元依次存储线性表的数据元素。

《线性表_动态分配一维数组、单链表、循环链表、双向链表》
顺序存储结构的动态分配封装的三个属性:

  • 数组的存储空间的起始位置,即数组elem
  • 线性表的当前长度:length
  • 线性表的最大存储容量:数组分配的容量listsize
//-----线性表的顺序存储结构(动态分配一维数组)-----
#define LIST_INIT_SIZE 100//线性表存储空间的初始分配量
#define LIST_INCREMENT 10//线性表存储空间的分配增量
typedef struct SqList Node
{ 
    ELemType *elem;//指向一维数组的指针,即数组名
    int length;//一维数组当前所用长度
    int listsize;//当前分配的存储空间(容量)
}SqList;

顺序存储结构中的任意元素都可以随机存取,不管是第一个还是最后一个,都是相同的时间O(1),是随机存取的存储结构

  • 由于数组下标从“0”开始,线性表中第i个元素L.elem[i-1]
  • 元素位置“i”[1,length],元素下标[0,length-1]

初始化线性表

//-----构造一个空的线性表L,即初始化线性表-----
void InitSqList(SqList *L)
{ 
    //初始化,分配一维数组的存储空间
    L->elem=(ElemType *)malloc(LIST_INIT_SIZE*sizeof(ElemType));
    if(!L->elem) exit(0);//存储分配失败
    L->length=0;
    L->listsize=LIST_INIT_SIZE;
}

获取元素

  • 判断位置是否合理 [1,length],(且线性表不为空)。
//-----获取第i个元素赋值给e-----
bool SqList_getelem(Sqlist *L,int i,ElemType *e)
{ 
    if(i<1||i>L.length||L.length==0)
        return flase;
    *e = L.data[i-1];
    return true;
}

插入元素

  • 判断插入位置是否合理 [1,length+1]
  • 判断线性表长度是否超出数组容量,若超出,则动态增加数组容量
  • 移动数组元素,将元素插入第i个位置
  • 线性表长度加1
//-----在第i个位置(之前)插入元素e-----
bool SqList_insert(Sqlist *L,int i,ElemType e)
{ 
    if(i<1||i>length+1)//判断插入位置是否合理,e的位置为[1,length+1]
        return false;
    if(L->length>=L->listsize)//存储空间已满,再分配空间,增大容量
    { 
        ElemType p=(ElemType *)realloc(L->elem,(listsize+LIST_INCREMENT)*sizeof(ElemType));
        if(!p) exit(0);
        L->elem=p;//数组地址
        L->listsize+=LIST_INCREMENT;//新的存储容量
    }
    for(int j=L->length-1;j>=i-1;j--)//移动元素
        L->elem[j+1]=L->elem[j];
    L->elem[i-1]=e;//插入元素,下标为i-1
    L->length++;//长度加一
    retrun true;
}

删除元素

  • 判断删除位置是否合理 [1,length]
  • 判断线性表是否为空(length=0)
  • 删除元素赋值给e,移动数组元素
  • 线性表长度减1
//-----删除第i个位置的元素,并赋值给e-----
bool SqList_delete(Sqlist *L,int i,ElemType *e)
{ 
    if(i<1||i>length)//判断伤删除位置是否合理,第i个元素[1,length]
        return false;
    /*if(L->length==0)//线性表为空 return false;*/
    *e=L->elem[i-1];//删除元素下标为i-1,赋值给e
    for(int j=i-1;j<length;j++)//移动元素
        L->elem[j]=L->elem[j+1];
    L->length--;//长度减一
    retrun true;
}

顺序表的合并

已知两个顺序表La,Lb已按值非递减排序,合并两个顺序表使新的顺序表Lc也按值非递减排序

  • 先设置空表Lc,将La,Lb中的元素逐个插入Lc中即可
  • 设置两个游标i,j(指针)分别指向La,Lb的元素,比较两个元素的大小,将小的元素插入Lc,La(Lb)指针后移,Lc指针后移
  • 插入La或Lb剩余元素
//-----归并非递减线性表-----
bool SqList_merge(SqList La,Sqlist Lb,Sqlist *Lc)
{ 
   int i=0,j=0,k=0;//i为La指针,j为Lb指针,k为Lc指针
   Lc->listsize=La.length+Lb.length+1;//归并后的线性表长度
   Lc->elem=(ElemType *)malloc(Lc->listsize*sizeof(ELemType));//分配存储空间
   if(!Lc->elem) return false;//分配失败
   //当两个指针都未到线性表尾部时,比较指针所指的元素大小,将小的插入线性表Lc
   while(i<La.length&&j<Lb.length)
   { 
       if(La.elem[i]<=Lb.elem[j])
           Lc->elem[k++]=La.elem[i++];//La指针后移,比较下一个元素
       else  Lc->elem[k++]=Lb.elem[j++];//Lb指针后移,比较下一个元素
       //Lc指针后移
   }
   while(i<La.length)
       Lc->elem[k++]=La.elem[i++];//插入La的剩余元素
   while(j<Lb.length)
       Lc->elem[k++]=Lb.elem[j++];//插入Lb的剩余元素
   return true;

线性表顺序存储结构的优缺点

线性表的顺序存储结构,存取数据的时间复杂度是O(1),为随机存取;插入、删除元素的时间复杂度都是O(n)。因此,顺序存储结构适合元素个数比较稳定,经常存取数据,不经常插入和删除元素的应用。

优点

  • 可以快速存取表中任意位置的元素。

缺点

  • 插入和删除操作需要移动大量元素。
  • 当线性表长度变化较大时,难以确定存储空间的容量。
  • 容易造成存储空间的 “碎片”,浪费存储空间。因为线性表申请内存空间是一整块一整块申请的,那么中间就会造成很多的 ”碎片空间“,而无法使用。

线性表的链式存储结构

  • 线性表的链式存储结构是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是任意在内存中未被占用的位置。
  • 顺序存储结构每个元素只需要存储数据;链式存储结构除了要存储数据外,还要存储它的后继元素的指针
  • 链表的结点包括,存储数据元素信息的域称为数据域,把存储直接后继位置的域称为指针域
  • 非随机存取结构

单链表

注意:此处都为有头结点的单链表

  • 每个结点中只包含一个指针域,叫做单链表(线性链表)。
  • 链表中的第一个结点为头结点,放在第一个元素的结点之前,其数据域一般无意义(可用来存放链表的长度)。有了头结点,对在第一个元素结点前插入结点和删除第一结点时,无需特殊处理第一个结点
  • 头指针指向头结点,最后一个结点指针为空NULL。
  • 若为空表,头结点的指针域为空NULL

《线性表_动态分配一维数组、单链表、循环链表、双向链表》

//-----单链表的存储结构-----
//结点结构
typedef struct LNode
{ 
    ElemType data;//数组域
    struct LNode *next;//指针域
}LNode,*LinkList;

获取结点元素

在单链表中,取得第i个元素必须从头指针出发寻找(有头结点从L->next开始遍历),若第i个元素存在,赋值给e。单链表为非随机存取的存储结构

  • 指针p指向链表第一个结点(L->next),初始化 j 从1开始
  • 当 j < i 时且p不为空时,遍历链表,指针p 向后移动,j ++
  • 若到链表末尾 p 为空,则说明第 i 个元素不存在;否则查找成功,返回结点 p 的数据
  • 时间复杂度为O(n).
  • 由于单链表的结构中没有定义表长,不知道循环多少次,不方便使用for来控制循环,选择while语句。
//-----带头结点的单链表获取第i个元素,并赋值给e-----
bool LinkList_getelem(Linklist L,int i,ElemType *e)
{ 
    //L为带头结点的单链表的头指针
    LinkList p=L->next;//从头结点后面的第一个结点元素开始
    int j=1;//计数器j初始化为1
    while(p!=NULL&&j<i)//直到p指向第i个元素或者p指向链表末尾空
    { 
        p=p->next;
        j++;
    }
    if(p==NULL||j>i) return false;//不存在第i个元素
    *e=p->data;//把第i个元素赋值给e
    return true;
}

插入结点

注意:单链表插入元素时指针的修改顺序

  • 指针p指向链表头结点(L),初始化 j 从 0 开始
  • 使p指向第 i-1 个结点,即 j < i-1
  • 建立数据域为x的结点,插入结点,注意顺序

《线性表_动态分配一维数组、单链表、循环链表、双向链表》

//-----带头结点的单链表,在第i个结点(之前)插入元素e-----
bool Linklist_insert(Linklist L,int i,ElemType e)
{ 
    Linklist p=L;//指针指向头结点
    int j=0;//计数器j初始化为0
    while(p!=NULL&&j<i-1)//使p指向第i-1个结点
    { 
        p=p->next;
        j++;
    }
    if(p==NULL||j>i-1) return false;//不存在第i个结点
    //找到第i-1个元素后,创建结点,插入元素
    LinkList s=(LinkList)malloc(sizeof(Lnode));//生成新结点
    s->data=e;
    s->next=p->next;p->next=s;//插入结点,注意顺序
    return true;
}

删除结点

  • 指针p指向链表头结点(L),初始化 j 从 0 开始
  • 使p指向第 i-1 个结点,即 j < i-1
  • 当删除的节点p->next不为空时
  • 删除结点,并释放空间

《线性表_动态分配一维数组、单链表、循环链表、双向链表》

//-----带头结点的单链表,删除第i个结点,并返回值给元素e-----
bool Linklist_insert(Linklist L,int i,ElemType *e)
{ 
    Linklist p=L;//指针指向头结点
    int j=0;//计数器j初始化为0
    while(p->next!=NULL&&j<i-1)//使p指向第i-1个结点,且要删除的结点不为空
    { 
        p=p->next;
        j++;
    }
    if(p->next==NULL||j>i-1) return false;//不存在第i个结点
    //删除结点,返回值并释放结点
    LinkList s=p->next;//s为要删除的结点
    p->next=s->next;//删除结点
    *e=s->data;free(s);//返回值并释放结点
    return true;
}

头插法建立链表

  • 建立一个带头结点的单链表,初始化空表,即头结点的指针为空
  • 把新生成的结点插入头结点后的第一个位置
  • 生成的链表中结点的次序和输入的顺序相反。
    《线性表_动态分配一维数组、单链表、循环链表、双向链表》
//-----头插法建立带头结点的单链表-----
void LinkList_creathead(LinkList L,int n)
{ 
    //先建立带头结点的单链表,头结点的指针为空
    L=(LinkList)malloc(sizeof(LNode));
    L->next=NULL;
    //头插法生成n个结点并输入元素值
    for(int i=0;i<n;i++)
    { 
        LinkList p=(LinkList)malloc(sizeof(LNode));//生成新结点
        scanf("%d",&p->data);
        p->next=L->next;L->next=p;//头插法
    }
}

尾插法建立链表

  • 建立一个带头结点的单链表,初始化空表,即头结点的指针为空
  • q指针指向最后一个结点,相当于尾指针
  • 把新生成的结点插入链表后
  • q指针重新指向新结点,即最后一个结点
    《线性表_动态分配一维数组、单链表、循环链表、双向链表》
//-----尾插法建立带头结点的单链表-----
void LinkList_creathead(LinkList L,int n)
{ 
    //先建立带头结点的单链表,头结点的指针为空
    L=(LinkList)malloc(sizeof(LNode));
    L->next=NULL;
    q=L;//q指向最后一个结点,空表时为头结点
    //尾插法生成n个结点并输入元素值
    for(int i=0;i<n;i++)
    { 
        //生成新结点
        LinkList p=(LinkList)malloc(sizeof(LNode));
        scanf("%d",&p->data);
        p->next=NULL;
        //尾插法
        q->next=p;
        q=p;
    }
}

清空、销毁单链表

单链表整表删除(清空clear):将其结点在内存中所占的空间释放,成为空表(只有头结点)
销毁:单链表不在存在,头结点也不存在

  • 将每个结点赋给q,下一个结点赋给p,释放q的空间
  • q=p
  • q不可省略。释放q的空间时,会把q的指针域也给删除了,会是后续链表的地址丢失,所以需要变量p来保存q的下一个结点的地址
//-----清空、销毁单链表-----
void LinkList_destroy(LinkList L)
{ 
    LinkList p=L->next,q;//清空单链表,指向第一个结点
    //LinkList p=L->next,q;//销毁单链表,指向头结点
    while(p!=NULL)
    { 
        q=p;//把当前结点赋值给q
        p=p->next;//记录下一结点位置
        free(q);//释放当前结点
    }
    L->next=NULL;//空表
    //L=NULL;//销毁单链表
}

合并单链表

已知两个链表La,Lb已按值非递减排序,合并两个链表表使新的链表Lc也按值非递减排序

  • 改变指针指向,将原有结点重新排序
  • 以链表La为基准,将Lb中的每个结点逐个插入Lc中,最后Lc的头结点与La相同
  • 设置两个指针pa,pb分别指向La,Lb的结点,比较两个结点元素的大小;pc指针指向Lc的最后一个结点,相当于尾指针
  • 若La中元素小,则pa指针后移;若Lb中元素小,插入Lc中(利用尾指针pc)
  • 将La或Lb剩余段与Lc连接起来,释放Lb头结点
//-----归并非递减单链表-----
bool LinkList_merge(LinkList La,Linklist Lb,Linklist Lc)
{ 
   LinkList pa=La->next,pb=Lb->next;//pa为La指针,pb为Lb指针,指向第一个结点
   Lc=La;
   LinkList pc=Lc//pc为Lc指针,用于指向Lc的最后一个结点
   //当两个指针都未到单链表尾部时,比较指针所指的元素大小
   while(pa!=NULL&&pb!=NULL)
   { 
       if(pa->data<=pb->data)//La中元素小,则pa指针后移,pc指针后移
           pa=pa->next;//pa指针后移,比较下一个元素
       else//Lb中元素小,插入Lc中
       { 
           pc->next=pb;
           pb=pb->next;//Lb指针后移,比较下一个元素
       }
       pc=pc->next;//pc指针后移,保持指向Lc最后一个结点
   }
   while(pa!=NULL)
       pc->next=pa;//将La剩余元素与Lc连接起来
   while(pb!=NULL)
       pc->next=pb;//将Lb剩余元素与Lc连接起来
   //pc->next=pa?pa:pb;//连接剩余段
   free(Lb);//释放Lb头结点
   return true;

链式存储结构的优缺点

  • 单链表的元素个数也不受限制,不会溢出
  • 插入、删除结点方便
  • 当线性表中的元素个数变化较大或者不知道有多大时,可用单链表结构,不需要考虑存储空间的大小问题;而如果事先知道线性表的大致长度,用顺序存储结构效率会更高
  • 若线性表需要频繁查找,很少进行插入和删除操作时,用顺序存储结构;若需要频繁插入和删除时,用单链表结构

静态链表

暂略

循环链表

带头结点的循环链表

  • 链表的最后一个结点指向头结点,整个链表形成一个环。从任一结点出发均可到达链表中的其他结点
  • 若为空表,头结点的指针域指向头结点自己
  • 常设立尾指针指向最后一个元素,而不设头指针。用尾指针rear,则查找终端结点是O(1),而第一个结点是rear->next->next,也是O(1)

循环链表的操作和单链表基本一致,差别仅在于算法中的循环调节不是p或者p->next是否为空,而是它们是否等于头指针(头结点)

《线性表_动态分配一维数组、单链表、循环链表、双向链表》
循环链表的结点与单链表一致

//-----循环链表的存储结构-----
//结点结构
typedef struct LNode
{ 
    ElemType data;//数组域
    struct LNode *next;//指针域
}LNode,*LinkList;

获取结点元素

  • L为头指针,指针p指向循环链表第一个结点(L->next),初始化 j 从1开始
  • 当 j < i 时且p不为空时,遍历链表,指针p 向后移动,j ++
  • 若到p指向头结点(p == rear->next或p == L),则说明第 i 个元素不存在;否则查找成功,返回结点 p 的数据
  • 时间复杂度为O(n).
//-----带头结点的循环链表获取第i个元素,并赋值给e-----
bool LinkList_getelem(Linklist L,int i,ElemType *e)
{ 
    //L为带头结点的循环链表的头指针,rear为尾指针
    LinkList p=rear->next->next;//从头结点后面的第一个结点元素开始
    //LinkList p=L->next;//从头结点后面的第一个结点元素开始
    int j=1;//计数器j初始化为1
    while(p!=rear->next&&j<i)//直到p指向第i个元素或者p指向头结点
    //while(p!=L&&j<i)
    { 
        p=p->next;
        j++;
    }
    if(p==rear->next||j>i) return false;//不存在第i个元素
    //if(p==L||j>i) return false;//不存在第i个元素
    *e=p->data;//把第i个元素赋值给e
    return true;
}

插入结点

//-----带头结点的循环链表,在第i个结点(之前)插入元素e-----
bool Linklist_insert(Linklist L,int i,ElemType e)
{ 
    Linklist p=rear->next;//指针指向头结点
    //Linklist p=L;//指针指向头结点
    int j=0;//计数器j初始化为0
    while(p!=rear->next&&j<i-1)//使p指向第i-1个结点
    //while(p!=L&&j<i-1)
    { 
        p=p->next;
        j++;
    }
    if(p==rear->next||j>i-1) return false;//不存在第i个结点
    //if(p==L||j>i-1) return false;//不存在第i个结点
    //找到第i-1个元素后,创建结点,插入元素
    LinkList s=(LinkList)malloc(sizeof(Lnode));//生成新结点
    s->data=e;
    s->next=p->next;p->next=s;//插入结点,注意顺序
    return true;
}

删除结点

  • 当删除的节点p->next不为头指针
  • 删除结点,并释放空间
//-----带头结点的单链表,删除第i个结点,并返回值给元素e-----
bool Linklist_insert(Linklist L,int i,ElemType *e)
{ 
    Linklist p=rear->next;//指针指向头结点
    //Linklist p=L;//指针指向头结点
    int j=0;//计数器j初始化为0
    while(p->next!=rear->next&&j<i-1)//使p指向第i-1个结点,且要删除的结点不指向头指针
    //while(p->next!=L&&j<i-1)
    { 
        p=p->next;
        j++;
    }
    if(p->next==rear->next||j>i-1) return false;//不存在第i个结点
    //if(p->next==L||j>i-1) return false;//不存在第i个结点
    //删除结点,返回值并释放结点
    LinkList s=p->next;//s为要删除的结点
    p->next=s->next;//删除结点
    *e=s->data;free(s);//返回值并释放结点
    return true;
}

头插法建立循环链表

  • 建立带头结点的循环链表,初始化空表,即头结点的指针指向头结点自己
  • 把新生成的结点插入头结点后的第一个位置
  • 生成的链表中结点的次序和输入的顺序相反。

《线性表_动态分配一维数组、单链表、循环链表、双向链表》

//-----头插法建立带头结点的循环链表-----
void LinkList_creathead(LinkList L,int n)
{ 
    //先建立带头结点的循环链表,头结点的指针指向头结点自己
    L=(LinkList)malloc(sizeof(LNode));
    L->next=L;
    //头插法生成n个结点并输入元素值
    for(int i=0;i<n;i++)
    { 
        LinkList p=(LinkList)malloc(sizeof(LNode));//生成新结点
        scanf("%d",&p->data);
        p->next=L->next;L->next=p;//头插法
    }
}

尾插法建立链表

  • 建立一个带头结点的单链表,初始化空表,即头结点的指针****
  • q指针指向最后一个结点,相当于尾指针
  • 把新生成的结点插入链表后
  • q指针重新指向新结点,即最后一个结点
    《线性表_动态分配一维数组、单链表、循环链表、双向链表》
//-----尾插法建立带头结点的循环链表-----
void LinkList_creathead(LinkList L,int n)
{ 
    //先建立带头结点的循链表,头结点的指针指向头结点自己
    L=(LinkList)malloc(sizeof(LNode));
    L->next=L;
    q=L;//q指向最后一个结点,空表时为头结点
    //尾插法生成n个结点并输入元素值
    for(int i=0;i<n;i++)
    { 
        //生成新结点
        LinkList p=(LinkList)malloc(sizeof(LNode));
        scanf("%d",&p->data);
        p->next=L;
        //尾插法
        q->next=p;
        q=p;
    }
    //rear=q;//尾指针
}

循环链表的合并

  • 记录第一个链表的头结点
  • 第一个链表尾指针指向第二个链表的第一个结点,将第二个链表连在第一个链表后面
  • 释放第二个链表的头结点
  • 将第二个链表的尾指针指向第一个链表的头结点,将合并后的链表头尾连接

《线性表_动态分配一维数组、单链表、循环链表、双向链表》

//-----合并循环链表-----
LinkList LinkList_merge(LinkList La,LinkList Lb)
{ 
    //La,Lb为尾指针
    LinkList p=La->next;//记录第一个链表的头结点
    La->next=Lb->next->next;//将第二个链表连在第一个链表后面
    free(Lb->next);//释放第二个链表的头结点
    Lb->next=p;//将合并后的链表头尾连接
    return Lb;//返回合并后的循环链表的尾指针
}

合并单链表要遍历第一个链表,找到最后一个结点,再将第二个链表链接到第一个的后面,时间复杂度是O(n);合并循环链表,只需修改指针,无需遍历,时间复杂度是O(1)

双向链表

  • 双向链表有两个指针域,一个指向后继结点,一个指向前驱结点
  • 单链表存在循环链表,双向链表也有循环链表,即双向循环链表
  • 空表只有一个头结点,其两个指针域均指向头结点自己
  • 有p->next->prior=p->prior->next=p
    《线性表_动态分配一维数组、单链表、循环链表、双向链表》
//-----双向链表的存储结构-----
//结点结构
typedef struct DulNode
{ 
    ElemType data;//数组域
    struct DulNode *next;//指针域
    struct DulNode *prior;//指针域
}DulNode,*DuLinkList;

插入结点

  • 注意顺序
  • 单链表插入、删除结点是使指针p指向前一个结点(i-1),而双向链表插入、删除结点是使指针p指向该结点(i)
  • 2,3顺序不能颠倒
    《线性表_动态分配一维数组、单链表、循环链表、双向链表》
//-----带头结点的双向循环链表在第i个位置(之前)插入结点e-----
bool DuList_insert(DuList L,int i,ElemType e)
{ 
    DuList p=L;//p指向头结点
    int j=0;//计数器j初始化为0
    while(p!=L&&j<i)//注意,双向循环链表p指向第i个位置
    {     
        p=p->next;
        j++;
    }
    if(p==L||j>i)  return false;//不存在第i个结点
    //生成新结点
    DuList s=(DuList)malloc(sizeof(DulNode));
    s->data=e;
    //插入结点,更改4个指针
    s->next=p;
    s->prior=p->prior;
    p->prior->next=s;
    p->prior=s;
    return true;
}

删除结点

  • 单链表插入、删除结点是使指针p指向前一个结点(i-1),而双向链表插入、删除结点是使指针p指向该结点(i)

《线性表_动态分配一维数组、单链表、循环链表、双向链表》

//-----带头结点的双向循环链表,删除第i个位置结点,赋值给e-----
bool DuList_delete(DuList L,int i,ElemType *e)
{ 
    DuList p=L,s;//p指向头结点
    int j=0;//计数器初始化为0
    while(p!=L&&j<i)//注意,p指向第i个结点
    { 
        p=p->next;
        j++;
    }
    if(p==L||j>i) return false;//不存在第i个结点
    *e=p->data;//返回值给e
    //删除结点,顺序没有要求
    p->prior->next=p->next;
    p->next->prior=p->prior;
    free(p);//释放结点空间
    return true;
}
    原文作者:棂卡卡
    原文地址: https://blog.csdn.net/m0_51770627/article/details/109427379
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞