1.1 单链表的特点
单链表最明显的一个特征是有结点的概念:
1.1.1 结点
作为一个单链表,每个结点中都有指针域和数据域:
- 数据域:顾名思义就是存储数据的空间
- 指针域:就是存储指针的空间,链表的连结就是靠着各个结点之间的指针域的指向进行连结
1.2.1 头结点和首元结点
设计一个链表时,要弄清楚头结点和首元结点的概念,才能更加清楚的理解链表,先看下两幅图:
其一:只有首元结点时链表的链表
其二:有头结点时链表的链表
两幅图对比可以明显的看到:
- 带头结点的链表多了一个结点,即头结点
- 头结点的数据域一般是不存储数据的,我们可以赋空值
- 带头结点的链表首指针指向了头结点,没有头结点的链表首指针指向了首元结点
头结点的作用:头结点相当于一个哨兵,不存储真实的数据,其设计的目的是为了后续在做链表的增、删、改、查的时候方便首元结点和尾节点的处理,包括空表和非空表的统一处理(这个在后续篇章的单向循环链表、双链表、双向循环链表时就能体会出其好处)
1.2 单链表的实现方式
1.2.1 构建单链表的结构
开始我们先定义一些常用状态和链表的结点,构建单链表的结构:
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define OK 1
#define MAXSIZE 20 /* 存储空间初始分配量 */
typedef int Status;/* Status是函数的类型,其值是函数结果状态代码,如OK等 */
typedef int ElemType;/* ElemType类型根据实际情况而定,这里假设为int */
//定义结点
typedef struct Node{
ElemType data;
struct Node *next;
}Node;
typedef struct Node * LinkList;
1.2.2 首先初始化单链表
- 创建头结点
- 判断头结点是否创建成功
- 将头结点的指针域置空
算法实现:
// 2.1 初始化单链表线性表
Status InitList(LinkList *L){
//产生头结点,并使用L指向此头结点
*L = (LinkList)malloc(sizeof(Node));
//存储空间分配失败
if(*L == NULL) return ERROR;
//将头结点的指针域置空
(*L)->next = NULL;
return OK;
}
// 这里先实现一下链表的打印,方便后续使用,没什么好说的拿到每个结点的数据打印即可
/* 初始条件:顺序线性表L已存在 */
/* 操作结果:依次对L的每个数据元素输出 */
Status ListTraverse(LinkList L)
{
LinkList p=L->next;
while(p)
{
printf("%d\n",p->data);
p=p->next;
}
printf("\n");
return OK;
}
main函数调用:
int main(int argc, const char * argv[]) {
// 单链表初始化
Status iStatus;
LinkList L;
//2.1 单链表初始化
iStatus = InitList(&L);
printf("L 是否初始化成功?(0:失败,1:成功) %d\n",iStatus);
}
运行结果:
L 是否初始化成功?(0:失败,1:成功) 1
Program ended with exit code: 0
1.2.3 单链表插入
分5步:
- 寻找要插入结点的前一个结点p
- 创建新的结点
- 给新的结点赋值
- 将新结点指向p->next(这里一定要先第4步在第5步,防止后面的结点丢失,找不到)
- 将p->next在指向新的结点
算法实现:
//2.2 单链表插入
/*
初始条件:顺序线性表L已存在,1≤i≤ListLength(L);
操作结果:在L中第i个位置之后插入新的数据元素e,L的长度加1;
*/
Status ListInsert(LinkList *L,int i,ElemType e){
int j;
LinkList p,s;
p = *L;
j = 1;
//寻找第i-1个结点
while (p && j<i) {
p = p->next;
++j;
}
//第i个元素不存在
if(!p || j>i) return ERROR;
//生成新结点s
s = (LinkList)malloc(sizeof(Node));
//将e赋值给s的数值域
s->data = e;
//将p的后继结点赋值给s的后继
s->next = p->next;
//将s赋值给p的后继
p->next = s;
return OK;
}
main函数调用:
int main(int argc, const char * argv[]) {
// 单链表初始化
Status iStatus;
LinkList L;
//2.1 单链表初始化
iStatus = InitList(&L);
printf("L 是否初始化成功?(0:失败,1:成功) %d\n",iStatus);
//2.2 单链表插入数据
for(int j = 1;j<=10;j++)
{
iStatus = ListInsert(&L, 1, j);
}
printf("L 插入后\n");
ListTraverse(L);
}
运行结果:
L 是否初始化成功?(0:失败,1:成功) 1
L 插入后
10
9
8
7
6
5
4
3
2
1
1.2.4 单链表取值
这个很简单,找到要取值的结点,将需要的值取出即可
- 我们从头结点的next,即首元结点1开始找即可
- 在找到要取值的结点p,记录下来,并判空处理
- 通知指针*e,拿到取值的结点的数据 *e = p->data
算法实现:
//2.3 单链表取值
/*
初始条件: 顺序线性表L已存在,1≤i≤ListLength(L);
操作结果:用e返回L中第i个数据元素的值
*/
Status GetElem(LinkList L,int i,ElemType *e){
//j: 计数.
int j;
//声明结点p;
LinkList p;
//将结点p 指向链表L的第一个结点;
p = L->next;
//j计算=1;
j = 1;
//p不为空,且计算j不等于i,则循环继续
while (p && j<i) {
//p指向下一个结点
p = p->next;
++j;
}
//如果p为空或者j>i,则返回error
if(!p || j > i) return ERROR;
//e = p所指的结点的data
*e = p->data;
return OK;
}
main函数调用:
int main(int argc, const char * argv[]) {
// 单链表初始化
Status iStatus;
LinkList L;
ElemType e;
//2.1 单链表初始化
iStatus = InitList(&L);
printf("L 是否初始化成功?(0:失败,1:成功) %d\n",iStatus);
//2.2 单链表插入数据
for(int j = 1;j<=10;j++)
{
iStatus = ListInsert(&L, 1, j);
}
printf("L 插入后\n");
ListTraverse(L);
//2.3 单链表获取元素
GetElem(L,5,&e);
printf("第5个元素的值为:%d\n",e);
}
运行结果:
L 是否初始化成功?(0:失败,1:成功) 1
L 插入后
10
9
8
7
6
5
4
3
2
1
第5个元素的值为:6
Program ended with exit code: 0
1.2.5 单链表删除元素
看图
算法实现:
//2.4 单链表删除元素
/*
初始条件:顺序线性表L已存在,1≤i≤ListLength(L)
操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减1
*/
Status ListDelete(LinkList *L,int i,ElemType *e){
int j;
LinkList p,q;
p = (*L);
j = 1;
//查找第i-1个结点,p指向该结点
while (p->next && j<i) {
p = p->next;
++j;
}
//当i>n 或者 i<1 时,删除位置不合理
if (!(p->next) || j>i) return ERROR;
//q指向要删除的结点
q = p->next;
//将q的后继赋值给p的后继
p->next = q->next;
//将q结点中的数据给e
*e = q->data;
//让系统回收此结点,释放内存;
free(q);
return OK;
}
main函数调用:
int main(int argc, const char * argv[]) {
// 单链表初始化
Status iStatus;
LinkList L;
ElemType e;
//2.1 单链表初始化
iStatus = InitList(&L);
printf("L 是否初始化成功?(0:失败,1:成功) %d\n",iStatus);
//2.2 单链表插入数据
for(int j = 1;j<=10;j++)
{
iStatus = ListInsert(&L, 1, j);
}
printf("L 插入后\n");
ListTraverse(L);
//2.3 单链表获取元素
GetElem(L,5,&e);
printf("第5个元素的值为:%d\n",e);
//2.4 删除第5个元素
iStatus = ListDelete(&L, 5, &e);
printf("删除第5个元素值为:%d\n",e);
ListTraverse(L);
}
运行结果:
L 是否初始化成功?(0:失败,1:成功) 1
L 插入后
10
9
8
7
6
5
4
3
2
1
第5个元素的值为:6
删除第5个元素值为:6
10
9
8
7
5
4
3
2
1
Program ended with exit code: 0
1.2.6 清空单链表
链表的清空只需要将头结点之后的所有结点释放,最后将头结点的next置空
算法实现:
/* 初始条件:顺序线性表L已存在。操作结果:将L重置为空表 */
Status ClearList(LinkList *L)
{
LinkList p,q;
p=(*L)->next; /* p指向第一个结点 */
while(p) /* 没到表尾 */
{
q=p->next;
free(p);
p=q;
}
(*L)->next=NULL; /* 头结点指针域为空 */
return OK;
}
main函数调用:
int main(int argc, const char * argv[]) {
// 单链表初始化
Status iStatus;
LinkList L;
ElemType e;
//2.1 单链表初始化
iStatus = InitList(&L);
printf("L 是否初始化成功?(0:失败,1:成功) %d\n",iStatus);
//2.2 单链表插入数据
for(int j = 1;j<=10;j++)
{
iStatus = ListInsert(&L, 1, j);
}
printf("L 插入后\n");
ListTraverse(L);
//2.3 单链表获取元素
GetElem(L,5,&e);
printf("第5个元素的值为:%d\n",e);
//2.4 删除第5个元素
iStatus = ListDelete(&L, 5, &e);
printf("删除第5个元素值为:%d\n",e);
ListTraverse(L);
iStatus = ClearList(&L);
ListTraverse(L);
}
运行结果:
L 是否初始化成功?(0:失败,1:成功) 1
L 插入后
10
9
8
7
6
5
4
3
2
1
第5个元素的值为:6
删除第5个元素值为:6
10
9
8
7
5
4
3
2
1
Program ended with exit code: 0
1.3 单链表的前插法和后插法
1.3.1 单链表前插入法
前插法的特点是每一个新的结点都是由头结点指向它的,这个新的结点指向之前头结点指向的结点。
- 先建立待头结点的单链表,并判空处理
- for循环插入随机数据
- 创建新的结点,并赋值
- 先将新结点的next指向头结点的next(即以前的首元结点)
- 再将头结点的next指向新的结点,完成前插法
算法实现:
//3.1 单链表前插入法
/* 随机产生n个元素值,建立带表头结点的单链线性表L(前插法)*/
void CreateListHead(LinkList *L, int n){
LinkList p;
//建立1个带头结点的单链表
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;
//循环前插入随机数据
for(int i = 0; i < n;i++)
{
//生成新结点
p = (LinkList)malloc(sizeof(Node));
//i赋值给新结点的data
p->data = i;
//p->next = 头结点的L->next
p->next = (*L)->next;
//将结点P插入到头结点之后;
(*L)->next = p;
}
}
main函数调用:
int main(int argc, const char * argv[]) {
// 单链表
LinkList L;
//3.1 前插法整理创建链表L
CreateListHead(&L, 10);
printf("整理创建L的元素(前插法):\n");
ListTraverse(L);
}
运行结果:
整理创建L的元素(前插法):
9
8
7
6
5
4
3
2
1
0
Program ended with exit code: 0
1.3.2 单链表后插入法
后插法的特点是所有新的结点都是由原来的尾节点指向新的结点,并且尾节点的next为null,其实尾插法是比较简单的,只需要知道链表的尾节点即可,也比较符合平时的开发使用
- 先建立待头结点的单链表,并判空处理,同时记录尾节点r(此时是头结点)
- for循环插入随机数据
- 创建新的结点,并赋值
- r->next执行新的结点,再记录新的结点为尾节点r
- 最后将尾节点的next置空即可
算法实现:
//3.2 单链表后插入法
/* 随机产生n个元素值,建立带表头结点的单链线性表L(后插法)*/
void CreateListTail(LinkList *L, int n){
LinkList p,r;
//建立1个带头结点的单链表
*L = (LinkList)malloc(sizeof(Node));
//r指向尾部的结点
r = *L;
for (int i=0; i<n; i++) {
//生成新结点
p = (Node *)malloc(sizeof(Node));
p->data = i;
//将表尾终端结点的指针指向新结点
r->next = p;
//将当前的新结点定义为表尾终端结点
r = p;
}
//将尾指针的next = null
r->next = NULL;
}
main函数调用:
int main(int argc, const char * argv[]) {
// 单链表
LinkList L;
//3.2 后插法整理创建链表L
CreateListTail(&L, 10);
printf("整理创建L的元素(后插法):\n");
ListTraverse(L);
}
运行结果:
整理创建L的元素(后插法):
0
1
2
3
4
5
6
7
8
9
Program ended with exit code: 0