数据结构(第二章)线性表2——链表
一、单链表
链式存储:不要求逻辑相邻的元素在物理位置上也相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系
- 优点:不会出现碎片现象,能充分利用所有存储单元
- 缺点:每个元素因指针而占用额外的存储空间,且只能实现顺序存取
声明:
// 单链表结点的描述如下:
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
对比下边两种其实表达的含义一样
LNode *L; // 声明一个指向单链表第一个节点的指针
LinkList L; // 声明一个指向单链表第一个节点的指针(代码可读性更强)
前者强调为结点,后者强调为单链表,本质意义无区别
单链表有带头结点和不带头结点两类,实际应用中带头结点更为简单,下边将围绕着两种方式进行简单讨论:初始化、创建、增删改查
初始化
// 定义一个单链表
typedef struct LNode{
int data;
struct LNode *next;
}LNode,*LinkList;
// 初始化不带头结点的单链表
bool InitList1(LinkList &L){
L= NULL;
return true;
}
// 初始化带头结点的单链表
bool InitList(LinkList &L){
L=(LNode *)malloc(sizeof(LNode)); //申请一个结点空间(头节点)
if(L==NULL){
return false; //申请内存空间失败
}
L->next=NULL;
return true;
}
// 判断单链表是否为空
bool Empty(LinkList L){
if(L->next==NULL){
return true;
}
else
return false;
}
创建——头插法与尾插法
创建一个单链表——尾插法、头插法
// 尾插法建立单链表
/*如果每次都是从头指针开始遍历一直到找到最后一个元素在进行插入,这样的时间复杂度太高,建立表的时间复杂度达到O(n^2)。所以我们需要 设立一个尾指针,这样时间复杂度为O(n)*/
LinkList List_TailInsert(LinkList &L){ //尾插法建立头指针
int x; //设置ElemType变量为整型
L=(LinkList)malloc(sizeof(LNode));//等价于L=(LNode *)malloc(sizeof(LNode));——初始化操作
LNode *s,*r=L; //定义两个指向结点的指针
scanf("%d",&x); // 依次输入x的数值
while(x != 521){ // 设置输入结束数值
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
r->next=s;
r=s;
scanf("%d",&x);
}
r->next=NULL; // 最后一个结点指向空
return L; // 最后返回L
}
// 头插法——(在头结点之后插入结点,建立单链表)
// 这样输入的元素顺序与单链表的顺序相反——逆序操作!!!!!!!
LinkList List_HeadInsert(LinkList &L){
LNode *s;
int x;
L=(LinkList)malloc(sizeof(LNode));
L->next=NULL;
scanf("%d",&x);
while(x != 521){
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
s->next=L->next;
L->next=s;
scanf("%d",&x);
}
return L;
}
插入
// 按位序插入操作(带头结点)
bool ListInsert(LinkList &L,int i,int e){ // 在第i个位置插入元素e
if(i<1)
return false;
LNode *p;
int j=0;
p=L;
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;
return true;
}
// 按位序插入操作(不带头结点)
bool ListInsert1(LinkList &L,int i,int e){// 在第i个位置插入元素e
if(i<1)
return false;
/*从这里显示出了不带头结点的链表的繁琐之处*/
if(i==1){ //如果在第一个结点的位置上(其实是在当前链表的i位之前插入)需要特殊处理
LNode *s=(LNode *)malloc(sizeof(LNode));
s->data=e;
s->next=L;
L->next=s;
}
LNode *p;
int j=1; // j=1
p=L; // p指向第一个结点
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;
return true;
}
// 指定结点后插入
bool InsertNextNode(LNode *p,int e){ // 在p结点之后插入data为e的结点
if(p==NULL)
return false;
LNode *s=(LNode *)malloc(sizeof(LNode));
if(s==NULL) //内存分配失败返回false
return false;
s->data=e;
s->next=p->next;
p->next=s;
return true;
}
// 前插操作(在p结点之前插入元素e)
// 指定结点前插——需要从头结点开始遍历整个链表,但无疑会增大时间开销,我们采用一种简约的方式
bool InsertPriorNode(LNode *p,int e){
if(p==NULL)
return false;
LNode *s=(LNode *)malloc(sizeof(LNode));
if(s==NULL)
return false;
s->next=p->next; //先将结点s插入到p结点之后
p->next=s;
s->data=p->data; //s结点的data数值赋值为p中元素的数值
p->data=e; // p中元素数值修改为e
}
删除
// 按照位序删除(带头结点)——头节点可以视为第0个结点
bool ListDelete(LinkList &L,int i,int &e){//删除表中第i个位置的元素,并用e返回删除元素的数值
if(i<1)
return false;
LNode *p;
p=L;
int j=0;
while(p!=NULL&&j<i-1){ // 找p结点的前驱
p=p->next;
j++;
}
if(p==NULL)
return false; // 老规矩,p的值不合法
if(p->next==NULL)
return false; // 第i-1个结点为最后一个结点
LNode *q=p->next;
e=q->data;
p->next=q->next;
free(q);
return true;
}
查找
// 查找
// 按位查找,返回第i个元素的数值(携带头结点)
LNode *GetElem(LinkList L,int i){
if(i==0)
return L;
if(i<1)
return NULL;
LNode *p=L->next;
int j=1;
while(p!=NULL&&j<i){
p=p->next;
j++;
}
return p;
}
// 按值查找
LNode * LocateElem(LinkList L,int e){
LNode *p=L->next;
while(p->data!=e && p!=NULL)
p=p->next;
return p;
}
求表长
//求表长
int length(LinkList L){
LNode *p=L;
int len=0;
while(p->next!=NULL){
p=p->next;
len++;
}
return len;
}
二、双链表
双链表结点的描述
// 双链表结点的描述
typedef struct DNode{
ELemType data;
struct DNode *next,*prior;
}DNode,*DLinklist;
初始化
#include <stdio.h>
#include <stdlib.h>
// 双链表结点的描述
typedef struct DNode{
int data;
struct DNode *next,*prior;
}DNode,*DLinklist;
//带头结点的双链表的初始化——piror、next指针都指向空
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){
if(L->next==NULL)
return true;
else
return false;
}
插入
// 插入
// 在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; // 若不是,则将p结点的后继结点的前驱指针指向s
}
s->prior=p;
p->next=s;
return true;
}
删除
// 删除结点
// 头结点的删除——销毁表时才可以删除头结点
// 销毁表,需要删除所有结点
void DestoryList(DLinklist &L){
while(L->next != NULL){
DeleteNextDNode(L); // 删除L结点的后继结点
}
free(L);
L=NULL; //头指针指向NULL
}
// 删除p结点的后继节点
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->next->prior=p; // q不是最后一个结点
free(q);
return true;
}
三、循环单链表
// 循环单链表
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode, *LinkList;
#include <stdio.h>
#include <stdlib.h>
// 循环单链表
typedef struct LNode{
int 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){
if(L->next==L)
return true;
else
return false;
}
// 判断结点怕是否为循环单链表的表尾结点
bool IsTail(LinkList L,LNode *p){
if(p->next==L)
return true;
else
return false;
}
四、循环双链表
// 循环双链表
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,*DLinklist;
// 循环双链表
typedef struct DNode{
int data;
struct DNode *prior,*next;
}DNode,*DLinklist;
// 初始化
bool InitDLinklist(DLinklist &L){
L=(DNode *)malloc(sizeof(LNode));
if(L==NULL)
return false;
L->prior = L;
L->next = L;
return true;
}
// 判空
bool Empty(DLinklist L){
if(L->next==L)
return true;
else
return false;
}
// 判断是否为表尾
bool IsTail(DLinklist L,DNode *p){
if(p->next==L)
return true;
else
return false;
}
// 插入
bool InsertNextDNode(DNode *p,DNode *s){
s->next=p->next;
p->next->prior=s;
s->prior=p;
p->next=s;
}
// 删除
DNode *q=p->next;
p->next=q->next;
q->next->prior=p;
free(q);
五、静态链表
#define MaxSize 10
typedef struct{
ElemType data;
int next;
}SLinklList[MaxSize];
typedef不同的定义方式
#define MaxSize 10
typedef struct{
int data;
int next;
}SLinklList[MaxSize];
#define MaxSize 10 // 这两种定义方式等价
struct Node{
int data;
int next;
};
typedef struct Node SLinklList[MaxSize];
// ————————————————————————————————————————————————————-
SLinklList a; //等价于下式
struct Node a[MaxSize];
SLinklList a; ——相当于定义了一个长度为MaxSize的Node型数组
操作:
- 查找:从头结点出发挨个往后遍历
- 插入位序为i的结点:
①找到一个空结点,存入数据元素
②从头结点出发找到位序为i-1的结点
③修改新结点的next
④修改i-1号结点的next
如何判断结点为空?让其next为某个特殊值,比如-3 - 删除某个结点:
①从头结点出发找到前驱结点
②修改前驱结点的游标
③本删除结点next设置为-3
顺序表和链表的比较
- 逻辑结构:都属于线性表,都是线性结构
- 存储结构:
顺序表:优点:支持随机存取、存储密度高;缺点:大片连续空间分配不方便,改变容量不方便
链表:优点:离散的小空间分配方便,改变容量方便;缺点:不可随机存取,存储密度低 -
- 基本操作:创建、销毁、增删改查
- 创建
- 顺序表:需要分配大片连续空间,若分配空间国校,则之后不方便扩展容量;若分配空间过大,则浪费内存资源;(静态分配:静态数组——容量不可更改;动态分配——动态数组(malloc、free)——容量可以更改,但需要引动大量元素、时间代价高)
- 链表:只需要分配一个头节点(也可以不要头节点,只声明一个头指针),之后方便拓展
- 销毁
- 顺序表:修改Length=0(静态分配:静态数组——系统自动回收空间;动态分配——动态数组(malloc、free)——需要手动free)
- 链表:依次删除各个节点(free)
- 注:有malloc创建的空间属于堆区,系统不会自动回收,因此malloc和free必须成对出现
- 增、删
- 顺序表:插入/删除元素要将后续元素都后移/前移(时间复杂度O(n),时间开销只要来自于移动元素——如果数据元素很大,则移动的时间代价很高)
- 链表:插入/删除元素只需要修改指针即可(时间复杂度O(n),时间开销只要来查找目标元素——查找元素的时间代价更低)
- 查找
- 顺序表:按位查找:O(1);按值查找:O(n),若表内元素有序,可在 O ( l o g 2 n ) O(log_2^n) O(log2n)(折半等查找方法)时间内找到
- 链表:按位查找、按值查找:O(n)