目录
题目:
构建一个单向链表,链表中包含一组整数数据。输出链表中的所有元素。
要求:
1. 使用自定义的链表数据结构
2. 提供一个 linkedList 类来管理链表,包含构建链表和输出链表元素的方法
3. 在 main 函数中,创建一个包含一组整数数据的链表,然后调用链表的输出方法将所有元素打印出来
一、 回顾
在之前的学习中,我们了解了字符串和数组这两种结构,它们具有着以下的共同点
- 元素按照一定的顺序来排列
- 可以通过索引来访问数组中的元素和字符串中的字符
但是它们也都有着一些缺点:
- 固定大小:数组的大小通常是固定的,一旦分配了内存空间,就难以动态地扩展或缩小,如果需要存储的元素数量超出了数组的大小,就需要重新分配更大的数组,并将原来数组的内容复制过去,需要执行很多额外的操作。
- 内存是连续的:正是因为元素按照一定的顺序来排列,它们在计算机内存中的存储也是连续的,这也就意味着,当需要存储一些需要占用空间较大的内容,也只能找一些大块的内存区域,而空间比较小的内存区域就被浪费了,从而导致了内存资源浪费。
- 固定的数据类型:数组要求所有元素具有相同的数据类型,字符串存储的都是字符,如果需要存储不同类型的数据,数组和字符串就不能满足以上需求了。
二、 指针
C++中的指针就像是一个地址的引用,它帮助访问和操作存储在计算机内存中的数据。
2.1 声明指针
声明指针,需要用到 * 符号
// 声明一个指向整数的指针a
方式一:
int *a;
方式二:
int* a;
2.2 指针获取地址
指针想要存放某个变量的地址,需要先使用取地址符 & 获取地址
int x = 10;
int *a = &x; // 将指针a初始化为变量x的地址
2.3 解引用
想要获取这个地址值,需要使用 * 符号来访问, 这个过程称为解引用
int value = *a; // 获取a指针指向的值(等于x的值,即10)
2.4 指针与数组的关系
指针和数组之间有密切的关系,数组名本质上是一个指向数组第一个元素的指针。
PS:即不需要写出arr[0],直接写出arr即可,就能知道指针指向为数组的第一个元素arr[0]
int arr[3] = {1, 2, 3}; //声明一个数组,并将其初始化
int *a = arr; // 数组名arr就是指向arr[0]的指针
//arr[0]即文字阐述部分的:数组名即指向数组第一个元素的指针
2.5 指针的算术操作及访问
指针还可以执行加法、减法等算术操作,以访问内存中的不同位置。
int arr[5] = {1, 2, 3, 4, 5};
int *a = arr; //指向数组的第一个元素
int value = *(a + 2); // 获取数组的第三个元素(值为3)
2.6 空指针值
一个特殊的空指针值,通常表示为nullptr
,用于表示指针不指向任何有效的内存地址。
int *a = nullptr; // 初始化为空指针
三、 链表
3.1 概念
与数组不同,链表的元素存储可以是连续的,也可以是不连续的,每个数据元素处理存储本身的信息(data数据域
)之外,还存储一个指示着下一个元素的地址的信息(next指针域
),即这些元素是通过一条“链”串起来的。
链表的第一个节点的存储位置被称为头指针,然后通过next
指针域找到下一个节点,直到找到最后一个节点,最后一个节点的next
指针域并不存在,也就是“空”的,在C++中,用null
来表示这个空指针。
3.2 虚拟头节点
为了简化链表的插入和删除操作,我们经常在链表的第一个节点前添加一个节点,称为虚拟头节点(dummyNode
),头节点的数据域可以是空的,但是指针域指向第一个节点的指针。
头指针是链表指向第一个节点的指针,访问链表的入口,经常使用头指针表示链表,头指针是链表必须的
头节点是为了方便操作添加的,不存储实际数据,头节点不一定是链表必须的
3.3 定义链表节点
使用struct
结构体来包含链表的数据域名、又包含指针域的复合结构。
//声明一个结构体类型 Books,变量为 book
struct Books{
char title[50];
char author[50];
char subject[100];
int book_id;
} book;
struct type_name{
member_type1 member_name1;
member_type2 member_name2;
member_type3 member_name3;
. .
} object_names;
3.4 结构体的成员变量
结构体可以组合多个不同类型的成员变量,成员变量可以是各种数据类型,包括整数、浮点数、字符串、其他结构体等,所以你可以根据需要定义自己的结构体来组织数据。
// 链表节点结构体
struct ListNode {
int val; // 存储节点的数据
ListNode *next; // 下一个节点也是链表节点,所以也是ListNode类型,*表示指针(地址),next是名称
}
3.5 初始化结构体
初始化结构体的方式有很多,这里我们使用构造函数的方式来进行,构造函数的名称与结构体的名称相同,和其他函数不一样的是,构造函数没有返回类型,除此之外类似于其他的函数,构造函数也有一个(可能为空)的参数列表和一个函数体(可能为空)。链表结构体的构造函数代码如下:
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x)
表示:定义一个接收整数参数X的ListNode
的构造函数(名称和结构体相同)
: 表示:初始化列表的开始
val(x)
表示:链表数据域的值被初始化为传递的参数 x
next(nullptr)
表示:next指针
被初始化为nullptr
,表示没有下一个节点。
3.6 完整结构体代码
完整代码定义一个ListNode结构体,用于表示链表中的一个节点,包含存储节点数据的数据域和存储下一个节点地址的指针域。
// 链表节点结构体
struct ListNode {
int val; // 存储节点的数据
ListNode *next; // 指向下一个节点的指针
// 构造函数,用于初始化节点, x接收数据作为数据域,next(nullptr)表示next指针为空
ListNode(int x) : val(x), next(nullptr) {}
};
四、 链表的插入
4.1 操作思路
-
创建一个新的链表节点,初始化它的值为
val
-
将新的节点放入到链表的尾部,接入链表,也就是原来链表尾部的指针
next
指向新节点 -
新接入的链表节点变为链表的尾部
PS:①中的next指向null,是指新节点的next指针指向原链表的null
ListNode *newNode = new ListNode(val); // 通过new构造一个新的节点,节点的值为val
cur -> next = newNode; // cur节点的next节点是新节点,从而将新节点接入链表
cur = cur -> next; // 新插入的节点变更为新的尾节点,即cur发生了变更
4.2 new
运算符和箭头语法->
new
是一个运算符,它的作用就是在堆内存中动态分配内存空间,并返回分配内存的地址,使用方式一般为指针变量 = new 数据类型
, 比如下面的代码
int *arr = new int[5]; // 分配一个包含5个整数的数组的内存空间,并返回一个地址,指针arr指向这个地址
箭头语法(->
):用于通过指针访问指针所指向的对象的成员,cur
是一个指向 ListNode
结构体对象的指针,而 next
是 ListNode
结构体内部的一个成员变量(指向下一个节点的指针)。使用 cur->next
表示访问 cur
所指向的节点的 next
成员变量。
五、 题解
#include <iostream>
using namespace std;
//定义一个链表节点
struct lianbiao{
int val; //整数类型的val
lianbiao *next;
lianbiao(int x) : val(x), next(nullptr){}
};
int main(){
int n, val;
//定义虚拟头节点,其节点值为0,dummHead指向其地址
lianbiao *dummyHead = new lianbiao(0);
while(cin >> n){
lianbiao *w = dummyHead; //定义一个临时变量w,构建链表
for(int i = 0; i < n; i++){
cin >> val; //输入值
//根据读取的值,创建一个新的链表节点,并初始化其值为val
lianbiao *newjiedian = new lianbiao(val);
w -> next = newjiedian; //将新节点newjiedian加入链表
w = w -> next; //临时变量w指向下一个节点
}
w = dummyHead; //将虚拟头节点给临时变量w
//遍历链表的节点并逐个输出
while(w -> next != NULL){
cout << w -> next -> val << " ";
w = w -> next;
}
cout << endl;
}
}