请添加图片描述

文章目录

  • 一、链表的概念
  • 二、链表的分类
  • 三、手动实现单链表
    • 1.链表的初始化
    • 2.链表的打印
    • 3.申请新的节点大小空间
    • 4.链表的尾插
    • 5.链表的头插
    • 6.链表的尾删
    • 7.链表的头删
    • 8.链表的查找
      • 9.在指定位置之前插入数据
      • 10.在指定位置之后插入数据
      • 11.删除指定节点
      • 12.删除指定节点之后的数据
      • 13.销毁链表
  • 四、单链表的思考
  • 五.经典链表OJ题
    • [1. 移除链表元素](https://leetcode.cn/problems/remove-linked-list-elements/description/)
    • [2. 反转链表](https://leetcode.cn/problems/reverse-linked-list/)
    • [3. 链表的中间节点](https://leetcode.cn/problems/middle-of-the-linked-list/)
    • [4. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/)
    • [5. 相交链表](https://leetcode.cn/problems/intersection-of-two-linked-lists/description/)
    • [6. 环形链表](https://leetcode.cn/problems/linked-list-cycle/)
    • [7. 环形链表II](https://leetcode.cn/problems/c32eOV/description/)
    • [8. 随机链表的复制](https://leetcode.cn/problems/copy-list-with-random-pointer/)
  • 六、单链表与双链表最全源码
    • 单链表源码
    • 双链表源码
  • 总结


一、链表的概念

       链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。

      上述是书本的定义,链表也是属于线性表,所以在逻辑结构上是线性的,而由于链表的数据与数据之间是通过地址(指针)连接的,所以地址不一定连续,具体要看操作系统分配的地址是否连续。

      下图是链表的形象表示:火车是通过一节一节车厢连接在一起,删除和增加某节车厢不影响其他车厢,即每个车厢都是独立存在,每节车厢是通过钩子连接起来这与链表中每个节点相互独立,却又通过该节点指向下一个节点的指针连接起来有异曲同工之处。
在这里插入图片描述

       链表是由一个一个节点组成,节点由要存储的数据+下一个节点地址的指针,如图在第一个节点中保存的数据是1,还有下一个节点的地址:0x0012FFB0,也就是第二个节点的地址。依次往后看,直到第四个节点保存的数据是4,由于它的后面没有节点了,所以它保存的下一个节点的地址是NULL。
在这里插入图片描述

二、链表的分类

链表的结构非常多,以下情况组合起来共有8种(2x2x2)链表结构:

在这里插入图片描述

1.单向或者双向

在双向链表中,存放了两个指针:next指向下一个节点(后继节点),prev指向前一个节点(前驱节点)
在这里插入图片描述
2.带头或者不带头

在这里插入图片描述
注意:在上课或者参考书上为了方便理解单链表会把链表的首节点称为头结点,但是这样的称呼是错误的,因为链表中存在一类链表叫做带头链表(不是指链表里的第一个有效的节点),这里的头结点即哨兵位(不保存任何有效数据,仅用来占位置,作用是不需要判断链表的头是否为空),如带头链表中只有头结点,那么我们就称该节点为空链表

3.循环或者不循环

在循环链表中尾节点的指向不为空,它指向了链表中第一个有效的节点

在这里插入图片描述
因此在前面我们实现的单链表的全称为:不带头单向不循环链表。链表有上面8种结构,但我们实际中用的最多的是单链表和双向带头循环链表!

三、手动实现单链表

1.链表的初始化

    节点里存储两个变量,这里数据存储使用typedef将全部int改名成SLTDataType,方便以后修改,由于保存的下一个节点的地址也是节点类型,所以使用SListNode*类型,最后也把struct SListNode改名成SLTNode,方便书写。

//定义节点的结构
typedef int SLTDataType;
typedef struct SListNode
{SLTDataType data;struct SListNode* next;}SLTNode;

2.链表的打印

    在实现链表的打印功能前需要手动构造一个链表,如下

SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));node1->data = 1;SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));node2->data = 2;SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));node3->data = 3;SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));node4->data = 4;node1->next = node2;node2->next = node3;node3->next = node4;node4->next = NULL;

以下是链表的打印,具体实现如下:
    1.定义链表结构体指针指向phead
    2.定义pcur等于phead即等于node1
    3.若pcur不为NULL,则打印当前节点的data(存储的数据) ,并让pcur继续往下走
    4.pcur走到为空,跳出while循环,打印NULL
    5.最后打印结果为:1->2->3->4->NULL

注意:在这里直接使用phead来遍历也是一样的,重新创建pcur指针是为了避免由于指针指向的改变,导致无法重新找到链表的首节点

void SLTPrint(SLTNode* phead)
{//pcur存放的是当前节点的地址SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}

3.申请新的节点大小空间

注意:这里要判断申请空间是否成功,如果失败退出,申请成功对data和next进行初始化

SLTNode* SLTBuyNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail!\n");exit(1);}newnode->data = x;newnode->next = NULL;return newnode;
}

4.链表的尾插

    向操作系统动态申请一个节点大小的空间 然后断言若传来的是NULL,那么不能解引用,所以需要断言pphead不能为空,但是*pphead可以为空,因为它代表的是空链表,所以可以存在。这里尾插分两种情况

    链表为空:即pphead为空,则让pphead等于newnode(为首节点)即1   

    链表不为空:即pphead不为空,具体步骤如下

     1.定义一个尾节点ptail,初始时等于首节点pphead
    2.遍历链表,结束条件为不能等于尾节点,注意这里结束条件是ptail->next
    3.若为尾节点,则让尾节点4的next指针指向新节点99

链表为空如下:
在这里插入图片描述
链表非空如下:
在这里插入图片描述

    这里我们发现形参phead的改变没有影响到实参plist的改变(在传值的时候,形参的改变不影响实参 传地址:形参的改变影响实参 ),所以plist应该传地址而不是值,应为&plist,而plist是指针,所以phead要传二级指针来接受一级指针的地址,所以为**pphead。在if语句中药对pphead解引用(*pphead=plist)为一级指针,后面也是同样的意思。
在这里插入图片描述
在这里插入图片描述

  这里解释下一级指针和二级指针的概念
第一个节点 (解引用) *plist<---------->**phead(解引用两次),左边例如plist是实参,右边是形参
指向第一个节点的指针 plist<--------->*phead(解引用一次后变成一级指针)
指向第一个节点的指针的地址 (取地址) &plist<----------->pphead(二级指针)
在这里插入图片描述

//尾差
void SLTPushBack(SLTNode**pphead, SLTDataType x)
{//如果传来的是NULL,那么不能解引用,所以需要断言pphead不能为空,但是*pphead可以为空,因为它代表的是空链表,所以可以存在assert(pphead);//*pphead就是指向第一个节点的指针//空链表和非空链表两种情况SLTNode* newnode = SLTBuyNode(x);if (*pphead == NULL){*pphead = newnode;}else{SLTNode* ptail = *pphead;//如果ptail为空,那么在循环里面不可以对空指针解引用,所以会报错while (ptail->next){ptail = ptail->next;}//ptail指向的就是尾节点ptail->next = newnode;}
}

5.链表的头插

    1.调用创建新节点的方法
    2.让新节点9的next指针指向原来的头结点1(*pphead)
    3.让新节点(newnode)为新的头结点
在这里插入图片描述

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{//空链表和非空链表都可以运行assert(pphead);SLTNode* newnode = SLTBuyNode(x);newnode->next = *pphead;*pphead = newnode;
}

6.链表的尾删

    代码思路如下:

    1.断言

    2.判断链表中有一个节点还是多个节点

    3.若只有一个节点(pphead->next==NULL),则直接释放,并把*pphead置为NULL

    4.若有多个节点则定义prev和ptail指针指向头结点,然后遍历链表直至到尾节点,然后跳出循环,释放尾节点ptail并置为空,为了不让尾节点前一个节点的next指针为野指针,所以让prev为前一个节点并让其next指向空

注意这里断言时:pphead(一级指针的地址)不能为空,否则不能对二级指针解引用,其次不能让链表为空,总不能删空吧,这不可能的,所以和头插尾插不同的是添加了个(pphead)不能为空

链表中只有一个节点:
在这里插入图片描述
链表中多个节点:

5455

//尾删
void SLTPopBack(SLTNode** pphead )
{//这里和头插尾差不一样,因为尾删的话,链表不能为空,总不能删空吧,这不可能的,所以和头插尾插不同的是添加了个*pphead不能为空assert(pphead && *pphead);//链表只有一个节点if ((*pphead)->next == NULL)//这里给*pphead加了个括号,因为有符号的优先级,->的优先级高于*号{free(*pphead);*pphead = NULL;}else{	//链表有多个节点SLTNode* prev = *pphead;SLTNode* ptail = *pphead;while (ptail->next){prev = ptail;ptail = ptail->next;}free(ptail);ptail = NULL;prev->next = NULL;}}

7.链表的头删

1.定义next变量指向头结点的下一个节点
2.释放头结点
2.让next节点变为新的头结点
注意:这里不能直接删除pphead,若删除找不到后面的节点2,所以需要定义next保存下一个节点2
在这里插入图片描述

//头删
void SLTPopFront(SLTNode** pphead)
{//这里和尾删是一样的,解释在上面assert(pphead && *pphead);//在这里不能直接删除第一个节点,这样就不能根据原本第一个节点的next指针找到原本第二个节点//在头删方法中,只有一个节点和多个节点如下方法都可以实现SLTNode* next = (*pphead)->next;//这里也是->的优先级大于*号free(*pphead);*pphead = next;}

8.链表的查找

代码思路如下:

1.定义一个变量pcur指向头结点

2.循环遍历,若(if里的语句)pcur存储的data等于要查找的x,则返回该节点

3.否则继续往后遍历链表

4.若循环遍历结束,没有数据等于x,则该数据在链表中不存在

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* pcur = phead;while (pcur){if (pcur->data == x){return pcur;}else{pcur = pcur->next;}}return NULL;
}

9.在指定位置之前插入数据

代码思路如下:
1.前置条件的断言,如pphead和pos不能为空

2.若指定位置pos为头结点,那么就等于是头插,直接调用头插函数即可(特殊位置)

3.若pos不是头结点,定义prev指向头结点,遍历循环,循环条件为prev的next不等于pos

4.prev->next为pos跳出循环,将新节点的next指向pos,prev的next指向新节点

//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead && pos);//当pos为首节点的时候,会出现对空指针解引用的操作导致下面代码报错//若pos直接等于*pphead的时候则为头插,那么直接调用头插函数if (pos == *pphead){SLTPushFront(pphead, x);}else{SLTNode* newnode = SLTBuyNode(x);SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}newnode->next = pos;prev->next = newnode;}}

10.在指定位置之后插入数据

      注意:1. 这里(newnode->next = pos->next; pos->next = newnode)顺序不能反过来,否则自己指向自己了

                 2.这里只需pos和插入数据两个变量,与在指定位置之前插入数据不一样,不需要遍历找下一个节点,因为pos的next指针存储的就是下一个节点,而在指定位置之前插入数据,pos没有保存前一个节点,需要用pphead遍历链表来寻找

在这里插入图片描述

//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = SLTBuyNode(x);//pos newnode -> (pos->next)newnode->next = pos->next;pos->next = newnode;
}

11.删除指定节点

代码思路:
1.断言pphead不为空且pos不为空,否则怎么删除pos节点
2.这里要判断特殊条件,若pos是头结点则直接调用已写好的头删方法
3.否则循环遍历链表,若prev的next指向pos,则退出循环
4.让prev的next指向pos的下一个节点后再释放pos,并把pos置为空
/9/

//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && pos);//要删除的节点就是头结点if (pos == *pphead){SLTPopFront(pphead);}else {SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}//prev pos pos->nextprev->next = pos->next;free(pos);pos = NULL;}
}

12.删除指定节点之后的数据

在下图我们要删除2节点(pos->next)

1.需要将1节点(pos)的next(pos->next)指向3(pos->next->next)

2.释放2节点(pos->next)
在上面操作中我们发现最后的pos->next指向3节点,也就是说最后释放的不是2而是3节点,所以我们需要定义del变量来存放pos->next

1.del赋值为pos->next,也就是2节点

2.让1节点(pos)的下一个节点为3(del->next)

3.释放节点2(del)并置为空

注意:这里断言不能让pos指向空,也不能为尾节点,因为尾节点之后没有数据,无法删除
+

//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos)
{assert(pos && pos->next);SLTNode* del = pos->next;//pos del del->nextpos->next = del->next;free(del);del = NULL;
}

13.销毁链表

 不能直接释放链表,应该一个一个释放链表里面的节点,这里不能直接释放节点,应该定义pcur保存该节点,并且还要保存下一个节点的地址,否则要销毁下一个节点的时候找不到该节点地址,变成野指针

代码思路如下:
1.定义pcur指向头结点
2.循环遍历链表,定义next并让他保存要销毁节点的下一个节点
2.释放要销毁的节点,并让链表继续往后遍历( pcur = next)
3.链表中所有节点都删除跳出循环,还需让*pphead置为空,否则它为野指针

在这里插入图片描述

//销毁链表
void SListDestroy(SLTNode** pphead)
{SLTNode* pcur = *pphead;while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}*pphead = NULL;
}

四、单链表的思考

在上一篇博客里面讲解了顺序表的一些问题,链表相对于顺序表而言有以下几个优势:

1.链表头部插入删除时间复杂度为0(1)
2.链表无需增容
3.链表不存在空间浪费

五.经典链表OJ题

1. 移除链表元素

在这里插入图片描述

题目解释:
这道题题目很容易读懂,和上述我实现链表方法中的删除指定位置的节点意思类似,这里只需要给头结点就可以知道整个链表

思路 1: 遍历链表找到值为val的结点,执行删除指定pos位置结点的操作,时间复杂度为O(n^2)

思路2:创建新的链表,遍历原链表,将链表中值不为val的节点尾插到新链表中
        1.遍历原链表,结束条件为pcur为空
        2.若pcur存储的值不为val尾插到新链表中
        这里尾插分两种情况:
新链表为空:pcur既是新链表的头结点也是尾节点
链表不为空:尾节点的next指向pcur,再让ptail从原来的尾节点走到pcur变成新的尾节点
        3.让pcur往后走
        4.走到空,遍历完链表跳出循环,然后一定要判断newtail不为空(若为空则不能解引用它的next,其中newtail为空时候原链表为空,如图1),让newtail的next指向空,因为newtail这个节点可能存储原链表的下一个节点(图2,图3),在图中5为尾节点,但是5保存了6的地址,所以让5的next置为空
图1:在这里插入图片描述
图2:
在这里插入图片描述
图3:
在这里插入图片描述

注意:这里不能使用双指针,因为双指针法只是修改节点里面的值却没有删除节点

代码实现:

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/typedef struct ListNode  ListNode;struct ListNode* removeElements(struct ListNode* head, int val) {//创建一个空链表ListNode *newhead,*newtail;newhead=newtail=NULL;//遍历原链表ListNode*pcur=head;while(pcur){//找值不为val的结点尾插到新链表中if(pcur->val!=val){if(newhead==NULL){//链表为空newhead=newtail=pcur;}else{//链表不为空newtail->next=pcur;newtail=newtail->next;}}//遍历原链表pcur=pcur->next; }if(newtail){newtail->next=NULL;}return newhead;
}

2. 反转链表

在这里插入图片描述
题目解释: 这里只需将反转链表后的头结点给返回即可·,题目意思很容易理解

思路1: 创建新链表,遍历旧链表中的节点拿过来头插

思路2: 创建3个指针,n1 初始指向空,n2指向头节点,n3指向头节点下一个节点,只要n2的节点不为空,就让n2的next指针不指向n3,改成指向n1。然后让n1走到n2的位置,n2走到n3的位置,n3走到下一个节点的位置。一直重复直到n2为空结束该过程,n1指向的节点即为头结点

注意: 这里有特殊情况,若链表为空直接返回头结点即可,因为n2为头结点,n2为空,那么n2->next是错误的,不能对空指针进行解引用

初始情况:
在这里插入图片描述
结束情况:
在这里插入图片描述

代码实现:

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/typedef struct ListNode ListNode;
struct ListNode* reverseList(struct ListNode* head) {if(head==NULL){return head;}ListNode*n1,*n2,*n3;n1=NULL,n2=head,n3=n2->next;while(n2){n2->next=n1;n1=n2;n2=n3;if(n3){n3=n3->next;}}return n1;
}

3. 链表的中间节点

在这里插入图片描述

题目解释: 若链表里的节点是奇数个数,则直接返回该节点,如果节点是偶数个,那么返回第二个中间节点即可,这里题目描述很简单,但要注意节点是奇数个还是偶数个这两种情况

思路 1: 求链表结点总数,除2求中间位置,返回中间位置结点O(n)。在图1中,共有5个节点,5/2=2,定义pcur从0开始遍历,到3这个节点正好是下标为2,即3是中间节点

思路 2: 快慢指针,慢指针每次走一步,快指针每次走两步 即 2*慢指针=快指针 。
在奇数节点个数中,若fast->next=NULL,则不能往下走,slow指向的节点3的位置就是中间节点
在偶数节点个数中,fast=NULL则不能往下走,slow指向的节点4为中间节点

这里判断节点是偶数个(fast=NULL)或者奇数个(fast->next=NULL)的时候,fast和fast->next顺序不能相反,具体解释在代码中注释有
在这里插入图片描述
在这里插入图片描述

注意:这里不能使用双指针向中间遍历,phead可以向右遍历,因为存放next指针指向下一个节点,但是ptail不能往左走,因为没有存放前一个节点

图1:
在这里插入图片描述

代码实现:

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/typedef struct ListNode ListNode;
struct ListNode* middleNode(struct ListNode* head) {ListNode*slow=head;ListNode*fast=head;while(fast&&fast->next){//fast和fast->next位置不能换,如果先fast-> next,fast都已经为空了,不能对空解引用//代码会报错,只要第一个为假,就不会判断fast->next为真为假slow=slow->next;fast=fast->next->next;}return slow;
}

4. 合并两个有序链表

在这里插入图片描述
题目解释: 题目给我们两个排好的升序链表,将他们连在一起合并成一个新的升序链表

思路: 1. 定义l1指向第一个链表,l2指向第二个链表,定义一个新链表
              2.循环遍历链表1和2,谁的值小放入新链表中,结束条件为:l1和l2其中一个走到为空就退出循环
              3.l1小则将l1数据插入到新链表中,若新链表是为空,则插入进来的l1既事头结点也是尾节点,若链表不为空,则让为节点的下一个节点指向l1,newtail走到l1位置,同理l2也是如此
              4. 跳出循环,只有两种情况,l1走到空,l2走到空,不可能有l1和l2均为空的情况,因为若l1和12存放的数据相等时,我们随机让l1或者l2走往后走,走到为空。
              5.若l1不为空,则让newtail的next指向l1,同理l2也是如此

以上思路看起来是不是理所当然,但是当我们调试代码的时候报错了,下面的报错是不是很熟悉呀,我们根据他给的示例带入我们上述思路跑一下,l1为空无法进入循环,直接跳到思路5中,新链表为空,其尾节点为空,不能对空进行解引用,我们的代码却让新节点的newtail的next指向l2,所以报错。
在这里插入图片描述

我们根据上面问题发现是没有对链表为空进行处理,所以我们在思路1后面直接进行判断,若l1为空,直接返回l2,同理l2也是如此。

在下面写代码时候我们发现思路中的步骤3判断链表是否为空,代码重复度过高,我们可以创建非空链表(malloc节点大小空间,别忘记最后要释放哦),则不需要进行判空号,直接尾插,如下图
在这里插入图片描述

代码实现:

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/typedef  struct ListNode ListNode;
struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) {ListNode*l1=list1;ListNode*l2=list2;if(list1==NULL){return list2;}if(list2==NULL){return list1;}//新链表ListNode*newhead,*newtail;newhead=newtail=NULL;while(l1&&l2){if(l1->val<l2->val){//l1插入到新链表中if(newhead==NULL){//链表为空newhead=newtail=l1;}else{newtail->next=l1;newtail=newtail->next;}l1=l1->next;}else{//l2插入到新链表中if(newhead==NULL){newhead=newtail=l2;}else{newtail->next=l2;newtail=newtail->next;}l2=l2->next;}}//跳出循环,只有两种情况,l1走到空,l2走到空//有没有可能l1l2同时为空?不可能if(l1){newtail->next=l1;}if(l2){newtail->next=l2;}return newhead;
}

5. 相交链表

在这里插入图片描述
题目解释: 如果两个链表相交则是相交链表,这里解释下啥是相交链表,若l1的某个节点和l2的某个节点相同,即是相交节点,有三种情况 l1和l2的头结点就相同,l1和l2中间有节点相同,l2和l3的尾节点相同。

注意大家可能疑惑有没有l1和l2链表相交成“x”字型,答案是否定的,若交叉点为n1,n1的next节点只能存放一个地址,而不是多个,若两个链表相交,则从交叉点开始后面的节点均相同

思路: 这里需要解决两个问题,1.如何判断链表是否相交 2.找相交的起始节点

试错: 一个个比较链表节点的next是否相等,这个方法仅限相交之前的节点个数相等(长度相同),所以该方法不行

问题1:看尾节点的地址是否相同

问题2:找长度差,也就是找大小链表(求出两个链表的长度差),大链表先走长度差次,然后再一起比较两个链表的next是否相同

代码实现:

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/typedef struct ListNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) {//求出两个链表的长度ListNode*pa=headA,*pb=headB;int sizeA=0,sizeB=0;while(pa){++sizeA;pa=pa->next;}while(pb){++sizeB;pb=pb->next;}int gap=abs(sizeA-sizeB);//abs:求绝对值的函数//让长链表先走gap步ListNode*shortlist=headA;ListNode*longlist=headB;if(sizeA>sizeB){longlist=headA;shortlist=headB;}while(gap--){longlist=longlist->next;}//shortlist  longlist在同一起跑线while(shortlist){if(shortlist==longlist){return shortlist;}shortlist=shortlist->next;longlist=longlist->next;}return NULL;
}

6. 环形链表

在这里插入图片描述
题目解释: 链表带环如上图一样,它具有尾节点的next指向不为空的性质,若链表带环则返回true否则返回false

思路: 快慢指针( 慢指针每次走一步,快指针每次走两步):在环内追逐,若链表带环,快慢指针会相遇

**证明:**这里大家如果感兴趣可以自己证明或者在网上搜下为啥在环形链表中使用快慢指针,最终两指针会相遇!
代码实现:

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/typedef struct ListNode ListNode;
bool hasCycle(struct ListNode *head) {ListNode*slow=head,*fast=head;while(fast&&fast->next){slow=slow->next;fast =fast->next->next;if(slow==fast){//相遇return true;}}return false;
}

7. 环形链表II

在这里插入图片描述
题目解释: 这与上题类似,不过多解释

思路: 这里还是快慢指针,但是要注意的是这里有个结论:相遇点到入环起始结点的距离等于链表头结点到入环起始结点的距离

这里大家也可以自己证明下该结论如何成立的

代码实现:

/*** Definition for singly-linked list.* struct ListNode {*     int val;*     struct ListNode *next;* };*/
typedef struct ListNode ListNode;
struct ListNode *detectCycle(struct ListNode *head) {ListNode*slow=head,*fast=head;while(fast&&fast->next){slow=slow->next;fast=fast->next->next;if(fast==slow){//相遇点ListNode*pcur=head;while(slow!=pcur ){slow=slow->next;pcur=pcur->next;}return pcur;}}return NULL;
}

8. 随机链表的复制

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

题目解释: 这道题提到我们要对这个链表进行深拷贝,这里我们要拷贝原链表中所有的数据,拷贝到一个全新的链表,即复制的链表中的指针不能指向原链表,藕断丝连。(该链表与普通节点不同的是额外添加了指针,该指针可以指向链表中的任何节点或空节点)

深拷贝:定义pcur指针指向当前节点,再来一个指针pcur2指向另外一个节点,但是该节点存储的值和前面一个节点存储的值相同,但是节点的地址互不相同(如图 1),简单理解就是把链表从一个地方拿到别的地方,链表里面存放到值完全一样,就是链表的位置即地址不一样而已

浅拷贝:定义pcur指针指向当前节点,再来一个指针pcur2也指向该节点,它们使用的是同一块地址即浅拷贝(如图2)

图 1:
在这里插入图片描述
图 2:
在这里插入图片描述

代码思路: 这里大家可能想到把链表中除了random指针其他全部复制过来,然后再遍历原链表设置新链表的random指针,这个想法是正确的,但是我们发现置random指针很麻烦,无法处理

思路2:

  1. 拷贝节点:在原链表上申请新的节点pcur,让它遍历原链表并不断创建和它当前位置节点一模一样的节点,然后让创建的这个节点尾插到该位置的后面(这里暂时只需改变next指向即可),依次重复该操作直至pcur走到空为止,链表拷贝完成

在这里插入图片描述2.置random指针:在复制链表中定义copy指针和pcur一起遍历新旧链表,让copy->random=pcur->random->next
在这里插入图片描述
3.断开新旧链表:pcur和copy同时遍历新旧链表,让copy->next不再指向pcur,而是指向pcur->next,依次重复执行直至copy走到空,那么新旧链表已断开

注意:若链表为空需要特殊处理,直接返回即可

代码实现:

/*** Definition for a Node.* struct Node {*     int val;*     struct Node *next;*     struct Node *random;* };*/
typedef struct Node Node;
//创建新节点
Node*buyNode(int x)
{Node*node=(Node*)malloc(sizeof(Node));node->val=x;node->next=node->random=NULL;return node;
}
void  AddNode(Node*head)
{Node*pcur=head;while(pcur){Node*next=pcur->next;//创建新节点,插入到pcur结点之后Node*newnode=buyNode(pcur->val);newnode->next=pcur->next;pcur->next=newnode;pcur=next;}
}void setRandom(Node*head)
{Node*pcur=head;while(pcur){Node*copy=pcur->next;if(pcur->random){copy->random=pcur->random->next;}pcur=copy->next;}
}
struct Node* copyRandomList(struct Node* head) {if(head==NULL){return head;}//1.  拷贝结点 AddNode(head); //2.   置random指针setRandom(head);//3  断开链表Node*pcur=head;Node*newHead,*newTail;newHead=newTail=pcur->next;while(newTail->next){pcur=newTail->next;newTail->next=pcur->next;newTail=newTail->next;}return newHead;
}

代码如下(示例):

data = pd.read_csv('https://labfile.oss.aliyuncs.com/courses/1283/adult.data.csv')
print(data.head())

该处使用的url网络请求的数据。


六、单链表与双链表最全源码

单链表源码

SList.h源码

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>//定义节点的结构
typedef int SLTDataType;
typedef struct SListNode
{SLTDataType data;struct SListNode* next;}SLTNode;void SLTPrint(SLTNode* phead);
//尾差
void SLTPushBack(SLTNode**pphead, SLTDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);//查找
SLTNode* SLTFind(SLTNode* pphead, SLTDataType x);//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead,SLTNode*pos ,SLTDataType x);//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);//顺序表的销毁
void SListDestroy(SLTNode**pphead);

SList.c源码

#include"SList.h"void SLTPrint(SLTNode* phead)
{//pcur存放的是当前节点的地址SLTNode* pcur = phead;while (pcur){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}SLTNode* SLTBuyNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail!\n");exit(1);}newnode->data = x;newnode->next = NULL;return newnode;
}//尾差
void SLTPushBack(SLTNode**pphead, SLTDataType x)
{//如果传来的是NULL,那么不能解引用,所以需要断言pphead不能为空,但是*pphead可以为空,因为它代表的是空链表,所以可以存在assert(pphead);//*pphead就是指向第一个节点的指针//空链表和非空链表两种情况SLTNode* newnode = SLTBuyNode(x);if (*pphead == NULL){*pphead = newnode;}else{SLTNode* ptail = *pphead;//如果ptail为空,那么在循环里面不可以对空指针解引用,所以会报错while (ptail->next){ptail = ptail->next;}//ptail指向的就是尾节点ptail->next = newnode;}
}//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{//空链表和非空链表都可以运行assert(pphead);SLTNode* newnode = SLTBuyNode(x);newnode->next = *pphead;*pphead = newnode;}//尾删
void SLTPopBack(SLTNode** pphead )
{//这里和头插尾差不一样,因为尾删的话,链表不能为空,总不能删空吧,这不可能的,所以和头插尾插不同的是添加了个*pphead不能为空assert(pphead && *pphead);//链表只有一个节点if ((*pphead)->next == NULL)//这里给*pphead加了个括号,因为有符号的优先级,->的优先级高于*号{free(*pphead);*pphead = NULL;}else{	//链表有多个节点SLTNode* prev = *pphead;SLTNode* ptail = *pphead;while (ptail->next){prev = ptail;ptail = ptail->next;}free(ptail);ptail = NULL;prev->next = NULL;}}//头删
void SLTPopFront(SLTNode** pphead)
{//这里和尾删是一样的,解释在上面assert(pphead && *pphead);//在这里不能直接删除第一个节点,这样就不能根据原本第一个节点的next指针找到原本第二个节点//在头删方法中,只有一个节点和多个节点如下方法都可以实现SLTNode* next = (*pphead)->next;//这里也是->的优先级大于*号free(*pphead);*pphead = next;}//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* pcur = phead;while (pcur){if (pcur->data == x){return pcur;}else{pcur = pcur->next;}}return NULL;
}//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead && pos);//当pos为首节点的时候,会出现对空指针解引用的操作导致下面代码报错//若pos直接等于*pphead的时候则为头插,那么直接调用头插函数if (pos == *pphead){SLTPushFront(pphead, x);}else{assert(pphead && *pphead);assert(pos);SLTNode* newnode = SLTBuyNode(x);SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}newnode->next = pos;prev->next = newnode;}}//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)//为啥没有头结点???
{assert(pos);SLTNode* newnode = SLTBuyNode(x);newnode->next = pos->next;pos->next = newnode;}//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead && *pphead);assert(pos);//pos是头结点,那么可以直接调用头删代码if (pos == *pphead){SLTPopFront(pphead);//测试过只能是pphead不能是*pphead}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}}//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{assert(pos&&pos->next);//这里注意pos的next也不能为空,不然怎么删除SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;//注意不能这样写:pos->next=pos->next->next//free(pos)//pos->next=NULL;因为这样写就是把pos后的节点的下一个节点也释放掉了}//顺序表的销毁
void SListDestroy(SLTNode**pphead)
{assert(pphead && *pphead);SLTNode* pcur = *pphead;while (pcur){SLTNode* next = pcur->next;free(pcur);pcur = next;}//pcur为空*pphead = NULL;}

test.c

#include"SList.h"void test()
{//手动构造一个链表SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));node1->data = 1;node2->data = 2;node3->data = 3;node4->data = 4;node1->next = node2;node2->next = node3;node3->next = node4;node4->next = NULL;SLTNode* plist = node1;SLTPrint(plist);
}void test01()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPrint(plist);//SLTPushFront(&plist, 1);//SLTPushFront(&plist, 2);//SLTPushFront(&plist, 3);//SLTPushFront(&plist, 4);//SLTPrint(plist);//SLTPopBack(&plist);//SLTPrint(plist);//SLTPopBack(&plist);//SLTPrint(plist);//SLTPopBack(&plist);//SLTPrint(plist);//SLTPopBack(&plist);//SLTPrint(plist);////SLTPopFront(&plist);//SLTPrint(plist);//SLTPopFront(&plist);//SLTPrint(plist);//SLTPopFront(&plist);//SLTPrint(plist);//SLTPopFront(&plist);//SLTPrint(plist);//SLTPopFront(&plist);//SLTPrint(plist);SLTNode* find = SLTFind(plist, 4);//if (find == NULL)//{//	printf("未找到!\n");//}//else//{//	printf("找到了!\n");//}//SLTInsert(&plist, find, 88);//1 88 2 3 4//SLTInsertAfter(find, 88);//1 2 88 3 4//SLTErase(&plist, find);//SLTEraseAfter(find);//1 3 4SListDestroy(&plist);SLTPrint(plist);
}struct ListNode {int val;//datastruct ListNode* next;
};
typedef struct ListNode ListNode;
struct ListNode* removeElements(struct ListNode* head, int val) {ListNode* newHead, * newTail;newHead = newTail = NULL;ListNode* pcur = head;while (pcur){//找值不为val的结点尾插到新链表中if (pcur->val != val){//尾插if (newHead == NULL){//链表为空newHead = newTail = pcur;}else {//链表不为空newTail->next = pcur;newTail = newTail->next;}}pcur = pcur->next;}return newHead;
}void test02()
{ListNode* node1 = (ListNode*)malloc(sizeof(ListNode));ListNode* node2 = (ListNode*)malloc(sizeof(ListNode));ListNode* node3 = (ListNode*)malloc(sizeof(ListNode));ListNode* node4 = (ListNode*)malloc(sizeof(ListNode));ListNode* node5 = (ListNode*)malloc(sizeof(ListNode));ListNode* node6 = (ListNode*)malloc(sizeof(ListNode));ListNode* node7 = (ListNode*)malloc(sizeof(ListNode));node1->val = 1;node2->val = 2;node3->val = 6;node4->val = 3;node5->val = 4;node6->val = 5;node7->val = 6;node1->next = node2;node2->next = node3;node3->next = node4;node4->next = node5;node5->next = node6;node6->next = node7;node7->next = NULL;ListNode* plist = node1;removeElements(plist, 6);
}int main()
{//test();//test01();test02();return 0;
}

双链表源码

List.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>typedef int LTDatatype;
//定义双向链表(结点)结构
typedef struct ListNode {LTDatatype data;struct ListNode* next;struct ListNode* prev;
}LTNode;//初始化
LTNode* LTInit();
//void LTDesTroy(LTNode** pphead);
//接口一致性,传一级,在函数调用结束后需要手动将实参置为NULL
void LTDesTroy(LTNode* phead);
//void LTInit(LTNode** pphead);void LTPrint(LTNode* phead);
bool LTEmpty(LTNode* phead);//尾插
//phead节点不会发生改变,参数就为一级
//phead节点发生改变,参数就为二级
void LTPushBack(LTNode* phead, LTDatatype x);
void LTPushFront(LTNode* phead, LTDatatype x);//尾删
void LTPopBack(LTNode* phead);
void LTPopFront(LTNode* phead);//在pos位置之后插⼊数据----pos位置之前插入代码基本是一样的(双向的)--课下可以实现以下在pos之前插入
void LTInsert(LTNode* pos, LTDatatype x);
void LTErase(LTNode* pos);LTNode* LTFind(LTNode* phead, LTDatatype x);

List.c

#include"List.h"LTNode* buyNode(LTDatatype x)
{LTNode* node = (LTNode*)malloc(sizeof(LTNode));node->data = x;node->next = node->prev = node;return node;
}LTNode* LTInit()
{//创建头结点//LTNode* phead = (LTNode*)malloc(sizeof(LTNode));//phead->data = -1;//phead->next = phead->prev = phead;LTNode* phead = buyNode(-1);return phead;
}
//void LTInit(LTNode** pphead)
//{
//	assert(pphead);
//	*pphead = (LTNode*)malloc(sizeof(LTNode));
//	(*pphead)->data = -1;
//	(*pphead)->next = (*pphead)->prev = *pphead;
//}//void LTDesTroy(LTNode** pphead)
//{
//	LTNode* pcur = (*pphead)->next;
//	while (pcur != *pphead)
//	{
//		LTNode* next = pcur->next;
//		free(pcur);
//		pcur = next;
//	}
//	free(*pphead);
//	*pphead = NULL;
//}
void LTDesTroy(LTNode* phead)
{LTNode* pcur = phead->next;while (pcur != phead){LTNode* next = pcur->next;free(pcur);pcur = next;}free(phead);phead = NULL;
}void LTPushBack(LTNode* phead, LTDatatype x)
{assert(phead);LTNode* newnode = buyNode(x);//phead phead->prev newnodenewnode->prev = phead->prev;newnode->next = phead;phead->prev->next = newnode;phead->prev = newnode;
}
void LTPushFront(LTNode* phead, LTDatatype x)
{assert(phead);LTNode* newnode = buyNode(x);//phead newnode phead->nextnewnode->next = phead->next;newnode->prev = phead;phead->next->prev = newnode;phead->next = newnode;
}void LTPrint(LTNode* phead)
{LTNode* pcur = phead->next;while (pcur != phead){printf("%d -> ", pcur->data);pcur = pcur->next;}printf("\n");
}bool LTEmpty(LTNode* phead)
{assert(phead);return phead->next == phead;
}//尾删
void LTPopBack(LTNode* phead)
{assert(!LTEmpty(phead));LTNode* del = phead->prev;//phead del->prev deldel->prev->next = phead;phead->prev = del->prev;free(del);del = NULL;
}
void LTPopFront(LTNode* phead)
{assert(!LTEmpty(phead));LTNode* del = phead->next;//phead del del->nextdel->next->prev = phead;phead->next = del->next;free(del);del = NULL;
}LTNode* LTFind(LTNode* phead, LTDatatype x)
{LTNode* pcur = phead->next;while (pcur != phead){if (pcur->data == x){return pcur;}pcur = pcur->next;}return NULL;
}
//在pos位置之后插⼊数据
void LTInsert(LTNode* pos, LTDatatype x)
{assert(pos);LTNode* newnode = buyNode(x);//pos newnode pos->nextnewnode->next = pos->next;newnode->prev = pos;pos->next->prev = newnode;pos->next = newnode;
}
void LTErase(LTNode* pos)
{assert(pos);//pos->prev pos pos->nextpos->prev->next = pos->next;pos->next->prev = pos->prev;free(pos);pos = NULL;
}

test.c

#include"List.h"void test()
{//LTNode* plist = NULL;//LTInit(&plist);LTNode* plist = LTInit();LTPushBack(plist, 1);LTPushBack(plist, 2);LTPushBack(plist, 3);LTPushBack(plist, 4);//LTPushFront(plist, 1);//LTPushFront(plist, 2);//LTPushFront(plist, 3);//LTPushFront(plist, 4);LTPrint(plist);//4 3 2 1////LTPopBack(plist);//LTPrint(plist);//LTPopBack(plist);//LTPrint(plist);//LTPopBack(plist);//LTPrint(plist);//LTPopBack(plist);//LTPrint(plist);////LTPopFront(plist);//LTPrint(plist);//LTPopFront(plist);//LTPrint(plist);//LTPopFront(plist);//LTPrint(plist);//LTPopFront(plist);//LTPrint(plist);//LTPopFront(plist);//LTPrint(plist);//LTNode* find = LTFind(plist, 3);//if (find == NULL)//{//	printf("ûҵ\n");//}//else {//	printf("ҵˣ\n");//}//LTInsert(find, 99);//LTErase(find);//LTPrint(plist);LTDesTroy(plist);plist = NULL;
}int main()
{test();return 0;
}

总结

链表作为数据结构中的重要线性结构,其灵活的存储方式和高效的插入删除操作使其在众多场景中不可或缺。本文从链表的基本概念入手,详细解析了链表的分类方式,通过手动实现单链表的各项核心操作,深入剖析了指针操作的细节与边界条件的处理,尤其对二级指针的使用场景和原理进行了重点说明。

在此基础上,通过对比顺序表,凸显了链表在头部操作、空间利用等方面的优势,并结合 8 道经典 OJ 题,展示了链表在实际问题中的应用技巧,如快慢指针、新链表构建、环形链表特性利用等。最后提供的单链表和双链表完整源码,为实践练习提供了直接参考。

掌握链表不仅是理解更复杂数据结构(如哈希表、树、图)的基础,其指针操作逻辑也能显著提升对内存管理和程序设计的理解。希望本文能帮助读者扎实掌握链表知识,为后续数据结构学习和算法提升奠定坚实基础。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/web/95205.shtml
繁体地址,请注明出处:http://hk.pswp.cn/web/95205.shtml
英文地址,请注明出处:http://en.pswp.cn/web/95205.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Spring 事件驱动编程初探:用 @EventListener 轻松处理业务通知

一、核心概念与模型Spring 的事件机制是观察者模式&#xff08;也叫发布-订阅模型&#xff09;的一种典型实现。它主要由三个核心部分组成&#xff1a;事件 (Event)&#xff1a; 承载信息的对象&#xff0c;通常是某种状态变化的通知。可以是继承 ApplicationEvent 的类&#x…

无人机也能称重?电力巡检称重传感器安装与使用指南

在无人机电力巡检中&#xff0c;工程师们常常面临一个棘手难题&#xff1a;如何精确知道新架设或老旧缆线的实际负重&#xff1f; 传统依靠老师傅“肉眼估算”的方法不仅风险极高&#xff0c;而且数据极不准确&#xff0c;给电网安全埋下巨大隐患。难道没有更科学的方法吗&…

第二阶段WinForm-8:特性和反射,加密和解密,单例模式

1_预处理指令 &#xff08;1&#xff09;源代码指定了程序的定义&#xff0c;预处理指令&#xff08;preprocessor directive&#xff09;指示编译器如何处理源代码。例如&#xff0c;在某些情况下&#xff0c;我们希望编译器能够忽略一部分代码&#xff0c;而在其他情况下&am…

基于mac的智能语音处理与应用开发-环境部署

上一次写文章还是上一次&#xff0c;时隔一年再次开启学习之路。新机mac没有开发环境&#xff0c;在gpt老师的指导下开始学习之路。 mac开发环境的部署参考了b站程序员云谦和Clover-You的视频教程&#xff0c;然后结合自身及gpt老师的帮助现在开始部署。 g老师的&#x1f34e…

Java中使用正则表达式的正确打开方式

正则表达式基础语法Java正则表达式基于java.util.regex包&#xff0c;核心类是Pattern和Matcher。基本语法遵循标准正则规范&#xff1a;. 匹配任意单个字符&#xff08;除换行符&#xff09;\d 匹配数字&#xff0c;等价于 [0-9]\w 匹配单词字符&#xff0c;等价于 [a-zA-Z0-9…

Docker中Mysql容器忽略大小写

场景说明 在数据迁移场景中&#xff0c;从一个数据库中将数据迁移到另一个数据&#xff0c;经常会遇到&#xff0c;两个不同数据库之间&#xff0c;一个默认忽略大小写&#xff0c;一个默认不忽略大小写&#xff0c;导致实际业务层服务进行数据库访问时&#xff0c;切换数据库之…

神经网络激活函数:从ReLU到前沿SwiGLU

摘要 本文全面介绍了神经网络中常用的激活函数,包括Sigmoid、Tanh、ReLU等传统函数,以及2017年后出现的Swish、Mish、SwiGLU等新兴函数。每个函数均提供数学定义、优缺点分析、Python实现代码和可视化图像,并附有实际应用建议和性能对比数据,帮助读者根据具体任务选择合适…

线程池常见面试问答

好嘞 &#x1f44d;&#xff0c;我帮你把这些 线程池 并发编程八股文 整理成 问答对照表&#xff08;Q & A&#xff09;&#xff0c;你面试时可以直接用。&#x1f9fe; 线程池常见面试问答一、基础语法 & STLQ1&#xff1a;std::function<void()> 和函数指针的…

Flutter 开发技巧 AI 快速构建 json_annotation model 的提示词

将下面这段复制到AI GPT、DeepSeek 、文心快码 试过效果都可以&#xff0c;不用做任何更改。将 json 数据丢给 AI 就行了 我会提供一段 JSON 数据&#xff0c;请帮我生成 Dart 模型&#xff0c;要求严格如下&#xff1a;1. 使用 json_annotation 包&#xff0c;包含&#xff1a…

【秋招笔试】2025.08.30科大讯飞秋招笔试题

📌 点击直达笔试专栏 👉《大厂笔试突围》 💻 春秋招笔试突围在线OJ 👉 笔试突围在线刷题 bishipass.com 科大讯飞 题目一:物品种类统计 1️⃣:使用集合或哈希表统计不同物品编号的数量 2️⃣:利用数学公式 n - 不同种类数 计算最终答案 难度:简单 这道题目的关…

AI 智能体汇总,自动执行任务的“真 Agent”

AI Agent 正在掀起一场静默的效率革命&#xff1a;当 AI 遇上 RPA&#xff0c;真正的“数字员工”时代已经到来最近一段时间&#xff0c;我密集关注了多场 AI Agent&#xff08;智能体&#xff09;的发布会&#xff0c;覆盖了从消费级到企业级的各类产品。一个越来越清晰的趋势…

vue布局

给于2个div块状元素的布局方案1&#xff1a;横向并排&#xff08;Flex Row&#xff09;<template><div class"container"><div class"background">背景</div><div class"panel">内容</div></div> <…

Hysplit大气传输和污染扩散-轨迹聚合标准20%30%用途

1、HYSPLIT轨迹聚合中的百分比标准在HYSPLIT模型中&#xff0c;轨迹聚合&#xff08;Trajectory Clustering&#xff09;用于将大量轨迹按相似性分组&#xff0c;20%和30%是常见的聚合阈值标准&#xff0c;反映轨迹间的空间相似度要求。2、20%和30%的具体含义这两个百分比代表轨…

Linux shell 脚本基础 003

目录 Linux shell 脚本语句 1. for 循环流程控制 1.1 基本语法格式 1.2 常见用法示例 1.3生产案例示例 2. while 循环 2.1 基本语法格式 2.2 常见用法示例 3. case 语句 3.1 基本语法格式 3.2 常见用法示例 3.3生产案例示例 4. shell 函数 4.1 函数的定义 4.2 函…

7.1elementplus的表单

Element Plus 表单由以下几个关键部分构成&#xff1a;<el-form>: 表单容器。它是整个表单的根组件&#xff0c;负责管理表单数据、校验规则、布局方式等。<el-form-item>: 表单项容器。用于包裹一个具体的表单控件&#xff08;如输入框、选择器等&#xff09;及其…

TF-IDF:文本分析的“火眼金睛”

TF-IDF&#xff1a;文本分析的“火眼金睛” 在信息爆炸的时代&#xff0c;我们每天都会接触到海量的文本数据。如何从这些数据中快速找出最重要的信息&#xff1f;TF-IDF&#xff08;Term Frequency-Inverse Document Frequency&#xff09;算法就是一种非常实用的工具&#xf…

传统星型拓扑结构的5G,WiFi无线通信网络与替代拓扑结构自组网

一、个人理解 区别自组网&#xff0c;5G和WiFi三者要抓住两个关键点&#xff1a;移动、组网。 5G通信是移动通信的最新一代&#xff1b;1940年代美军的手持对讲机虽然可以移动&#xff0c;但是算不上网络&#xff0c;后面的第一代移动通信蜂窝网络才能算上网络。 WiFi通信虽…

REST API 是无状态的吗,如何保障 API 的安全调用?

当然&#xff0c;这是一个非常核心的API设计和安全领域的问题。我们来详细拆解一下。1. REST API 是无状态的吗&#xff1f;​是的&#xff0c;REST API 的核心原则之一就是“无状态”&#xff08;Statelessness&#xff09;。​​这意味着&#xff1a;​服务器不保存客户端的会…

Machine Learning HW3 report:图像分类(Hongyi Lee)

任务&#xff1a;使用CNN把食物图片分为11类&#xff08;不能使用预训练的模型&#xff09;。此任务很耗时&#xff0c;一次训练至少1h&#xff0c;所以要利用好Kaggle notebook中Save Version功能&#xff0c;并行训练节省时间。 基准 Simple : 0.50099Medium : 0.73207 Train…

Element整体操作样式

Element: 给表格整体设置斑马纹 在main.js中 ElementUI.Table.props.stripe {type: Boolean,default: true }在element-ui.scss中 // // 为所有 el-table 设置默认斑马纹 // .el-table { // &.el-table--enable-row-hover .el-table__body tr:hover > td { // ba…