第2章:线性表

116 阅读10分钟

1、线性表的定义和基本操作

1.1、线性表的定义

线性表是具有相同数据类型的 n(n0)n(n\ge0) 个数据元素的有限序列,其中 nn 为表长,当 n=0n=0 时线性表是一个空表

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

线性表特点

  1. 表中元素个数有限
  2. 表中元素具有逻辑上的顺序性,表中元素有其先后次序
  3. 表中元素都是数据元素,每个元素都是单个元素
  4. 表中元素的数据类型都相同,这意味着每个元素占有相同大小的存储空间
  5. 表中元素具有抽象性,即仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容

1.2、线性表的基本操作

  • InitList(&L):初始化表。构造一个空的线性表
  • Length(L):求表长。返回线性表 L 的长度,即 L 中数据元素的个数
  • LocateElem(L,e):按值查找操作。在表 L 中查找具有给定关键字值的元素
  • GetElem(L,i):按位查找操作。获取表 L 中的第 i 个位置的元素的值
  • ListInsert(&L,i,e):插入操作。在表 L 中的第 i 个位置上插入指定元素 e
  • ListDelete(&L,i,&e):删除操作。删除表 L 中第 i 个位置的元素,并用 e 返回删除元素的值
  • PrintList(L):输出操作。按前后顺序输出线性表 L 的所有元素值
  • Empty(L):判空操作。若 L 为空表,则返回 True,否则返回 False
  • DestoryList(&L):销毁操作。销毁线性表,并释放线性表 L 所占用的内存空间

2、线性表的顺序表示

2.1、顺序表的定义

线性表的顺序存储也称顺序表。他是用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理位置上也相邻

#define MaxSize 50	//定义线性表的最大长度
typedef struct{
    ElemType data[MaxSize];  //顺序表的元素
    int length;				 //顺序表当前长度
}SqList;					 //顺序表的类型定义
#define InitSize 100  //表长度的初始定义
typedef struct{
    ElemType *data;     //指示动态分配数组的指针
    int MaxSize,length; //数组的最大容量和当前个数
}SqList;			    //动态分配数组顺序表的类型定义

顺序表的优点

  1. 可以进行随机访问(存取)。即通过首地址和元素序号可以再O(1)O(1)时间内找到指定元素
  2. 存储密度高。每个结点只存储数据元素

顺序表的缺点

  1. 元素的插入和删除需要移动大量元素。插入操作平均需要移动 n2\frac{n}{2} 个元素;删除操作平均需要移动 n12\frac{n-1}{2} 个元素
  2. 顺序存储分配需要一段连续的存储空间,不够灵活

2.2、顺序表基本操作的实现

//静态初始化
SqList L;   //声明一个顺序表(此时就会分配数组空间)  
void InitList(SqList &L){
    L.length=0;	  //顺序表初始长度为0
}

//动态初始化
SqList L;  //声明一个顺序表(此时未分配内存空间)
void InitList(SqList &L){
    L.data=(ElemType *)malloc(InitSize * sizeof(ElemType));  //分配存储空间
    L.lenght=0;				//顺序表初始长度为0
    L.MaxSize=InitSize;		//初始最大存储容量
}
bool ListInsert(SqList &L, int i, ElemType e){
    if(i<1 || i>L.length+1)			//判断i的范围是否有效
        return false;
    if(L.length >= MaxSize)			//判断存储空间是否已满
        return false;
    for(int j=L.length; j>=i; j--)	//将第i个元素及之后的元素后移
        L.data[j]=L.data[j-1];
    L.data[i-1]=e;	  //在位置i处放入e
    L.lenght++;		  //线性表长度加1
    return true;
}

//时间复杂度:最好O(1); 最坏O(n); 平均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++)	//将第i个位置后的元素前移
        L.data[j-1]=L.data[j];
    L.length--;		//线性表长度减1
    return true;
}

//时间复杂度:最好O(1); 最坏O(n); 平均O(n)
int LocateElem(SqList &L, ElemType e){
    int i;
    for(int i=0; i<L.length; i++){
        if(L.data[i]==e)
            return i+1;		//下标为i的元素值等于e,返回其位序i+1
    }
    return 0;		//退出循环,说明查找失败
}

//时间复杂度:最好O(1); 最坏O(n); 平均O(n)

3、线性表的链式表示

3.1、单链表的定义

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

存放数据的空间称为数据域,存放指针的空间称为指针域

typedef struct LNode{	//定义单链表结点类型
    ElemType data;		//数据域
    struct LNode *next;	//指针域
}LNode, *LinkList;

头指针

用来标识一个单链表,指出链表的起始位置,头指针为 Null 时表示一个空表

头结点

为了操作上的方便,在单链表第一个数据结点之前附加一个结点,称为头结点,并且数据域不存放任何信息

头指针与头结点的关系

不管带不带头结点,头指针始终指向链表中的第一个结点,而头结点是带头结点链表中的第一个结点,结点内通常不存放数据

引入头结点的好处

  1. 第一个数据结点的位置存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置上的操作一致,无需进行特殊处理
  2. 无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也得到了统一

3.2、单链表基本操作的实现

bool InitList(LinkList L){				//带头结点的单链表的初始化
    L=(LNode *)malloc(sizeof(LNode));	//创建头结点
    L->next=NULL;	//头结点之后暂时没有元素结点
    return true;
}
int Length(LinkList L){
    int len = 0;		//计数变量,初始为 0
    LNode *p = L;
    while(p->next != NULL){	  //判断是否为空表
        p = p->next;
        len++;			//每访问一个结点,计数加 1
    }
    return len;
}
时间复杂度:O(n)
LNode *GetElem(LinkList L, int i){
    LNode *p = L;		//指针p指向当前扫描到的结点
    int j = 0;			//记录当前结点的位序,头结点是第0个结点
    while(p!=NULL && j<i){	//循环找到第i个结点
        p=p->next;
        j++l
    }
    return p;		//返回第i个结点的指针或NULL
}
时间复杂度:O(n)
LNode *LocateElem(LinkList L, ElemType e){
    LNode *p = L->next;
    while(p!=NULL && p->data!=e)	//从第一个结点开始查找数据域为e的结点
        p=p->next;
    return p;	//找到后返回该结点指针,否则返回NULL
}
时间复杂度:O(n)
bool ListInsert(LinkList &L, int i, ElemType e){
    LNode *p=L;		//指针p指向当前扫描到的节点
    int j=0;		//记录当前结点的位序,头结点是第0个结点
    while(p!=NULL && j<i-1){	//循环找到第i-1个结点
        p=p->next;
        j++;
    }
    if(p==NULL)		//i值不合法
        return false;
    LNode *s=(LNode *)malloc(sizeof(LNode));
    s->data = e;
    s->next = p->next;
    p->next = s;
    return true;
}
时间复杂度:O(n)
bool ListDelete(LinkList &L, int i, ElemType &e){
    LNode *p=L;		//指针p指向当前扫描到的结点
    int j=0;		//记录当前结点的位序,头结点是第0个结点
    while(p->next!=NULL && j<i-1){	//循环找到第i-1个结点
        p=p->next;
        j++;
    }
    if(p->next==NULL || j>i-1)  //i值不合法
        return false;
    LNode *q=p->next;		//令q指向被删除结点
    e=q->data;				//用e返回元素的值
    p->next=q->next;		//将*q结点从链中断开
    free(q);				//释放结点的存储空间
    return true;
}
时间复杂度:O(n)
LinkList List_HeadInsert(LinkList &L){		//逆向建立单链表
    LNode *s;	
    int x;		//设置元素类型为整型
    L=(LNode *)malloc(sizeof(LNode));	//创建头结点
    L->next=NULL;		//初始为空链表
    scanf("%d", &x);	//输入结点的值
    while(x!=9999){		//输入9999表示结束
        s=(LNode *)malloc(sizeof(LNode));
        s->data=x;
        s->next = L->next;
        L->next = s;	//将新结点插入表中,L为头指针
        scanf("%d", &x);
    }
}
时间复杂度:O(n)
LinkList List_TailInsert(LinkList &L){	//正向建立单链表
    int x;		//设置元素类型为整型
    L=(LNode *)malloc(sizeof(LNode));	//创建头结点
    LNode *s,*r = L;	//r为表尾指针
    scanf("%d", &x);	//输入结点的值
    while(x!=9999){		//输入9999表示结束
        s=(LNode *)malloc(sizeof(LNode));
        s->data = x;
        r->next = s;
        r = s;		//r指向新的表尾结点
        scanf("%d", &x);
    }
    r->next = NULL;		//尾结点指针置空
    return true;
}
时间复杂度:O(n)

3.3、双链表

双链表结点中有两个指针 prior 和 next,分别指向其直接前驱和直接后继。解决单链表中访问某个结点前驱(插入、删除等操作),只能从头开始遍历的缺点

typedef struct DNode{			//定义双链表结点类型
    ElemType data;				//数据域
    struct DNode *prior, *next;	//前驱和后继指针
}DNode, *DLinklist
// 在双链表p所指的结点之后插入结点*s
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s;

时间复杂度:O(1)
// 删除双链表中结点*p和后继节点*q
p->next=q->next;
q->next->prior=p;
free(q);

时间复杂度:O(1)

3.4、循环链表

循环单链表

与单链表相比,表中最后一个结点的指针不是 NULL,而改为指向头结点,从而整个链表形成一个环。

判断循环单链表为空的条件不是头结点指针是否为空,而是它是否等于头指针 L

循环双链表

循环双链表中,头结点的 prior 指针指向表尾结点。最后一个结点的 next 指针指向头结点

3.5、静态链表

静态链表

静态链表是用数组来描述线性表的链式存储结构,结点也有数据域 data 和指针域 next,这里的指针域与之前不同,这里的指针是结点在数组中的数组下标,也称游标。和顺序表一样,静态链表也要预先分配一块连续空间

静态链表以 next==1 作为其结束标志

#define MaxSize 50		//静态链表最大长度
typedef struct{			//静态链表结构类型定义
    ElemType data;		//存储数据类型
    int next;			//下一个元素的数组下标
}SLinkList[MaxSize];

静态链表是顺序表和链表的折中方案,适合对内存分配有严格限制,又需要链表操作灵活性的场景

3.6、顺序表和链表的比较

存取(读/写)方式

顺序表既可以顺序存取,也可以随机存取,链表只能从表头开始依次顺序存取

逻辑结构与物理结构

采用顺序存储时,逻辑上相邻的元素,对应的物理存储位置也相邻。而采用链式存储时,逻辑上相邻的元素,物理位置不一定相邻

查找、插入和删除操作

对于按值查找,顺序表无序时,两者的时间复杂度均为O(n)O(n);顺序表有序时,可采用折半查找,此时的时间复杂度为O(log2n)O(\log_2n)

对于按序查找,顺序表支持随机访问,时间复杂度仅为O(1)O(1),而链表的平均复杂度为O(n)O(n)

顺序表的插入、删除操作,平均需要移动半个表长的元素
链表的插入、删除操作,只需要修改相关结点指针域

空间分配

顺序表需要分配连续的存储空间,并且一旦存储空间装满就不能扩充;链表的结点空间只在需要时申请分配,操作灵活高效