题目:
代码:
1、 未构造函数实现构建链表和输出链表元素功能
#include<iostream>
using namespace std;
//链表节点结构体(定义存储数据val、指针next的整体的新类型为ListNode类型)
struct ListNode
{
int val;//存储节点的数据
//next指针指向下一个节点,下一个节点也是链表节点,所以也是ListNode类型
ListNode *next;
ListNode(int x):val(x),next(NULL) {}//使用构造函数初始化val和next
};
int main()
{
ListNode *dummyHead=new ListNode(0);//定义了虚拟头节点
int n;
while(cin>>n)
{
//可以定义一个指向当前节点的指针 cur,刚开始指向虚拟头结点。
ListNode *cur=dummyHead;
while(n--)
{
int val;
cin>>val;//输入节点数据
//根据val值构造一个新的节点
ListNode *newNode=new ListNode(val);
//使当前尾节点next指针指向下一个新节点,从而将新节点接入链表
cur ->next=newNode;
//使cur指针指向新创建的节点
cur=cur ->next;
}
//将cur重新指向链表的虚拟头节点,从头循环输出链表
cur =dummyHead;
while(cur->next!=NULL)
{
cur=cur ->next;//指针指向下一个节点
cout<<cur ->val<<' ';
}
cout<<endl;
}
return 0;
}
注意: ListNode *cur=dummyHead;指针cur的初始化需要在大循环里面,如果在大循环外面,后面组元素的输出会加上前面组的元素输出。
2、 通过构造函数实现构建链表和输出链表元素功能
#include<iostream>
using namespace std;
//链表节点结构体(定义存储数据val、指针next的整体的新类型为ListNode类型)
struct ListNode
{
int val;//存储节点的数据
//next指针指向下一个节点,下一个节点也是链表节点,所以也是ListNode类型
ListNode *next;
ListNode(int x):val(x),next(NULL) {}//使用构造函数初始化val和next
};
//构建链表
void getList(int n,ListNode *dummyHead)
{
//定义一个指向当前节点的指针 cur,刚开始指向虚拟头结点。
ListNode *cur=dummyHead;
while(n--)
{
int val;
cin>>val;
//根据val值构造一个新的节点
ListNode *newNode=new ListNode(val);
//使当前尾节点next指针指向下一个新节点,从而将新节点接入链表
cur ->next=newNode;
//使cur指针指向新创建的节点
cur=cur ->next;
}
}
//打印链表元素
void printList(ListNode *dummyHead)
{
ListNode *cur=dummyHead;
while(cur ->next!=NULL)
{
cur=cur ->next;
cout<<cur ->val<<' ';
}
}
int main()
{
ListNode *dummyHead=new ListNode(0);//定义了虚拟头节点
int n;
while(cin>>n)
{
getList(n,dummyHead);
printList(dummyHead);
cout<<endl;
}
return 0;
}
1、字符串和数组
在之前的学习中,我们接触到了字符串和数组这两种结构,它们具有着以下的共同点
- 元素按照一定的顺序来排列
- 可以通过索引来访问数组中的元素和字符串中的字符
但是它们也都有着一些缺点:
- 固定大小:数组的大小通常是固定的,一旦分配了内存空间,就难以动态地扩展或缩小,如果需要存储的元素数量超出了数组的大小,就需要重新分配更大的数组,并将原来数组的内容复制过去,需要执行很多额外的操作。
- 内存是连续的:正是因为元素按照一定的顺序来排列,它们在计算机内存中的存储也是连续的,这也就意味着,当需要存储一些需要占用空间较大的内容,也只能找一些大块的内存区域,而空间比较小的内存区域就被浪费了,从而导致了内存资源浪费。
- 固定的数据类型:数组要求所有元素具有相同的数据类型,字符串存储的都是字符,如果需要存储不同类型的数据,数组和字符串就显得无能为力了。
还有重要的一点是,如果我们想要往数组中新增加或者删除一个元素,会特别麻烦!
比如下面的图例,想要往数组中删除第三个元素,当完成删除后,还需要从删除元素位置遍历到最后一个元素位置,分别将它们都向前移动一个位置,也就是说后续的所有元素都要改变自己的位置,这是十分耗时的操作。
那有没有什么数据结构能够解决上面的问题呢?
那就是我们这节课中将要学习到的链表!
2、链表
与数组不同,链表的元素存储可以是连续的,也可以是不连续的,每个数据元素处理存储本身的信息(data数据域
)之外,还存储一个指示着下一个元素的地址的信息(next指针域
),给人的感受就好像这些元素是通过一条“链”串起来的。
链表的第一个节点的存储位置被称为头指针,然后通过next
指针域找到下一个节点,直到找到最后一个节点,最后一个节点的next
指针域并不存在,也就是“空”的,在C++中,用null
来表示这个空指针。
为了简化链表的插入和删除操作,我们经常在链表的第一个节点前添加一个节点,称为虚拟头节点(dummyNode
),头节点的数据域可以是空的,但是指针域指向第一个节点的指针。
头指针是链表指向第一个节点的指针,访问链表的入口,经常使用头指针表示链表,头指针是链表必须的
头节点是为了方便操作添加的,不存储实际数据,头节点不一定是链表必须的
那在C++中如何定义链表结构呢,传统的定义变量的方式只能使用一种数据类型,无法处理链表这种既包含数据域名、又包含指针域的复合结构,这就需要使用到struct
结构体,结构体是一种用户自定义的数据类型。
定义链表节点:
// 链表节点结构体
struct ListNode {
int val; // 存储节点的数据
ListNode *next; // 指向下一个节点的指针
// 构造函数,用于初始化节点, x接收数据作为数据域,next(nullptr)表示next指针为空
ListNode(int x) : val(x), next(nullptr) {}
};
我们完成了定义链表节点的操作,那应该完成怎样的操作将链表节点插入到链表的尾端,从而形成一个完整的链表呢?至少应该包括以下操作:
-
创建一个新的链表节点,初始化它的值为
val
-
将新的节点放入到链表的尾部,接入链表,也就是当前链表的尾部的
next
指向新节点 -
新接入的链表节点变为链表的尾部
假设我们用cur
来表示当前链表的尾节点
上面的链表的插入操作用代码来表示如下:
ListNode *newNode = new ListNode(val); // 通过new构造一个新的节点,节点的值为val
cur -> next = newNode; // cur节点的next节点是新节点,从而将新节点接入链表
cur = cur -> next; // 新插入的节点变更为新的尾节点,即cur发生了变更
这里有两个新的语法:new
运算符和箭头语法 ->
new
是一个运算符,它的作用就是在堆内存中动态分配内存空间,并返回分配内存的地址,使用方式一般为指针变量 = new 数据类型
, 比如下面的代码:
new int[5]; // 分配一个包含5个整数的数组的内存空间,并返回一个地址,指针arr指向这个地址
箭头语法(->
):用于通过指针访问指针所指向的对象的成员,cur
是一个指向 ListNode
结构体对象的指针,而 next
是 ListNode
结构体内部的一个成员变量(指向下一个节点的指针)。使用 cur->next
表示访问 cur
所指向的节点的 next
成员变量。