2.线性表
线性表
-
线性表的定义
A[1]---A[2]---A[3]---A[4]---A[5]
线性表是具有相同类型的n(n>=0)个元素的有限序列,其中n为表长,n=0时线性表是一个空表。若用L命名线性表,其表示形式为:
L=(a1,a2,...,a[i],a[i+1],...,a[n]
a[1]可以是int,也可以是 struct A(结构体序列:有次序,有限制
特点:出第一个元素外,每个元素有且仅有一个直接前去;除最后一个外,每个元素都有且只有一个直接后继
位序:位序从1开始,数组下表从0开始
-
基本操作
InitList(&L):初始化表,构造一个空的线性表L,分配内存空间
DestroyList(&L):销毁操作。销毁线性表,并释放线性表L所占用的内存空间
ListInsert(&L,i,e):插入操作。在表L中的第i个位置插入指定元素e
ListDelete(&L,i,e):删除操作。删除表L中第i个位置上的元素,并用e返回删除元素的值
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素
GetElem(L,e):按位查找操作。获取表L中第i个元素的值
-
其他常用操作
Length(L):求表长。返回线性表L的长度,即L中数据元素的个数
PrintList(L):输出操作。按前后顺序输出线性表L的所有元素值
Empty(L):判空操作。L为空返回true,否则返回false
-
其他建议
实际开发中,可以根据实际需求定义其他基本操作
函数名、参数的形式、命名都可以改变
什么时候引用“&"——对参数的修改结果要带回来

为什么要实现对数据库的基本操作?
团队合作编程,定义的数据结构要让别人方便使用(封装)
把常用操作封装成函数,避免重复工作,降低出错风险
注:数据的三要素,逻辑结构, 数据的运算,存储结构
定义——逻辑结构
基本操作——运算
顺序表(顺序存储)
-
定义(如何用代码实现)
顺序表——用顺序的方式实现线性表
把逻辑上相邻的元素存储在物理上也相邻的存储单元中,元素之间的关系由存储单元关系的邻接关系来实现

注:每个元素所占空间一样大
设线性表第一个元素的存放位置是LOC(L)
LOC--->a[1]
LOC+数据元素的大小--->a[2]
LOC+数据元素的大小*2--->a[3]
...
LOC+数据元素的大小*(n-1)--->a[n]
如何知道一个数据元素的大小?
C语言 sizeof(Elemtype)函数
EG:
sizeof(int)=4B;typedef struct{ int num; //号数 int people; //人数 }Customer;sizeof(Customer)=8B; -
顺序表的实现方式——静态分配

#define Maxsize 10 //定义最大长度 typedef struct{ ElemType data[MaxSize]; //用静态的“数组”存放元素数据 int length; //顺序表当前长度 }Sqlist; //顺序表的类型定义(静态分配方式)给各个元素分配连续的存储空间,大小为MaxSize*sizeof(ElemType)

#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.lenth=0; //此步骤不能省略,不然系统遗留的仓数据会使length失效 }如果数组满了怎么办?
顺序表的表长刚开始确定之后就无法更改,建议放弃治疗(存储空间是静态的)思考:如果一开始就声明很大的空间呢?
会导致严重的内存浪费,降低内存利用率 -
顺序表的实现方式——动态分配

key:动态申请和释放内存空间
C—— malloc、free函数malloc函数
L.data=(ElemType)malloc(sizeof(ElemType)InitSize);
#include <stdlib.h> #define InitSIze 10; //默认最大长度 typedef struct{ Elemtype *data; //指示动态分配数组的指针 int MaxSize; //顺序的最大容量 int length; //顺序表的当前最大长度 }SeqList; //用malloc申请一片连续的存储空间 void InitList(SeqList &L){ L.data=(int *)malloc(IntiSize*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;i<L.length;i++){ L.data[i]=p[i]; //复制数据到新区域,但是时间开销大 } L.MaxSize=L.MaxSize+len; //更新最大长度 free(p); //释放原来内容 } int main(){ SeqList L; InitList(L); //...插入元素 IncreaseSize(L,5); return 0; } -
顺序表的特点
随机访问,可以在O(1)时间内找到第i个元素
代码实现:data[i-1]
静态分配、动态分配都一样存储密度高,每个节点只存储数据元素,链式存储需要存储指针
扩展容量不方便(即便采用动态方式,扩展长度的时间复杂度也比较高)
插入、删除操作不方便,需要移动大量元素
-
插入数据

ListInsert(&L,i,e)
插入操作:在表L的第i个位置上插入指定元素e#define MaxSize 10 typedef struct{ Elemtype data[MaxSize]; int length; }SqList; bool ListInsert(Sqlist &L,int i,int e){ if(i<1||i>L.lengh+1) return false; if(L.length>=maxSize)return false; for(int j=L.lenth;j>=i;j--){ //将第i个元素及之后的数据后移 L.data[j]=L.data[j-1]; } L.data[i-1]=e; //在位置i处放入e L.length++; //长度加一 return true; } int main(){ SqList L; InitList(L); //插入元素代码 ListInsert(L,3,3); return 0; } -
插入操作的时间复杂度
最好情况:新元素插入到表尾,不需要移动元素
i=n+1,循环0次;最好时间复杂度=O(1)最坏情况:新元素插入到表头,要将原有的n个元素全部往后移动
i=1,循环n次;最好时间复杂度=O(n)平均情况:假设新元素插入到任何一个未知的概率相同,i=1,2,3,...,length+1的概率是p=1/(1+n)
平均循环次数=np+(n-1)p+...+1p=n(n+1)/2p=n(n+1)/2*(1/(n+1))=n/2 -
顺序表的基本操作——删除

ListDelete(&L,i,&e)
删除操作:删除表L中第i个位置的元素,并用e返回删除的值。#define MaxSize 10 typedef struct{ Elemtype data[MaxSize]; int length; }SqList; bool ListDelete(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; } int main(){ SqList L; InitList(L); //插入元素 int e=-1; if(Listdelete(L,3,e)) printf("删除成功,元素值=%d",e); else printf("位序i不合法,删除失败"); return 0; } -
删除操作的时间复杂度
最好情况:删除表尾元素,不需要移动其他元素
i=n,循环0次;最好时间复杂度O(1)最坏情况:删除表首元素,需要将后续的n-1个元素向后移动
i=1,循环n-1次;最坏时间复杂度O(n)平均情况:假设删除任何一个元素的概率相同,即i=1,2,3...,length的概率都是p=1/n
平均循环次数=(n-1)p+(n-2)p+...+1p=n(n-1)/2p=(n-1)/2--->平均时间复杂度O(n) -
顺序表的查找——按位查找

GetElem(L,i)
按位查找操作:获取表L中第i个位置元素的值/* #define MaxSize 10 typedef struct(){ Elemtype data[MaxSize]; int length; }SqList; */ #define InitSize 10 typedef struct{ Elemtype *data; int MaxSize int length; }SqList; ElemType GetElem(SqList L,int i){ return L.data[i-1]; /* 这里可以理解为何malloc函数返回存储空间的存储元素的骑士地址要转换为 与数据元素的数据类型相对应的指针 */ }时间复杂度O(1)
由于顺序表的各个元素在内存中连续存放,因此可以根据起始地址和数据元素大小立即找到第i个元素——“随机存取”特性 -
顺序表的查找——按值查找

LocateElem(L,e)
按值查找操作:在表L中查找具有给定关键字值的元素#define InitSize 10 typedef struct{ int *data; int MaxSize int length; }SqList; int LocateElem(SqList L,int e){ for(int i=0;i<L.length;i++){ if(e==L.data[i]) return i+1; } return 0; }调用:LocateElem(L,9);
注:对于结构类型,不能直接用==来判断结构体的内容是否相等,要分别判断
typeof struct{ int num; int people; } Customer; void test(){ Customer a,b; a.num=1; a.people=1; b.num=1; b.people=1; //错误方式: /* if(a==b)printf("相等"); */ if(a.num==b.num&&a.people==b.people){ printf("相等"); }else{ printf("不相等"); } }时间复杂度
最好:表首=O(1)
最坏:表尾=O(n)
平均:(1+2+...+n)/n=(n+1)/2->O(n)
单链表
-
单链表的定义


每个节点除了存放数据外,还要存储指向下一个结点的指针
/* struct LNode{ //定义单链表节点类型 ElemType data; //每个节点存放一个数据元素 struct LNode *next; //指针指向下一个及节点 }; typedef struct LNode LNode; typedef struct LNode *LinkList; */ typedef struct LNode{ ElemType data; struct LNode *next; }LNode,*LinkList; struct LNode *p=(struct LNode *)malloc(sizeof(struct LNode));实际上,要声明一个单链表时,只需声明一个头指针L,指向单链表的第一个节点
LNode * L; //声明指向单链表第一个节点的指针 LinkList L; //声明指向单链表第一个节点的指针——代码可读性更好
eg:
typedef struct LNode{ ElemType data; struct LNode *next; }LNode,*LinkList; LNode * GetElem(LinkList L,int i){ int j=1; LNode *p=L->next; if(i==0) return L; if(i<1) return null; while(p!=null&&j<i){ p=p->next; j++; } return p; } -
初始化不带头结点单链表

typedef struct LNode{ ElemType data; struct LNode *next; }LNode,*LinkList; //初始化链表 bool InitList(LinkList &L){ L=null; return true; } void test(){ LinkList L; InitList(L); //后续代码 } //判断链表是否为空 bool Empty(LinkList L) return (L==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=null; return true; } void test(){ LinkList L; InitList(L); //后续代码 } //判断链表是否为空 bool Empty(LinkList L) return (L->next==null); -
单链表的插入
按位序插入(带头节点)

ListInsert(&L,i,e)
插入操作:在表L中的第i个位置插入指定元素etypedef struct LNode{ ElemType data; struct LNode *next; }LNode,*LinkList; bool ListInsert(LinkList &L,int i,ELemtype e){ if(i<1) return false; LNode *p; //指针p当前扫描到的节点 int j=0; //当前p指向的是第几个节点 p=L; //L指向头结点,头结点是第0个节点,不存数据 //循环找到i-1个节点 while(P!=null&&j<i-1){ p=p->next; j++; } if(p==null) return false; //i值不合法 LNode *s=(LNode *)malloc(sizeof(LNode)); s->data=e; s->next=p->next; p->next=s; //将节点s连到p之后 return true; //插入成功 }
按位序插入(不带头结点)

typedef struct LNode{ ElemType data; struct LNode *next; }LNode,*LinkList; 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指向头结点,头结点是第0个节点,不存数据 //循环找到i-1个节点 while(P!=null&&j<i-1){ p=p->next; j++; } if(p==null) return false; //i值不合法 LNode *s=(LNode *)malloc(sizeof(LNode)); s->data=e; s->next=p->next; p->next=s; //将节点s连到p之后 return true; //插入成功 } -
指定节点的后插操作

typedef struct LNode{ ElemType data; struct LNode *next; }LNode,*LinkList; //后插操作:在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->next=p->next; p->next=s; return true; }
代码优化:

typedef struct LNode{ ElemType data; struct LNode *next; }LNode,*LinkList; //在第i个位置元素插入元素e bool ListInsert(LinkList &L,int i,ELemtype e){ if(i<1) return false; LNode *p; //指针p当前扫描到的节点 int j=0; //当前p指向的是第几个节点 p=L; //L指向头结点,头结点是第0个节点,不存数据 //循环找到i-1个节点 while(P!=null&&j<i-1){ p=p->next; j++; } /*代码封装 if(p==null) return false; //i值不合法 LNode *s=(LNode *)malloc(sizeof(LNode)); s->data=e; s->next=p->next; p->next=s; //将节点s连到p之后 return true; //插入成功 */ return InsertNextNode(p,e); } //后插操作:在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->next=p->next; p->next=s; return true; } -
指定节点的前插操作
bool InsertPriorNode(*P,e)
前插操作:在p节点之前插入元素e由于无法知道前面的节点,需要传入头指针L
bool InsertPriorNode(L,*P,e)
但是随之而来的时间复杂度变成O(n)

typedef struct LNode{ ElemType data; struct LNode *next; }LNode,*LinkList; 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 InsertPriorNode(LNode *P,LNode *s){ if(p==null||s==null) return false; s->next=p->next; p->next=s; Elemtype temp=p->data; p->data=s->data; s->data=temp; return true; } -
按位删除节点

ListDelete(&L,i,&e)
删除操作:删除表L中第i个位置的元素,并用e返回删除元素的值#include <stdlib.h> typedef struct LNode{ ElemType data; struct LNode *next; }LNode,*LinkList; bool ListDelete(LinkList &L,int i,ElemType &e){ if(i<1) return false; LNode *p; //指针p当前扫描到的节点 int j=0; //当前p指向的是第几个节点 p=L; //L指向头结点,头结点是第0个节点,不存数据 //循环找到i-1个节点 while(P!=null&&j<i-1){ p=p->next; j++; } if(p==null) return false; //i值不合法 if(p->next==null) return false; //第i-1节点之后已无其他节点 LNode *q=p->next; //令q指向被删除节点 e=q->data; //用e返回元素的值 p->next=q->next; //将*q节点从链中断开 free(q); //释放节点 return true; } -
删除指定节点

bool DeleteNode(LNode *P); //删除指定节点p
#include <stdlib.h> typedef struct LNode{ ElemType data; struct LNode *next; }LNode,*LinkList; bool DeleteNode(LNode *P){ if(p==null) return false; LNode *q=p->next; //令q指向*P的后继节点 p->data=p->next->data; //和后继节点交换数据 p->next=q->next; //将*p节点从链中断开 free(q); //释放后继节点的存储空间 return true; }极限情况,如果p节点是最后一个节点
神秘未知区域——>[x]——>null
p->[x]
*q->p.next=null;
这些代码就会产生bug,这是,我们只能从表头开始依次查找p的前驱,时间复杂度O(n) -
单链表的查找——按位查找
GetElem(L,i)
按位查找:获取表L中第i个位置的元素。LNode * GetElem(LinkList L,int i){ if(i<0) return null; LNode *p; //指针p当前扫描到的节点 int j=0; //当前p指向的是第几个节点 p=L; //L指向头结点,头结点是第0个节点,不存数据 //循环找到i个节点 while(P!=null&&j<i){ p=p->next; j++; } return p; }平均时间复杂度和最坏时间复杂度O(n)
最好时间复杂度O(1)代码封装的好处:


易扩展性,可读性,模块化,简洁...
按位插入代码优化:

typedef struct LNode{ ElemType data; struct LNode *next; }LNode,*LinkList; //在第i个位置元素插入元素e bool ListInsert(LinkList &L,int i,ELemtype e){ if(i<1) return false; /*代码封装 LNode *p;int j=0;p=L;while(P!=null&&j<i-1){p=p->next;j++;}*/ LNode *p=GetElem(L,i-1); /*代码封装 if(p==null) return false;LNode *s=(LNode *)malloc(sizeof(LNode));s->data=e;s->next=p->next;p->next=s;return true;*/ return InsertNextNode(p,e); } //按位查找:获取表L中第i个位置的元素。 LNode * GetElem(LinkList L,int i){ if(i<0) return null; LNode *p; //指针p当前扫描到的节点 int j=0; //当前p指向的是第几个节点 p=L; //L指向头结点,头结点是第0个节点,不存数据 //循环找到i个节点 while(P!=null&&j<i){ p=p->next; j++; } return p; } //后插操作:在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; } -
单链表的查找——按值查找

LocateElem(L,e)
按值查找操作:在表L中查找具有给定关键字值的元素LNode *p=LocateELem(LinkList L,ElemType e){ LNode *P=L->next; //从第一个几点开始查找数据为e的节点 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; } -
单链表的建立——尾插法
typedef struct LNode{ ElemType data; struct LNode *next; }LNode,*LinkList; void test(){ LinkList L; InitList(L); while(1){ Insert(L,length+1,e); length++; if(...)break; } }每次都要从头开始遍历,时间复杂度是O(n^2)
王道书版本,在表尾设置尾指针:
LinkList ListTailInsert(LinkList &L){ int x; L=(LinkList)malloc(sizeof(LNode)); LNode *s,*r=L; scanf("%d",&x); while(x!=9999){//9999结束 s=(LNode *)malloc(sizeof(LNode)); s->data=x; r->next=s; r=s; scanf("%d",&x); } r->next=null; return L; } -
单链表的建立——头插法
typedef struct LNode{ ElemType data; struct LNode *next; }LNode,*LinkList; 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; } while(<条件>){ InsertNextNode(p, e); }王道书版本

LinkList ListHeadInsert(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)); if(s==null) return false; //内存分配失败 s->data=x; //用s保存数据元素x s->next=L->next; L->next=s; //将新节点插入表中,L为头指针 scanf("%d",&x); } return L; }
双链表
-
双链表的定义

typedef struct DNode{ ELemType data; struct DNode *prior, *next; }DNode,*DLinkList; -
双链表的初始化(带头结点)

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; L->next=null; return true; } //判空操作 bool Empty(DLinkList L) return (L->next==null); void tsetDLinkList(){ DLinkList L; InitDLinkList(L); //后续代码 } -
双链表的插入

bool InsertNextDNode(DNode *p,DNode *s){ s->next=p->next; p->next->prior=s; s->prior=p; p->next=s; } //优化 bool InsertNextDNode(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->next=q->next; if(q->next!=null) q->next->prior=p; free(q); return true; } //销毁双链表 void DestroyList(DLinkList &L){ while(L->next!=null) DeleteNextDNode(L); free(L); L=null; } -
双链表的遍历
//后向遍历 while(p!=null) p=p->next; //前向遍历 while(p!=null) p=p->prior; -
双链表的特点
双链表不可随机存取,按值查找、按位查找操作都只能拥便利的方法实现。时间复杂度O(n)
循环链表
-
循环单链表

表尾的next指针指向头结点
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; //判尾操作 bool isTail(LinkList L,LNode *p) return L->next==L;从头部找到尾部,时间复杂度O(n);从尾部找到头部,时间复杂度O(1)
很多时候,对链表的操作,都是在头部或者尾部,可以让L指向尾部(插入,删除时可能需要修改L) -
循环双链表

表头节点的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=L; L->next=L; return true; } void testDLinkList(){ DLinkList L; InitDLinkList(L); //后续代码... } //判空操作 bool Empty(DLinkList L) return L->next==L; //判尾操作 bool isTail(LinkList L,LNode *p) return L->next==L; //插入数据 bool InsertNextDNode(DNode *p,DNode *s){ s->next=p->next; p->next-prior=s; s->prior=p; p->next=s; } //删除数据 bool DeleteNextDNode(DNode *p){ DNode *q=p->next; p->next=q->next; q->next->prior=p; free(q); return true; }
静态链表
-
什么是静态链表?

分配一整片的内存空间,各个节点集中安置
-
如何定义一个静态链表

#define MaxSize 10 //静态链表最大长度 typedef struct { ElemType data; //存储数据元素 int next; //下一个数组下标 }SLinkList[MaxSize]; //--- #define MaxSize 10 struct Node{ ElemType data;A int next; }; typedef struct Node SLinkList[MaxSize]; void testSLinkList(){ SLinkList a; struct Node a[MaxSize]; } -
基本操作的实现
//初始化 a[0]->next=-1; //同时把其他空闲节点的next=-2 //查找 /* 从头节点触发依次往后遍历节点,时间复杂度O(n) */ //插入节点 /* 找到空闲节点,存入数据元素 找到位序为i-1的节点 修改新节点的next=(i-1)->next 修改(i-1)->next=新节点的下标 */ //删除节点 /* 从头找到前驱节点 修改前驱节点的游标 被删除节点的游标=-2 */
顺序表和链表的比较
| 顺序表 | 链表 | |
|---|---|---|
| 弹性 | × | √ |
| 增、删 | × | √ |
| 查 | √ | × |

浙公网安备 33010602011771号