408数据结构知识点——第二章 线性表


注:内容参考王道2024考研复习指导以及《数据结构》

线性表的定义和基本操作

线性表是具有相同数据类型的 n ( n > 0 ) n(n>0) n(n>0)数据元素有限序列,其中 n n n表长,当 n = 0 n=0 n=0时线性表是一个空表。线性表一般表示为 L = ( a 1 , a 2 , . . . , a i , . . . , a n ) L=(a_1,a_2,...,a_i,...,a_n) L=(a1,a2,...,ai,...,an)

几个概念

a i a_i ai是线性表中的“第i个”元素在线性表中的位序。

a 1 a_1 a1是表头元素, a i a_i ai是表尾元素。

除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。

线性表的抽象数据类型定义

ADT{
数据对象:$D$={$a_i|a_i\in ElemSet,i=1,2,3,...,n,n\geq0$}
数据关系:$R$={$<a_{i-1},a_i>|a_{i-1},a_i \in D,i=2,3,...n$}
基本操作:
InitList(&L)
	操作结果:构造一个空的线性表L;
Destory(&L)
	初始条件:线性表L已经存在   
	操作结果:销毁线性表L
ClearList(&L)
	初始条件:线性表L已经存在   
	操作结果:将L重置为空表
ListEmpty(L)
	初始条件:线性表L已存在
	操作结果:若L为空表,则返回TRUE,否则返回FALSE
ListLength(L)
	初始条件:线性表L已存在;
	操作结果:返回线性表L中的数据元素个数;
GetElem(L,i,&e)
	初始条件:线性表L已存在,1<=i<=ListLengh(L)
	操作结果:用e返回L中第i个数据元素的值;
LocateElem(L,e)
	初始条件:线性表L已存在
	操作结果:返回L中第1个值与e相同的元素在L中的位置。若这样的数据元素不存在,则返回值为0。
PriorElem(L,cur_e,&pre_e)
	初始条件:线性表L已存在
	操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e 返回其前驱,否则操作失败, pre_e 无定义
NextElem(L,cur_e,&next_e)
	初始条件:线性表L已存在
	操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回其后继,否则操作失败, next_e无定义
ListInsert(L,i,&e)
	初始条件:线性表L已存在,1≦i≦ListLength(L) +1
	操作结果:在线性表L中的第i个位置之前插入新的元素e,L长度加1;
ListDelete(&l,i)
	初始条件:线性表L已存在且非空,1≦i≦ListLength(L) 
	操作结果:删除L的第i个数据元素,L长度减1
TraverseList(L)
	初始条件:线性表L已存在
	操作结果:对线性表L进行遍历,在遍历过程中对L的每个 结点访问一次
}ADT	List

线性表的顺序表示

顺序表的定义

顺序表——用顺序结构的方式实现线性表

特点:

  1. 随机访问,即可以在 O(1) 时间内找到第 i 个元素。
  2. 存储密度高,每个节点只存储数据元素。
  3. 拓展容量不方便(即便采用动态分配的方式实现,拓展长度的时间复杂度也比较高)。
  4. 插入、删除操作不方便,需要移动大量元素。

顺序表的实现——静态分配

给各个数据元素分配连续的存储空间,大小为MaxSize*sizeof(ElemType)。

“数组”存满后无法更改。

#define MaxSize 10//定义最大长度
typedef struct {
    ElemType data[MaxSize];
    int length;
}SqList;
//初始化线性表
void Initlist(SqList &L){
	L.length=0;
}

顺序表的实现——动态分配

#define InitSize 10//定义最大长度
typedef struct {
    ElemType *data;
    int MaxSize;
    int length;
}SqList;
//初始化线性表
void Initlist(SqList &L){
    L.data=(ElemType *)malloc(InitSize*sizeof(ElemType));
	L.length=0;
    L.MaxSize=InitSize;
}
void IncreaseSize(SqList &L,int len){
    int *p=L.data;
    L.data=(ElemType *)malloc((L.MaxSize+len)*sizeof(ElemType));
    forint i=0;i<L.length;i++{
        L.data[i]=p[i];//将数据复制到新区域
        L.MaxSize=L.MaxSize+len;//顺序表最大长度增加1en
        free(p);
}

顺序表的基本操作

顺序表的基本操作——插入

bool ListInsert(SqList &L,int i,ElemType e){
    if(i<1 || i>L.length+1){
        return false;
    }
    if(L.length >= MaxSize){
        return false;
    }
    for(int j=L.lengthlj>=i;j--){
        L.data[j]=L.data[j-1];
    }
	L.data[i-1]=e;
    L.length++;
}

时间复杂度分析

最好情况:新元素插入到表尾,不需要移动元素i = n+1,循环0次;最好时间复杂度 = O(1);

最坏情况:新元素插入到表头,需要将原有的 n 个元素全都向后移动i = 1,循环 n 次;最坏时间复杂度 = O(n);

平均情况:假设新元素插入到任何一个位置的概率相同,即 i = 1,2,3, … , length+1 的概率都是 p = 1 n + 1 p=\frac{1}{n+1} p=n+11,循环 n 次;i=2 时,循环 n-1 次;i=3,循环 n-2 次 …… i =n+1时,循环0次。平均循环次数 = n p + ( n − 1 ) p + ( n − 2 ) p + … … + 1 ⋅ p = n ( n + 1 ) 2 1 n + 1 = n 2 np + (n-1)p + (n-2)p + …… + 1⋅p =\frac{n(n+1)}{2}\frac{1}{n+1}=\frac{n}{2} np+(n1)p+(n2)p+……+1p=2n(n+1)n+11=2n;**平均时间复杂度 = O(n) **

顺序表的基本操作——删除

bool ListDelete(SqList &L,int i,ElemType &e){
    if(i<1||i>L.length)//判断i的范围是否有效
        return false;
    e=L.data[i-1];//将被删除的元素赋值给e
    for(int j=i;j<L.length;j++){
        L.data[j-1]=L.data[j];
    }
    L.length--;
    return true;
}

时间复杂度分析

最好情况:新元素插入到表尾,不需要移动元素i = n+1,循环0次;最好时间复杂度 = O(1);

最坏情况:新元素插入到表头,需要将原有的 n 个元素全都向后移动i = 1,循环 n 次;最坏时间复杂度 = O(n);

平均情况:假设新元素插入到任何一个位置的概率相同,即 i = 1,2,3, … , length+1 的概率都是 p = 1 n p=\frac{1}{n} p=n1,循环 n 次;i=2 时,循环 n-1 次;i=3,循环 n-2 次 …… i =n+1时,循环0次。平均循环次数 = n p + ( n − 1 ) p + ( n − 2 ) p + … … + 1 ⋅ p = n ( n − 1 ) 2 1 n = n − 1 2 np + (n-1)p + (n-2)p + …… + 1⋅p =\frac{n(n-1)}{2}\frac{1}{n}=\frac{n-1}{2} np+(n1)p+(n2)p+……+1p=2n(n1)n1=2n1;**平均时间复杂度 = O(n) **

顺序表的按位查找

ElemType GetElem(SqList L, int i){
    return L.data[i-1];
}

顺序表的按值查找

int LocateElem(SeqList L,ElemType e){
    for(int i=0;i<L.length;i++){
        if(L.data[i]==e){
            return i+1; //数组下标为i的元素值等于e,返回其位序i+1
        }
    }
	return 0; //退出循环,说明查找失败
}

时间复杂度分析

最好情况:新元素插入到表尾,不需要移动元素i = n+1,循环0次;最好时间复杂度 = O(1);

最坏情况:新元素插入到表头,需要将原有的 n 个元素全都向后移动i = 1,循环 n 次;最坏时间复杂度 = O(n);

平均情况:假设新元素插入到任何一个位置的概率相同,即 i = 1,2,3, … , length+1 的概率都是 p = 1 n p=\frac{1}{n} p=n1,循环 n 次;i=2 时,循环 n-1 次;i=3,循环 n-2 次 …… i =n+1时,循环0次。平均循环次数 = n p + ( n − 1 ) p + ( n − 2 ) p + … … + 1 ⋅ p = n ( n + 1 ) 2 1 n = n 2 np + (n-1)p + (n-2)p + …… + 1⋅p =\frac{n(n+1)}{2}\frac{1}{n}=\frac{n}{2} np+(n1)p+(n2)p+……+1p=2n(n+1)n1=2n;**平均时间复杂度 = O(n) **

线性表的链式表示

单链表的定义

链表——用链式结构的方式实现线性表

代码定义如下:

typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode,*LinkList;

不带头结点的单链表

bool InitList(LinkList &L){
    L=NULL;
    return true;
}
bool ListEmpty(LinkList L){
    return (L==NULL);
}

带头结点的单链表

bool InitList(LinkList &L){
    L=(LNode *)malloc(sizeof(LNode));
    if (L==NULL){
       return false; 
    }
    L->next=NULL;
    return true;
}
bool ListEmpty(LinkList L){
	return (L->next==NULL);
}

:不带头结点,写代码更麻烦,对第一个数据结点和后续数据结点的处理需要用不同的代码逻辑,对空表和非空表的处理需要用不同的代码逻辑。

单链表的基本操作

单链表的插入

按位插入(带头结点)

bool ListInsert(LinkList &L,int i,ElemType e){
    if(i<1){
        return false;
    }
    LNode *p;
    int j=0;
    p=L;
    while(p!=NULL && j<i-1){
        p=p->next;
        j++;
    }
    //以下代码可以在后插操作封装后替换称InsertNextNode(p,e)
    if(p==NULL){
        return false;
    }
    LNode *s=(LNode *)malloc(sizeof(LNode));
    s->data=e;
    s-next=p->next;
    p->next=s;
    return true;
}

:不带头结点的链表按位插入,需要对插入第1个结点单独处理,即 i = = 1 i==1 i==1时做单独处理。

指定结点的后插操作

bool InsertNextNode(LNode *p,ElemType e){
    if(p==NULL){
        return false;
    }
    LNode *s=(LNode *)malloc(sizeof(LNode));
    if(s==NULL){
        return false;
    }
    s->data=e;
    s-next=p->next;
    p->next=s;
    return true;
}

指定结点的前插操作

bool InsertPriorNode(LNode *p,ElemType e){
    if(p==NULL){
        return false;
    }
    LNode *s=(LNode *)malloc(sizeof(LNode));
    if(s==NULL){
        return false;
    }
    s->next=p-next;
    p->next=s;
    s->data=p->data;
    p->data=e;
    return true;
}

单链表的删除

按位删除

bool ListDelete(LinkList &L,int i,ElemType &e){
    if(i<1){
        return false;
    }
    LNode *p;
    int j=0;
    p=L;
    while(p!=NULL && j<i-1){
        p=p->next;
        j++;
    }
    if(p->next==NULL){
        return false;
    }
    LNode *q=p->next
    p-next=q->next;
	e=q->data;
    free(q);
    return true;
}

指定结点的删除

bool DeleteNode(LNode *p){
    if(p==NULL){
        return false;
    }
    LNode *q=p->next;
    p->data=q->data;
    p->next=q->next;
    free(p);
    return true;
}

单链表的查找

按位查找

LNode *GetElem(LinkList L,int i){
    if(i<0){
        return false;
    }
    LNode *p;
    int j=0;
    while(p!=NULL && j<i){
        p=p->next;
        j++;
    }
    return p;
}

按值查找

LNode *LocateElem(LinkList L,ElemType e){
    LNode *p=L->next;
    while(p!=NULL && p-data!=e){
        p=p->next;
    }
    return p;
}

单链表的建立

尾插法建立单链表

LinkList List_TailInsert(LinkList &L){
    int x;
    L=(LinkList)malloc(sizeof(LNode));
    L->next=NULL;
    LNode *s,*r=L;
    scanf("%d",&x);
    while(x!=-1){
        s=(LNode *)malloc(sizeof(LNode));
        s->data=x;
        r->next=s;
        r=s;
        scanf("%d",&x);
    }
    r->next=NULL;
    return L;
}

当输入:10 16 27时,单链表如下所示

image-20240117232225712

头插法建立单链表

重要应用于链表的逆置

LinkList List_HeadInsert(LinkList &L){
    int x;
    L=(LinkList)malloc(sizeof(LNode));
    L->next=NULL;
    LNode *s;
    scanf("%d",&x);
    while(x!=-1){
        s=(LNode *)malloc(sizeof(LNode));
        s->data=x;
        s->next=L->next;
        L->next=s;
        scanf("%d",&x);
    }
    return L;
}

当输入:10 16 27时,单链表如下所示

image-20240117232431931

双链表

双链表结点中有两个指针prior和next,分别指向其直接前驱和直接后继。表头结点的prior域和尾结点的next域都是NULL。对于单链表存在无法逆向检索的问题,双链表可进可退,但是存储密度相对于单链表更低一些。

image-20240117232835107

结点定义如下:

typedef struct DNode{
    ElemType data;
    struct DNode *prior,*next;
}DNode,*DLinkList;

双链表的初始化(带头结点)

bool InitDLinkList(DLinkList &L){
    L=(DlinkList)malloc(sizeof(DNode));
    if(L==NULL){
        return false;
    }
    L->next=NULL;
    L->prior=NULL;
    return true;
}

双链表的插入

在双链表中p所指的结点之后插入结点*s

bool InsertNextDNode(DNode *p,DNode *s){
    if (p==NULL || s==NULL)//非法参数
        return false;
    s->next=p->next;
    if(p->next != NULL)//如果p结点有后继结点
        p->next->prior=s;
    s->prior=p;
    p->next=s;
    return true;
}

双链表的删除

void DestoryList(DLinkList &L){
    while(L->next != NULL){
        DeleteNextDNode(L);
    }
    free(L);
    L=NULL;
}
bool DeleteNextDNode(DNode *p){
    if(p==NULL){
        return false;
    }
    DNode *q=p-next;
    if(q==NULL){
        return false;
    }
    p->next=q->next;
    if(q->next!=NULL){
        q->next->prior=p;
    }
    free(q);
    return true;
}

双链表的遍历

while (p!=NULL){//后向遍历
    p =p->next;
}
while (p!=NULL){//前向遍历
    p= p->prior;
}
while (p-> prior != NULL){//跳过头结点的前向遍历
	p = p->prior;
}

循环链表

循环单链表

表尾结点的next指针指向头结点。

从一个结点出发可以找到其他任何一个结点。

在循环单链表中,表尾结点*r的next域指向L,故表中没有指针域为NULL的结点,因此,循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针L。

image-20240117234421866

typedef struct LNode{
    ElemType data;
    struct LNode *next;
}LNode,*LinkList;

bool InitList(LinkList &L){
    L=(LNode *)malloc(sizeof(LNode));
    if (L==NULL){
       return false; 
    }
    L->next=L;//与单链表不同之处
    return true;
}

循环双链表

表头结点的 prior 指向表尾结点,表尾结点的 next 指向头结点。

image-20240117234724541

typedef struct DNode{
    ElemType data;
    struct DNode *prior,*next;
}DNode,*DLinkList;

bool InitDLinkList(DLinkList &L){
    L=(DlinkList)malloc(sizeof(DNode));
    if(L==NULL){
        return false;
    }
    L->next=L;
    L->prior=L;
    return true;
}

静态链表

分配一整片连续的内存空间,各个结点集中安置。

image-20240117235106190

代码定义如下:

#define MaxSize 10
typedef struct {
    ElemType data;
    int next;
}SLinkList[MaxSize];
void testSLinkList(){
    SLinkList a;//a为静态链表
}

查找:从头结点出发挨个往后遍历结点

插入位序为 i 的结点

  1. 找到一个空的结点(next为特殊值,例如-2表示结点为空),存入数据元素
  2. 从头结点出发找到位序为 i-1 的结点
  3. 修改新结点的 next
  4. 修改 i-1 号结点的 next

删除某个结点

  1. 从头结点出发找到前驱结点
  2. 修改前驱结点的游标
  3. 被删除结点 next 设为 -2
  • 优点:增、删 操作不需要大量移动元素

  • 缺点:不能随机存取,只能从头结点开始依次往后查找;容量固定不可变

  • 适用场景:①不支持指针的低级语言;②数据元素数量固定不变的场景(如操作系统的文件分配表FAT)

顺序表和链表的比较

类型逻辑结构存储结构基本操作
顺序表线性表,线性结构。支持随机存取、存取密度高;但是大片连续空间分配不方便、改变容量不方便。在查找方面,顺序表支持随机存取,时间复杂度为 O ( 1 ) O(1) O(1),优于链表;在插入元素和删除元素方面,顺序表时间开销主要来自移动元素。
链表线性表,线性结构。离散的小空间分配方便、改变容量方便;但是不可随机存取、存储密度低。创建方面,链表具有弹性,可扩容,优于顺序表;在插入元素和删除元素方面,链表只需要修改指针,时间开销主要来自查找元素,时间代价更低,优于顺序表。

问题: 请描述顺序表和链表的…实现线性表时,用顺序表还是链表好?
:顺序表和链表的逻辑结构都是线性结构,都属于线性表。但是二者的存储结构不同,顺序表采用顺序存储…(特点,带来的优点缺点);链表采用链式存储…(特点、导致的优缺点)。由于采用不同的存储方式实现,因此基本操作的实现效率也不同。当初始化时…;当插入一个数据元素时…;当删除一个数据元素时…;当查找一个数据元素时…

  • 25
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值