数据结构与算法线性表

Posted 生命是有光的

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构与算法线性表相关的知识,希望对你有一定的参考价值。

✍、目录总览

1、线性表

定义:线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列,其中n为表长,当n = 0 时线性表是一个空表。若用L命名线性表,则其一般表示为:

L = (a1,a2,....,ai,ai+1,.....,an)

线性表的特点

  • 线性表中的元素的个数是有限的
  • 线性表中的元素有先后次序
  • 线性表中的数据类型都相同,这意味着每个元素占有相同大小的存储空间
  • ai 是线性表中的 “第i个” 元素线性表中的位序(位序是从1开始,数组下标从0开始)
  • a1表头元素,an表尾元素
  • 除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继

2、顺序表

定义:把用顺序存储的方式实现的线性表叫做顺序表

顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。

顺序表有两种实现方式:静态分配实现和动态分配实现

2.1、静态分配实现

#define MaxSize 10;				//定义最大长度
typedef struct{
    ElemType data[MaxSize];		//用静态的"数组"存放数据元素
    int length;					//顺序表的当前长度
}SqList;						//顺序表的类型定义(静态分配方式)

2.2.1、初始化顺序表

#include<stdio.h>
#define MaxSize 10;				//定义最大长度
typedef struct{
    ElemType data[MaxSize];		//用静态的"数组"存放数据元素
    int length;					//顺序表的当前长度
}SqList;						//顺序表的类型定义

// 基本操作-初始化一个顺序表
void InitList(SqList &L){
    for(int i=0;i<MaxSize;i++)
    {
        L.data[i] = 0;			//将所有数据元素设置为默认初始值
    }
    L.length = 0;				//顺序表初始长度为0
}


int main(){
    SqList L; 					//声明一个顺序表
    InitList(L);				//初始化顺序表
    
    return 0;
}

2.2、动态分配实现

#define MaxSize 10;				//定义最大长度
typedef struct{
    ElemType *data;				//指针指向第一个数据元素
    int MaxSize;				//顺序表的最大容量
    int length;					//顺序表的当前长度
}SeqList;						//顺序表的类型定义(动态分配方式)

2.2.1、初始化顺序表

#define MaxSize 10;				//定义最大长度
#include<stdio.h>
#include<stdlib.h>
typedef struct{
    ElemType *data;				//指针指向第一个数据元素
    int MaxSize;				//顺序表的最大容量
    int length;					//顺序表的当前长度
}SeqList;						//顺序表的类型定义

//	基本操作-初始化一个顺序表
void InitList(SeqList &L){
    //用malloc函数申请一片连续的存储空间
    L.data=(ElemType *)malloc(sizeof(ElemTyoe)*InitSize);
    L.length = 0;			//顺序表初始长度为0
    L.MaxSize = InitSize;	//顺序表的最大长度
}

// 增加动态数组的长度
void IncreaseSize(SeqList &L,int len){
    int *p = L.data;		//让指针p指向顺序表中的第一个数据元素
    L.data=(ElemType *)malloc(sizeof(ElemType)*(L.MaxSize+len));
    for(int i=0; i<L.length; i++){
        L.data[i] = p[i];	//将数据复制到新区域
    }
    L.MaxSize = L.MaxSize + len;	//顺序表最大长度增加 len
    free(p);						//释放原来的内存空间
}


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

2.3、顺序表的特点

  1. 随机访问:即可以在O(1)时间内中找到第i个元素

  2. 存储密度高,每个节点只存储数据元素

  3. 扩展容量不方便

  4. 插入、删除操作不方便,需要移动大量元素

2.4、顺序表的插入

ListInsert(&L,i,e) :插入操作,在表L中的第 i 个位置上插入指定元素e

#define MaxSize 10;				//定义最大长度

typedef struct{
    ElemType data[MaxSize];		//用静态的"数组"存放数据元素
    int length;					//顺序表的当前长度
}SqList;						//顺序表的类型定义(静态分配方式)

// 插入操作:在L的位序 i 处插入元素e
bool ListInsert(SqList &L,int i,int e){
    if(i<1 || i>L.length+1)		//判断i的范围是否有效
        return false;
    if(L.length >= MaxSize)		//当前存储空间已满,不能插入
        return false;
    //将第i个元素及之后的元素后移
    for(int j=L.length;j>=i;j--)
    {
        L.data[j] = L.data[j-1];
    }
    L.data[i-1]=e;				//在位置i处放入e,注意位序、数组下标的关系
    L.length++;					//长度加1
    return true;
}



int main(){
    SqList L;	//声明一个顺序表	
    InitList(L);//初始化顺序表
    //...此处省略一些代码,插入几个元素
    ListInsert(L,3,3);
    return 0;
}

2.4.1、插入操作时间复杂度

只需关注最深层循环语句的执行次数与问题规模n的关系

//将第i个元素及之后的元素后移
for(int j=L.length;j>=i;j--)
{
    L.data[j] = L.data[j-1];
}
  • 最好情况:新元素插入到表尾,不需要移动元素,时间复杂度O(1)
  • 最坏情况:新元素插入到表头,需要将原有的n个元素全都向后移动,循环n次,时间复杂度O(n)
  • 平均情况:假设新元素插入到任何一个位置的概率相同,时间复杂度O(n)

2.5、顺序表的删除

ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值

bool ListDelete(SqList &L,int i,int &e){
    if(i<1 || i>L.length+1)		//判断i的范围是否有效
        return false;
    e = L.data[i-1];			//将被删除的元素赋值给e
    //将第i个位置后的元素前移
    for(int j=i;j<length;j++)
    {
        L.data[j-1] = L.data[j];
    }
    
    L.length--;					//线性表长度减1
    return true;
}



int main(){
    SqList L;	//声明一个顺序表	
    InitList(L);//初始化顺序表
    //...此处省略一些代码,插入几个元素
    int e = -1;	//用变量e把删除的元素"带回来"
   	if(ListDelete(L,3,e)){
        print("已删除第3个元素,删除元素值为=%d\\n",e);
    }else{
        print("位序i不合法,删除失败\\n");
    }
    return 0;
}

  • 这里函数定义中的参数e加了引用符号,目的是使得在此时声明的变量e和main函数中声明的变量e是同一片内存空间

2.5.1、删除操作时间复杂度

只需关注最深层循环语句的执行次数与问题规模n的关系

//将第i个位置后的元素前移
for(int j=i;j<length;j++)
{
    L.data[j-1] = L.data[j];
}
  • 最好情况:删除表尾元素,不需要移动其他元素,最好时间复杂度O(1)
  • 最坏情况:删除表头元素,需要将后续的 n-1 个元素全都向前移动。循环n-1 次,最坏时间复杂度O(n)
  • 平均情况:平均时间复杂度O(n)

2.5.2、总结

2.6、顺序表的查找

2.6.1、静态分配按位查找

GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值

typedef struct{
    ElemType data[MaxSize];		//用静态的"数组"存放数据元素
    int length;					//顺序表的当前长度
}SqList;						//顺序表的类型定义(静态分配方式)



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

2.6.2、动态分配按位查找

#define InitSize 10;			//顺序表的初始长度
typedef struct{
    ElemType *data;				//指针指向第一个数据元素
    int MaxSize;				//顺序表的最大容量
    int length;					//顺序表的当前长度
}SeqList;						//顺序表的类型定义(动态分配方式)


ElemType GetElem(SqList L,int i){
    return L.data[i-1];			//和访问普通数组的方法一样
}


2.6.3、按位查找时间复杂度

由于顺序表的各个数据元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第i个元素➡"随机存取"特性,按位查找的时间复杂度为O(1)

2.6.4、按值查找

LocateElem(L,e):按值查找操作。在表L中查找值为e的元素

#define InitSize 10;			//顺序表的初始长度
typedef struct{
    ElemType *data;				//指针指向第一个数据元素
    int MaxSize;				//顺序表的最大容量
    int length;					//顺序表的当前长度
}SeqList;						//顺序表的类型定义(动态分配方式)

//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
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;			//退出循环,说明查找失败
    }
}

2.6.5、按值查找时间复杂度

关注最深层循环语句的执行次数与问题规模n的关系,问题规模n=L.length(表长)

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;			//退出循环,说明查找失败
    }
}
  • 最好情况:目标元素在表头,只需要循环1次,最好的时间复杂度为O(1)
  • 最坏情况:目标元素在表尾,需要循环n次,最坏时间复杂度为O(n)
  • 平均情况:O(n)

2.6.6、总结

3、单链表

顺序表:用顺序存储结构实现的线性表叫做顺序表

  • 顺序表优点:可随机存取,存储密度高
  • 顺序表缺点:要求大片连续空间,改变容量不方便

链表:用链式存储结构实现的线性表叫做链表,链表分为:

  • 单链表
  • 双链表
  • 循环链表
  • 静态链表

单链表优点:不要求大片连续空间,改变容量方便

单链表缺点:不可随机存取,要耗费一定空间存放指针

3.1、单链表的定义

typedef struct LNode{				//定义单链表结点类型
    ElemType data;					//定义单链表结点类型(数据域)
    struct LNode * next;			//每个节点存放一个数据元素(指针域)
}LNode,*LinkList;					//LinkList为指向结构体LNode的指针类型
//增加一个新的结点:在内存中申请一个结点所需空间,并用指针p指向这个结点
LNode *p =(LNode *)malloc(sizeof(LNode));

// 上述定义代码等价于
struct LNode{
    ElemType data;
    struct LNode *next;
};
typedef struct LNode LNode;			//struct LNode = LNode
typedef struct LNode * LinkList;	//struct LNode *= LinkList 

//增加一个新的结点:在内存中申请一个结点所需空间,并用指针p指向这个结点
struct LNode *p =(struct LNode *)malloc(sizeof(struct LNode))

要表示一个单链表时,只需声明一个头指针L,指向单链表的第一个结点

LNode *L;		//声明一个指向单链表的第一个结点的指针
LinkList L;		//声明一个指向单链表的第一个结点的指针

上述两种声明方式有什么区别呢?

  • LNode * : 强调这是一个结点
  • LinkList:强调这是一个单链表

3.2、初始化不带头结点的单链表

typedef struct LNode{				//定义单链表结点类型
    ElemType data;					//定义单链表结点类型(数据域)
    struct LNode * next;			//每个节点存放一个数据元素(指针域)
}LNode,*LinkList;					//LinkList为指向结构体LNode的指针类型

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

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

3.3、不带头结点的单链表是否为空

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

3.4、初始化带头结点的单链表

typedef struct LNode{				//定义单链表结点类型
    ElemType data;					//定义单链表结点类型(数据域)
    struct LNode * next;			//每个节点存放一个数据元素(指针域)
}LNode,*LinkList;					//LinkList为指向结构体LNode的指针类型


//初始化一个单链表(带头结点)
bool InitList(LinkList &L){
    //用malloc申请一片空间存一个结点
    //并且把malloc返回的地址赋给头指针L,也就是说头指针L是指向了这个结点
    L =(LNode *)malloc(sizeof(LNode));	
    if(L==NULL)							//分配不足,分配失败
    {
       return false; 
    }
    L->next = NULL;						//头节点之后暂时还没有结点
    return true;
}

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

3.5、带头结点的单链表是否为空

//判断单链表是否为空(带头结点)
bool Empty(LinkList L){
    if(L->next == NULL){
        return true;
    }else{
        return false;
    }
}

3.6、单链表的插入

3.6.1、带头结点按位序插入

ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e

(找到第i-1个结点,将新结点插入其后)

bool ListInsert(LinkList &L,int i,ElemType e){
    if(i<1){
        return false;
    }
    LNode *p;		//指针p指向当前扫描到的结点
    int j=0;		//当前p指向的是第几个结点
    p = L;			//L指向头结点,头结点是第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指针
    s->next = p->next;	
    p->next = s;	//将p指向结点的next指针指向s
    return true;	//插入成功
}

分析:

  1. 如果 i=1(也就是在表头插入元素):时间复杂度O(1)

  1. 如果 i = 3(也就是在表中插入元素)

  1. 如果 i = 5(也就是在表尾插入元素):时间复杂度O(n)

3.6.2、不带头结点按位序插入

ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e

(找到第i-1个结点,将新结点插入其后)


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;		//指针p指向当前扫描到的结点
    int j=1;		//当前p指向的是第几个结点
    p = L;			//p指向第1个结点(注意:不是头结点)
    
    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指针
    s->next = p->next;	
    p->next = s;	//将p指向结点的next指针指向s
    return true;	//插入成功
}
    
}
  • 结论:不带头结点写代码不方便,推荐用带头结点
  • 注意:考试中带头、不带头都有可能考察,注意审题

3.7、指定结点的后插操作

后插操作:在结点之后插入元素

//后插操作:在p结点之后插入元素e
bool Inse

以上是关于数据结构与算法线性表的主要内容,如果未能解决你的问题,请参考以下文章

数据结构与算法-线性表之双向链表

顺序表——“数据结构与算法”

数据结构与算法全套数据结构笔记持续更新

数据结构与算法全套数据结构笔记持续更新

数据结构与算法分析(线性表实现)

数据结构与算法学习笔记:线性表Ⅰ