目录
前言
刚才我们了解到,数组作为数据存储结构有一定的缺点。在无序数组中,搜索时十分低效。在有序数组中,插入的效率很低。不管在哪种数组中,删除的效率都很低。而且大小无法改变。为了应对顺序表的缺陷,链表就此诞生。链表也是继数组之后第二种使用的最广泛的通用数据结构。
链表结构:在物理上不连续,在逻辑上连续。大小不固定。
链式存储结构是基于指针实现的。我们把一个数据元素和一个指针称之为节点
数据域:存数据元素的区域
指针域:存储直接后继位置的区域。
链式存储,其实就是用指针将相互关联的结点链接起来。
链表种类
一.单链表
概念:链表的每个节点只包含一个指针域。叫做单链表(即构成链表的每个节点只有一个指向后继节点的指针)
1.头指针与头节点
单链表有带头节点和不带头结点的两种结构。
链表中,第一个结点存储的位置叫头指针,如果链表有头结点,那么头指针就是指向头结点的指针。
头指针所指的不存在数据元素的第一个结点就叫做头结点(而头结点又指向首元结点)。
头结点一般不放数据(有的时候也是放的,比如链表的长度,用做监视)。
存放第一个数据元素的结点叫做第一个数据元素结点,也叫做首元结点。
2.不带头结点的单链表的插入操作
上图中,是不带头结点的单链表的插⼊操作。如果我们在非第一个结点之前做插入操作,我们只需要把新结点的指针域指向a(i),然后将a(i - 1)的指针域指向新的结点。如果我们在第一个结点之前进行插入操作,那么头指针就要等于新插入的节点,这和在非第一个数据元素结点前插入时的情况不同。而且,还有一些不同的情况需要我们做考虑。
所以,当我们设计此类链表的时候,就要分别设计实现方法。不如带头结点的方便。
3.带头结点的单链表的插入操作
上图中,如果采用带头结点的单链表结构,算法实现的时候,p指向头结点,改变p的指针的next的值,就可以了,而头指针head的值不变。因此,算法的实现方法比较简单,其操作与对其他结点的操作统一。
带头结点的好处就是,方便对于链表的操作。对于空表、非空表的情况以及对于首元结点都可以进行统一的处理。
4.删除操作
我们统一用带头结点的情况
设a(i)是要删除的,要实现将a(i)删除,就是将它的前继节点的指针绕过,指向它的后继节点就ok。
5.代码展示
#include<stdio.h>
#include<stdlib.h>
//声明链表的节点结构
typedef struct ListNode{
int data;
struct ListNode* next;
}Node,*linklist;
//linklist与*Node,都是结构体指针,用linklist声明的指针,强调标记了一个单链表,Node*声明的指针,强调标记了一个节点
//初始化一个带头节点的单链表
linklist initlist()
{
linklist l=(Node*)malloc(sizeof(Node));
if(l==NULL) printf("分配失败\n");
else l->next=NULL;
return l;
}
//头插法
linklist head_insert(linklist head,int k)
{
Node* s=(Node*)malloc(sizeof(Node));
if(s==NULL) printf("分配失败\n");
else {
s->data=k;
s->next=head->next;
head->next=s;
}
return head;
}
//尾插法
linklinst tail_insert(linklist head,int k)
{
Node* s=(Node*)malloc(sizeof(Node));
if(s==NULL) printf("分配失败\n");
else{
s->data=k;
s->next=NULL;
//先找到最后一个节点
Node* t=head;
while(t){
if(t->next==NULL) break;
t=t->next;
}
t->next=s;
}
return head;
}
//在数据x后插入k
linklist mid_insert(linklist head,int x,int k)
{
Node* s=(Node*)malloc(sizeof(Node));
if(s==NULL) printf("分配失败\n");
else{
s->data=k;
//找到x在的节点
Node* t=head->next;
while(t->data!=x) t=t->next;
s->next=t->next;
t->next=s;
}
return head;
}
//删除数据k
linklist del(linklist head,int k)
{
//找到k的位置
Node* p=head->next;//找到k的节点
Node* pre=head;//找p的前一个节点
while(p->data!=k){
p=p->next;
pre=pre->next;
}
pre->next=p->next;
p->next=NULL;
free(p);//释放p指的节点空间
p->NULL;//释放p节点,防止变成野指针
return head;
}
//打印链表
void print(linklist head)
{
Node* p=head->next;
while(p){
printf("%d ",p->data);
p=p->next;
}
printf("\n");
}
int main()
{
//运行结果3 2 7 4 8 5 6
linklist head=initlist();
head=head_insert(head,1);
head=head_insert(head,2);
head=head_insert(head,3);
head=tail_insert(head,4);
head=tail_insert(head,5);
head=tail_insert(head,6);
head=mid_insert(head,2,7);
head=mid_insert(head,4,8);
del(head,1);
print(head);
return 0;
}
二.双向链表
双向链表在单链表的基础上维护前继指针即可
1.优点
单链表并不能访问前继节点,如果要寻找的话,就需要遍历,最坏的时间复杂度是O(n),此时双向链表的作用就显示出来了
2.插入操作
s->pre=p;//1
s->next=p->next;//2
p->next->pre=s;//3
p->next=s;//4
3.删除操作
p->pre->next=p->next;//1
p->next->pre=p->pre;//2
4.代码演示
#include<stdio.h>
#include<stdlib.h>
//声明链表
typedef struct ListNode {
int data;
struct ListNode* next;
struct ListNode* pre;
}Node,*linklist;
//初始化
linklist initlist()
{
linklist s = (Node*)malloc(sizeof(Node));
if (s == NULL) printf("失败\n");
else {
s->next = s;
s->pre = s;
}
return s;
}
//头插法
linklist head_insert(linklist head, int k)
{
Node* s = (Node*)malloc(sizeof(Node));
if (s == NULL) printf("失败\n");
else {
s->data = k;
s->pre = head;
s->next = head->next;
head->next = s;
if (s->next != NULL)
s->next->pre = s;
}
return head;
}
//尾插法
linklist tail_insert(linklist head,int k)
{
Node* s = (Node*)malloc(sizeof(Node));
if (s == NULL) printf("失败\n");
else {
s->data = k;
s->next = NULL;
Node* t = head;
while (t != NULL)
{
if (t->next == NULL) break;
t = t->next;
}
t->next = s;
s->pre = t;
}
return head;
}
//在数据x后面插入k
linklist mid_insert(linklist head, int x, int k)
{
Node* s = (Node*)malloc(sizeof(Node));
if (s == NULL) printf("失败\n");
else {
s->data = k;
Node* t = head->next;
while (t->data != x && t != NULL)
t = t->next;
s->pre = t;
s->next = t->next;
t->next = s;
if (s->next != NULL)
s->next->pre=s;
}
return head;
}
//删除数据k
linklist del(linklist head, int k)
{
Node* p = head->next;
Node* pr = head;
while (p != NULL && p->data != k)
{
p = p->next;
pr = pr ->next;
}
pr->next = p->next;
if (p->next != NULL)
p->next->pre = pr;
p->next = p ->pre = NULL;
free(p);
p = NULL;
return head;
}
//打印
void print(linklist head)
{
Node* p = head->next;
while (p != NULL)
{
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
//主函数
int main()
{
linklist head = initlist();
head = head_insert(head, 1);
head = head_insert(head, 2);
head = head_insert(head, 3);
head = tail_insert(head, 4);
head = tail_insert(head, 5);
head = tail_insert(head, 6);
head = mid_insert(head, 2, 7);
head = mid_insert(head, 4, 8);
del(head, 1);
print(head);
return 0;
}
三. 循环链表
1.优点
循环列表是一种特殊的链表,它的最后一个节点指向第一个节点,形成了一个环形的结构,方便循环操作和轮流访问,在需要循环遍历链表,轮流访问数据,在链表头尾进行插入和删除的操作场景下非常有用。插入和删除和单链表差不多因此就不单独讨论。
2.合并两个循环链表
rearA->next和rearB->next都是空的头节点,合并时被合并的那个链表中的头节点被删掉,它们的尾指针分别是rearA和rearB,需要合并他们,只需要如下操作
p=rearA->next;//保存A表的头节点即1
rearA->next=rearB->next->next;//跳过B的头节点
q=rearB->next;//赋值给rearA->next即2
rearB->next=p;//将原来A表的头节点赋值给rearB->next即3
忘记加上free(q)操作了
3.代码演示
#include<stdio.h>
#include<stdlib.h>
//循环链表->除开初始化操作和单链表不同其他相同
//需要在初始化时,让头节点自己指向自己
typedef struct Nodelist {
int data;
struct Nodelist* next;
}node,*linklist;
//初始化带头节点的循环单链表
linklist initlist()
{
linklist s = (linklist)malloc(sizeof(node));
if (s == NULL) printf("失败\n");
else {
s->next = s;
}
return s;
}
四.循环双向链表
名字可知道是结合了循环链表和双向链表的产物,同样重复操作过多,因此代码展示不同
//循环双向链表
typedef struct NodeList {
int data;
struct NodeList* next;
struct Nodelist* pr;
}node,*linklist;
//初始化
linklist initlist()
{
linklist s = (linklist)malloc(sizeof(node));
if (s == NULL) printf("失败\n");
else {
s->next = s;
s->pr = s;
}
return s;
}
总结
综上所述,在单链表的基础上演化了众多链表,本质就是处理不同情况下特有的高效结构。处理上述几种还有静态链表等,但是静态链表本质是还是数组操作,有兴趣可以看看以下的静态链表代码:
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 1000 /* 存储空间初始分配量 */
typedef int Status; /* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef char ElemType; /* ElemType类型根据实际情况而定,这里假设为char */
Status visit(ElemType c)
{
printf("%c ",c);
return OK;
}
/* 线性表的静态链表存储结构 */
typedef struct
{
ElemType data;
int cur; /* 游标(Cursor) ,为0时表示无指向 */
} Component,StaticLinkList[MAXSIZE];
/* 将一维数组space中各分量链成一个备用链表,space[0].cur为头指针,"0"表示空指针 */
Status InitList(StaticLinkList space)
{
int i;
for (i=0; i<MAXSIZE-1; i++)
space[i].cur = i+1;
space[MAXSIZE-1].cur = 0; /* 目前静态链表为空,最后一个元素的cur为0 */
return OK;
}
/* 若备用空间链表非空,则返回分配的结点下标,否则返回0 */
int Malloc_SSL(StaticLinkList space)
{
int i = space[0].cur; /* 当前数组第一个元素的cur存的值 */
/* 就是要返回的第一个备用空闲的下标 */
if (space[0]. cur)
space[0]. cur = space[i].cur; /* 由于要拿出一个分量来使用了, */
/* 所以我们就得把它的下一个 */
/* 分量用来做备用 */
return i;
}
/* 将下标为k的空闲结点回收到备用链表 */
void Free_SSL(StaticLinkList space, int k)
{
space[k].cur = space[0].cur; /* 把第一个元素的cur值赋给要删除的分量cur */
space[0].cur = k; /* 把要删除的分量下标赋值给第一个元素的cur */
}
/* 初始条件:静态链表L已存在。操作结果:返回L中数据元素个数 */
int ListLength(StaticLinkList L)
{
int j=0;
int i=L[MAXSIZE-1].cur;
while(i)
{
i=L[i].cur;
j++;
}
return j;
}
/* 在L中第i个元素之前插入新的数据元素e */
Status ListInsert(StaticLinkList L, int i, ElemType e)
{
int j, k, l;
k = MAXSIZE - 1; /* 注意k首先是最后一个元素的下标 */
if (i < 1 || i > ListLength(L) + 1)
return ERROR;
j = Malloc_SSL(L); /* 获得空闲分量的下标 */
if (j)
{
L[j].data = e; /* 将数据赋值给此分量的data */
for(l = 1; l <= i - 1; l++) /* 找到第i个元素之前的位置 */
k = L[k].cur;
L[j].cur = L[k].cur; /* 把第i个元素之前的cur赋值给新元素的cur */
L[k].cur = j; /* 把新元素的下标赋值给第i个元素之前元素的ur */
return OK;
}
return ERROR;
}
/* 删除在L中第i个数据元素 */
Status ListDelete(StaticLinkList L, int i)
{
int j, k;
if (i < 1 || i > ListLength(L))
return ERROR;
k = MAXSIZE - 1;
for (j = 1; j <= i - 1; j++)
k = L[k].cur;
j = L[k].cur;
L[k].cur = L[j].cur;
Free_SSL(L, j);
return OK;
}
Status ListTraverse(StaticLinkList L)
{
int j=0;
int i=L[MAXSIZE-1].cur;
while(i)
{
visit(L[i].data);
i=L[i].cur;
j++;
}
return j;
printf("\n");
return OK;
}
int main()
{
StaticLinkList L;
Status i;
i=InitList(L);
printf("初始化L后:L.length=%d\n",ListLength(L));
i=ListInsert(L,1,'F');
i=ListInsert(L,1,'E');
i=ListInsert(L,1,'D');
i=ListInsert(L,1,'B');
i=ListInsert(L,1,'A');
printf("\n在L的表头依次插入FEDBA后:\nL.data=");
ListTraverse(L);
i=ListInsert(L,3,'C');
printf("\n在L的“B”与“D”之间插入“C”后:\nL.data=");
ListTraverse(L);
i=ListDelete(L,1);
printf("\n在L的删除“A”后:\nL.data=");
ListTraverse(L);
printf("\n");
return 0;
}