数据结构P1.2: 线性表-链表
线性表:概念和基本操作
- 线性表的逻辑结构:
线性表是具有相同数据类型(每个数据元素所占的空间一样大)的n(n >=0)个数据元素的有限序列。
直白理解:一条线串起了一个序列,上面的数据元素类型是相同的。各个数据元素之间有前后顺序。可以通过位序表示数据元素
下图是一个长度为5的线性表
- 线性表的基本操作–创销、增删改查:
初始化:构建一个空的线性表,分配内存空间
销毁操作:销毁线性表并释放线性表所占的内存空间
插入操作:在表中的第 i 个位置上插入指定的元素a
删除操作:删除表中的第 i 个位置上元素,并用e返回删除元素的值
按值查找:在表中寻找给定关键值的元素
按位查找:获取表中第 i 个位置上元素的值
- 线性表的存储/物理结构:
关注定义,如何用代码实现,基本操作如何实现
分类:顺序表(顺序存储) 和链表(链式存储):
线性表:单链表
- 链表概念: 一种数据元素按照链式存储结构进行存储的数据结构,这种存储结构具有在
物理上
存在非连续
的特点。 - 单链表概念:单链表由一个个结点组成,节点中有一片空间是存放
数据元素
的,还需要一片空间指向下一个结点的指针
- 单链表分类:带头结点和不带头结点
- 单链表优点:不要求大片连续空间,改变容量方便
- 单链表缺点:不可随机读取,利用指针依次找到数据,要耗费一定空间存放指针
- 单链表的局限性:无法逆向检索,会造成检索不易
- 单链表简化图:由一个个结点node串联而成
单链表的实现
- 代码实现定义一个单链表:
/*单链表由一个个结点组成,节点中有一片空间是存放数据元素的,还需要一片空间指向下一个结点的指针*/
/*定义一个单链表,结构体就是结点结构*/
/*方法一*/:
struct LNode{
变量类型 data;//数据域,每个节点存放一个数据元素
struct LNode *next;//指针域,单链表指针指向下一个结点
};
//增加一个新的结点:就在内存中申请一个结点 LNode 所需的空间,并用指针p指向这个结点
struct LNode *p=(struct LNode *)malloc(sizeof)(struct LNode);//增加一个结点至链表:内存中申请一个结点所需要的空间,并用指针p指向这个结点
/*方法二*/:
typedef struct LNode LNode;//类型重命名 typedef <数据类型> <别名>
struct LNode{
变量类型 data;//数据域,每个节点存放一个数据元素
struct LNode *next;//指针域,单链表指针指向下一个结点
};
LNode *p = (LNode *)malloc(sizeof)(LNode));//LNode = struct LNode LNode
/*方法三*/:
struct LNode{
变量类型 data;//数据域,每个节点存放一个数据元素
struct LNode *next;//指针域,单链表指针指向下一个结点
}LNode;*LinkList;
/*表示一个单链表时,只需声明一个指向单链表的第一个结点的头指针L*/
LNode *L; //强调这是一个结点
//或 LinkList L; //强调这是一个单链表
- 代码实现初始化一个单链表:
//声明一个不带头结点的单链表
struct LNode{
变量类型 data;
struct LNode *next;
}LNode;*LinkList;
bool InitList(LinkList &L){
L = NULL; //空表,头指针为NULL即没有任何结点
return true;
}
bool Empty(LinkList L){
if(L==NULL)
return true;
else
return false;
}
void test(){
LinkList L; //声明一个指向单链表的第一个结点的头指针L,防止脏数据
InitList(L);//初始化一个空表
Empty(L);//判断单链表是否为空
}
//声明一个带头结点的单链表
struct LNode{
变量类型 data;
struct LNode *next;
}LNode;*LinkList;
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof)(LNode));//分配一个头结点
if(L==NULL)
return false; //内存不足,分配失败
L->next = NULL; //头结点之后暂时还没有结点,并且头结点上不存数据
}
bool Empty(LinkList L){
if(L->next==NULL)
return true;
else
return false;
}
void test(){
LinkList L; //声明一个指向单链表的第一个结点的头指针L,防止脏数据
InitList(L);//初始化一个空表
Empty(L);//判断单链表是否为空(带头结点)
}
单链表的插入删除
插入
- 按位插入
- 指定结点的后插
- 指定结点的前插
//按位插入操作(带头结点):在表L中的第i个位置上插入元素e
//怎么做?1找到第(i-1)个node;2.将新结点插入其后面
bool InsertList(LinkList &L,int i,元素类型 e){
//按位插入,先判断插入位置的合法性
if(i<1)
return false;
LNode *p; //强调结点,p指向当前扫描到的结点
p = L; //L是链表的头结点,也是第0个结点,此结点不存放数据。借助p从头开始扫描
int j=0;
//循环条件:找到第(i-1)个结点,找到了(i-1)个结点时,跳出循环
while(p!NULL && j<i-1){
p=p->next;
j++;
}
if(p==NULL) //判断找到的这个i-1是否合法
return false;
//开始进行插入操作
LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个新的结点空间 地址为s
if(s==NULL) //内存申请失败
return false;
s->data = e; //在这个结点上存放e元素
s->next = p->next; //将新的结点s指向原表第i个结点
p->next = s; //将新结点s连到p(第i-个结点)之后
return true;
}
//后插操作:在给定的P结点后插入元素e
bool InsertNextNode(LNode *p,元素类型 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;
}
//前插操作
//偷天换日,先创建p的后继结点,再传数据
bool InsertPriorNode(LNode *p,元素类型 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中的元素覆盖overwrite为e
return true;
}
删除
- 按位序删除
- 指定结点的删除
//按位删除:删除表L中的第i个结点
//思路:删除第i个结点,找到第i-1个结点,将其指针指向i+1个结点,最后释放第 i 个结点
bool DelList(LinkList &L,int i,元素类型 &e){
if(i<1)
return false;
//找到前驱结点,并判断合法性
LNode *p;
p = L;
int j = 0;
while(p!=NULL && j<i-1){
p=p->next;
j++;
}
if(p==NULL)
return false;
if (p>next==NULL) //前驱结点i-1个结点后无其他结点
return false;
//找到了前驱结点
LNode *q = p->next //指针q指向要被删除的第i个结点(p->next)
e = q->data; //用e返回元素的值
p->next=q->next; //把结点q从链表中断开
free(q); //释放结点q的存储空间
return true;
}
//删除指定结点 p
bool DelNode(LNode *p){
if (p==NULL)
return false;
LNode *q = p->next; //声明一个结点q,令q指向p的后继结点
p->data=p->next->data; //p结点后继结点上的数据 复制到 p结点
p->next=q->nextl //p->next和q->next指向一个地址,目的是断开q
free(q);
return q;
}
单链表的查找
单链表的查找只能依次扫描,不像顺序表那样具有随机性
- 按位查找:找到单链表L中第i个位置元素
- 按值查找:查找给定关键字
//按位查找,返回第i个元素
LNode *GetElem(LinkList L,int i){
if(i<0)
return NULL;
LNode *p; //声明一个指针,指向当前扫描到的结点
p = L; //L指向头结点,L的地址赋值给p,p也指向了头结点
int j=0; //代表当前p的位序
while(p!=NULL && j<i){
p=p->next;
j++;
}
return p; //返回指针p
}
//按值查找,找到数据e的结点
LNode *LocateElem(LinkList L,元素类型 e){
LNode *p=L->next;
//从第一个结点开始查找
while(p!==NULL && p->data!=e)
p=p->next;
return p; //找到后就跳出循环,返回该结点指针,否则反返回NULL
}
单链表的建立
- 尾插法
- 头插法
//尾插法建立单链表
//创建表尾指针,在这个结点后依次进行后插操作
LinkList List_TailInsert(LinkList &L){
int e; //插入的元素位int型
L = (LinkList)malloc(sizeof(LNode)) //建立头结点
LNode *s,*r = L; //s为新结点指针,r为表尾指针
scanf("%d",&x); //输入需要插入的元素
while(x!=99999){ //表示结束
//在r结点后插元素x
s = (LNode *)malloc(sizeof(LNode));
s->data =x;
r->next=s; //表L尾结点指针指向s
r=s; //r指针永远指向新的表尾结点
scanf("%d",&x);
}
r->next=NULL;
return L;
}
//头尾插法建立单链表
//依次在头结点后进行后插
LinkList List_TailInsert(LinkList &L){
LNode *s;
int x;
L = (LinkList)malloc(sizeof(LNode)) //建立头结点
L->next = NULL;
scanf("%d",&x);
while(x!=99999){
//在头结点后插元素x
s = (LNode *)malloc(sizeof(LNode));
s->data =x;
s->next = L->next;
L->next = s; //将新的结点插入表中,L指向该结点
scanf("%d",&x);
}
return L;
}
线性表:双链表
- 单链表:只有一个后驱指针,因此无法逆向检索,有时会带来不便
- 双链表:同时拥有前驱和后继指针,指向前驱和后继结点。可前向检索也可后向检索,存储密度更低一点
- 双链表定义
typedef struct DNode{ //定义双链表指针
元素变量类型 data;
struct DNode *prior,*next; //前驱和后继指针
}DNode,*Dlinklist;
双链表的实现
- 对于链表中的某个结点
p
,它的后继的前驱是它自己,同样,它的前驱的后继也是它自己
p->next->prior = p;
p->prior->next = p;
//初始化双链表
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){
if(L->next==NULL)
return true;
else
return false;
}
void test(){
Dlinklist L; //声明指向头结点的指针L
InitDlinklist(L);
Empty(L);
}
双链表的插入
//双链表的后插.在p结点后插入新的s结点
bool InsetNextDNode(DNode *p,DNode *s){
if(p==NULL || s==NULL)
return FALSE;
s->next = p->next; //结点后继指针指向的下一个结点是当前结点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的后继结点q
bool DeleteNextDNode(DNode *p){
DNode *q=p->next; //找到了p的后继结点q
if(p==NULL|| q==NULL)
return false;
p->next=q->next;
if(q->next!=NULL) //结点q不是最后一个结点
q->next->prior=p;
free(q); //释放结点q空间
}
双链表的销毁
//销毁双链表
void DestoryList(Dlinklist &L){
//循环释放头结点L的后继结点
while(L->next!=NULL)
DeleteNextDNode(L); //删除头结点L的后继结点
free(L); //释放头结点L空间
L=NULL; //头结点L指向NULL
}
线性表:循环链表
- 循环单链表:单链表最后一个结点
p->next=NULL
;循环单链表最后一个结点指向头结点即p->next=L
- 循环双链表:双链表表头结点
L->prior=NULL
,表尾结点p->next=NULL
;循环双链表表头结点L->prior=p
,表尾结点p->prior=L
循环单链表的实现
//初始化循环单链表
bool InitDlinklist(Linklist &L){
L = (DNode*)malloc(sizeof(DNode)); //分配一个头结点
if (L==NULL)
return false;
L->next = L; //头结点为当前的最后一个结点,L->next指向的是头结点
return true;
}
//判断循环单链表是否为空
bool Empty(Linklist L){
if(L->next==L)
return true;
else
return false;
}
//判断结点p是否为循环单链表的表尾结点
bool isTail(Linklist L,LNode *p){
if(p->next==L)
return true;
else
return false;
}
循环双链表的实现
//初始化循环双链表
bool InitDlinklist(Dlinklist &L){
L = (DNode*)malloc(sizeof(DNode)); //分配一个头结点给
if (L==NULL)
return false;
L->prior = L; //头结点的前驱指针prior指向头结点L
L->next = L; //头结点后驱结点指针next指向指向头结点L
return true;
}
//判断循环双链表是否为空
bool Empty(Dlinklist L){
if(L->next==L)
return true;
else
return false;
}
//判断结点p是否为循环双链表的表尾结点
bool isTail(Dlinklist L,DNode *p){
if(p->next==L)
return true;
else
return false;
}
线性表:静态链表
- 单链表:各个结点在内存中是离散的。一个结点由
数据元素
和指向下一个结点的指针(地址)
组成 - 静态链表:各个结点集中安置在内存中连续的空间内一个结点由
数据元素
和指向下一个结点的数组下标(游标)
组成。内存的物理结构和链表的逻辑结构如图所示
- 静态链表优点:增、删不需要移动大量元素
- 静态链表缺点:不能随机读取,只能从头结点开始遍历;容量是固定不变的
静态链表的实现
#define MaxSize //静态链表的长度
//静态链表结构体类型的定义
struct Node{
元素变量类型 data;
int next; //下一个结点元素的数组下标,也就是地址
}
void test(){
struct Node a[MaxSize]; //数组a为静态链表,数组中的每一个元素类型都是结构体Node
}