第二章 线性表

本文详细介绍了线性表的概念和两种主要表示方式:顺序表和链式表。顺序表通过数组实现,适合随机访问但插入删除操作不便;链式表包括单链表、双链表和循环链表,插入删除灵活但无法随机访问。文章讨论了各种操作的时间复杂度,并对比了顺序表与链表的优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

第二章 线性表

定义:线性表是具有相同数据类型的n(n>=0)个**数据元素有限**序列。

“&”表示C++语言中的引用调用,在C语言中采用指针也可以达到相同的效果。

线性表主要操作(参考即可):

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所占的内存空间。

线性表的顺序表示

定义:线性表的顺序存储又称顺序表

特点:

  1. 表中元素的逻辑顺序和物理顺序相同
  2. 最主要的特点是随机访问,具有随机存取的存储结构
  3. 通常用数组来描述
  4. 存储密度高,每个节点只存储数据元素。
  5. 拓展容量不方便(即使使用动态分配的方式实现,时间复杂度也比较高)
  6. 插入、删除操作不方便,需要移动大量元素

空间分配

静态分配

缺点:由于数组的大小和空间实现固定,一旦空间占满,再加入新的数据会导致溢出,进而程序崩溃。

#define MaxSize 50			//定义线性表的最大长度
typedef struct{				//定义结构体
    ElemType data[MaxSize];	//顺序表的元素
    int length;				//顺序表的当前长度
}SqList;					//定义结构体的名字为Sqlist
//定义了一个名为SqList的结构体
//SqList L;				//即声明一个SqList类型的变量L
//L.length;	L.data[i];	//获取成员变量
动态分配

存储数组的空间是在程序执行过程中,通过动态存储分配语句分配的,一旦数据空间占满,就另外开辟一块更大的存储空间,用以替换原本的存储空间。

#include<stdlib.h>			//malloc、free函数的头文件

#define InitSize 100		//表长度的初始定义

//定义结构体
typedef struct{				
    ElemType *data;		//指示动态分配数组的指针
    int MaxSize;		//数组的最大容量
    int length;			//数组的当前长度
}SqList;				//使用typedef将结构体命名为Sqlist

void InitList(SqList &L){
    //用malloc函数申请一片连续的存储空间
    L.data=(int*)malloc(InitSize*sizeof(int));
    L.length=0;
    L.MaxSize=InitSize;
}

void IncreaseSize(Sqlist &L,int len){
    //增加动态数组的长度
    int *p=L.data;			//“跟搬家公司说老房子的位置”	
    L.data=(int*)malloc((L.MaxSize+len)*sizeof(int));	//“买一个更大的房子”
    for(int i=0;i<L.length;i++){		
        L.data[i]=p[i];		//“将老房子的行李搬到新房子”:将数据复制到新区域
    }
    L.MaxSize=L.MaxSize+len;//	“将房产证的面积改成新房子的面积”:顺序表最大长度增加len
    free(p);				//“卖掉老房子”:释放原来的内存空间
}

int main(){
    SqList L;		//声明一个顺序表
    InitList(L);	//初始化顺序表
    //往顺序表中随便插入几个元素
    IncreaseSize(L,5);	
    return 0;
}

基本操作

插入操作

在顺序表的第i个位置插入新元素e。

时间复杂度:最好O(1)、最差O(n)、平均O(n)

//'&L'表示取L的地址,直接通过地址对L进行操作,不用返回值,因为操作的就是L本体。
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.length++;						//线性表长度加1
    return true;
}
//注意区分顺序表的位序和数组下标,位序从1开始,下标从0开始
删除操作

删除顺序表L中第 i ( i <= i <= L.length)个位置的元素,用引用变量e返回。

时间复杂度:最好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]=L.data[j-1];		
    L.length--;						//线性表长度减1
    return true;
}
按值查找(顺序查找)

在顺序表L中查找第一个元素值等于e的元素,并返回其位序。

时间复杂度:最好O(1)、最差O(n)、平均O(n)

int LocateElem(SqList L, ElemType e){	//无需修改L,故只需正常传入L即可
    for(int i=0; i<L.length; i++)
        if(L.data[i]==e)
            return i+1;			//下标为i的元素值等于e,返回其位序i+1
    return 0;					//退出循环,说明查找失败
}

线性表的链式表示

链式存储线性表时,不需要使用地址连续的存储单元,即不要求逻辑上相邻的元素在物理上也相邻。插入或删除只需要修改指针就可,不可以随机存取

单链表

定义:线性表的链式存储又称单链表。通过一组任意的存储单元来存储线性表中的数据元素。非随机存取的存储结构。

单链表中结点类型的描述如下:

typedef struct LNode{		//定义单链表结点类型
    ElemType data;			//数据域——存放数据元素
    struct LNode *next;		//指针域——指向其后继的指针//next指针指向的类型为LNode结构体类型
}LNode, *LinkList;		

为了便于理解,将其简化,等价于:

//定义了一个struct LNode类型并初始化一个结点
struct LNode{		//标准定义,定义一个LNode结构类型
	ElemType data;
    struct LNode *next;
};				
typedef struct LNode LNode1;	//使用typedef将struct LNode重命名为LNode1
LNode1 *LinkList;		//定义一个名为LinkList的指向LNode1数据类型的指针
//本质上等价
//LinkList	————强调这是一个单链表
//LNode*	————强调这是一个结点

优缺点:利用单链表可以解决顺序表需要大量连续存储单元的缺点,但附加指针域,造成浪费存储空间的缺点。

头指针头指针通常用来标识一个单链表,如单链表L,头指针为NULL时表示一个空表。

头节点:为了操作方便,在单链表第一个结点之前附加一个结点,称为头节点。头节点数据域可以为空,也可以用来记录表长等信息。

引入头节点的优点

  1. 由于第一个数据结点的位置被存放在头结点的指针域中,因此在链表的第一个位置上的操作和在表的其他位置的操作一致,无须进行特殊处理。
  2. 无论链表是否为空,其头指针都指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就得到了统一。
头结点
不带头结点的单链表
typedef struct LNode{	//定义单链表节点类型
	ElemType data;		//每个节点存放一个数据元素
	struct LNode *next;	//指针指向下一节点
}LNode,*LinkList;

//初始化一个空的单链表
bool InitList(LinkList &L){
	L=NULL;		//空表,暂时还没有任何节点
	return true;
}

//判断单链表是否为空
bool Empty(LinkList L){
	return (L==NULL);
}

void test(){
    LinkList L;	//声明一个指向单链表的指针
    InitList(L);//初始化一个空表
    //后续代码
}
带头结点的单链表
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=NULL;	//头结点之后暂时还没有节点
    return true;
}

//判断单链表是否为空
bool Empty(LinkList L){
	return (L->next==NULL);
}

void test(){
    LinkList L;	//声明一个指向单链表的指针
    InitList(L);//初始化一个空表
    //后续代码
}
比较

带头结点更方便

查找
按位查找

时间复杂度:O(n)

//按位查找,返回第i个元素(带头结点)
LNode *GetElem(LinkList L, int i){
    if(i<0)
        return NULL;
	LNode *p;	//指针p指向当前扫描到的结点
    int j=0;	//当前p指向的是第几个结点
    p=L; 		//L指向头结点,头结点是第0个结点(不存数据)
    while(p!=NULL&&j<i){      //从第i个结点开始找,查找第i个结点
        p=p->next;
        j++;
    }
    return p;			//返回第i个结点的指针,若i大于表长则返回NULL
}
按值查找

时间复杂度:O(n)

//按值查找,找到数据域==e的结点
LNode * LocateElem(LinkList L, ElemType e){
    LNode *p=L->next;
    //从第一个结点开始查找数据域为e的结点
    while(p!=NULL&&p->data!=e)
        p=p->next;
    return p;	//找到后返回该结点指针,否则返回NULL
}
求表的长度

时间复杂度:O(n)

//求表的长度
int Length(LinkList L){
    int len=0;	//统计表长
    LNode *p=L;
    while(p->next!=NULL){
        p=p->next;
        len++;
    }
    return len;
}
插入
按位序插入(带头结点)

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

//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, ElemType e){
    if(i<1)
        return false;
    LNode *p=GetElem(L,i-1);	//找到第i-1个结点
    if(p==NULL)		//i值不合法
        return false;
    LNode *s=(LNode*)malloc(sizeof(LNode));
    s->data=e;
    s->next=p->next;
    p->next=s;		//将结点s连到p之后
    return true;	//插入成功
}
按位序插入(不带头结点)
//在第i个位置插入元素e(不带头结点)
bool ListInsert(LinkList &L, int i, ElemType e){
    if(i<1)
        return false;
    if(i==1){	//插入第1个结点的操作与其他结点操作不同
        LNode *s=(LNode*)malloc(sizeof(LNode));
        s->data=e;
        s->next=L;
        L=s;
        return true;
    }
    LNode *p=GetElem(L,i-1);	//找到第i-1个结点
    if(p==NULL)		//i值不合法
        return false;
    LNode *s=(LNode*)malloc(sizeof(LNode));
    s->data=e;
    s->next=p->next;
    p->next=s;		//将结点s连到p之后
    return true;	//插入成功
}
指定节点的后插操作

时间复杂度O(1)

//后插操作:在p结点之后插入元素e
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保存数据元素e
    s->next=p->next;
    p->next=s;		//将结点s连到p之后
    return true;
}

//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, ElemType e){
    if(i<1)
        return false;
    LNode *p=GetElem(L,i-1);	//找到第i-1个结点
	return InsertNextNode(p,e);	//体会封装!
}
指定结点的前插操作
//前插操作:在p结点之前插入元素e
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连到p之后
    s->data=p->data;//将p中元素复制到e
    p->data=e;		//p中元素覆盖为e
    return true;
}

//前插操作:在p结点之前插入结点s
bool InsertPriorNode(LNode *p, LNode *s){
    if(p==NULL||s==NULL)
        return false;
    s->next=p->next;
    p->next=s;				//将结点s连到p之后
    ElemType temp=p->data;	//交换数据域部分
    p->data=s->data;		
    s->data=temp;			
    return true;
}
删除
按位序删除(带头结点)
//按位序删除
bool ListDelete(LinkList &L, int i, ElemType &e){
    if(i<1)
        return false;
    LNode *p=GetElem(L,i-1);	//找到第i-1个结点
    if(p==NULL)	//i值不合法
        return false;
    if(p->next == NULL)	//第i-1个结点之后已无其他结点
        return false;
    LNode *q=p->next;	//令q指向被删除结点
    e=q->data;		//用e返回元素的值
    p->next=q->next;	//将*q结点从链中“断开”
    free(q);			//释放结点的存储空间
    return true;		//删除成功
}
指定结点的删除
//删除指定结点p
bool DeleteNode(LNode *p){
    if(p==NULL)
        return false;
    LNode *q=p->next;		//令q指向*p的后继节点
    p->data=p->next->data;	//和后继节点交换数据域
    p->next=q->next;		//将*q结点从链中“断开”
    free(q);				//释放后继节点的存储空间
    return true;
}
建立
头插法建立单链表

从空表开始,生成新节点,将读取到的数据存放在新节点的数据域,然后将新结点插入到当前链表的表头,即头结点之后。读入数据的顺序与生成的链表中的元素顺序相反。设单链表长为n。

时间复杂度:O(n)

可以用于链表逆置,常考

头插法建立单链表的算法如下:

Linklist List_HeadInsert(LinkList &L){	//逆向建立单链表
    LNode *s;			//定义一个指向LNode类型的指针s
    int x;
    L=(LinkList)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);
    }
    return L;
}
尾插法建立单链表

头插法简单,但顺序不一致。尾插法可以使两者次序一致。此法将新节点插到当前链表的表尾,为此需要增加一个尾指针r,使其始终指向当前链表的尾结点。

时间复杂度:O(n)

尾插法建立单链表的算法如下:

Linklist List_TailInsert(LinkList &L){  //正向建立单链表
    int x;					//设元素类型为整型
    L=(LinkList)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 L;
}
*插入结点操作

插入结点操作将值为x的新结点插入到单链表的第i个位置上。先检查插入位置的合法性,然后找到待插入位置的前驱结点,即第i-1个结点,再在其后插入新节点

首先调用按序号查找算法GetElem(L, i-1),查找第i-1个结点。假设返回的第i-1个结点为* p,然后令新节点* s的指针域指向* p的后继结点,再令结点* p的指针域指向新插入的结点* s。

实现插入结点的代码片段如下:(O(n))

p=GetElem(L,i-1);	//查找插入位置的前驱结点
s->next=p->next;	//令待插入结点s的指针域指向p的后继结点
p->next=s;			//将s赋给p的后继指针,即将p的后继节点改为s
*删除结点操作

即删除单链表的第i个结点。先检查删除位置合法性,后查找出表中第i-1个结点,即被删结点的前驱结点,再将其删除

实现删除结点的代码片段如下:(O(n))

p=GetElem(L, i-1);		//查找删除位置的前驱结点
q=p->next;				//令q指向被删除结点
p->next=q->next			//将*q结点从链中“断开”
free(q);				//释放结点的存储空间

双链表

因为单链表只有一个向后的指针,所以只能从前往后按顺序遍历。为了克服这个问题,引入双链表,双链表有两个指针prior和next,分别指向其前驱和后继结点。

双链表中结点类型的描述如下:

typedef struct DNode{			//定义双链表结点类型
	ElemType data;				//数据域
	struct DNode *prior,*next;	//前驱和后继指针
}DNode,*DLinklist;
//初始化双链表
bool InitDLinkList(DLinklist &L){
    L=(DNode *)malloc(sizeof(DNode));	//分配一个头结点
    if(L==NULL)		//内存不足,分配失败
        return false;
    L->prior=NULL;	//头结点的prior永远指向NULL
    L->next=NULL;	//头结点之后暂时还没有结点
    return true;
}

//判断双链表是否为空(带头结点)
bool Empty(DLinklist L){
    return(L->next==NULL);
}

void testDLinkList(){
    //初始化双链表
    DLinklist L;
    InitDLinkList(L);
    //后续代码
}
插入
//在双链表中p所指的结点之后插入结点*s。
bool InsertNextDNode(DNode *p,DNode *s){
    //以p为基准来找后面的结点的,所以p的后继指针时最后改的。
    if(p==NULL||s==NULL)	//非法参数
        return false;
    s->next=p->next;//将结点*s插入到结点*p之后。待插入节点s的后继指针——>p结点的后继结点
    if(p->next!=NULL)	//若p结点有后继结点
        p->next->prior=s;	//p后继结点的前驱指针指向s
    s->prior=p;				//s的前驱指针指向p
	p->next=s;				//p的后继指针指向s
    return true;
}
删除
//删除p结点的后继结点
bool DeleteNextDNode(DNode *p){
    if(p==NULL)
        return false;
    DNode *q=p->next;	//找到p的后继节点q
    if(q==NULL)	//p没有后继
        return false;
    p->next=q->next;//将待删除结点q的后继结点与结点p的后继指针链接,从前往后
    if(q->next!=NULL)	//q不是最后一个结点
        q->next->prior=p;//将待删除结点的后继节点的前驱指针赋为p,从后往前
    free(q);
    return true;
}

//循环释放各个数据结点
void DestoryList(DLinklist &L){
    while(L->next!=NULL)
        DeleteNextDNode(L);
    free(L);	//释放头结点
    L=NULL;		//头指针指向NULL
}
遍历
//后向遍历
while(p!=NULL){
    //对结点p做相应处理,如打印
    p=p->next;
}

//前向遍历
while(p!=NULL){
    //对结点p做相应处理,如打印
    p=p->prior;
}

//前向遍历(跳过头结点)
while(p->prior!=NULL){
    //对结点p做相应处理,如打印
    p=p->prior;
}
查找

双链表不可随机存取,按位查找、按值查找操作都只能用遍历的方式实现。时间复杂度为O(n)。

循环链表

循环单链表

定义:在单链表的基础上,最后一个结点的指针不是NULL,而改为指向头结点,从而形成一个环。

判空条件:头结点的指针是否等于头指针

特点:循环单链表可以从表中任意一个结点开始遍历整个链表。有时对单链表常做的操作在表头和表尾进行的,此时就对循环单链表不设头指针而仅设尾指针,效率更高。

(简而言之,因为它是一个环,而且单链表只能向一个方向遍历,若设头指针,每次就需要从第一个开始,遍历一圈,而设的是尾指针的话,有两个优点,第一可以很容易的找到尾结点,第二,因为是一个环,所以尾结点的下一个就是头结点,十分方便首位的操作)

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;	//头结点的next指向头结点
    return true;
}

//判断循环单链表是否为空
bool Empty(LinkList L){
    return(L->next==L);
}
循环双链表

定义:在双链表的基础上,首尾相连,形成一个环。

判空条件:头结点的next指针是否指向了它自身

在循环双链表L中,某节点*p为尾结点时,p->next==L;当循环双链表为空表时,其头结点的prior和next都为L。

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

//初始化空的循环双链表
bool InitDLinkList(DLinklist &L){
    L=(DNode *)malloc(sizeof(DNode));
    if(L==NULL)
        return false;
    L->prior=L;
    L->next=L;
    return true;
}

void testDLinkList(){
    //初始化循环双链表
    DLinklist L;
    InitDLinkList(L);
    //后续代码
}

//判空
bool Empty(DLinklist L){
    return(L->next==L);
}

静态链表

定义:借助数组来描述线性表的链式存储结构。结点也有数据域data和指针域next,静态链表的指针是结点的相对地址,又称游标。同顺序表相同,静态链表也要预先分配一块连续的内存空间。

静态链表结构类型的描述如下:

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

next==-1为结束标志。

顺序表与链表的比较

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值