单链表
**单链表:**链式存储,每个结点处理存放数据元素外,还要存储指向下一个节点的指针
- 不要求大片的连续空间,改变容量方便
- 不可随机存取,要耗费一定空间存放指针
定义单链表的结点类型:
struct LNode{ //定义单链表结点类型
ElemType data; //每个结点存放一个数据元素,数据域
struct LNode *next; //指针指向下一个结点,指针域
};
增加一个新的结点,在内存中申请一片空间,并用指针p指向这个结点
struct LNode *p=(struct LNode*)malloc(sizeof(struct LNode));
,之后可以使用一些代码逻辑,将p插入链表中
数据类型重命名
typedef<数据类型> <别名>
typedef struct LNode LNode;
即可以使用LNode代替struct LNode
typedef struct LNode{ //定义单链表结点类型
ElemType data; //每个结点存放一个数据元素,数据域
struct LNode *next; //指针指向下一个结点,指针域
}LNode,*LinkList;
等价于
struct LNode{ //定义单链表结点类型
ElemType data; //每个结点存放一个数据元素,数据域
struct LNode *next; //指针指向下一个结点,指针域
};
typedef struct LNode LNode;
typedef struct LNode *LinkList; //这是一个指向LNode的指针
//要表示一个单链表时,只需声明一个头指针L,指向单链表的第一个结点,再由next指针连接
声明一个指向单链表第一个结点的指针:
LNode *L;
LinkList L
两种方式
LNode*L 强调这是一个结点
LinkList 强调这是一个链表
如何初始化一个链表
不带头节点的单链表
typedef struct LNode{ //定义单链表结点类型
ElemType data; //每个结点存放一个数据元素,数据域
struct LNode *next; //指针指向下一个结点,指针域
}LNode,*LinkList;
//初始化一个空的链表
bool InitList(LinkList &L){
L=NULL; //空表,暂时没有任何结点
return true;
}
//判断链表是否为空
bool Empty(LinkList L){
return(L==NULL);
}
void test(){
LinkList L; //声明一个指向单链表的指针
//初始化一个空表
InitList(L);
//....后续操作
}
带头节点的单链表
typedef struct LNode{ //定义单链表结点类型
ElemType data; //每个结点存放一个数据元素,数据域
struct LNode *next; //指针指向下一个结点,指针域
}LNode,*LinkList;
//初始化一个带头节点的链表
bool InitList(LinkList &L){
L=(LNode*)malloc(sizeof(LNode)); //分配一个头节点
if(L==NULL) //内存不足,分配失败
return false;
L->next =NULL; //头结点之后暂时还没有结点
return true;
} //头结点不存储数据
//判断链表是否为空
bool Empty(LinkList L){
return(L->next==NULL);
}
void test(){
LinkList L; //声明一个指向单链表的指针
//初始化一个空表
InitList(L);
//....后续操作
}
带头节点,写代码更方便
单链表的操作
插入删除
1. 带头节点的的按位序插入
LIstInsert(&L,i,e):插入操作,在表中第i个位置,插入指定元素e
找到第i-1个结点,修改其next值,头节点可以看作第0个位置
typedef struct LNode{ //定义单链表结点类型
ElemType data; //每个结点存放一个数据元素,数据域
struct LNode *next; //指针指向下一个结点,指针域
}LNode,*LinkList;
//在第i个位置插入元素e(带头节点)
bool ListInsert(LinkInsert &L,int i,elemType e) {
if(i<1) return false;
LNode *p; //指针p指向当钱扫描到的结点
int j=0; //当前p指向的是第几个结点
p=L; //L指向头节点,头节点是第0个结点(不存数据)
while(p!=NULL&&j<i-1){
//循环找到第i-1个结点
p=p->next;
j++;
}
if(p==NULL)
return false;
LNode *s=(LNode*)malloc(sizeof(LNode));
s->data =e;
s->next=p->next; //这一句
p->next=s; //与这一句不可以颠倒
return true;
}
平均时间复杂度O(n)
2. 不带头节点的按位序插入(因为不带头节点所以i=1需要特殊处理)
typedef struct LNode{ //定义单链表结点类型
ElemType data; //每个结点存放一个数据元素,数据域
struct LNode *next; //指针指向下一个结点,指针域
}LNode,*LinkList;
//在第i个位置插入元素e(带头节点)
bool ListInsert(LinkInsert &L,int i,elemType e) {
if(i<1) return false;
if(i==1){
//插入第一个结点的操作与其他不同
LNode *s=(LNode*)malloc(sizeof(LNode));
s->data=e;
s->next=L;
L=s; //头指针指向新结点
return true;
}
LNode *p; //指针p指向当钱扫描到的结点
int j=0; //当前p指向的是第几个结点
p=L; //L指向头节点,头节点是第0个结点(不存数据)
while(p!=NULL&&j<i-1){
//循环找到第i-1个结点
p=p->next;
j++;
}
if(p==NULL)
return false;
LNode *s=(LNode*)malloc(sizeof(LNode));
s->data =e;
s->next=p->next; //这一句
p->next=s; //与这一句不可以颠倒
return true;
}
不带头节点的话,则插入和删除第一个元素时,需要更改头指针L
3指定结点的后插操作
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->next=p->next;
p->next=s;
return true;
}
可以替换插入操作的后半部分:return InsertNextNode(p,e)
4.指定结点的前插操作
Q:如何找到p的前驱结点
- 传入头指针
bool InsertPriorNode(LinkList L,LNode *p,elemtype e)
循环查找p的前趋q,再对q后插 ,时间复杂度为O(n) - 在p结点之后插入一个结点,将p的值复制到该节点中,并将e传入到p结点中 ,时间复杂度为O(1)
bool InsertPriorNode(LNode *p,ElemType 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->data=p->data;
p->data=e;
return true;
}
5.带头节点的按位序删除
ListDelete(&L,i,&e):删除操作。删除表中第i个位置的元素,并用e返回删除元素的值
找到第i-1个节点,将其指针指向i+1个结点,并释放第i个结点
typedef struct LNode{ //定义单链表结点类型
ElemType data; //每个结点存放一个数据元素,数据域
struct LNode *next; //指针指向下一个结点,指针域
}LNode,*LinkList;
bool ListDelete(LinkList &L,int i,ElemType &e){
if(i<1) return false;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p=L; //L指向头节点,头节点是第0个结点(不存数据)
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; //用e返回被删除的值
p->next=q->next;
free(q);
return true;
}
最坏,平均时间复杂度O(n)
6.指定结点的删除
bool DeleteNode(LNode *p)
Q:删除结点p需要找到他的前趋结点的next指针
- 传入头指针,循环寻找p的前趋结点
- 偷天换日
bool DeleteNode(LNode *p){
if(p==NULL)
return false;
LNode *q=p->next; //让q指向p的后继结点
p->data=p->next->data; //p和后继结点q交换数据域
p->next=q->next; // 将*q结点从连中断开
free(q);
return true;
}
如果p是最后一个结点,只能从表头开始依次寻找p的前驱,时间复杂度O(n)
- 单链表的局限性:无法逆向检索,有时候不太方便
单链表的查找操作
只讨论带头节点的情况
GetElem(L,i):按位查找操作,获取表中第i个位置的元素的值
LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。
注:在插入和删除中已经进行了查找的操作,找到i-1罢了
1. 按位查找
LNode*GetElem(LinkList L,int i){
if(i<0)
return NULL;
LNode *p; //指针p指向当前扫描到的结点
int j=0; //当前p指向的是第几个结点
p=L; //L指向头节点,头节点是第0个结点
while(p!=NULL&&j<i){
//循环找到第i个结点
p=p-next;
j++;
}
return p;
}
平均时间复杂度O(n)
2. 按值查找
LNode*LocateElem(LinkList L,ElemType e){
LNode *p=L->next;
//从第一个结点开始查找数据域为e的结点
while(p!=NULL&&p->data!=e)
p=p->next;
return p; //找到后返回该结点的指针,否则返回NULL
}
求表的长度
int Length(LinkList L){
int len=0;
LNode *p=L;
while(p->next!=NULL){
p=p->next;
len++;
}
return len;
}
单恋表的两种建立方法:头插法,尾插法
带头节点的单链表的情况
尾插法
- 初始化单链表
- 设置变量length记录链表长度
- while{
每次取出一个元素e;
ListInsert(L,length+1,e)
length++;
} - 每次都从头开始遍历,时间复杂度为O(n²)
后插操作
LinkList List_TailInsert(LinkList &L){ //正向建立单向表
int x; //设ElemType为整形
L=(LinkList)malloc(sizeof(LNode)); //建立头节点,初始化空表
LNode *s,*r=L; //r为表尾指针
scanf("%d",&x); //输入结点的值
while(x!=9999){ //输入9999表示结束
s=(LNode*)malloc(sizeof(LNode));
s->data=x;
r->next=s; //在r结点之后插入元素x
r=s; //r指向新的表尾结点,永远保持r指向最后一个结点
scanf("%d",&x);
}
r->next=NULL;
return L;
}
头插法建立单链表
每一次取得一个新的数据元素,都插入到单链表的表头的位置
- 即每次都对头节点进行一次后插操作
LinkList List_HeadInsert(LinkList &L){ //正向建立单向表
LNode *s;
int x;
L=(LinkList)malloc(sizeof(LNode)); //创建头节点
L->next=NULL; //初始为空链表,注意尾插法没有这一步
scanf("%d",&x); //输入结点的值
while(x!=9999){
s=(LNode *)malloc(sizeof(LNode));//创建新结点
s->data=x;
s->next=L-next;
L-next=s; //将新结点插入表中,L为头指针
scanf("%d",&x);
}
return L;
}
- 好习惯:只要是初始化单链表,都先把头指针指向NULL
- 重要应用:链表的逆置