数据结构与算法(青岛大学-王卓老师)——学习笔记(第3周)
线性表的链式表示和实现
1. 知识点回顾
- 顺序表的特点:以物理位置相邻表示逻辑关系。
- 顺序表的优点:任一元素均可随机存取。
- 顺序表的缺点:进行插入和删除操作时,需移动大量的元素。存储空间不灵活。
2. 线性表链式表示的定义
- 用一组物理位置任意的存储单元来存放线性表的数据元素。
- 这组存储单元既可以是连的,也可以是不廷的,甚至是散分布在内存中的任意位置上的。
- 链表中元素的逻辑次序和物理次序不一定相同。
2.1 知识点
链表=节点+指针链
、
节点=数据域+指针域
数据域:存储元素数值数据
指针域:存储直接后继结点的存储位置
单链表是由头指针唯一确定,因此单链表可以用头指针的名字来命名。
3. 链表的分类
单链表、双链表、循环链表
- 结点只有一个指针域的链表,称为单链表或线性链表;
- 结点有两个指针域的链表,称为双链表;
- 首尾相接的链表称为循环链表;
4. 头指针、头结点和首元结点
- 头指针:是指向链表中第一个结点的指针 ;
- 首元结点:是指链表中存储第一个数据元素a1的结点;
- 头结点:是在链表的首元结点之前附设的一个结点;
4.1 知识点
单链表一般有两种形式:带头结点和不带头结点。
其中带头结点是我们经常使用的。
讨论1:如何表示空表?
- 无头结点时,头指针为空时表示空表;
- 有头结点时,当头结点的指针域为空时表示空表。
讨论2:在链表中设置头结点有什么好处?
- 便于首元结点的处理首元结点的地址保存在头结点的指针域中,所以在链表的第一个位置上的操作和其它位置一致,无须进行特殊处理。
- 便于空表和非空表的统一处理无论链表是否为空,头指针都是指向头结点的非空指针,因此空表和非空表的处理也就统一了。
讨论3:头结点的数据域内装的是什么?
- 头结点的数据域可以为空,也可存放线性表长度等附加信息,但此结点不能计入链表长度值。
5.链表(链式存储结构)的特点
- 结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻。
- 访问时只能通过头指针进入链表,并通过每个结点的指针域依次向后顺序扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等。
6.单链表的定义
typedef struct Lnode//声明结点的类型和指向结点的指针类型
{
Elemtype data;//结点的数据域
struct Lnode *next;//结点的指针域,此处为嵌套定义
}Lnode, *Linklist// Linklist为指向结构体 Lnode的指针类型
注意:结构体中next的类型为strcuct Lnode指针类型,与定义的结构体类型struct Lnode 加上*后一致。表明指针指向struct Lnode类型的结构体。
Lnode *p;//一般定义结点指针
Linklist L;//一般定义链表
注意:上述两种定义本质无区别,只是因为“约定俗称”,才会出现“人造”区别。
拓展:指针类型引用和一般类型的引用
- 指针类型的引用
Lnode* p // 定义为指针类型
p->data = 2;
p->next = 0x12213;
LinkList L; // 相当于Lnode* p
L->data = xx;
l->next = xx;
- 一般类型的引用
Lnode a; // 定义为普通类型
a.data = 2;
a.next = 0x12213;
6.1单链表定义实例
存储学生学号、姓名、成绩的单链表结点类型定义
typedef Struct student
{
char num[8];//数据域
char name[8];//数据域
int score;//数据域
struct student*next;//指针域
}Lnode. *Linklist.
但是通常情况下我们一般不这样定义,通常将要存储的结构类型定义为一个结构类型,然后用这个结构类型定义数据域,实现了一致性
typedef Struct{
char num [8];//数据域
char name[8];//数据域
int score;//数据域
}Elemtype;
typedef struct Lnode{
Elemtype data; //数据域
struct Lnode *next;//指针域
}Lnode,*Linklist
7. 单链表的基本操作
7.1单链表的初始化
【算法步骤】
- 生成新结点作头结点,用头指针指向头结点。
- 将头结点的指针域置空**
// 初始化链表,参数为引用类型,直接对链表进行操作
status InitList_L(LinkList& L)//这里为引用类型,需要对链表做更改
{
L = new LNode;//或者(Linklist)mallod(sizeof(Lnode));
L->next = NULL;// 头节点指针域为空
return OK;
}
7.2 判断链表是否为空
【算法思路】判断头结点指针域是否为空
// 判断链表是否为空,参数为指针类型,不对链表做出更改
int IsEmpty_L(LinkList L)//这里没有加&,因为不需要对链表做出更改
{
if(L->next == NULL)
return 0;
else:
return 1;
}
7.3单链表的销毁
【算法思路】从头指针开始,依次释放所有结点
//销毁单链表 参数为引用类型,直接对链表进行操作
status Destory_L(LinkList& L)
{
Lnode* p;
while(L)
{
p = L;
L = L->next;
delete p ;//C语言用free(p)
}
return ok;
}
7.4 单链表的清空
链表仍存在,但链表中无元素,成为空链表(头指针和头结点仍然在)
【算法思路】年所有结点、井将头结指针域没置为空。
// 清空链表(保留头节点和头指针)
status Clear_L(LinkList& L)
{
Lnode* p;
Lnode* q;
p = L->next;
while(p)
{
q = p->next;
delete p;
p = q;
}
L->next = NULL;
return OK;
}
总结归纳:
从首元节点开始操作时, p = L->next;
从头结点开始操作时, p = L;
当删除到最后一个节点时,q先指向最后一个节点的指针域^(null),下一次循环p也指向NULL。循环终止的条件就是p=NULL。
7.5 求链表表长
// 求链表表长
int Get_len_L(LinkList L)
{
Lnode* p ;
int i = 0;
p = L->next;
while(p)
{
i++;
p = p->next;
}
}
8. 单链表的进阶操作
8.1 单链表取值
【思路】从链表的头指针出发,顺着链域next逐个结点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存取结构。
- 特殊情况1:要找的第i个元素超过链表长度
- 特殊情况2:要找的第i个元素小于1
【算法步骤】 - 从第1个结点(L->next)顺链扫描,用指针p指向当前扫描到的结 点,p初值p=L->next。
- j做计数器,累计当前扫描过的结点数,j初值为1。
- 当p指向扫描到的下ー结点时,计数器j加1。
- 当j==i时,p所指的结点就是要找的第i个结点。
status Get_elem_data(LinkList L,int i,Elementtype& e) // 找的的结果返回给e
{
Lnode* p;
p = L->next;
int j =1;
while(p&&j<i)
{
p = p->next;
j++;
}
if(!p||j>i) return ERROR;
e = p->data;
return OK;
}
8.2 单链表查找
8.2.1 返回指针
// 根据值查找链表中的节点,返回节点的地址
//找到,则返回L中值为e的数据元素的地址,查找失败返回NULL
Lnode* LocateElem(LinkList L,ElementType e)
{
Lnode* p;
p = L->next;
while(p&&p->data!=e) // p不为^或者未找到这个节点
p = p->next;
return p;
}
8.2.2 返回序号
int LocateElem(LinkList L, ElementType e)
{
Lnode *p;
p=L->next;
int count=0;
whlie(p->date!=e&&p)
{
count++;
p=p->next;
}
if(p) retunrn count;
else retunrn error;
}
注意:如果要返回查找节点的序号,需要多做一次判断后返回。
- 如果没找到,则p指向^,序号返回0;
- 如果找到,则直接返回序号。
8.3 单链表的插入
status insert_L(Linklist L,int i,Elemtpe e)
{
Lnode* p;
p = L->next;
int j =1;
while(p&&j<i-1)
{
p = p->next;
j++;
}
if(!p||j>i) return ERROR;
s=new Lnode;
s->date=e;
s->next=p->next;
p->next=s;
reruen OK;
}
8.4 单链表的删除
status delete_L(Linklist& L,int i,Elemtype &e)
{
Lnode *p;
Lnode *s;
p=L->next;
int j=1;
while(p&&j<i-1)
{
p=p->next;
j++;
}
if(j>i||!p) return error;
s=p->next;
p-next=s->next;
e=s->date;
delete s;
retunrn OK;
}
8.5 单链表的查找、插入和删除时间复杂度
1.查找
因线性链表只能顺序存取,即在査找时要从头指针找起,査找的时间
复杂度为O(n);
2.插入和删除
因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度
为O(1);
但是,如果要在单链表中进行前插或删除操作,由于要从头看找前驱
结点,所耗时间复杂度为(n)。
8.4 单链表的建立
8.4.1 头插法
// 建立单链表,长度为n
void CreateList_L(LinkList& L , int n)
{
L = new Lnode;//C语言为 L=(Linklist)malloc(sizeof(Lnode));
L->next = NULL; // 初始化一个头结点
for(int i = n;i>0;i--)
{
p = new Lnode;
cin>>p->data;//c语言为scanf("%Elemtype",&(p->date);
p-next = L->next;
L->next = p;
}
}
时间复杂度:O(n);
8.4.1 尾插法
// 建立单链表(尾插法),长度为n
void CreateList_wL(LinkList& L,int n)
{
Lnode *r;
L = new Lnode;
L->next = NULL;
r = L;
for(int i = 0;i<n;i++)
{
p = new Lnode;
cin>>p->data;
p->next = NULL;
r->next = p;
r = p;
}
}
时间复杂度:O(n);