[JY的数据结构学习] 第一节 手撕链表 (单向)
一、首先先写主函数,该有的都得有!
#include<iostream>
using namespace std;
int main(){
return 0;
}
二、实现链表节点
先进行DataType
的定义,数据类型为int
typedef int DataType;
再对链表进行定义,期中包括
(
1
)
(1)
(1)val
表示链表中的数据、
(
2
)
(2)
(2)next
指针表示链表的后继节点
链表单个节点图例
struct ListNode{
DataType val;//(1)
ListNode* next;//(2)
};
三、链表的增删改查
1. 首先实现创建链表节点的函数
ListNode* ListCreateNode(DataType data){
ListNode *node=new ListNode;//(1)
node->val=data;//(2)
node->next=nullptr;//(3)
return node;
}
(
1
)
(1)
(1) 先使用new
开辟一块内存空间用来存放节点
(
2
)
(2)
(2) 将这个节点的val
值赋值为创建链表的形参data
的值
(
3
)
(3)
(3) 最后将这个节点的next
指针设定为空,这样一个链表节点就创建完成了
在调用函数时:
ListNode *vtx;
DataType val=10;
vtx=ListCreateNode(val);
2. 创建链表
现在拥有了创建链表节点的函数,可以创建链表节点
接下来就可以进行链表的创建
在这里有两种创建方式,分别是:
- 尾插法
- 头插法
1. 尾插法
ListNode* ListCreateListByTail(int n,int nums[]){
ListNode *head,*tail,*vtx;//(1)
int index;//(2)
if(n<=0){//(3)
return NULL;
}
index=0;//(4)
vtx=ListCreateNode(nums[0]);//(5)
head=tail=vtx;//(6)
while(++index<n){//(7)
vtx=ListCreateNode(nums[index]);//(8)
tail->next=vtx;//(9)
tail=vtx;//(10)
}
return head;
}
(
1
)
(1)
(1) 定义三个指针,分别表示 头结点、尾结点、临时创建的中间节点
(
2
)
(2)
(2) 定义index
表示坐标
(
3
)
(3)
(3) 当传入的数组长度n
小于定于
0
0
0 时,证明要创建的链表是空链表,返回NULL
(
4
)
(4)
(4) 初始化index
为
0
0
0
(
5
)
(5)
(5) 创建链表节点并赋值给vtx
指针
(
6
)
(6)
(6) 让head
、tail
指针都赋值为vtx
的值,他们的next
同时指向同一个空指针
(
7
)
(7)
(7) 进入循环,没次循环在链表结尾位置插入一个新的链表节点,循环n
次
(
8
)
(8)
(8) vtx
再次赋值为 以 下一个 以index
为索引 的 数组中的数据 创建的链表节点
(
9
)
(9)
(9) 并让tail
控制之前的空指针指向这个新的vtx
节点
(
10
)
(10)
(10) 将tail
移动到下一个节点的位置
2. 头插法
ListNode* ListCreateListByHead(int n,int nums[]){
ListNode *head=nullptr,*vtx;//(1)
while(n--){
vtx=ListCreateNode(nums[n]);//(2)
vtx->next=head;//(3)
head=vtx;//(4)
}
return head;
}
这一种就相对简单很多了
(
1
)
(1)
(1) 创建一个链表指针head
为空,和一个链表指针vtx
(
2
)
(2)
(2) 进入循环,循环n次,数组从最后一个索引处向前遍历,创建链表节点并赋值给vtx
(
3
)
(3)
(3) 让vtx
的下一个节点为head
(这一步就是在链表前插入节点)
(
4
)
(4)
(4) 让head
指针的值更新为vtx
当前指向内存
在调用函数时:
int n;//表示链表长度
cin>>n;
int arr[n];
for(int i=0;i<n;++i){//输入数据
cin>>arr[i];
}
ListCreateListByTail(n,arr);
ListCreateListByHead(n,arr);
这就是一个链表的创建,现在我们有了一个创建好的链表了,接下来要对这个链表进行增、删、改、查操作。
我们先创建一个链表,方便后面的使用
int main(){
int n=10;
int arr[n]={0,1,2,3,8,7,6,5,4,9};
//头插尾插都可以实现同样的链表创建
ListNode *head=ListCreateListByHead(n,arr);
return 0;
}
3. 链表的查找
由于链表的查找理解较为简单,我们先从链表的查找开始进行
遍历链表
在学习数组的时候,我们都能发现,遍历数据 是一个十分重要的操作。通过遍历,可以获取一个数据结构中的 全部数据
下面是通过遍历链表来输出链表内容的函数
void ListPrint(ListNode *head){
ListNode *tmp=head;
while(tmp){
cout<<tmp->val<<"->";
tmp=tmp->next;
}
cout<<"NULL"<<endl;
}
这是我们的输出:
0->1->2->3->8->7->6->5->4->9->NULL
现在我们学会了遍历链表,下一步我们要对链表进行查找
查找链表中的数据
链表的索引
学习数组时,我们都知道查找数组中的值时通过索引进行遍历,而在链表中,只存在节点的概念,所以,链表的索引就是节点。
查找链表中的数据
查找数值位置
查找链表中的 数值的位置 时,我们只需要对链表进行 遍历,当链表该节点的
val
值和要查找的target
值相同时,返回 该节点 (也就是第几个节点) 即可,遍历后未找到则返回为空
下面是查找链表数值位置函数的实现:
ListNode* ListFindNodeByValue(ListNode* head , DataType target){
ListNode *tmp=head;//(1)
while(tmp){//(2)
if(tmp->val==target){//(3)
return tmp;
}
tmp=tmp->next;//(4)
}
return NULL;//(5)
}
(
1
)
(1)
(1) 创建临时节点tmp
,复制head
中数据到tmp
中
(
2
)
(2)
(2) 遍历tmp
(
3
)
(3)
(3) 如果,tmp
中的val
值和我要查找的target
值相等,则返回该节点
(
4
)
(4)
(4) 如果判断不成立,让tmp
改变为tmp
的下一个节点
(
5
)
(5)
(5) 遍历之后未查找到需要的值,返回NULL
查找某位置下的数值
查找链表中 某节点位置下的数值 时,我们只需要遍历链表的前
i
项,当查询到链表该节点时,返回 该节点 ,若不存在第i
项,则返回NULL
下面是查找 链表某节点 函数的实现:
ListNode* ListFindNode(ListNode *head,int i){
ListNode *tmp=head;
int j=0;//(1)
while(tmp&&j<i){//(2)
tmp=tmp->next;
++j;
}
if(!tmp||j>i){//(3)
return NULL;
}
return tmp;
}
(
1
)
(1)
(1) 设定坐标j
,用来表示第j
个节点
(
2
)
(2)
(2) 循环的执行条件是tmp
不为空指针,切循环i
次,每次都是和遍历相同的操作,只不过每次循环都要++j
确保遍历i
次
(
3
)
(3)
(3) 如果tmp
是空指针,或者遍历后的j
越界超过i
,则返回为空
(
4
)
(4)
(4) 返回这个节点
4. 链表的修改
我们可以修改链表的很多东西,比如修改链表节点的
val
值,或者是修改链表节点的指向,比如删除,插入,这都属于修改的范围,在接下来就会实现链表的删除、插入等,所以在这里就不过多赘述
链表修改节点的
val
值,只需要遍历到该节点并将节点的val
重新赋值即可,就不在单独实现函数了,直接进入下一个问题
5. 链表的插入
链表的插入有三种插入方式
分别是:
- 头插法
- 尾插法
- 在链表中间进行插入
我们在链表的创建中介绍过了头插法和尾插法
所以在这里将实现在链表的中间进行插入
在链表中间位置插入一个新的链表
可以插入一个链表,也可以插入一个链表节点
实现思路:让 插入的链表 的 尾结点 指向 从空指针转变到 要插入位置的 后继节点,让 要 插入位置的 前驱节点的 指针 指向 插入链表的 头节点
一般情况下:我们都是插入一个节点,这时候我们只需要输入一个数据对相应节点的val
进行赋值即可
ListNode* ListInsertNode(ListNode *head,int i,DataType n){
ListNode *pre=head,*aft,*vtx;//(1)
int j=0;//(2)
while(pre&& j++ < i ){//(3)
pre=pre->next;
}
if(!pre){//(4)
return NULL;
}
vtx=ListCreateNode(n);//(5)
aft=pre->next;
vtx->next=aft;
pre->next=vtx;
return vtx;//返回插入的节点
}
(
1
)
(1)
(1) 设定三个链表指针,分别表示 前驱节点(初始化为head)、后继节点、插入节点
(
2
)
(2)
(2) 设定坐标j
初始化为
0
0
0,用来计数,判断遍历到第几个节点
(
3
)
(3)
(3) 循环遍历到第i
个节点
(
4
)
(4)
(4) 判断该节点是否成立,不成立就返回NULL
(
5
)
(5)
(5) 用传入的DataType n
定义插入的节点vtx
,并使用本段开头的方法对链表进行连接
在调用函数时:
int main(){
int n=10;
int arr[n]={0,1,2,3,8,7,6,5,4,9};
//头插尾插都可以实现同样的链表创建
ListNode *head=ListCreateListByHead(n,arr);
ListPrint(head);
ListInsertNode(head,5,11);
ListPrint(head);
return 0;
}
输出结果为:
0->1->2->3->8->7->6->5->4->9->NULL 0->1->2->3->8->7->11->6->5->4->9->NULL
6. 链表的删除
链表的删除是一个模糊的描述,具体则分为了:链表节点的删除,以及链表整体的清除
链表节点的删除
ListNode* ListDeleteNode(ListNode *head,int i){
ListNode *pre,*aft,*del;//这里都和之前的一样
int j=0;
if(!head){
return NULL;
}
if(i==0){//如果是头结点则直接按如下操作删除
del=head;
head=head->next;
delete del;
del=NULL;
return head;
}
//如果不是,按如下操作删除
pre=head;
while(pre&&j++<i-1){//依旧是遍历,不过这次是遍历到要删除的前一位
pre=pre->next;
}
if(!pre||!pre->next){//当要删除的节点或者前驱节点为空则不能进行删除,直接返回原链表
return head;
}
del=pre->next;//这一部分同上插入部分
aft=pre->next->next;
pre->next=aft;
delete del;//这里和头结点删除操作相同,记得释放堆区内存
del=NULL;
return head;
}
这次在代码内增加了注释,因为和之前的操作大多相同,可以单看注释,就不进行详解了
在调用函数时:
int main(){
int n=10;
int arr[n]={0,1,2,3,8,7,6,5,4,9};
//头插尾插都可以实现同样的链表创建
ListNode *head=ListCreateListByHead(n,arr);
ListPrint(head);
ListInsertNode(head,5,11);
ListPrint(head);
ListDeleteNode(head,6);
ListPrint(head);
return 0;
}
输出结果为:
0->1->2->3->8->7->6->5->4->9->NULL 0->1->2->3->8->7->11->6->5->4->9->NULL 0->1->2->3->8->7->6->5->4->9->NULL
链表的清除
链表清除必须传入一个二级指针,因为我要删除的是指针及指向的内存。若不用二级指针,传入一级指针,形参复制的是指针,清除的是复制的一级指针,而使用二级指针则是清楚的二级指针指向的这块内存(指向的是一级指针这个指针所申请的内存)
我们来看看代码:
void ListDestroyList(ListNode **pHead){
ListNode *head=*pHead;//这里解引用解出的是指针
while(head){
head=ListDeleteNode(head,0);//(1)
}
*pHead=NULL;//(2)
}
(
1
)
(1)
(1) 每次循环删除头结点,让头结点指向第二个节点,这时第二个节点即为新的头结点
(
2
)
(2)
(2) 最后这一步其实已经清除链表了,这样做更加的安全,防止内存泄漏
四、我们为什么要使用链表?
- 链表的内存是随机分配的,不存在需要预先分配空间的问题
- 插入时的速度更快,时间复杂度是 O ( 1 ) O(1) O(1) ,而数组还需要另外创建一个空间更大的数组进行插入
- 删除同理,时间复杂度为 O ( 1 ) O(1) O(1)
- 空间复杂度永远为 O ( 1 ) O(1) O(1) ,因为空间仅仅只有头结点这一块内存
五、为什么链表那么多优点,我们不一直使用它?
- 查询时的索引在第几个节点,就需要进行遍历,时间复杂度为 O ( N ) O(N) O(N)
六、接下来我把整个程序的代码放出来功大家参考
#include<iostream>
using namespace std;
typedef int DataType;
struct ListNode{
DataType val;
ListNode* next;
};
ListNode* ListCreateNode(DataType data){
ListNode *node=new ListNode;
node->val=data;
node->next=nullptr;
return node;
}
ListNode* ListCreateListByTail(int n,int nums[]){
ListNode *head,*tail,*vtx;
int index;
if(n<=0){
return NULL;
}
index=0;
vtx=ListCreateNode(nums[0]);
head=tail=vtx;
while(++index<n){
vtx=ListCreateNode(nums[index]);
tail->next=vtx;
tail=vtx;
}
return head;
}
ListNode* ListCreateListByHead(int n,int nums[]){
ListNode *head=nullptr,*vtx;
while(n--){
vtx=ListCreateNode(nums[n]);
vtx->next=head;
head=vtx;
}
return head;
}
void ListPrint(ListNode *head){
ListNode *tmp=head;
while(tmp){
cout<<tmp->val<<"->";
tmp=tmp->next;
}
cout<<"NULL"<<endl;
}
ListNode* ListFindNodeByValue(ListNode* head , DataType target){
ListNode *tmp=head;
while(tmp){
if(tmp->val==target){
return tmp;
}
tmp=tmp->next;
}
return NULL;
}
ListNode* ListFindNode(ListNode *head,int i){
ListNode *tmp=head;
int j=0;
while(tmp&&j<i){
tmp=tmp->next;
++j;
}
if(!tmp||j>i){
return NULL;
}
return tmp;
}
ListNode* ListInsertNode(ListNode *head,int i,DataType n){
ListNode *pre=head,*aft,*vtx;
int j=0;
while(pre&& j++ < i ){
pre=pre->next;
}
if(!pre){
return NULL;
}
vtx=ListCreateNode(n);
aft=pre->next;
vtx->next=aft;
pre->next=vtx;
return vtx;//返回插入的节点
}
ListNode* ListDeleteNode(ListNode *head,int i){
ListNode *pre,*aft,*del;//这里都和之前的一样
int j=0;
if(!head){
return NULL;
}
if(i==0){//如果是头结点则直接按如下操作删除
del=head;
head=head->next;
delete del;
del=NULL;
return head;
}
//如果不是,按如下操作删除
pre=head;
while(pre&&j++<i-1){//依旧是遍历,不过这次是遍历到要删除的前一位
pre=pre->next;
}
if(!pre||!pre->next){//当要删除的节点或者前驱节点为空则不能进行删除,直接返回原链表
return head;
}
del=pre->next;//这一部分同上插入部分
aft=pre->next->next;
pre->next=aft;
delete del;//这里和头结点删除操作相同,记得释放堆区内存
del=NULL;
return head;
}
void ListDestroyList(ListNode **pHead){
ListNode *head=*pHead;//这里解引用解出的是指针
while(head){
head=ListDeleteNode(head,0);
}
*pHead=NULL;
}
int main(){
int n=10;
int arr[n]={0,1,2,3,8,7,6,5,4,9};
//头插尾插都可以实现同样的链表创建
ListNode *head=ListCreateListByHead(n,arr);
return 0;
}
七、结语
用了一天的时间学习了链表的知识,并且希望分享给大家,所以又花费了一天的时间写了这篇文章。
通过这篇文章,我自己实现了一个链表并对其进行管理,增强了自己对链表这种基础数据结构的理解,并提高了对于链表的熟练度
最后,感谢大家的阅读,希望大家能够给我的文章点一个赞,谢谢!
如果对文章有什么问题,可以在评论区提出
学习内容、部分内容参考 :
《画解数据结构》— 英雄哪里出来