Part3_线性链表
1 概念
用一组任意的存储单元存储线性表的数据元素。
(存储单元可以是连续的,也可以是不连续的)
1.1 特点
随机存储,但需要知道后继元素的地址,即线性表的链式存储结构
1.2 数据域
存储数据元素信息的域称为数据域
1.3 指针域
存储直接后继位置的域称为指针域
1.4 结点
这两部分的信息组成数据元素的存储镜像,称为结点(node)
1.5 链
指针域中存储的信息称做指针或链
1.6 链表
n个结点链结成一个链表,即为线性表的链式存储结构。
1.7 单链表
因为此链表的每个结点中只包含一个指针域,所以叫做单链表。
1.8 其他链表
根据链表结点所含指针个数、指针指向和指针连接方式,可以将链表分为单链表、循环链表;双向链表,二叉链表等。
其中:
单链表、循环链表、和双向链表用于实现线性表的链式存储结构
其他形式多用于实现树、和图等非线性结构。
单链表 图示:
2 单链表的实现
2.1 属性
2.1.1 单链表 示例:
2.1.2 头指针
链表中的第一个结点的存储位置叫做头指针。
分析:整个链表的存取必须从头指针开始进行。
同时:由于最后一个数据元素没有直接后继,则单链表中最后一个结点的指针为空;
头指针的逻辑示意图:
单链表的结点
typedef struct *l_node link_list //强调其为指向某个单链表的头指针 link_list p; 逻辑含义: p:指向某结点的指针变量,该结点的地址 *p:某结点本身
2.1.3 头结点
一般情况下。为了处理方便,在 单链表的第一个结点之前附设一个结点,称之为头结点。
(1)头结点的逻辑示意图:
(2)其数据域:
可以不存储任何信息。
也可以存储如线性表的长度等附加信息。
(3)其指针域:
存储指向第一个结点的指针
(4)优势:
消除首元结点(L->data 其他结点 L->next->data)与其他数据元素的操作差异,无需进行特殊处理 消除空表与非空表的操作差异
头指针:空表:L==NULL
非空表:指向首元结点首地址
头结点:空表:头指针指向头结点首地址(L->next ==NULL)
非空表:头指针指向头结点首地址
2.1.4 对比
分类 | 首元结点 | 头指针 | 头结点 |
---|---|---|---|
概念 | 指链表中存储第一个数据元素的结点(ZHAO) | 头指针是指链表指向第一个结点的指针。 若链表有头结点,则是指向头结点的指针 | 首元结点前的附设结点 |
逻辑示意图 | 常用头指针作为链表的名字 | 有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其他结点的操作统一 | |
必要性 | 空链表中不存在 | 头指针必不为空,链表的必要元素 | 链表的非必须要素 |
2.2 建表---带头结点的单向链表
创建单链表的过程就是一个动态生成链表的过程。(从空表的初始状态起,依次建立各元素的结点,并逐个插入链表)
2.2.1 空表
算法步骤:
(1)生成新的结点作为头结点,用头指针指向头结点 (2)头结点的指针域置空
示例:
代码解析: status init_list(link_list *pl) 函数执行状态 函数名 函数参数:单链表的头指针的地址 参数: pl:头指针的指针 *pl:头指针的值 malloc(sizeof(Node));//申请新结点作为头结点 *pl=(link_list)malloc(sizeof(Node));//头指针=头结点的首地址 *pl:头结点的首地址 *pl->next:头结点的指针域 *pl->data:头结点的数据域
# 2.2.2 头插法
将新结点逐个插入链表的头部来创建链表,每次申请一个新结点,读入相应的数据元素值,然后将新结点插入到头结点之后。
算法步骤
(1)创建一个只有头结点的空链表 (2)根据待创建链表包括的元素个数n,循环n次执行一下操作 ·生成一个新的结点p; ·输入元素值赋给新结点p的数据域 ·将新结点p插入到头结点之后
参考代码:
练习:上述代码
2.2.3 尾插法
将新结点逐个插入到链表的尾部来创建链表。
需要一个尾指针指向链表的尾节点。
算法步骤:
(1)创建一个只有头结点的空链表 (2)尾指针r初始化,指向头结点。 (3)根据创建链表包括的元素个数n,循环n次执行一下操作 ·生成一个新节点p; ·输入元素值赋给新节点p的数据域; ·将新节点p插入到尾结点r之后; ·尾指针r指向新的结点p;
算法示意图:
参考代码:
练习:上述代码
2.3 按位取值
只能从链表的首元结点出发,顺着指针域逐个结点向下访问
(无间道的单线联系)
算法步骤:
(1)用指针p指向首元结点,用j做计数器,初值为1 link_list p=l; int j=1; (2)首元结点开始,依次顺着指针域next向下访问, 只要指向当前结点的指针p不为空,并且j != i(位置序号),便循环执行 p指向下一个结点; 计数器j+1; (3)退出循环时,如果指针p为空,或者计数器j大于i;说明位置序号i不合法。 取值失败,返回error; 否则取值成功,此时j=i,p所指向的结点就是第i个结点 参数e保存p->data;返回OK;
示例代码:
#include "02_my.h" Status get_elem(LinkList L,int i,ElemType *e) { int j;//计算数器变量 LinkList p; p=L->next;//第一个结点的首地址 j=1; while(p && j<i) { p=p->next; j++; } if(!p || j>i) //p为空,空表,超过链表长度||j>i ,i小于1 return ERROR;//读取失败 *e=p->data; return OK; }
算法分析:O(n)
练习:整表查询
2.5 插入
逻辑示意图
图解:假设存储元素e的结点为s,要实现结点p、p->next,和s之间的逻辑关系的变化,只需要将结点s插入到结点p和p->next 之间即可
2.5.1 算法步骤
(1)查找结点a(i-1),(第ai个数据元素前一个)。 (2)生成新的结点s(局部变量?全局变量,动态内存分配!) (3)将新的结点数据域设置为e; (4)将新结点的指针域指向结点ai;(小明在终点线超过了第二名,成为了第几名?) (5)将结点p的指针域指向新结点s
算法示意图:
详细解析:
###
2.5.2 代码
2.5.3 算法分析O(n)
找到第ai-1个结点
2.6 删除
逻辑示意图:
图解:
要实现将结点b删除,将它的前继结点a的指针绕过b,指向它的后继结点c即可。
p->next =p-next->next; //把p的后继结点改为p的后继的后继结点
2.6.1 算法步骤
(1)查找结点a(i-1),并使指针p指向该节点 (2)临时保存待删除结点ai的地址在q中。以备释放 (3)将结点指针p的指针域指向ai的直接后继结点 (4)释放结点ai的空间
算法示意图:
2.6.2 参考代码:
2.6.3 时间复杂度O(n)
算法:遍历查找第i个结点(线性)O(n)
2.6.4 单链表整表删除 练习
算法思路
声明一结点p和q; 将头指针赋值给p; 循环: (1)备份p的后继结点地址 给q; (2)释放p; (3)将q赋值给p
参考代码
#include"02_ my.h" Status clear_list(LinkList *L) { LinkList p,q; p=(*L)->next;//头结点 while(p)//表尾 { q=p->next; free(p); p=q; } (*L)->next=NULL;//头结点的指针域为空 return OK; }
2.7 表长
算法步骤:遍历找到空指针
3 循环链表
单向链表:表中最后一个结点的指针域指向空
循环链表:表中最后一个结点的指针域指向头结点
相同: 其他数据元素的操作基本一致
区别: 判断链表是否结束的条件不同
分类 | 判别条件 | 判别条件 |
---|---|---|
无头结点 | 有头结点 | |
单向链表 | p !=NULL | p->next !=NULL |
循环链表 | p !=L | p->next !=L |
示例:计算循环链表的长度
优势:合并线性表
4 合并线性表
逻辑示意图:
左头右尾
算法步骤
(1)找到B的首元结点给p,备份B的头结点以备释放 (2)表B的尾指针指向表A的头结点 (3)将表A的尾指针指向p (3)释放表B的头结点
//主要语句(A,B是尾指针) //B:尾指针 //B->next:头结点 //B->next->next:首元结点 tmp=B->next;//备份B的头结点 p=B->next->next;//备份首元结点的地址 B->next=A->NEXT;//B的尾指针指向A的头结点 A->next=p;//表A尾指针指向B的首元结点 free(tmp);
参考代码: