一、单链表
(1)单链表的定义
单链表是通过任意的存储单元来存储线性表中的数据元素的,每个链表结点,不仅包含元素的信息,而且还有一个指向其后继结点的指针。
其结构如下图:
typedef struct LNode{
ElemType data; //数据域
struct LNode *next; //指针域
}LNode,*LinkList;
因为单链表的元素在存储空间的分布是离散的,所以区别于顺序表,单链表不能采取随机存取。只能从表头遍历,依次查找。
头指针指向链表的起始地址,当头指针为NULL时,说明这个链表为空链表。通常,会在链表的第一个数据结点前添加一个头节点,头结点的数据域可以不设信息,但头结点的指针域要指向后继结点,若无后继结点则为NULL。在单链表中,表的最后的结点的指针域也为NULL。
(2)基本操作的实现
①初始化
单链表的初始化有两种,分别为带头结点以及不带头结点。
而这两种方式的初始化操作也存在一定的差异。
a.带头结点初始化:
bool InitList(LinkList &L){
L=(LNode*)malloc(sizeof(LNode); //创建头结点
L.next->NULL; //头结点的指针域为空
return true;
}
b.不带头结点初始化:
bool InitList(LinkList &L){
L=NULL; //头指针为空
return true;
}
②求表长操作
求表长,即计算含数据信息结点的个数,需要从第一个结点开始依次遍历,每完成依次遍历则让计数变量加一,直至访问到指针域为NULL的结点。
int Length(LinkList L){
int length=0;
LNode *p=L; //创建p指针,指向链表的头指针
while(p!=NULL){ //依次遍历,直到表尾
p=p->next;
length++;
}
return length;
}
③按序号查找表结点
从单链表的第一个元素开始遍历,找到所查序号的结点结束,并返回该结点的指针。若没有,返回NULL。
LNode *GetElem(LinkList L,int i){
LNode *p=L;
int j=0;
while(p!=NULL && j<i){ //找到第i个结点时跳出循环
p=p->next;
j++;
}
return p;
}
④按值查找表结点
从单链表的第一个元素开始遍历,依次与结点的指针域作比较,若找到结点的数据域等于给定的值,则输出指向该节点的指针;若没有,返回NULL。
LNode *LocateElem(LinkList L,int x){
LNode *p=L;
while(p!=NULL && p->data!=x)
p=p->next;
return p;
}
⑤插入结点
插入一个新的结点到第i个位置上。首先判断i的值是否合法,再找到第i个位置的前驱,再进行插入操作。
bool InsertList(LinkList &L,int i,ElemType e){
LNode *p=L;
int j=0;
while(p!=NULL && j<i-1){ //找到第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;
}
⑥删除结点
将单链表中第i个结点删除。首先查找第i-1个结点,再检查输入的i值是否合法,再删除第i个结点。最后,返回所删除结点的数据域信息。
bool DeleteList(LinkList &L,int i,ElemType &x){
LNode *p=L;
int j=0;
while(p!=NULL && j<i-1){ //找到删除结点的前驱结点
p=p->next;
j++;
}
if(p==NULL) return false; //i值不合法
LNode *q=p->next; //令q指针指向第i个结点
x=q->data; //存储所删除结点的数据
p->next=q->next;
free(q); //释放q结点
return true;
}
⑦头插法建立单链表
#define quitnum 999 //退出数字
LinkList HeadInsert(LinkList &L){
LNode *s;
int x;
L=(LNode*)malloc(sizeof(LNode)); //创建头结点
L->next=NULL;
scanf("%d",&x);
while(x!=quitnum){
s=(LNode*)malloc(sizeof(LNode)); //创建新结点
s->data=x;
s->next=L->next;
L->next=s;
scanf("%d",&x);
}
return L;
}
⑧尾插法建立单链表
#define quitnum 999 //退出数字
LinkList RearInsert(LinkList &L){
int x;
L=(LNode*)malloc(sizeof(LNode)); //创建头结点
LNode *s,*r=L; //r指针为表尾指针
scanf("%d",&x);
while(x!=quitnum){
s=(LNode*)malloc(sizeof(LNode)); //创建新结点
s->data=x;
r->next=s;
r=s; //r指向新的表尾结点
scanf("%d",&x);
}
r->next=NULL;
return L;
}
二、双链表
(1)双链表的定义
与单链表相区别,双链表的结点中有两个指针prior和next,这两个结点分别指向该结点的前驱结点及后继结点。特别地,双链表的表头结点的prior指针为NULL,表尾结点的next指针为NULL。
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,*DLinkList;
(2)基本操作的实现
①插入结点
插入一个新的结点到第i个位置上。首先判断i的值是否合法,再找到第i个位置的前驱,再进行插入操作。
bool InsertDList(DLinkList &L,int i,ElemType e){
DLNode *p=L;
int j=0;
while(p!=NULL && j<i-1){ //找到第i-1个结点
p=p->next;
j++;
}
if(p==NULL) return false; //i值不合法退出程序
DLNode *s=(DLNode*)malloc(sizeof(DLNode)); //为新结点分配存储空间
s->data=e;
s->next=p->next;
s->prior=p->next->prior;
p->next->prior=s;
p->next=s;
return true;
}
②删除结点
将单链表中第i个结点删除。首先查找第i-1个结点,再检查输入的i值是否合法,再删除第i个结点。最后,返回所删除结点的数据域信息。
bool DeleteDList(DLinkList &L,int i,ElemType &x){
DLNode *p=L;
int j=0;
while(p!=NULL && j<i-1){ //找到删除结点的前驱结点
p=p->next;
j++;
}
if(p==NULL) return false; //i值不合法
DLNode *q=p->next; //令q指针指向第i个结点
x=q->data; //存储所删除结点的数据
p->next=q->next;
q->next->prior=p;
free(q); //释放q结点
return true;
}
三、循环链表
(1)循环单链表
定义一个表尾指针r,r->next==L即r的指针域指向L,从而形成一个环。
(2)循环双链表
L为头结点,r为尾结点。
L->prior==r
r->next==L
四、顺序表和链表的差异
(1)底层存储空间:顺序表使用一块连续的内存空间来存储元素,并通过下标来访问和操作元素。而链表则不同,它的各个节点在物理存储上并不连续,而是通过指针或引用进行连接。
(2)插入和删除操作:由于顺序表是连续存储的,因此在插入或删除元素时,可能需要移动其他元素以保持顺序。这种操作的时间复杂度较高,通常为O(n)。而链表在插入或删除元素时,只需要修改相关节点的指针或引用,无需移动大量元素,因此效率较高,时间复杂度通常为O(1)。
(3)随机访问:顺序表支持随机访问,即可以通过下标直接访问任意位置的元素,具有快速的随机访问能力。而链表则不支持随机访问,只能通过从头节点开始遍历来访问元素,访问时间复杂度较高。
(4)扩容:顺序表在插入元素时,如果当前空间已满,需要进行扩容操作,即开辟新的内存空间并复制原有元素。而链表在插入元素时无需扩容,只需动态分配新的节点即可。
(5)空间利用率:顺序表在预先分配的内存空间内可以充分利用空间,空间利用率较高。而链表由于需要存储节点的指针或引用信息,因此在空间利用率上相对较低。
(6)灵活性:链表相对于顺序表具有更高的灵活性。链表可以在运行时动态地创建和销毁节点,以适应不同规模的数据集。而顺序表则需要预先分配足够的内存空间,一旦空间不足就需要进行扩容操作,这可能会带来额外的开销。