0
点赞
收藏
分享

微信扫一扫

数据结构-线性表详解(类C语言版)

8052cf60ff5c 2022-03-31 阅读 103

目录

线性表的定义和特点

线性表的顺序表示

顺序表的基本操作

顺序表的存储结构

顺序表的初始化

顺序表的取值

顺序表的查找

顺序表的插入

顺序表的删除

顺序表的操作算法分析

时间复杂度

空间复杂度

顺序表优缺点

优点

缺点

顺序表总结

线性表的链式表示——单链表

 概念

如何表示空表

 设置头结点的好处

链式存储结构的特点

单链表的基本操作

单链表的存储结构

单链表的初始化

单链表的建立

单链表的取值

单链表的按值查找

单链表的插入

单链表的删除

判断单链表是否为空

销毁单链表

清空单链表

求单链表L的表长

链式存储结构的优缺点

优点

缺点

线性表的链式表示——循环链表

循环链表的操作

遍历循环链表的判断

 合并两个循环链表

线性表的链式表示——双向链表

双向链表的操作

双向链表的结构定义

双向链表的插入

双向链表的删除

 双向循环链表

各种线性表的对比

顺序表和链表的比较

单链表、循环链表和双向链表的时间效率比较

线性表的合并

线性表的合并

有序表的合并

链式有序表的合并


线性表的定义和特点

线性表是具有相同特性的数据元素的一个有限序列。

线性表的顺序表示

顺序表的基本操作

InitList(&L)            //初始化操作,建立一个空的线性表L
DestroyList(&L)         //销毁已存在的线性表L
Clearlist(&L)           //将线性表清空
ListInsert(&L, i, e)    //在线性表L中第i个位置插入新元素e
ListDelete(&L, i, &e)   //删除线性表L中第i位置元素,用e返回
IsEmpty(L)              //若线性表为空,返回true,否则false
ListLength(L)           //返回线性表L的元素个数
LocateElem(L, e)        //L中查找与给定值e相等的元素,若成功返回该元素在表中的序号,否则返回0
GetElem(L, i, &e)       //将线性表L中的第i个位置元素返回给e

顺序表的存储结构

// ---------顺序表的存储结构--------
#define MAXSIZE 100    // 顺序表可能达到的最大长度
typedef struct
{
ElemType *elem;        // 存储空间的基地址
int length;            // 当前长度
} SqList;              // 顺序表的结构类型为SqList

顺序表的初始化

① 为顺序表L动态分配一个预定义大小的数组空间,使elem指向这段空间的基地址。
② 将表的当前长度设为0。

Status InitList(SqList &L)
{// 构造一个空的顺序表 L
    L.elem= new ElemType[MAXSIZE];    // 为顺序表分配一个大小为MAXSIZE的数组空间
    if(! L.elem) exit(OVERFLOW);      // 存储分配失败退出
    L.length=O;                       // 空表长度为0
    return OK;
}

动态分配线性表的存储区域可以更有效地利用系统的资源 , 当不需要该线性表时 , 可以使用销毁操作及时释放占用的存储空间。


顺序表的取值

① 判断指定的位置序号 i 值是否合理 (I ≤ i ≤ L.length), 若不合理,则返回ERROR。
② 若 i 值合理,则将第 i 个数据元素 L.elem[i-1]赋给参数 e, 通过 e 返回第 1 个数据元素的传值。

Status GetElem(SqList L,int i,ElemType &e)
{
    if (i<1 || i>L.length) return ERROR;  //判断i值是否合理,若不合理, 返回 ERROR
    e=L.elem[i-1];                         //elem[i-1] 单元存储第 i 个数据元素
    return OK;
}

显然,顺序表取值算法的时间复杂度为O(1)。


顺序表的查找

① 从第一个元素起,依次和 e相比较,若找到与 e相等的元素 L.elem[i], 则查找成功,返回该元素的序号 i+1。
② 若查遍整个顺序表都没有找到,则查找失败, 返回0。

int LocateELem(SqList L,ElemType e) 
{//在顺序表1中查找值为e的数据元素, 返回其序号
    for(i=O;i< L.length;i++) 
    if(L.elem[i)==e) return i+1;  // 查找成功,返回需要i+1
    return O;                     // 查找失败, 返回 0
}

由此可见,顺序表按值查找算法的平均时间复杂度为 O(n)。

顺序表的插入

线性表的插入操作是指在表的第i个位置插入一个新的数据元素 e, 使长度为 n 的线性表变成长度为n+1的线性表。

① 判断插入位置i是否合法(i 值的合法范围是i ≤ i ≤ n+1), 若不合法 则返回 ERROR。
② 判断顺序表的存储空间是否已满,若满则返回 ERROR。
③ 将第n个至第i个位置的元素依次向后移动一个位置,空出第i个位置(i =n+1时无需
移动)。

④ 将要插入的新元素e放入第i个位置。
⑤ 表长加1。

Status ListInsert(SqList &L,int i ,ElemType e) 
{ // 在顺序表 L 中第 l 个位置之前插入新的元素 e, i值的合法范围是 1≤i≤L.length+l 
    if((i<l) || (i>L.length+1)) return ERROR;   // i值不合法
    if(L.length==MAXSIZE) return ERROR;         // 当前存储空间已满
    for (j=L.length-1; j>=i-1; j--)
        L.elem[j+1]=L.elem[j];                  // 插入位置及之后的元素后移
    L.elem[i-1]=e;                              // 将新元素e放入第i个位置
    ++L.length;                                 // 表长加1
    return OK;
}

顺序表的删除

① 判断删除位置 i 是否合法(合法值为1 ≤ i ≤ n), 若不合法则返回 ERROR。

② 将第i+1个至第n个的元素依次向前移动一个位置 (i = n时无需移动)。
③ 表长减1。

Status ListDelete(SqList &L,int i) 
{//在顺序表L中删除第i个元素,i值的合法范围是 1≤ i≤L.length 
    if((i<1) || (i>L.length)) return ERROR;     // i值不合法
    for (j=i; j <=L.length-1; j ++)
        L.elem[j-1]=1.elem[j];                  // 被删除元素之后的元素前移 
    --L.length;                                 // 表长减1
    return OK;
}

顺序表的操作算法分析

时间复杂度

查找、插入、删除算法的平均时间复杂度为O(n)。

空间复杂度

显然,顺序表操作算法的空间复杂度S(n)=O(1)(没有占用辅助空间)

顺序表优缺点

优点

存储密度大(结点本身所占存储量 / 结点结构所占存储量)。

可以随机存取表中任一元素。

缺点

在插入、删除某一元素时,需要移动大量元素。

浪费存储空间(必须定义最大长度)。

属于静态存储形式,数据元素的个数不能自由扩充。

顺序表总结

顺序表的特点:以物理位置相邻表示逻辑关系。

顺序表的优点:任一元素均可随机存取。

顺序表的缺点:进行插入和删除操作时,需移动大量的元素。存储空间不灵活。

线性表的链式表示——单链表

结点只有一个指针域的链表,称为单链表或线性链表。

 概念

 头指针:是指向链表中第一个结点的指针。

首元结点:是指链表中存储第一个数据元素a1的结点。

头结点:是在链表的首元结点之前附设的一个结点。

如何表示空表

无头结点时,头指针为空时表示空表。

有头结点时,当头结点的指针域为空时表示空表。

 

 设置头结点的好处

1.便于首元结点的处理

首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其他位置一致,无须进行特殊处理。

2.便于空表和非空表的统一处理

无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了。

链式存储结构的特点

1.结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。

2.访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等。

单链表的基本操作

单链表的存储结构

typedef struct Lnode{ // 声明结点的类型和指向结点的指针类型
    ElemType     data; // 结点的数据域
    struct Lnode *next;// 结点的指针域
} Lnode, *LinkList;    // LinkList为指向结构体Lnode的指针类型

定义链表L:LinkList L;

定义结点指针p:Lnode *p;   或者LinkList p;

单链表的初始化

① 生成新结点作为头结点,用头指针L 指向头结点。
② 头结点的指针域置空。

Status InitList(LinkList &L) 
{   // 构造一个空的单链表L
    L=new LNode;     // 生成新结点作为头结点, 用头指针L指向头结点
    L->next=NULL;    // 头结点的指针域置空
    return OK;
}

单链表的建立

头插法

void CreateList_H(LinkList &L, int n) {
    L=new LNode;
    L->next=NULL;    // 先建立一个带头结点的单链表
    for(i=n;i>0;--i){
        p=new LNode;    // 生成新结点(p=(LNode*)malloc(sizeof(LNode));)
        cin>>p->data;   // 输入元素值scanf(&p->data);
        p->next=L->next;// 插入到表头
        L->next=p;
    }
}

尾插法

// 正位序输入n个元素的值,建立带表头节点的单链表L
void CreateList_R(LinkList &L, int n) {
    L=new LNode;    L->next=NULL;
    r=L    // 尾指针r指向头结点
    for(i=0;i<n;++i){
        p=new LNode; cin>>p->data;    // 生成新节点,输入元素值
        p->next=NULL;
        r->next=p;    // 插入到表尾
        r=p;    // r指向新的尾结点
    }
}

单链表的取值

① 用指针p指向首元结点,用 j 做计数器初值赋为1。
② 从首元结点开始依次顺着链域 next 向下访问,只要指向当前结点的指针 p 不为空(NULL), 并且没有到达序号为i的结点,则循环执行以下操作:
        • p指向下一个结点;
        • 计数器 j 相应加1。

③ 退出循环时, 如果指针p为空, 或者计数器 j 大于 i, 说明指定的序号 i 值不合法(i大于表长n或i小于等于0), 取值失败返回ERROR; 否则取值成功, 此时 j=i 时,p所指的结点就是要找的第 i 个结点,用参数e保存当前结点的数据域, 返回OK。

Status GetElem(LinkList L, int i, ElemType &e) 
{ // 在带头结点的单链表L中根据序号i获取元素的值,用e返回L中第i个数据元素的值
    p=L->next;j=l;             // 初始化,p指向首元结点,计数器j初值赋为1
    while(p&&j<i)              // 顺链域向后扫描,直到p为空或p指向第i个元素
    {
        p=p->next;             // p指向下一个结点
        ++j;                   // 计数器j相应加1
    }
    if(!p||j>i)return ERROR;   // i值不合法 i>n或i≤0
    e=p->data;                 // 取第i个结点的数据域
    return OK;
}

单链表的按值查找

① 用指针p指向首元结点 。
② 从首元结点开始依次顺着链域next向下查找, 只要指向当前结点的指针p不为空, 并且p所指结点的数据域不等于给定值e, 则循环执行以下操作: p指向下一个结点 。
③ 返回p。若查找成功,p此时即为结点的地址值,若查找失败,p的值即为NULL。

LNode *LocateELem(LinkList L, Elemtype e) 
{ // 在带头结点的单链表L中查找值为e的元素
    p=L->next;     // 初始化,p指向首元结点
    while(p && p->data!=e)     // 顺链域向后扫描,直到p为空或p所指结点的数据域等于e
        p=p->next;             // p指向下一个结点
    return p;                  // 查找成功返回值为e的结点地址p, 查找失败p为NULL
}

单链表的插入

将值为 e 的新结点插人到表的第 i 个结点的位置上, 即插入到结点 ai-1 与 ai 之间,具体插入过程如图所示, 图中对应的 5 个步骤说明如下。
① 查找结点ai-1 并由指针p指向该结点。
② 生成一个新结点*s。
③ 将新结点*s 的数据域置为 e。
④ 将新结点*s 的指针域指向结点ai。
⑤ 将结点*p 的指针域指向新结点*s。

Status ListInsert(LinkList &L,int i,ElemType e) 
{ // 在带头结点的单链表L中第i个位置插入值为e的新结点
    p=L;j=0; 
    while (p && (j<i-1)) 
        {p=p->next;++j;}             // 查找第i-1个结点,p指向该结点
    if (!p || j>i-1) return ERROR;   // i>n+l或者i<1
    s=new LNode;                     // 生成新结点*s
    s->data= e;                      // 将结点*s的数据域置为e
    s->next=p->next;                 // 将结点 *s的指针域指向结点 ai
    p->next=s;                       // 将结点*p的指针域指向结点*s
    return OK;
}

单链表的删除

删除单链表的 第 i 个结点 ai 的具体过程如图所示,图中的对应的4个步骤说明如下。
① 查找结点ai-1 并由指针p指向该 结点。

② 临时保存待删除结点ai的地址在q中 ,以备释放。
③ 将结点*p的指针域指向ai的直接后继结点。
④ 释放结点ai的空间。

Status ListDelete(LinkList &L,int i) 
{ // 在带头结点的单链表L中,删除第i个元素
    p=L;j=0;
    while ((p->next) && (j<i-1))              // 查找第i-1个结点,p指向该结点
        {p=p->next; ++j;}
    if ( !(p->next) || (j>i-1)) return ERROR; //当i>n或i<1时,删除位置不合理
    q=p->next;                                // 临时保存被删结点的地址以备释放
    p->next=q->next;                          // 改变删除结点前驱结点的指针域
    delete q;                                 // 释放删除结点的空间
    return OK;
}

判断单链表是否为空

空表:链表中无元素,称为空链表(头指针和头结点仍然存在)。

int ListEmpty(LinkList L){ // 若L为空表,则返回1,否则返回0
    if(L->next) // 非空
        return 0;
    else 
        return 1;
}

销毁单链表

Status DestroyList_L(LinkList &L){ // 销毁单链表L
    Lnode *p; // 或LinkList p;
    while(L){
        p=L;
        L=L->next;
        delete p;
    }
    return OK;
}

清空单链表

Status ClearList(LinkList &L){    // 将L重置为空表
    Lnode *p,*q;    // 或LinkList p,q;
    p=L->next;
    while(p){    // 没到表尾
        q=p->next;
        delete p;
        p=q;
    }
    L->next=NULL;    // 头结点指针域为空
    return OK;
}

求单链表L的表长

int ListLength_L(LinkList L){    // 返回L中数据元素个数
    LinkList p;
    p=L->next;    // p指向第一个结点
    i=0;
    while(p){    // 遍历单链表,统计结点数
        i++;
        p=p->next;
    }
    return i;
}

链式存储结构的优缺点

优点

① 结点空间可以动态申请和释放。

② 数据元素的逻辑次序靠结点的指针来表示,插入和删除时不需要移动数据元素。

缺点

① 存储密度小,每个结点的指针域需额外占用存储空间。当每个结点的数据域所占字节不多时,指针域所占存储空间的比重显得很大。

② 链式存储结构是非随机存取结构。对任一结点的操作都要从头指针依指针链查找到该节点,这增加了算法的复杂度。

线性表的链式表示——循环链表

首尾相接的链表称为循环链表。

 

循环链表的操作

遍历循环链表的判断

注意:由于循环链表中没有NULL指针,故涉及遍历操作时,其终止条件就不再像非循环链表那样判断p或p->next是否为空,而是判断它们是否等于头指针。

循环条件:

 合并两个循环链表

尾指针循环链表的合并(将Tb合并在Ta之后)

 

LinkList Connect(LinkList Ta, LinkList Tb){
                                // 假设Ta、Tb都是非空的单循环链表
    p=Ta->next;                 // ① p存表头节点
    Ta->next=Tb->next->next;    //② Tb表头节点连结Ta表尾
    delete Tb->next;            // ③ 释放Tb表头节点
    Tb->next=p;                 // ④ 修改指针
    return Tb;
}

线性表的链式表示——双向链表

结点有两个指针域的链表,称为双链表。

双向链表的操作

双向链表的结构定义

双向链表结点结构

 

 

typedef struct DuLNode{
    Elemtype data;
    struct DulNode *prior, *next;
} DulNode, *DuLinkList;

双向链表的插入

 

void ListInsert_Dul(DuLinkList &L, int i, ElemType e) {
    // 在带头结点的双向循环链表L中第i个位置之前插入元素e
    if(!(p=GetElemP_Dul(L,i))) return ERROR;
    s=new DulNode;    s->data=e;
    s->prior=p->prior;    p->prior->next=s;
    s->next=p;    p->prior=s;
    return OK;
}

双向链表的删除

 

void ListDelete_Dul(DuLink &L, int i, ElemType &e) {
    // 删除带头结点的双向循环链表L的第i个元素,并用e返回。
    if(!(p=GetElemP_Dul(L,i))) return ERROR;
    e=p->data;
    p->prior->next=p->next;
    p->next-prior=p->prior;
    free(p);
    return OK;
}

 双向循环链表

和单链表的循环表类似,双向链表也可以有循环表

· 让头结点的前驱指针指向链表的最后一个结点。

· 让最后一个结点的后继指针指向头结点。

各种线性表的对比

顺序表和链表的比较

顺序表链表
空间存储空间预先分配,会导致空间闲置或溢出现象动态分配,不会出现存储空间闲置或溢出现象
存储密度不用为表示结点间的逻辑关系而增加额外的存储开销,存储密度等于1需要借助指针来体现元素间的逻辑关系,存储密度小于1
时间存取元素随机存取,按位置访问元素的时间复杂度未O(1)顺序存取,按位置访问元素时间复杂度为O(n)
插入、删除平均移动约表中一半元素,时间复杂度为O(n)不需移动元素,确定插入、删除位置后,时间复杂度为O(1)
适用情况

① 表长变化不大,且能事先确定变化的范围

② 很少进行插入或删除操作,经常按元素位置序号访问数据元素

① 长度变化较大

② 频繁进行插入或删除操作

单链表、循环链表和双向链表的时间效率比较

查找表头节点(首元结点)查找表尾结点查找结点*p的前驱结点
带头结点的单链表L

L->next

时间复杂度O(1)

从L->next依次向后遍历

时间复杂度O(n)

通过p->next无法找到其前驱
带头结点仅设头指针L的循环单链表

L->next

时间复杂度O(1)

从L->next依次向后遍历

时间复杂度O(n)

通过p->next可以找到其前驱

时间复杂度O(n)

带头结点仅设尾指针R的循环单链表

R->next

时间复杂度O(1)

R

时间复杂度O(1)

通过p->next可以找到其前驱

时间复杂度O(n)

带头结点的双向循环链表L

L->next

时间复杂度O(1)

L->prior

时间复杂度O(1)

p->prior

时间复杂度O(1)

线性表的合并

线性表的合并

① 分别获取 LA表长 m和 LB 表长n。
② 从 LB 中第 1 个数据元素开始, 循环n次执行以下操作:
• 从 LB 中查找第 i (1 ≤ i ≤ n) 个数据元素赋给 e;
• 在 LA 中查找元素 e, 如果不存在, 则将 e 插在表LA 的最后。

void MergeList(List &LA,List LB) 
{ // 将所有在线性表 LB中但不在LA中的数据元素插入到LA中
    m=ListLength(LA); n=ListLength(LB);     //求线性表的长度
    for(i=1;i<=n;i++)
    {
        GetElem(LB,i,e);                    // 取 LB中第1个数据元素赋给 e
        if (! LocateElem(LA, e))            // LA中不存在和 e 相同的数据元素
            ListInsert(LA,++m,e);           // 将 e 插在LA的最后
    }
}

算法时间复杂度为O(m*n)

有序表的合并

① 创建一个表长 为m+n 的空表LC。
② 指针pc初始化, 指向LC的第一个元素。
③ 指针pa和pb初始化,分别指向LA和LB的第一个元素。
④ 当指针pa和pb均未到达相应表尾时, 则依次比较pa和pb所指向的元素值 , 从LA或LB中 "摘取“ 元素值 较小的结点插入到LC的最后。
⑤ 如果pb巳到达LB的表尾, 依次将LA的剩余元素插入LC的最后。
⑥ 如果pa已到达LA的表尾, 依次将LB的剩余元素插入LC的最后。

void MergeList_Sq(SqList LA,SqList LB,SqList &LC) 
{ // 已知顺序有序表LA和LB的元素按值非递减排列
  // 归并LA和LB得到新的顺序有序表LC, LC的元素也按值非递减排列
    LC.length=LA.length+LB.length;         // 新表长度为待合并两表的长度之和
    LC.elem=new ElemType[LC.length];       // 为合并后的新表分配一个数组空间
    pc=LC.elem;                            // 指针pc 指向新表的第一个元素
    pa=LA.elem; pb=LB.elem;                // 指针pa 和pb 的初值分别指向两个表的第一个元素
    pa_last=LA.elem+LA.length-1;           // 指针pa_last指向LA的最后一个元素
    pb_last=LB.elem+LB.length-1;           // 指针pb_last指向LB的最后一个元素
    while ((pa<=pa_last) && (pb<=pb_last)) // LA和LB均未到达表尾
    { 
        if(*pa< =*pb ) *pc++ = *pa++;      // 依次 "摘取“ 两表中值较小的结点插人到LC的最后
        else *pc++ = *pb++; 
    }
    while (pa<=pa_last) *pc++=*pa++;       // LB已到达表尾,依次将LA的剩余元素插人LC的最后
    while (pb<=pb_last) *pc++=*pb++;       // LA已到达表尾,依次将LB的剩余元素插入LC的最后
}

算法的时间复杂度为O(m+ n)。

空间复杂度也为O(m+ n)。

链式有序表的合并

① 指针 pa和 pb 初始化,分别指向LA和LB的第一个结点。
② LC的结点取值为LA的头结点。
③ 指针 pc初始化,指向LC的头结点。

④ 当指针 pa 和 pb 均未到达相应表尾时, 则依次比较 pa 和 pb 所指向的元素值, 从 LA 或LB 中 "摘取“ 元素值较小的结点插入到 LC 的最后。
⑤ 将非空表的剩余段插入到 pc 所指结点之后。
⑥ 释放 LB 的头结点。

void MergeList_L(LinkList &LA,LinkList &LB,LinkList &LC) 
{ // 已知单链表 LA和LB的元素按值非递减排列
  // 归并LA 和 LB得到新的单链表 LC, LC的元素也按值非递减排列
    pa=LA->next;pb=LB->next;         // pa 和 pb的初值分别指向两个表的第一个结点
    LC=LA;                           // 用LA的头结点作为LC的头结点
    pc=LC;                           //pc的初值指向LC的头结点
    while(pa&&pb) 
    { // LA 和 LB均未到达表尾,依次 “摘取”两表中值较小的结点插人到LC的最后
        if(pa->data<=pb->data)       // '摘取"pa所指结点
        {
            pc->next=pa;             // 将pa所指结点链接到pc所指结点之后
            pc=pa;                   // pc指向pa
            pa=pa->next;             // pa指向下一结点
        }
        else                         // "摘取"pb所指结点
        {
            pc->next=pb;             // 将pb所指结点链接到pc所指结点之后
            pc=pb;                   // pc指向pb
            pb=pb->next;             // pb指向下一结点
        }
    }                                // while 

    pc->next=pa?pa:pb;               // 将非空表的剩余段插入到pc所指结点之后
    delete LB;                       // 释放LB的头结点
}

算法的时间复杂度为O(m+ n)。

空间复杂度为O(1)。

举报

相关推荐

0 条评论