当前位置: 首页 > news >正文

数据结构与算法 第二章 线性表

2.1 线性表的定义和基础操作

简单介绍

易错:线性表是 逻辑 结构,顺序表和链表是它具体实现的 存储 结构,选择题容易在此混淆。

线性表:具有 相同数据类型 (表示每个数据元素所占空间一样大,这使计算机方便的快速查找指定下标的元素)、有次序有限序列。

注意:位序从1开始,数组下标从0开始。

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

几个基本操作

InitList(&L):构造一个空的线性表

DestroyList(&L):销毁表并释放内存空间

ListInsert(&L,i,e):在表L的第i个位置插入元素e

ListDelete(&L,i,&e):删除表L中第i个位置的元素,通过变量e返回删除元素值

LocateElem(L,e):在表L中查找具有给定关键字值的元素

GetElem(L,i):获取表L中第i个位置的元素的值

Length(L):求表长

PrintList(L):前后顺序输出表中所有元素值

Empty(L):若表空则为true,否则为false

  1. 函数命名不必跟上面一模一样,只需具有可读性即可
  2. &理解为对参数的修改结果需要带回来


2.2 顺序表的定义

顺序表——用顺序存储的方式实现线性表

顺序存储:把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中。

顺序表的实现——静态分配
#define MaxSize 10	// 定义最大长度
typeof struct{ElemType date[MaxSize];	// 用静态的“数组”存放数据元素int length;		// 顺序表的当前长度
}SqList;	// 顺序表的类型定义(静态分配方式)void InitList(SqList &L){L.length = 0;	//顺序表初始长度为0
}int main(){SqList L;	// 声明一个顺序表InitList(L);	// 初始化顺序表for(int i = 0;i < L.length;i++){printf("data[%d] = %d\n", i, L.data[i]);};return 0;
}

注: 顺序表的表长刚开始确定后就无法更改(存储空间是静态的)

顺序表的实现——动态分配
#include <stdlib.h>
#define InitSize 10		// 默认的最大长度
typedef struct{int *data;	// 指示动态分配数组的指针int MaxSize;	// 顺序表的最大容量int length;		// 顺序表的当前长度
}void InitList(SeqList &L){// 用malloc函数申请一片连续的存储空间L.data=(int *)malloc(InitSIze*sizeof(int));L.length=0;L.MaxSize=InitSize;
}//增加动态数组的长度
void IncreaseSize(SeqList &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;	// 顺序表最大长度增加lenfree(p)		// 释放原来的内存空间
}int main(){SeqList L;	//声明一个顺序表InitList(L);	//初始化顺序表IncreaseSize(L,5);	//往顺序表增加5个单位长度return 0;
}
顺序表的特点
  1. 随机访问,可以在O(1)时间内找到第i个元素
  2. 存储密度高,每个节点只存储数据元素(链表存储方式还需存储下一个节点的指针,密度相对低)
  3. 拓展容量时间复杂度高
  4. 插入删除操作需移动大量元素


2.3 顺序表的插入删除

顺序表的基本操作——插入
#define MaxSize 10	// 定义最大长度
typedef struct{int data[MaxSize];	// 用静态的“数组”存放数据元素int length;	// 顺序表的当前长度
}SqList;	// 顺序表的类型定义bool ListInsert(SqList &L, int i, int 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处放入eL.length++;		// 长度加1return true;
}

时间复杂度

  1. 最好情况:新元素插入到表尾,不需要移动元素

    i = n + 1,循环0次;最好时间复杂度=O(1)

  2. 最坏情况:新元素插入到表头,将原有的n个元素全都向后移动

    i = 1,循环n次;最坏时间复杂度=O(n)

  3. 平均情况:假设新元素插入到任何一个位置的概率相同,即i=1,2,3,…,length+1的概率都是p=1/n+1

    i=1,循环n次;i=2时,循环n-1次;i=3,循环n-2次…i=n+1时,循环0次

    平均循环次数=np+(n-1)p+(x-2)p+…+p=n/2

    所以平均时间复杂度为=O(n)


顺序表的基本操作——删除
bool ListDelete(SqList &L, int i, int &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--;		// 线性表长度减1return true;
}

时间复杂度

  1. 最好情况:删除表尾元素,不需要移动其他元素

    i=n,循环0次;最好时间复杂度=O(1)

  2. 最坏情况:删除表头元素,需要将后续的n-1个元素全都向前移动

    i=1,循环n-1次;最坏时间复杂度=O(n)

  3. 平均情况:假设删除任何一个元素的概率相同,即i=1,2,3,…,length的概率都是p=1/n

    i=1,循环n-1次;i=2,循环n-2次;i=3,循环n-3次…i=n时,循环0次

    平均循环次数=(n-1)p+(n-2)p+…+p=(n-1)/2

    所以平均时间复杂度为O(n)



2.4 顺序表的查找

顺序表按位查找
ElemType GetElem(SeqList L, int i){return L.data[i-1];
}	// 无论静态分配还是动态分配都是这种写法

时间复杂度

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

顺序表按值查找
// 在顺序表L中查找第一个元素值等于e的元素,并返回其位序
int LocateElem(SeqList L, int e){for(int i=0;i<L.length;i++)if(L.data[i]==e)return i+1;return 0;
}

注意:c语言中结构体的比较不能直接用"==",需要依次对比各个分量来判断两个结构体是否相等

时间复杂度

  1. 最好情况:目标元素在表头

    循环1次,O(1)

  2. 最坏情况:目标元素在表尾

    循环n次,O(n)

  3. 平均情况:假设目标元素出现在任何一个位置的概率相同,都是1/n

    平均循环次数1·1/n+2·1/n+…+n·1/n=(n+1)/2

    平均时间复杂度=O(n)



2.5 单链表的定义

用链式存储的方式实现线性表

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

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

typedef <数据类型> <别名>

typedef int zhengshu;

typedef int* zhengshuzhizhen

int x=1; 等价于zhengshu x=1

int *p; 等价于zhengshuzhizhen p;

struct LNode{	// 定义单链表结点类型ElemType data;	// 每个节点存放一个数据元素struct LNode *next;		// 指针指向下一个节点
}
struct LNode *p=(struct LNode *)malloc(sizeof(struct LNode));// 用typedef关键字来简化typedef struct LNode{ElemType data;struct LNode *next
}LNode, *LinkList;
// 写法等价于
typedef struct LNode LNode;
typedef struct LNode *LinkList;
不带头结点的单链表
bool InitList(LinkList &L){L=NULL;		// 空表,暂时还没有任何节点(置空是防止脏数据)return true;
}void test(){LinkList L;	// 声明一个指向单链表的指针InitList(L);
}// 判断单链表是否为空
bool Empty(LinkList L){return (L==NULL);
}

在这里插入图片描述

带头结点的单链表
bool InitList(LinkList &L){L=(LNode *)malloc(sizeof(LNode));	// 分配一个头结点if(L==NULL)		// 内存不足,分配失败return false;L->next=NULL;	// 头结点之后暂时还没有节点return true;
}void test(){LinkList L;		// 声明一个指向单链表的指针InitList(L);
}// 判断单链表是否为空
bool Empty(LinkList L){return (L->next==NULL)
}

在这里插入图片描述

注意:“LinkList”等价于“LNode ”,前者强调链表,后者强调结点。



2.6 单链表的插入删除

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

时间复杂度为O(n)

按位序插入(不带头结点)
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=2;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;p->next=s;return true		// 插入成功
}
指定结点的后插操作
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保存数据元素es->next=p->next;p->next=s;	// 将结点s连到p之后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连到p之后s->data=p.data;		// 将p中元素复制到s中p->data=e;		// p中元素覆盖为ereturn true;
}
按位序删除(带头结点)
bool ListDelete(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;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;	// 删除成功
}
指定结点的删除
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;
}

注意: 如果要删除的是最后一个结点,则不存在下一个结点的数据域,因此只能从表头开始依次寻找p的前驱,时间复杂度为O(n)



2.7 单链表的查找

按位查找
// 返回第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个结点p=p->next;j++;}return p;
}
按值查找
// 找到数据域==e的结点(带头结点)
LNode * LocateElem(LinkList L, ElemType e){LNode *p=L->next;while (p!=NULL && p->data!=e)p=p->next;return p;	// 找到后返回该结点指针,否则返回NULL
}
求表长
int Length(LinkList L){int len=0;LNode *p=L;while (p->next!=NULL){p=p->next;len++;}return len;
}


2.8 单链表的建立

尾插法建立单链表
LinkList List_Tailsert(LinkList &L){	// 正向建立单链表int x;L=(LinkList)malloc(sizeof(LNode));	// 建立头结点LNode *s, *r=L;		// r为表尾指针scanf("%d", &x);	// 输入结点的值while(x!=9999){s=(LNode*)malloc(sizeof(LNode));s->data=x;r->next=s;r=s;scanf("%d", &x);}r->next=NULL;	// 尾结点指针置空return L;
}
头插法建立单链表
LinkList List_HeadInsert(LinkList &L){	// 逆向建立单链表LNode *s;int x;L=(LinkList)malloc(sizeof(LNode));	// 创建头结点L->next=NULL;	// 初始为空链表scanf("%d", &x);	// 输入结点的值while(x!=9999){s=(LNode*)malloc(sizeof(LNode);		// 创建新结点s->data=x;s->next=L->next;L->next=s;		// 将新结点插入表中,L为头指针scanf("%d", &x);             }return L;       
}

应用:链表的逆置



2.9 双链表

双链表的初始化(带头结点)
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永远指向NULLL->next=NULL;	// 头结点之后暂时还没有节点return true;
}// 判断双链表是否为空(带头结点)
bool Empty(DLinklist L){return (L->next==NULL)
}
双链表的插入
// 在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;
}
双链表的删除
// 删除p结点的后继结点
bool DeleteNextDNode(DNode *p){if (p==NULL)return false;DNode *q=p->next;	// 找到p的后继结点qif (q->next!=NULL)	// q结点不是最后一个结点q->next->prior=p;free(q);	// 释放结点空间return true;    
}


2.10 循环链表

循环单链表

表尾结点的next指针指向头结点,从一个结点出发可以找到任何一个结点。

在这里插入图片描述

// 初始化一个循环单链表
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)
}// 判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L, LNode *p){return (p->next==L)
}
循环双链表

在这里插入图片描述

// 初始化空的循环双链表
bool InitDLinkList(DLinklist &L){L=(DNode *)malloc(sizeof(DNode));	// 分配一个头结点if (L==NULL)return false;L->prior=L;L->next=L;return true;
}// 判断循环双链表是否为空
bool Empty(DLinklist L){return (L->next==L)
}// 判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L, LNode *p){return (p->next==L)
}// 在p结点之后插入s结点
bool InsertNextDNode(DNode *p, DNode *s){s->next=p->next;p->next->prior=s;	// 不用特判p是最后一个结点s->prior=p;p->next=s;return true;
}// 删除p结点的后继结点
bool DeleteNextDNode(DNode *p){DNode *q=p->next;	// 找到p的后继结点qq->next->prior=p;	// 这里也不用特判最后一个结点free(q);	// 释放结点空间return true;    
}


2.11 静态链表

什么是静态链表

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

在这里插入图片描述

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

SLinkList表示一个长度为MaxSize的Node型数组

初始化静态链表:把a[0]的next设为-1,把其他结点的next设为一个特殊值用来表示结点空闲,如-2

插入位序为i的结点:

  1. 找到一个空的结点,存入数据元素
  2. 从头结点出发找到位序为i-1的结点
  3. 修改新结点的next
  4. 修改i-1号结点的next

删除某个结点:

  1. 从头结点出发找到前驱结点
  2. 修改前驱结点的游标
  3. 被删除结点next设为-2

缺点:容量固定不可变



2.12 顺序表和链表的比较

  1. 逻辑结构:都属于线性表,都是线性结构

  2. 存储结构:

    顺序表:顺序存储

    优点:支持随机存取,存储密度高

    缺点: 大片连续空间分配不方便,改变容量不方便

    链表:链式存储

    优点:离散的小空间分配方便,改变容量方便

    缺点:不可随机存取,存储密度低

  3. 基本操作

    创:

    顺序表:需预分配大片连续空间

    链表:只需声明一个头指针即可

    销:

    顺序表:静态分配的数组系统自动回收,动态分配需要手动free

    链表:依次删除各个结点

    增、删:

    顺序表移动元素的时间开销较大,链表只需移动指针即可

    查:

    顺序表按位查找的时间复杂度为O(1),按值查找的时间复杂度为O(n)

    链表按位查找与按值查找的时间复杂度都为O(n)

在这里插入图片描述

http://www.dtcms.com/a/263886.html

相关文章:

  • Disruptor架构哲学
  • 【算法 day13】LeetCode 110.平衡二叉树 | 257. 二叉树的所有路径| 404.左叶子之和 |222.完全二叉树的节点个数
  • 【Python】字典get方法介绍
  • C++中的虚函数与纯虚函数
  • 【PaddleOCR】快速集成 PP-OCRv5 的 Python 实战秘籍--- PaddleOCR实例化 OCR 对象的参数介绍
  • css函数写个loading动画 | css预编译scss使用
  • YOLOv11性能评估全解析:从理论到实战的指标指南
  • GitHub已破4.5w star,从“零样本”到“少样本”TTS,5秒克隆声音,冲击传统录音棚!
  • 智能客服的进化论:当服务遇见 AI 的化学反应
  • 数据结构:递归:斐波那契数列(Fibonacci Sequence)
  • Android布局管理器实战指南:从LinearLayout到ConstraintLayout的优化之旅
  • 计算机网络第十章——网络层
  • 基于Netty-WebSocket构建高性能实时通信服务
  • nginx的管理员启动,停止,重启
  • 前端处理跨域的4种方式
  • uniapp+vue写小程序页面,实现一张图片默认放大后,可以在容器内上下左右拖动查看
  • JavaScript 安装使用教程
  • Web3区块链有哪些岗位?
  • 141.在 Vue 3 中使用 OpenLayers Link 交互:把地图中心点 / 缩放级别 / 旋转角度实时写进 URL,并同步解析显示
  • 【MyBatis保姆级教程下】万字XML进阶实战:配置指南与深度解析
  • python高级变量VIII
  • 转录组分析流程(四):Cox+Lasso筛选预后基因
  • JVM内存模型与垃圾回收机制分析
  • 【java链式调用流操作】
  • Python实现NuScenes数据集可视化:从3D边界框到2D图像的投影原理与实践
  • mac部署dify
  • 笔记/计算机网络
  • 【数据结构】 排序算法
  • beego打包发布到Centos系统及国产麒麟系统完整教程
  • 【文件读取】open | with | as