[JY的数据结构学习] 手撕基础数据结构 第一练(单向链表)

[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)headtail指针都赋值为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) 最后这一步其实已经清除链表了,这样做更加的安全,防止内存泄漏

四、我们为什么要使用链表?

  1. 链表的内存是随机分配的,不存在需要预先分配空间的问题
  2. 插入时的速度更快,时间复杂度是 O ( 1 ) O(1) O(1) ,而数组还需要另外创建一个空间更大的数组进行插入
  3. 删除同理,时间复杂度为 O ( 1 ) O(1) O(1)
  4. 空间复杂度永远为 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;
}

七、结语

用了一天的时间学习了链表的知识,并且希望分享给大家,所以又花费了一天的时间写了这篇文章。
通过这篇文章,我自己实现了一个链表并对其进行管理,增强了自己对链表这种基础数据结构的理解,并提高了对于链表的熟练度
最后,感谢大家的阅读,希望大家能够给我的文章点一个赞,谢谢!
如果对文章有什么问题,可以在评论区提出

学习内容、部分内容参考 :

《画解数据结构》— 英雄哪里出来

  • 16
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值