数据结构——线性表
前置知识
1.数据结构三要素——逻辑结构、数据的运算、存储结构(物理结构)
2.存储结构不同,数据运算的实现方式不同
逻辑结构:一个个前赴后继的信息世是线性结构。逻辑结构有集合结构、线性结构、树结构、图结构或网状结构。与数据元素本身的形式、内容、相对位置、个数无关。
存储结构:有顺序存储结构和链式存储结构。链接存储的存储结构所占存储空间分两部分,一部分存放结点值,另一部分存放表示结点间关系的指针。
3.值得注意的代码问题!!!
(1)如何判空
(2)如何判断节点p石头是表头/表尾节点(向前/后遍历的while语句)
(3)如何在表头、表中、表尾插入/删除一个节点
4.分析时间复杂度
x=90; y=100; while(y>0) if(x>100) {x=x-10;y--;} else x++;
答案: O(1)
解释:程序的执行次数为常数阶。
for (i=0; i<n; i++) for (j=0; j<m; j++) a[i][j]=0;
答案: O(m*n)
解释:语句 a[i][j]=0; 的执行次数为 m*n。
s=0; for i=0; i<n; i++) for(j=0; j<n; j++) s+=B[i][j]; sum=s;
答案: O(n 2)
解释:语句 s+=B[i][j]
; 的执行次数为 n2。
i=1; while(i<=n) i=i*3;
答案: O(log 3n)
解释:语句 i=i*3; 的执行次数为 log 3n 。
x=0; for(i=1; i<n; i++) for (j=1; j<=n-i; j++) x++;
答案: O(n 2)
解释:语句 x++; 的执行次数为 n-1+n-2+ …+ 1= n(n-1)/2 。
x=n; //n>1 y=0; while(x ≥ (y+1)* (y+1)) y ++;
答案: O( 根号n ) 解释:语句 y++; 的执行次数为 n的二分之一次方。
线性表
特点:(1)相同数据类型(2)有限序列
L=(a1,a2,,,,an)ai是位序,位序从1开始,数组下标从0开始
初始化表——分配内存空间
销毁表——释放内存空间
“&”引用型——对参数的修改结果需要返回时使用(c++)
将基本操作用函数封装起来,方便适用
顺序表——用顺序存储结构实现的线性表
sizeof(int)——返回int型数据元素占用空间大小
线性表中数据元素相同,所以占用空间相同,下移同样空间sizeof(int)就能找到下一个元素,所以知道第一个元素的位置,就能通过是sizeof知道后面元素的位置。
#include<stdio.h> //静态分配 #define MaxSize 10 typedef struct{ int data[MaxSize];//定义数组 int length;//当前数据的长度 }SqList;
malloc free函数 ——malloc申请一片连续的存储空间,返回这片空间首个元素的指针(即地址)(可用new delete)
//动态分配 #include<stdio.h> #define InitSize 10 typedef struct{ int *data;//指示动态分配数组的指针 int MaxSize; int length;//顺序表当前长度 }SqList; int main(){ SqList L;//声明一个顺序表 InitList(L);//初始化顺序表 IncreaseSize(SqList &L ,int len)//增加动态数组长度 } void InitList(SqList &L){ //用malloc函数申请一片连续的存储空间 L.data(int *)malloc(InitList*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=0li<L.length;i++){ L.data[i]=p[i]; } L.MaxSize=L.MaxSize+len; free(p); }
malloc()从堆里面获得空间。也就是说函数返回的指针是指向堆里面的一块内存。操作系统中有一个记录空闲内存地址的链表。当操作系统收到程序的申请时,就会遍历该链表,然后就寻找第一个空间大于所申请空间的堆结点,然后就将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
插入操作: 插入的时候先移动后面的操作,一个个往后移。
i的合法值范围 [1,length+1]
//顺序表插入 void ListInsert(SqList &L,int i,int e){ for(int j=L.length;j>=i;j--){//将第i个元素及之后的元素后移 L.data[j]=L.data[j-1]; L.data[i-1]=e; L.length++; } }
可读性更强的:
//顺序表插入 void ListInsert(SqList &L,int i,int e){ if(i<1||i>L.length+1) return false; if(L.length>=MaxSize) return false; for(int j=L.length;j>=i;j--){ L.data[j]=L.data[j-1];// L.data[i-1]=e;//在第i个位置放e L.length++; } }
插入操作的时间复杂度
删除操作:删除的时候先移动前面的数据,一个个往前移
//顺序表删除 void DeleteInsert(SqList &L,int i,int &e){ if(i<1||i>L.length) return false; e=L.data[i-1]; for(int j=i;j<L.length;j++){ L.data[j-1]=L.data[j]; L.length--; return true; } }
顺序表的查找——按位、按值
按位:用数组下标即可得到第i个元素L.data [i-1]
L.data [i-1]用数组下标的方式访问元素
按值:从第一个元素开始依次往后检索
//结构类型比较 bool isCustomerEqual(Customer a,Customer b) if(a.num == b.num && a.people == b.people){ printf("相等"); }else{ printf("不相等"); }
单链表——不支持随机存取,因为要用链式依次往下找
链表:链式存储结构实现线性表
//定义节点类型 struct LNode{ ElemType data; struct LNode *next;//用next指针链接各个LNode };
typedef关键字——数据类型重命名
typedef struct LNode LNode;//将struct LNode重命名为 LNode typedef struct LNode *LinkList;//*LinkList是指向struct LNode的指针 typedef struct LNode{ ElemType data; struct LNode *next; }LNode,*LinkList;
初始化
//初始化不带头节点的单链表 bool InitLinst(LinkList &L){ L = NULL;//判空:暂时没有节点,防止脏数据 return true; }
//初始化带头节点的单链表 bool InitLinst(LinkList &L){ L = (LNode *) malloc (sizeof(LNode));//指向单链表的头指针给他分配一个头结点 if(L == NULL)//内存不足,分配失败 return false; L->next = NULL;//判空:头结点之后暂时没有节点 return true; }
单链表的插入删除操作
按位插入
//带头节点按位插入 bool ListInsert(LinkList &L,int i,ElemType e){ if(i<1) return false; LNode *p;//指针P指向当前扫描到的节点 int j=0;//当前P指向那个节点 p = L;//L指向头结点 while (p!==NULL && j<i-1){ p=p->next; j++; } if(p!==NULL) return false; LNode *s = (LNode *)malloc(sizeof(LNode)); s->data = e; s->next=p->next;//这两句不能颠倒 p->next=s return true; }
//不带头节点按位插入 bool ListInsert(LinkList &L,int i,ElemType e){ if(i<1) return false; if(i==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;//L指向头结点 while (p!==NULL && j<i-1){ p=p->next; j++; } if(p!==NULL) return false; LNode *s = (LNode *)malloc(sizeof(LNode)); 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;//指针P指向当前扫描到的节点 int j=0;//当前P指向那个节点 p = L;//L指向头结点 while (p!==NULL && j<i-1){ p=p->next; j++; } if(p!==NULL) return false; if(p->next==NULL) return false; LNode *q=p->next; e=q->data; p->next=q->next;//这两句不能颠倒 free(q); return true; }
//删除指定节点p bool DeleteNode(LNode *p){ if(p==NULL) return false; LNode *q=p->next; p->data=p->next->data; p->next=q->next; free(q); return true; }
单链表查找
按位查找:获取表L中第I个位置的元素的值
在插入删除操作中可封装为LNode *p = GetElem(L,i-1);
//按位查找 LNode * GetElem(LinkList L,int i){ if(i<0) return NULL; LNode *p; int j=0; p = L; while (p!==NULL && j<i){ p=p->next; j++; } return p; }
按值查找
//按值查找 LNode * GetElem(LinkList L,ElemType e){ LNode *p = L->next;//指向头节点的下一个节点也就是第一个节点 while(p!=NULL && p->data!=e) p = p->next; return p; }
求表长
//求表长 int Length(LinkList L){ int len = 0; LNode *p = L; while(p->next !=NULL){ p = p->next; len++; } return len; }
单链表的建立——很多个元素存到一个单链表中
先初始化一个点链表,在每次去一个元素插入到表头/表尾
头插法
初始化单链表
while循环{
每次取一个元素e;
InterNestNode(L,e);//制定节点的后插操作
}
LinkList List_Taillnsert(LinkList &L){ LNode *s; int x; L=(LinkList)malloc(sizeof(LNode)); L->next=NULL;//初始为空链表,防止引入脏数据 scanf("%d",&x); whilex!=9999){ s=(LNode *)malloc(sizeof(LNode));//申请新节点 s->data=x;//把S的值赋给新节点 r->next=s;//和前面的节点连接上 r=s;//r s共同指向该新节点即r指向最后一个节点 scanf("%d",&x); } return L; }
重要应用 ! ! ! 头插法可实现链表的逆置
双链表
DNode * 等价于 DLinklist 前者强调链表,后者强调节点
//双链表的初始化 bool InitDLinkLIst(DLinkList &L) { s=(DNode *)malloc(sizeof(DNode)); if(L==NULL) return false; L->priot=NULL; L->next=NULL; return true; } typedef struct DNode{ ElemType data; struct DNode *prior,*next; }DNode,*DLinklist;//*DLinklist指向DNode的指针的类型
插入
s是要插入的节点,p是s的前驱节点
s 后向指针先指向 p后继节点的前项
p后继节点的前向指针指向s
s前项指针指向p
p后项指针指向s
//双链表的插入 bool InserNextDNode(DNode *p DNode *s){ if(p==NULL||s==NULL) return false; s->next=p->next; if(p->next !=NULL) p->next->prior=s; s->prior=p; p->next=s; }
删除
//双链表的删除 bool DeleteNextDNode(DNode *p){ if(p==NULL) return false; DNode *q=p->next; if(q==NULL) return false;//p没有后继 p->next=q->next; if(q->next!=NULL)//q节点不是最后一个节点 q->next->prior=p; ferr(q); return true; }
//销毁双链表 void DestoryList(DLinklist &L){ while(L->next!=NULL) DeleteNextDNode(L);//释放各个指针 free(L);//释放头指针 L=NULL;//头指针指向NULL }
循环链表
//初始化循环单链表 bool InitLinst(LinkList &L){ L = (LNode *) malloc (sizeof(LNode));//指向单链表的头指针给他分配一个头结点 if(L == NULL)//内存不足,分配失败 return false; L->next = L;//头结点next指向头节点 return true; } //初始化循环双链表 bool InitDLinkLIst(DLinkList &L) { s=(DNode *)malloc(sizeof(DNode)); if(L==NULL) return false; L->priot=L; L->next=L; return true; } //判空 L->next==NULL
静态链表
单链表:各个节点在内存中星罗密布
静态链表:分配一整片连续的存储空间,各个节点集中安置,是一种用数组的方式实现的链表。
游标指向的是数组下标,指针指向地址,游标设为-1代表NULL,后面没节点了
//定义静态链表 #define MaxSize 10 struct Node{ //定义结构类型 ElemType data; int next; //下一个数组的下标 }; void testSLinkList(){ struct Node a[MaxSize];//struct Node型的数组a作为静态链表 }
另一种定义方式
typedef 将struct Node重命名为SLinkList[MaxSize],接下来可以直接定义数组SLinkList a,a就是Node型。
应用:操作系统的文件分配表FAT
顺序表与链表的比较
顺序表 | 链表 | |
优点 | 支持随机存取、存储密度高 | 离散的小空间分配方便,改变容量方便 |
缺点 | 大片连续空间分配不方便,改善容量不方便 | 不可随机存取,存储密度低 |
初始化 | 需要预分配大片连续空间 | 只需分配头结点(也可不要头结点只声明一个头指针) |
销毁 |
静态分配:静态数组——程序自动回收空间 动态分配:动态数组(malloc、free) |
free() |
增删 |
需要将元素前移/后移 |
只需要修改指针 |
查找 |
按位:O(1) 按值:O(n),若表内元素有序,课通过其他算法在O(log2n时间内找到) |
按位:O(n) 按值:O(n) |
增删的时间复杂度都是O(n),但顺序表时间开销主要来自移动元素,链表主要来自查找元素。前者若数据元素很大,则移动的时间代价很高。
顺序表 | 链表 | |
扩容 |
× |
√ |
增删 |
× |
√ |
查找 |
√ |
× |

【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix