目录
一 线性表
线性表是由n(n>=0)个相同类型的数据元素组成的有限序列,它是最基本、最常用的一种线性结构。顾名思义,线性表就像一条线,不会分叉。线性表有唯一的开始和结束,除了第一个元素外,每个元素都有唯一的直接前驱:除了最后一个元素外,每个元素都有唯一的直接后继。
线性表有两种存储方式:顺序存储和链式存储。采用顺序存储的线性表称为顺序表,采用链式存储的线性表称为链表。链表又分为单链表、双向链表和循环链表。
二 顺序表
顺序表采用顺序存储方式,即逻辑上相邻的数据在计算机内的存储位置也是相邻的。顺序存储方式,元素存储是连续的,中间不允许有空,可以快速定位第几个元素,但是插入和删除时需要移动大量元素。根据分配空间方法不同,顺序表可以静态分配和动态分配两种方法。
1.静态分配
顺序表最简单的方法是使用一个定长数组data[ ]存储数据,最大空间为Maxsize,用length记录实际的元素个数,即顺序表的长度。这种用定长数组存储的方法称为静态分配。
0 | 1 | 2 | 3 | 4 | 5 | 6 | .... | Maxsize - 1 | |
data[ ] | 1 | 2 | 5 | 3 | 6 | 8 | 4 | ||
实际 | 的 | 元素 | 个数 | length | = | 7 |
采用静态分配方法,定长数组需要预先分配一段固定大小的连续空间。但是在运算的过程中,如合并、插入等操作,容易超过预分配的空间长度,出现溢出。解决静态分配的溢出问题,可以采用动态分配的方法。
2.动态分配
在程序运行过程中,根据需要动态分配一段连续的空间(大小为Maxsize),用elem记录该空间的基地址(首地址),用length记录实际的元素个数,即顺序表的长度。
基地址 | M | a | x | s | i | z | e | ||
elem -> |
L | e | n | g | t | h | |||
a1 | a2 | ... | ai | ... | an |
采用动态存储方法,在运算过程中,如果发生溢出,可以另外开辟一块更大的存储空间,用以替换原来的存储空间,从而达到扩充存储空间的目的。
结构体定义的解释说明如下:
1.使用typedef有什么用处
typeof是C/C++语言的关键字,用于给原有数据类型起一个别名,在程序中可以等价使用,语法规则如下:
type 类型名称 类型标识符;
“类型名称”为已知数据类型,包括基本数据类型(如int、float等)和用户自定义数据类型(如用struct自定义的结构体)。
“类型标志符”是为原有数据类型起的别名,需要满足标识符命名规则。
2.使用typeof有什么好处
1.简化比较复杂的类型说明
给复杂的结构体类型起一个别名,这样就可以使用这个别名等价该结构体类型,在声明该类型变量时就方便多了。
不使用typeof的顺序表定义:
struct SqList{
int* elem; //顺序表的基地址
int length; //顺序表的长度
};
如果需要定义一个顺序表,需要写:
struct SqList L; //定义时需要加上struct(c需要,c++不需要),L为顺序表的名字
使用typeof的顺序表定义:
typedef struct{
int* elem; //顺序表的基地址
int length; //顺序表的长度
}SqList;
如果需要定义一个顺序表,需要写:
SqList L; //不需要写struct,直接用别名定义
2.提高程序的可移植性
在程序中使用这样的语句:
typedef int ElemType; //给int起个别名ElemType
在程序中,假如有n个地方用到了ElemType类型,如果现在处理的类型变为字符型了,那么就可以将上面类型定义中的int直接改为char。
typedef char ElemType;
这样只需要修改类型定义,不需要改动程序中的代码。如果不使用typedef类型定义,就需要把程序中n个用到int类型的地方全部改为char类型。如果某处忘记修改,就会产生错误。
3.为什么使用ElemType作为数据类型
使用ElemType是为了让算法的通用性更好,因为使用线性表的结构体定义后,并不清楚具体问题处理的数据是什么类型,不能简单地写成某一类型,不能简单地写成某一种类型。结合typedef使用,可以提高算法的通用性和可移植性。
以int型元素为例,如果使用顺序表的动态分配结构体定义,就可以直接将ElemType写成int。
typedef struct{
int *elem; //顺序表的基地址
int length; //顺序表的长度
}SqList;
也可以使用类型定义,给int起个别名:
typedef int struct; //给int起个别名ElemType,两者等价
typedef struct{
ElemType* elem; //顺序表的基地址
int length; //顺序表的长度
}SqList;
显然,后一种定义的通用性和可移植性更好,当然第一种定义也没有错。
3.顺序表的基本操作
1.初始化
初始化是指为顺序表分配一段预定义大小的连续空间,用elem记录这段空间的基地址,当前空间内没有任何数据元素,因此元素的实际个数为零。假设我们已经预定义了一个最大空间数Maxsize,那么就用new分配大小为Maxsize的空间,分配成功会返回空间的首地址,分配失败会返回空指针。
bool InitList(SqList &L) //构造一个空的顺序表L
{ //L前面加&表示引用参数,函数内部的改变挑出函数后仍然有效
//如果不加&,函数内部的改变在跳出函数后便会无效
L.elem = new int {Maxsize}; //为顺序表动态分配,axsize个空间
if(!l.elem)
return false; //分配空间失败
L.length = ; //顺序表长度为0
return ture;
}
初始化后的顺序表
L.elem | 0 | 1 | 2 | 3 | 4 | 5 | 6 | ... | Maxsize - 1 |
L.length = 0 |
2.创建
顺序表创建是向顺序表中输入数据,输入数据的类型必须与类型定义中的类型一致
算法步骤
- 初始化下标变量 i = 0,判断顺序表是否已满,如果是则结束;否则执行第二步。
- 输入一个变量元素 x。
- 将数据 x 存入顺序表的第 i 个位置,即L.elem[i] = x,然后i++。
- 顺序表长度加1,即L.length++。
- 直到数据输入完毕。
图解
1.输入元素:5。将数据元素5存入顺序表的第0个位置,即L.elem[0] = 5,然后i++。
L.elem-> | 0 | 1 | 2 | 3 | 4 | 5 | 6 | ... | Maxsize - 1 |
5 | |||||||||
L.length = 1 | i |
2.输入元素:3。将数据元素3存入顺序表的第1个位置,即L.elem[1] = 3,然后i++。
L.elem-> | 0 | 1 | 2 | 3 | 4 | 5 | 6 | ... | Maxsize - 1 |
5 | 3 | ||||||||
L.length = 2 | i |
3.输入元素:9。将数据元素9存入顺序表的第2个位置,即L.elem[2] = 9,然后i++。
L.elem-> | 0 | 1 | 2 | 3 | 4 | 5 | 6 | ... | Maxsize - 1 |
5 | 3 | 9 | |||||||
L.length = 3 | i |
代码实现
bool CreateList(SqLisr &L) //创建一个顺序表L
{ //L加&表示引用类型参数,函数内部的改变跳出函数仍然有效
//不加&则在内部改变时,跳出函数后无效
int x,i = 0;
while(x != -1) //输入-1时结束,也可以设置其他的结束条件
{
if(L.length == Maxsize)
{
cout << "顺序表已满!";
return flase;
}
cin >> x; //输入一个元素
L.elem[i++] = x; //将数据存入第i个位置,然后i++
L.length++; //顺序表长度加1
}
return ture;
}
3.取值
顺序表中的任何一个元素都可以立即找到,称为随机存取方式。
例如,要取第i个元素时,只要i值是合法的(1<= i <= L.length),那么立即可以找到该元素。由于下标是从0开始的,因此第i个元素,其下标为i - 1,即对应元素为L.elem[i - 1]。
L.eken-> | 0 | 1 | 2 | ... | i - 1 | ... | Maxsize - 1 | ||
a1 | a2 | a3 | ... | ai | ... | ... | an | ||
i |
注意:位序是指第几个元素,位序和下标差1。
代码实现:
bool GetElen(SqList L, int i, int &e)
{
if(i < 1 || i > L.length) return false;
//判断i值是否合理,若不合理,则返回flase
e = L.elem[i - 1]; //第i - 1个单元存储着第i个数据
return ture;
}
4.查找
在顺序表中查找一个元素e,可以从第一个元素开始顺序查找,依次比较每一次元素值。如果相等,则返回-1.
代码实现:
int LocateElem(Sqlist L. int e)
{
for (i = 0; i < L.length; i++)
if (L.elem[i] == e) return i + 1; //下标为i,实际为i+1个元素
return -1; //如果没找到,则返回-1
}
5.插入
在顺序表中第i个位置之前插入一个元素e,需要从最后一个元素开始,后移一位……直到把第i个元素也后移一位,然后把e放入第i个位置。
基地址L.elem-> | n - i + 1个 | 后移一位 | |||||||
a1 | a2 | ... | ai -> | ... -> | an -> |
算法步骤
- 判断插入位置i是否合法(1 <= i <= L.length +1 ),可以在第一个元素之前插入,也可以在第L.length + 1个元素之前插入。
- 判断顺序表的存储空间是否已满。
- 将第L.length至第i个元素依次向后移动一个位置,空出第i个位置。
- 将要插入的新元素e放入第i个位置。
- 表长加1,插入成功返回ture。
完美图解:
例:在顺序表中的第5个位置之前插入一个元素9。
后 | 移 | 一 | 位 -> | ||||||
L.elem -> | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | Maxsize - 1 |
3 | 5 | 6 | 7 | 2 | 8 | 10 | 1 | ||
L.length = 8 |
插入过程如下:
1.移动元素。从最后一个元素(下标为L.length - 1)开始后移一位。
L.elem -> | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | Maxsize - 1 | |
3 | 5 | 6 | 7 | 2 | 8 | 10 | -> | 1 | ||
L.length = 8 |
L.elem -> | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | Maxsize - 1 | |
3 | 5 | 6 | 7 | 2 | 8 | -> | 10 | 1 | ||
L.length = 8 |
L.elem -> | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | Maxsize - 1 | |
3 | 5 | 6 | 7 | 2 | -> | 8 | 10 | 1 | ||
L.length = 8 |
|
L.elem -> | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | Maxsize - 1 | |
3 | 5 | 6 | 7 | -> | 2 | 8 | 10 | 1 | ||
L.length = 8 |
|
2.插入元素。此时第5个位置空出来,将要插入的新元素9放入第5个位置,表长加1。
L.elem -> | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | Maxsize - 1 | |
3 | 5 | 6 | 7 | 9 | 2 | 8 | 10 | 1 | ||
L.length = 8 |
|
代码实现:
bool ListInsert_Sq(SqList &L, int i, int e)
{
if(i < 1 || i > L.length + 1) return false; //i值不合法
if(L.length == Maxsize) return false; //存储空间已满
for(int j = L.length - 1; j >= i - 1; j--)
L.elem[j + 1] = L.elem[j]; //从最后一个元素开始后移,直到第i个元素后移
l.elem[i - 1] = e; //将新元素e放入第i个位置
L.length++; //表长加1
return ture;
}
6.删除
在顺序表中删除第i个元素,需要把该元素暂存到变量e中,然后从i + 1个元素开始前移……直到把第n个元素也前移一位,即可完成删除操作。
基地址L.elem -> | 删除元素 | n - i个 | 前移一位 | ||||
a1 | a2 | ... | ai | <- ai+1 | <-... | <- an |
算法步骤:
- 判断删除位置i是否合法(1 <= i <= L.length),
- 将欲删除的元素保存在e中。
- 将第i + 1至第n个元素依次向前移动一个位置。
- 表长减1,删除成功,返回ture。
图解:
从顺序表中删除第5个元素。
删除元素 | <- | 前移一位 | ||||||||
L.enem -> | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | Maxsize - 1 | |
L.length = 8 | 3 | 5 | 6 | 7 | 2 | 8 | 10 | 1 |
删除过程如下:
1.移动元素。首先将待删除元素2暂存到变量e中,以后可能有用,如果不暂存,将会被覆盖,然后从第6个元素开始前移一位。
L.enem -> | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | Maxsize - 1 | |
L.length = 8 | 3 | 5 | 6 | 7 | 8 | <- | 10 | 1 |
L.enem -> | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | Maxsize - 1 | |
L.length = 8 | 3 | 5 | 6 | 7 | 8 | 10 | <- | 1 |
L.enem -> | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | Maxsize - 1 | |
L.length = 8 | 3 | 5 | 6 | 7 | 8 | 10 | <- | 1 |
2.表长减1。
L.enem -> | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | Maxsize - 1 | |
L.length = 8 | 3 | 5 | 6 | 7 | 8 | 10 | 1 |
代码实现:
bool ListDelete_Sq(SqList &L, int i, int &e)
{
if(i < 1 || i >L.Length) return false; //i值不合法
e = L.elem[i - 1]; //将欲删除的元素保存在e中
for (int j = 1;j <= L.length - 1; j++)
L.elem[j - 1] = L.elem[j]; //被删除元素之后的元素前移
L.length--; //表长减1
return true;
}
顺序表的优点:
操作简单,存储密度高,可以随机存取,只需要O(1)的时间就可以取出第i个元素。
顺序表的缺点:
需要预先分配最大空间,最大空间数估计过大或过小会造成空间浪费或溢出。插入和删除操作需要移动大量元素。
三 单链表
链表是线性表的链式存储方式。逻辑上相邻的数据在计算机内的存储位置不一定相邻。
1.单链表的存储方式
可以给每个元素附加一个指针域,指向下一个元素的存储位置。
数据元素 | 下一个元素的地址 | |||||
-> | ai | -> | ai+1 | -> |
每个节点包括两个域:数据域和指针域。数据域存储数据元素,指针域存储下一个节点的地址,因此指针指向的类型也是节点类型。每个指针都指向下一个节点,都是朝一个方向的,这样的链表称为单向链表或单链表。
定义了节点结构体之后,就可以把若干个节点连接在一起,形成一个单链表。
NULL | |||||||||||||
a1 | -> | a2 | -> | ... | ai | -> | ... | -> | an | ^ |
只要给这个单链表设置一个头指针,这个链表中的每个节点就都可以找到了。
头指针L | NULL | ||||||||||||
a1 | -> | a2 | -> | ... | ai | -> | ... | -> | an | ^ |
有时为了操作方便,还会给链表增加一个不存放数据的头节点(也可以存放表长等信息)。
头指针L | 头节点 | NULL | ||||||||||||||
a1 | -> | a2 | -> | ... | ai | -> | ... | -> | an | ^ |
在顺序表中,想找第i个元素,可以立即通过L.elem[i -1]找到,想找哪个找哪个,称为随机存取。但是在单链表中,想找第i个元素就没那么容易,必须从头开始,按顺序一个一个找,一直数到第i个元素,称为顺序存取。
2.单链表的基本操作
下面以带头节点的单链表为例,讲解单链表的初始化、创建、取值、查找、插入、删除等基本操作。
1.初始化
单链表的初始化是指构建一个空表。先创建一个头节点,不存储数据,然后令其指针域为空。
头指针L | NULL |
^ |
代码实现:
bool InitList_L(LinkList &L)
{
L = new LNode; //生成新节点作为头节点,用头指针L指向头节点
if(!L) return false; //生成节点失败
L->next = NULL; //头节点的指针域置空
return ture;
}
2.创建
创建单列表分为头插法和尾插法两种,头插法是指每次把新节点插到头节点之后,其创建的单链表和数据输入顺序正好相反,因此也称为逆序建表。尾插法是指每次把新节点链接到链表的尾部,其创建的单链表和数据输入顺序一致,因此也称为正序链表。
我们先讲头插法建表,头插法每次把新节点插入到头节点之后,创建的单链表和数据输入顺序相反。
图解:
1.初始状态。初始状态是指初始化后的空表,只有一个头节点。
2.输入数据元素1,创建新节点,把元素1放入新节点数据域。
头指针L | NULL |
^ |
s | |
1 |
-> |
s = new LNode;
cin >> s->data;
3.头插操作,插入头-节点的后面。
头指针L | ||
^ | ->[2] |
s | |
1 |
^[1] |
s->next = L->next; //[1]
L->next = s; //[2]
4.输入数据元素2,创建新节点,把元素2放入新节点数值域。
s | |
2 |
-> |
5.头插操作,插入头节点的后面。
头指针L | |
^[2] |
s | |
2 |
->[1] |
s | |
1 |
-> |
s->next = L->next; //[1]
L->next = s; //[2]
赋值解释
假设赋值之前节点的地址计指针为
头指针L | |
9630 |
9630 | ||
-> | 1 | ^ |
2046 | s |
2 |
赋值语句两端,等号的右侧是节点的位置,等号的左端是节点的指针域。
1.s->next = L->next:L->next存储的是下一个节点地址“9630”,将该地址赋值给s->next指针域,即s节点的next指针指向1节点。
头指针L | |
9630 |
9630 | ||
-> | 1 | ^ |
2046 | s | |
<-[1] | 2 | 9630 |
2.L->next = s:将s节点的地址“2046”赋值给L->next指针域,即L节点的next指针指向s节点。
头指针L | |
9630 |
2046 | s | |
->[2] | 2 | 9630 |
9630 | ||
->[1] | 1 | ^ |
修改指针顺序
为什么要修改后面的那个指针?
因为一旦要修改了L节点的指针域指向s,那么原来的L节点就找不到了,因此修改指针是有顺序的。
修改指针的顺序原则:先修改没有指针标记的那一端。
如果要插入节点的两端都有标记。例如,再定义一个指针q指向L节点后面的节点,那么先修改哪个指针都无所谓了。
6.拉直链表。
头指针L | ||
-> |
2 |
-> |
1 |
^ |
7.继续依次输入数据元素3、4、5、6、7、8、9、10。
头插法创建的单链表(逆序):
头指针L | |||||||||||||||
-> | 10 | -> | 9 | -> | ... | -> | 2 | -> | 1 |
可以看出,头插法创建的单链表与数据输入顺序正好相反。
代码实现:
void CreateList_H(LinkList &L) //头插法创建单链表
{
int n; //输入n个元素的值,建立到头节点的单链表L
LinkList s; //定义一个指针变量
L = new LNode;
L->next = NULL; //先建立一个带头节点的空链表
cout << "请输入元素个数n:" << endl;
cout << "头插法创建单链表……" << endl;
while(n--)
{
s = new LNode; //生成新节点s
cin >> s->data; //输入元素值赋值给新节点的数值域
s->next = L->next;
L->next = s; //将新节点s插入头节点之后
}
}
尾插法每次把新节点链接到链表的尾部,因此需要一个尾指针永远指向链表的尾结点。
1.初始状态。初始状态是指初始化的空表,只有一个头节点,设置一个尾指针r指向该节点。
头指针L | 尾指针r |
^ |
2.输入数据元素1,创建新节点,把元素1放入新节点数据域。
s = new LNode; //生成新节点s
cin >> s->data; //输入数据元素赋值给新节点的数据域
s | |
1 |
-> |
3.完成尾插操作,插入尾结点的后面。
头指针L | r |
^ |
s | r[3] | |
->[2] | 1 |
->[1] |
赋值解释:
s->next = NULL; //【1】:s节点的指针域置空
r->next = s; //【2】:将s节点的地址赋值给r节点的指针域,即将新节点s插入尾结点r之后
r = s; //【3】:将s节点的地址赋值给r,即r指向新的尾结点s。
4.输入数据元素2,创建新节点,把元素2放入新节点数据域。
s | |
2 |
-> |
5.完成尾插操作,插入尾结点的后面。
头指针L | r |
^ |
r | |||
-> | 1 |
->[2] |
s | r[3] |
2 |
6.继续依次输入数据元素3、4、5、6、7、8、9、10。
头指针L | |
-> | 1 |
-> |
2 |
-> |
3 |
-> | ... | -> | 9 | -> | 10 |
代码实现:
void CreateList_R(LinkList &L) //尾插法创建单链表
{
//输入n个元素的之,建立代表头节点的单链表L
int n;
LinkList s, t;
L = new LNode;
L->next = NULL; //先建立一个带头结点的空链表
r = L; //尾指针r指向头节点
cout << "请依次输入n个元素:" << endl;
cin >> n;
cout << "尾插法创建单链表……" << endl;
while(n--)
{
s = new LNode; //生成新节点
cin >> s->data; //输入元素值赋给新节点的数值域
s->next = NULL;
r->next = s; //将新节点s插入尾结点之后
r = s; //指向新的尾结点s
}
}
3.取值
单链表的取值不像顺序表那样可以随机访问任何一个元素,单链表只有头指针,各个节点的物理地址是不连续的。要想找到第i个节点,就必须从第一个节点开始按照顺序向后找,一直找到第i个节点。
注意:链表的头指针不可以随意改动!
一个链表是由头指针来标识的,一旦头指针改动或丢失,这个链表就不完整或找不到了。所以链表的头指针是不能随意改动的,如果需要用指针移动,可定义一个指针变量进行移动。
算法步骤:
- 先定义一个p指针,指向第一个元素节点,用j作为计数器,j = 1。
- 如果p不为空且j < 1,则p指向p的下一个节点,然后j加1,即:p = p->necxt; j++。
- 直到p为空或者j = i时停止。p为空,说明没有数到i,链表就结束了,即不存在第i个节点:j = i,说明找到了第i个节点。
完美图解:
1.p指针指向第一个元素节点,j = 1。
头指针L | p | j = 1 | ||||||||||
-> | a1 | -> | a2 | -> | ... | -> | an | ^ |
2.p指针指向第二个元素节点,j = 2.
头指针L | p | j = 2 | ||||||||||
-> | a1 | -> | a2 | -> | ... | -> | an | ^ |
3.p指针指向第i个节点,j = i。
头指针L | p | j = 1 | ||||||||||
-> | a1 | -> | ... | -> | ai | -> | an | ^ |
代码实现:
bool GetElem_L(LinkList L, int i, int &e)
{
//在带头节点的单链表L中查找第i个元素
//用e记录L中第i个数据元素的值
int j;
LinkList p;
p = L->next; //p指向第一个数据节点
j = 1; //j为计数器
while(j < i && p) //顺着链表向后扫描,直到p指向第i个元素或p为空
{
p = p->next; //p指向下一个节点
j++; //计数器
}
if(!p || j > i) //i值不合法
return false;
e = p->data; //取第i个节点的数据域
return true;
}
4.查找
在一个单链表中查找是否存在元素e,可以定义一个p指针,指向第一个元素节点,比较p指向节点的数据域是否等于e。如果相等,查找成功,返回true;如果不等,则p指向下一个节点,继续比较,如果p为空,查找失败,返回false。
头指针L | p | p | ||||||||||
-> | a1 | -> | ... | -> | ai | -> | an | ^ |
代码实现:
bool LocaeElem_L(LinkList L, int e) //在带头节点的单链表L中查找值为e的元素
{
LinkList p;
p = L->next;
while(p && p->data != e) //沿着链表向后扫描,直到p为空或p所指节点数据域等于e
p = p->next; //p指向下一个节点
if(!p) return false; //查找失败,p为NULL
}
5.插入
如果要在第i个节点之前插入一个元素,则必须先找到第i - 1个节点。
单链表只有一个指针域,是向后操作的,不可以向前操作。如果直接找到第i个节点,就无法向前操作,把新节点插入第i个节点之前。实际上,在第i个节点之前插入一个元素相当于在第i - 1个节点之后插入一个元素,因此先找到第i - 1个节点,然后将新节点插在其后面即可。
算法步骤:
- 定义一个p指针,指向头节点,用j作为计数器,j = 0。
- 如果p不为空且j < i - 1,则p指向p的下一个节点,然后j + 1,即:p = p->next;j++。
- 直到p为空或j >= i - 1停止。
- p为空,说明没有数列i - 1,链表就结束了,即i > n + 1,i值不合法;j > i - 1说明i < 1,此时i值不合法,返回false。如果j = i - 1说明找到了第i - 1个节点。
- 将新节点插到第i - 1个节点之后。
图解:
假设已经找到了第i - 1个节点,并用p指针指向该节点,s指向待插入的新节点,则插入操作为:
p | |||
-> | ai-1 | ->[2] |
s | ||
e | ->[1] |
s | |||
- -> | e | -> |
赋值解释:
- s->next = p->next:将p节点后面的节点地址赋值给s节点的指针域,即s节点的next指针指向p后面的节点。
- p->next = s:将s节点的地址赋值给p节点的指针域,即p节点的next指针指向s节点。
前插法建链表,就是将新节点插到头节点之后,现在是将新节点插到第i - 1个节点之后。
bool ListInsert_L(LinkList &L, int i, int e)
{
//在带头节点的单链表L中第i个位置之前插入值为e的新节点
int j;
LinkList p, s;
p = L;
j = 0;
while(p && j < i - 1)
{
p = p->next;
j++;
}
if(!p || j > i - 1) //i > n + 1或者i < 1
return false;
s = new LNode; //生成新节点
s->next = p->next; //将新节点的指针域指向第i个节点
p->next = s; //将节点p的指针域指向节点s
return ture;
}
6.删除
删除一个节点,实际上是把这个节点跳过去。根据单向链表向后操作的特性,要想跳过第i个节点,就必须先找到第i - 1个节点,否则是无法跳过去的。
p |
q | ||||||||
-> | ai - 1 | -> |
ai |
-> | ai + 1 | -> | |||
|
|____ | ___ | ___ | ___ | ___ | ____ | ___| |
赋值解释:
p->next = q->next的含义是将q节点的下一个节点地址赋值给p节点的指针域。等号的右侧是节点的地址,等号的左侧是节点的指针域。
0985 p | 2046 q | 1013 | |||||||
-> | ai - 1 | 2046 | - - - > | ai | 1013 | --> | ai + 1 | --> | |
|___________ | _______ | _____ | _______ | ____ | ___ | ____| | |||
节点的指针域 | p->next | = | q->next | 节点的位置 |
假设q节点的下一个节点地址是1013,该地址存储在q0>next里面,因此等号右侧的q->next的值为1013。把该地址赋值给p节点的next指针域,把原来的值2046覆盖掉,这样p->next也为1013,相当于把q节点跳过去了。赋值之后,用delete q释放被删除节点的空间
0985 p | 2046 q | 1013 | |||||||
-> | ai - 1 | 1013 | - - - > | ai | 1013 | --> | ai + 1 | --> | |
|___________ | _______ | _____ | _______ | ____ | ___ | ____| |
代码实现:
bool ListDelete_L(LinkList &L, int i) //单链表的删除
{
//在带头节点的单链表L中,删除第i个位置
LinkList p, q;
int j;
p = L;
j = 0;
while((p->next) && (j < i - 1)) //查找第i - 1个节点,p指向该节点
{
p = p->next;
j++;
}
if(!(p->next) || (j > i - 1)) //当i > n或i < 1时,删除位置不合理
return false;
q = p->next; //临时保存被删节点的地址以备释放空间
p->next = q->next; //将q节点的下一个节点地址赋值给p节点的指针域
delete q; //释放被删除节点的空间
return true;
}
在单链表中,每个节点除存储自身数据之外,还存储了下一个节点的地址,因此可以轻松的访问下一个节点,以及后面的所有后继节点。但是,如果想访问前面的节点就不行了,再也回不去了。
例如,删除节点q时,要先找到它的前一个节点p,然后才能删掉q节点,单向链表只能向后操作,不可以像前操作。
四 双向链表
鸽了
五 循环链表
鸽了
六 线性表的应用
鸽了
参考文献:
1.趣学数据结构.陈小玉.2019
来源:CSDN
作者:桜小路七葉
链接:https://blog.csdn.net/qq_45696480/article/details/103828287