文章目录
一、顺序表
二、单链表
1.概念:
每个结点,除了存放自身数据外,还需要存放一个指向其后继的指针。
单链表是非随机存取的,查找某个结点时,需要从表头开始遍历。
头结点与头指针的区别:
不管带不带头结点,头指针始终指向链表的第一个结点。而头结点是带头结点的第一个结点。
通常带头结点的链表更方便,在插入或删除时不用再进行特殊判断。
1.定义
typedef struct LNode{
ElemType data;
struct LNode * next;
}LNode,*LinkList;
2.创建
创建带头节点的单链表、不带头节点的单链表(空链表)
// 带头结点
bool InitList(LinkList &L){
L=(LNode *)malloc(sizeof(Lnode));
if(L==NULL)
return false;//内存不足,分配失败
L->next=null;
return true;
}
// 不带头结点
bool InitList(LinkList &L){
L=null;
return true;
}
3.插入
(1)头插法建立单链表:
采用头插法时,输入顺序与生成的链表中的元素顺序是相反的。
LinkList List_HeadInsert(LinkList &L){//逆向建立单链表
Lnode *s;
int x;
L=(LinkList)malloc(sizeof(LNode));//创建头结点
L->next=NULL;//创建初始为空的链表,初始为空
scanf("%d",&x);
while(x!=9999){//输入9999表示结束
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
//将新结点插入表中,L为头结点
s->next=NULL;
s->next=L->next;
L->next=s;
scanf("%d",&x);
}
return L;
}
(2)尾插法建立单链表
能够保持输入顺序与链表中的元素顺序一致。
为此必须增加尾指针r,始终指向最后一个元素。
LinkList List_TailInsesrt(LinkList &L){
int x;//设置元素类型为整型
L=(LinkList)malloc(sizeof(LNode));//创建头结点
Lnode *s, *r=L;//尾指针初始时指向头结点
scanf("%d",&x);
while(x != 9999){
s=(LNode *)malloc(sizeof(LNode));
s->data=x;
r->next=s; //r指向新的表尾结点
r=s;
scanf("%d",&x);
}
r->next=NULL;
return L;
}
(3)插入结点(在其前驱结点进行后插操作)
方法一:查找前驱结点+后插的代码如下:
bool ListInsert(LinkList &L,int i,ElemType e){
if(i<1)
return false;
int j=0; //当前p指向第几个结点
Lnode *p=L; //L指向头结点,将L赋值给p,p指向头结点,头结点为第0个结点
while(p!=NULL && j < i-1){ //循环找到第i-1个结点
p=p->next;
j++;
}
//在p结点进行后插操作
return InsesrtNextNode(p,e);
}
方法二:或者封装两个函数,一是按位查找结点,二是在指定结点作后插方法。
LNode *p=GetElem(L,i-1); //查找第i-1个结点
InsesrtNextNode(p,e); //在指定结点执行后插方法
下面是封装好的函数
// 按位查找第i个结点
LNode *GetElem(LinkList L,int i){
if(i==0)
return L; //若i等于0,则返回头结点
if(i<1)
return NULL; //i无效,返回NULL
int j=1; //当前p指向第几个结点
LNode *p=L->next; //头结点的指针域赋值给p,p指向第一个结点
while(p!=NULL && j<i){ //找到第i-1个结点
p=p->next;
j++;
//j等于1时,p后移指向第2个结点
//j等于2时,p后移指向第3个结点
//...
//j等于i-1时,p指向第i个结点
}
return p;
}
// 在指定结点后插新元素
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结点
s->next = p->next;
p->next = s;
return true;
}
(4)前插法
由于单链表是单项的,给出结点p,只能顺藤摸瓜地找到它的后继结点。但是没有办法逆向找到它的前驱结点。
思路1:先从头到尾查找p的直接前驱,然后进行后插。代码同上面的后插法
思路2:将新结点s,后插到p后面,然后交换data,看起来不就是s前插到了p之前嘛。
思路2:代码
bool InsertPriorNode(LNode *p,Lnode *s){
if(p==NULL || s==NULL)
return false;
s->next = p->next; //s后插到p之后
p->next = s;
ElemType temp = p->data; //交换数据域
p->data = s->data;
s->data = temp;
}
4.删除
ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
王道视频里将头结点看作是第0个结点
方法1:遍历找到前驱结点,然后断链删除之。
e在形参中使用&,可以直接返回其值。
bool ListDelete(LinkList &L,int i,ElemType &e){
if(i<1){
return false;
}
int j=0; //j表示当前p指向第几个结点,头结点为第0个
LNode *p=L; //L指向头结点
while(p!=NULL && j<i-1){//查找第i-1个结点
p=p->next;
j++;
}
if(p==NULL){ //i值不合法
return false;
}
if(p->next==NULL){//第i-1个结点之后已无其他结点
return false;
}
LNode *q = p->next; //q指向待删除结点
e = q->data;
p->next = q->next;
free(q);
return true;
}
方法2:偷天换日骚操作。删除某个结点p,交换p与后继结点的值,删除p的后继即可。
bool DeleteNode(LNode *p){
if(p==NULL || p->next ==NULL){
return false; //p不合法或者p是最后一个结点,该方法失效
}
LNode *q = p->next; //q指向p的后继结点
p->data = p->next->data; //p和后继交换数据信息
p->next = q->next; //删除q
free(q);
return true;
}
5.查找
不管是按位查找,还是按位查找,都需要从前往后遍历,所以时间复杂度都是O(n)
(1)按位查找
LNode *GetElem(LinkList L,int i){
if(i<1) return NULL; //i值不合法
if(i == 0) return L; //第0个结点为头结点
int j= 1;
LNode *p = L->next; //p指向第1个结点
while(p != NULL && j < i){
p = p->next;
j++;
}
return p; //返回第i个结点的指针,若i大于表长,则返回NULL
}
(2)按值查找
注意:C语言比较struct类型的变量时,不能使用 == 比较相等。
可以封装一个判等方法。
LNode *LocateElem(LinkList L,ElmeType e){
LNode *p = L->next; //从第一个结点开始查找data域为e的结点
while(p != NULL && p->data != e){
p = p->next;
}
return p; //找到后返回该结点的指针,找不到饭返回NULL
}
6.表长
表长 : 单链表中数据节点的个数。头结点不计入表长
因此带头结点与不带头结点的单链表,在计算表长时的代码逻辑有所不同。
三、双链表
1.定义
typedef struct DNode{
ElemType data;
struct DNode *prior,*next;
}DNode,*DlinkList;
多了一个指针域prior,
- 双链表的按位查找与按值查找代码逻辑不变
- 双链表的插入、删除操作与单链表的代码出现不同。需要增加对prior指针修改代码。
- 其关键是保证修改的过程中不断链外,双链表还可以方便的找到前驱结点,因此插入、删除操作的时间复杂度仅为O(1)
- 例如,在p前插(后插)一个s,删除结点p
2.双链表的插入操作
①②两步,必须在④之前,否则*p之后的后继结点的指针就会丢失,导致插入失败。
s->next = p->next;
if(p->next != NULL)
p->next->prior = s;
s->prior = p;
p->next = s;
3.双链表的删除操作
p->next = q->next;
if(q->next != NULL)
q->next->prior = p;
free(q);
四、循环链表
1.定义
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList;
2.初始化一个循环单链表
bool InitList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode));
if(L == NULL)
return false;
L->next = L;
return true;
}
3.初始化一个循环双链表
bool InitDList(DLinkList &L){
L = (LNode *)malloc(sizeof(LNode));
if(L == NULL)
return false;
L->next = L;
L->prior = L;
return true;
}
五、静态链表
单链表:各个结点在内存中星罗棋布、散落天涯。
静态链表:分配一整片连续的内存空间,各个结点集中安置。
1.定义
# define MaxSize 50
struct Node{
ElemType data; //存储数据元素
int next; //下个数组元素的下标
};
typedef struct Node SLinkList[MaxSize];
//可用 SLinkList 定义“一个长度为 MaxSize 的 Node 型数组”
SLinkList a; //定义了一个数组,等同于 struct Node a[MaxSize];
next == -1 表示链表的结束。
插入、删除的操作只需要修改 “指针”,即next的数值
静态链表在不支持真实指针的高级语言里是一种非常巧妙的设计。
2.查找
必须从头往后遍历,所以时间复杂度为O(n)
3.性质
静态链表:用数组的方式实现的链表
优点:增、删 操作不需要大量移动元素
缺点:不能随机存取,只能从头结点开始依次往后查
找;容量固定不可变
适用场景:①不支持指针的低级语言;②数据元素数
量固定不变的场景(如操作系统的文件分配表FAT)