【数据结构】吃透单链表!!!(详细解析~)

目录

  • 前言:
  • 一.顺序表的缺陷 && 介绍链表
    • 1.顺序表的缺陷
    • 2.介绍链表
      • (1)链表的概念
      • (2)链表的结构
      • (3)链表的功能
  • 二.单链表的实现
    • 1.创建节点的结构
    • 2.头文件函数的声明
    • 3.函数的实现
      • (1)打印单链表
      • (2)创建一个节点
      • (3)尾插
      • (4)头插
      • (5)尾删
      • (6)头删
      • (7)查找
      • (8)在pos位置前插入
      • (9)在pos位置后插入
      • (10)删除pos位置
      • (11)删除pos位置后的节点
      • (12)清理单链表
  • 三.全部代码
    • 1.SList.h
    • 2.SList.c
    • 3.Test.c

前言:

上篇文章介绍了顺序表,这篇文章开始着重讲解链表了。
链表有很多种:单、双链表,循环、非循环链表还有带头、不带头的链表。本篇的主要内容是单链表(无头,单向,非循环)
链表对比顺序表有哪些不同之处,接下来会带大家一起了解~

一.顺序表的缺陷 && 介绍链表

1.顺序表的缺陷

1.头部和中间的插入删除效率都较低,时间复杂度为O(N)。需要挪动数据。
2.空间不够用了,增容需要申请新空间拷贝数据释放旧空间。会有不小的消耗。(尤其是异地扩容)
3.扩容会有一定的空间浪费。(例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间)

2.介绍链表

针对顺序表的缺陷,就有了链表这个数据结构

(1)链表的概念

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
特点:按需申请释放
在这里插入图片描述

顺序表是数组存储数据的,空间是连续的(可通过一个指针找到所有的值),通过size标记直到没有数据(前面的为size的个数即有效数据)。
链表的每个节点的大小没有关系,也不连续(多次malloc开辟出来的空间是随机的)。它是通过一个头指针(phead)先找到第一个节点,然后通过第一个节点的指针找到第二个节点,第二个节点的指针找到第三个节点,以此类推(通过指针链接)。每个位置的节点都有指针指向下一个,当下一个为空指针的时候,就结束。

(2)链表的结构

物理图:
在这里插入图片描述
逻辑图:
在这里插入图片描述
链表的节点组成(单链表):
在这里插入图片描述

注意:链表的最后一个节点的next指向空

看到这有些小伙伴可能有些疑惑,链表的每个节点是不连续的,为什么上面的两个图中每个节点都有线连接起来变成看似连续的呢?其实不是这样的,以上的两张图是为了方便理解。实际在内存中每个节点的地址是随机的,只不过用这个节点的指针(next)找到了下一个节点的地址,所以才能实现链接。

(3)链表的功能

链表的功能与顺序表类似,无非是增删查改,在某位置的插入与删除,对数据内容进行管理和操作。

二.单链表的实现

还是以多文件的形式分模块写

SList.h——函数和类型的声明
SList.c——函数的实现
Test.c——进行测试

1.创建节点的结构

单链表一个节点的结构:

存放数据:data
结构体指针:next

注意:不能这样写:

typedef int SListDataType;//方便更改存储的数据类型
typedef struct SListNode
{SListDataType data;SLTNode* next;
}SLTNode;//  <-重定义开始生效的位置

因为typedef重定义结构体类型的名称是在上面有箭头的一行开始生效,生效了才能使用,在前面就提前使用就会出现错误。

正确写法:

typedef int SListDataType;//方便更改存储的数据类型
typedef struct SListNode
{SListDataType data;struct SListNode* next;
}SLTNode;

2.头文件函数的声明

1.打印单链表
2.创建一个节点
3.尾插
4.头插
5.尾删
6.头删
7.查找(包含修改)
8.在pos位置前插入
9.在pos位置后插入
10.删除pos位置的节点
11.删除pos位置后一个的节点
12.清理单链表

//打印单链表
void SLTPrint(SLTNode* phead);
//创建一个节点
SLTNode* BuySLTNode(SListDataType x);
//尾插
void SLTPushBack(SLTNode** pphead, SListDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SListDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SListDataType x);
//在pos位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SListDataType x);
//在pos位置后插入
void SLTInsertAfter(SLTNode* pos, SListDataType x);
//删除pos位置的节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos位置后一个的节点
void SLTEraseAfter(SLTNode* pos);
//清理单链表
void SLTDestroy(SLTNode** pphead);

3.函数的实现

(1)打印单链表

创建一个结构体指针变量(cur),使它指向第一个节点(把头指针覆给cur)。利用循环如果cur不是空指针,就打印cur所指向的数据,然后cur往后走(到下一个节点)。直到cur为空跳出,最后打印NULL(最后一个节点为空指针)。

逻辑图:
在这里插入图片描述
物理图:
在这里插入图片描述
注意:与顺序表不同,顺序表传过来的指针一定不为空;链表传过来的指针可能为空,比如链表没有节点,头指针指向的就是NULL,所以不需要断言头指针。

所以在测试的文件里(Test.c)刚开始要让头指针指向NULL

SLTNode * plist = NULL;

void SLTPrint(SLTNode* phead)
{SLTNode* cur = phead;while (cur){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}

(2)创建一个节点

为了方便后面的尾插、头插等操作,所以写个函数来创建一个新节点。
新节点的类型也是结构体指针,用malloc函数开辟一个新节点。如果新节点为空就报错。然后给新节点的data赋值,next为空,返回这个节点(方便其他的函数使用)

SLTNode* BuySLTNode(SListDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}

(3)尾插

尾插一个新节点,假设有多个节点,首先要找到尾,定义一个变量tail去遍历链表找到尾
注意:用tail遍历找尾再尾插时不能写成:

       SLTNode* tail = *pphead;while (tail){tail = tail->next;}tail = newnode;		

这段代码看似没有什么问题,其实是与正确的代码差别很大。
tail刚开始指向第一个节点,如果不为空,到下一个节点;当tail为空时跳出循环,把newnode的值(新节点的地址)赋给tail。
如图:
在这里插入图片描述
这里有一个问题,tail里面存放的是新节点的地址,但是原来链表的最后一个节点的next指针并没有存放新节点的地址,也就是说最后一个节点没有与新节点连接起来,就没有尾插了。其次还有可能存在内存泄漏,新创建的节点丢了。

因为tail是局部变量,newnode和phead也是,它们出这个函数就销毁了,所以给tail这个变量赋新节点的地址没有用。

要成功完成尾插,就必须改变结构体的内容,让最后一个节点的next指针指向新节点的地址。

这里大家可能有些疑惑,既然tail销毁了,那么链表的这些节点会不会销毁呢?
答案是不会,因为这些节点是malloc出来的,malloc在堆上开辟的空间,只有自己主动free释放掉才能销毁。

正确的思路:
首先想到的是要改变结构体(节点)的内容,那么tail这个指针变量就不能到空结束,而是到最后一个节点结束(tail的next为空就结束,tail的位置指向最后一个节点)。

此时尾节点的next为空,我们要做的是让尾节点的next存放新节点的地址。让tail的next存放newnode的值(新节点的地址),就可以改变结构体的内容。
在这里插入图片描述

找尾尾插正确的一小段代码:

		SLTNode* tail = *pphead;while (tail->next){tail = tail->next;}tail->next = newnode;

还有一种情况,如果刚开始链表没有节点,就不需要找尾了。直接将新节点的地址给头指针(plist)就行

但是这种情况要注意什么呢?
以下是错误示范:

	if (phead == NULL){phead = newnode;}

这个代码的意思如图所示:
在这里插入图片描述
有两个问题:
一:plist没有改变,还是指向空指针;新节点丢了,可能造成内存泄漏。
二:newnode和phead是形参,形参是实参的拷贝,出这个函数就销毁了,改变phead并没有改变plist。

注意!!!:plist是一级指针,改变一级指针需要用到二级指针,并且有解引用操作。所以在函数的参数应该用二级指针来接收(传参时plist要有取地址符才能与二级指针类型对应)
在这里插入图片描述
正确的一小段代码:

	if (*pphead == NULL){*pphead = newnode;}

总结:
1.改变结构体,要用结构体指针
2.改变结构体指针,要有结构体指针的指针(即二级指针)

最后一点:什么时候要断言指针
当一级指针(* pphead)为空时不需要断言,因为如果刚开始链表没有节点,* pphead所指向的就是空指针。二级指针pphead存放的是一级指针的地址,一级指针的地址不可能为空,所以二级指针需要断言。

void SLTPushBack(SLTNode** pphead, SListDataType x)
{assert(pphead);SLTNode* newnode = BuySLTNode(x);//原来没有节点,改变结构体指针,用二级指针if (*pphead == NULL){*pphead = newnode;}//原来有节点,改变结构体,用结构体指针else{SLTNode* tail = *pphead;while (tail->next){tail = tail->next;}tail->next = newnode;}
}

(4)头插

头插也需要用到二级指针,因为每次头插头指针(plist)都要连接新的节点。(改变了头指针)
头插时原来链表没有节点与原来链表有节点的思路是一样的
在这里插入图片描述
新节点连接第一个节点或者空指针,然后plist连接新节点

注意:两者的顺序不能换,因为如果先让plist连接newnode,那么原来链表plist头指针后面的节点就找不到了。newnode再连接plist所指向的下一个节点就是自己,导致死循环。

void SLTPushFront(SLTNode** pphead, SListDataType x)
{assert(pphead);SLTNode* newnode = BuySLTNode(x);newnode->next = *pphead;*pphead = newnode;
}

(5)尾删

前面的尾插、头插都有用到二级指针,那么尾删需不需要二级指针呢?接下来我们一点一点的分析:

尾删的大体思路是:找到尾,然后free释放掉尾节点就行。

但是链表有一个很重要的点:前后关联

这里我们定义一个指针变量tail去找尾,把尾节点删掉了,那么原来前一个节点变成新的尾节点,还需要用另一个变量当作原来尾节点的前一个节点,新的尾节点next指针就必须指向NULL只需要改变结构体),否则就访问野指针了。
有两种写法,这里只展现一种,就用tail一个指针变量,让它的下一个的下一个指针为空时停下(tail->next->next==NULL),此时tail->next就是最后一个节点,tail是前一个节点,修改新的尾节点的next,让tail->next为NULL(改变结构体)就行了。

以上只是包括一类情况:一个以上节点的时候是这样的
如果尾删把节点只删到剩下一个节点时,还是如此吗?

在这里插入图片描述
按前面的思路来走,遇到尾节点就把它的前一个节点的next置空。

依图分析,只有一个节点时,前一个节点就不是节点了,是头指针。要让头指针指向NULL,即改变头指针,就要用到二级指针了。
让 * pphead置空,就可以改变头指针

plist 等价于 * pphead

没有节点的情况:
断言 * pphead,为空就不能再删了

void SLTPopBack(SLTNode** pphead)
{assert(pphead);//空assert(*pphead);//一个节点if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}//一个以上的节点else{SLTNode* tail = *pphead;while (tail->next->next){tail = tail->next;}free(tail->next);tail->next = NULL;}
}

(6)头删

通过前面的分析发现,有改变头指针所指向的内容就要用到二级指针,头删是把第一个节点除去,让头指针指向新的头节点。

画图分析:
在这里插入图片描述
当链表没有节点时不能再删了,所以要对 * pphead断言( * pphead等价于plist即第一个节点)

只有一个节点和有多个节点不需要分开处理,定义一个变量记录原来链表的第二个节点(新的头节点),free释放掉第一个节点,让头指针连接新的头节点

void SLTPopFront(SLTNode** pphead)
{assert(pphead);//空assert(*pphead);//非空SLTNode* newhead = (*pphead)->next;//注意优先级free(*pphead);//不需要置空,因为头指针直接连接新的头*pphead = newhead;
}

(7)查找

定义一个变量cur遍历链表,先判断cur所指向的数据是否等于x,如果相等,返回cur,否则往后走;找不到返回空指针。

SLTNode* SLTFind(SLTNode* phead, SListDataType x)
{assert(phead);SLTNode* cur = phead;while (cur){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}

查找可以包含修改这个节点的数据

	SLTNode* pos1 = SLTFind(plist, 2);//测试查找+修改if (pos1 != NULL){printf("找到了\n");pos1->data *= 100;SLTPrint(plist);}else{printf("找不到\n");}

(8)在pos位置前插入

要在pos位置前插入一个新节点,首先pos这个位置的节点必须存在,所以要断言pos(后面有pos位置插入删除的函数也要用到)
pos可能在任意一个位置,如果pos在第一个节点,就相当于头插了。头插要改变头指针所指向的内容,所以要用二级指针。直接调用头插的函数即可。

pos不在第一个节点的情况:
首先要定义一个变量prev,遍历链表找到并指向pos的前一个节点,因为插入新的节点必须前后连接起来(单链表的不足之处,后期文章用双向循环带头链表就非常简单)。
当prev->next != pos,往后走;==pos时跳出循环,让prev->next连接新节点,新节点的next连接pos,完成插入。
在这里插入图片描述

void SLTInsert(SLTNode** pphead, SLTNode* pos, SListDataType x)
{assert(pphead);assert(pos);//pos在第一个节点就是头插if (pos == *pphead){SLTPushFront(pphead, x);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}SLTNode* newnode = BuySLTNode(x);prev->next = newnode;newnode->next = pos;}
}

(9)在pos位置后插入

因为在pos位置后插入新的节点,所以可以不用头指针了,找到pos位置的下一个节点即可。可以定义一个变量posNext为pos位置的下一个节点,让新节点的next连接posNext,pos->next连接新节点
不需要考虑是不是尾插,因为在哪插入都是一样的
在这里插入图片描述

void SLTInsertAfter(SLTNode* pos, SListDataType x)
{assert(pos);SLTNode* newnode = BuySLTNode(x);SLTNode* posNext = pos->next;newnode->next = posNext;pos->next = newnode;
}

(10)删除pos位置

删除pos位置的节点,必须把它的前一个节点与后一个节点连接起来,这里就要有头指针,去找pos位置的前一个节点。
我们要考虑一些情况,pos在第一个节点、中间某个节点和尾节点

当pos在第一个节点时,就是头删,要改变头指针指向的内容,所以要用二级指针,然后调用头删的函数即可

如果pos是在中间的某个节点或者尾节点呢?
其实两者的思路是一致的,把pos位置的节点删除,让前一个节点连接后一个节点就行(是尾节点的话,让前一个节点连接空指针)
在这里插入图片描述

void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(pos);if (pos == *pphead){SLTPopFront(pphead);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;}
}

(11)删除pos位置后的节点

要删除pos位置的后一个节点,除了pos这个位置要存在之外,pos位置的后一个节点也必须存在,所以pos->next要断言。假如pos是在尾节点,就没有意义了。

定义一个变量posNext为pos的下一个节点,然后使pos->next指向posNext->next,即把pos位置的节点与posNext的下一个节点连接起来,最后释放掉posNext

在这里插入图片描述

void SLTEraseAfter(SLTNode* pos)
{assert(pos);assert(pos->next);//检查是否为尾节点SLTNode* posNext = pos->next;pos->next = posNext->next;free(posNext);posNext = NULL;
}

(12)清理单链表

清理(销毁)链表,必须要一个一个节点清理,因为链表在物理结构上是不连续的。

定义一个变量cur遍历链表,每到一个节点把它释放掉。但是这里又有一个问题,当前节点被释放了,怎么到下一个节点呢?所以我们循环里再定义一个变量next为cur的下一个节点,释放完当前的cur,然后把next赋给cur,这样cur就能到下一个节点了。

最后全部节点释放完,头指针要指向空,这里又有改变头指针了,所以有二级指针。
在这里插入图片描述

void SLTDestroy(SLTNode** pphead)
{assert(pphead);SLTNode* cur = *pphead;while (cur){SLTNode* next = cur->next;free(cur);cur = next;}*pphead = NULL;
}

三.全部代码

1.SList.h

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int SListDataType;//方便更改存储的数据类型
typedef struct SListNode
{SListDataType data;struct SListNode* next;
}SLTNode;
//打印单链表
void SLTPrint(SLTNode* phead);
//创建一个节点
SLTNode* BuySLTNode(SListDataType x);
//尾插
void SLTPushBack(SLTNode** pphead, SListDataType x);
//头插
void SLTPushFront(SLTNode** pphead, SListDataType x);
//尾删
void SLTPopBack(SLTNode** pphead);
//头删
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SListDataType x);
//在pos位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SListDataType x);
//在pos位置后插入
void SLTInsertAfter(SLTNode* pos, SListDataType x);
//删除pos位置的节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos位置后一个的节点
void SLTEraseAfter(SLTNode* pos);
//清理单链表
void SLTDestroy(SLTNode** pphead);

2.SList.c

#include "SList.h"
//打印
void SLTPrint(SLTNode* phead)
{SLTNode* cur = phead;while (cur){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}
//创建一个节点
SLTNode* BuySLTNode(SListDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}
//尾插
void SLTPushBack(SLTNode** pphead, SListDataType x)
{assert(pphead);SLTNode* newnode = BuySLTNode(x);//原来没有节点,改变结构体指针,用二级指针if (*pphead == NULL){*pphead = newnode;}//原来有节点,改变结构体,用结构体指针else{SLTNode* tail = *pphead;while (tail->next){tail = tail->next;}tail->next = newnode;}
}
//头插
void SLTPushFront(SLTNode** pphead, SListDataType x)
{assert(pphead);SLTNode* newnode = BuySLTNode(x);newnode->next = *pphead;*pphead = newnode;
}
//尾删
void SLTPopBack(SLTNode** pphead)
{assert(pphead);//空assert(*pphead);//一个节点if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}//一个以上的节点else{SLTNode* tail = *pphead;while (tail->next->next){tail = tail->next;}free(tail->next);tail->next = NULL;}
}
//头删
void SLTPopFront(SLTNode** pphead)
{assert(pphead);//空assert(*pphead);//非空SLTNode* newhead = (*pphead)->next;//注意优先级free(*pphead);//不需要置空,因为头指针直接连接新的头*pphead = newhead;
}
//查找
SLTNode* SLTFind(SLTNode* phead, SListDataType x)
{assert(phead);SLTNode* cur = phead;while (cur){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}
//在pos位置前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SListDataType x)
{assert(pphead);assert(pos);//pos在第一个节点就是头插if (pos == *pphead){SLTPushFront(pphead, x);}else{SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}SLTNode* newnode = BuySLTNode(x);prev->next = newnode;newnode->next = pos;}
}
//在pos位置后插入
void SLTInsertAfter(SLTNode* pos, SListDataType x)
{assert(pos);SLTNode* newnode = BuySLTNode(x);SLTNode* posNext = pos->next;newnode->next = posNext;pos->next = newnode;
}
//删除pos位置
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(pos);if (pos == *pphead){SLTPopFront(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);assert(pos->next);//检查是否为尾节点SLTNode* posNext = pos->next;pos->next = posNext->next;free(posNext);posNext = NULL;
}
//清理
void SLTDestroy(SLTNode** pphead)
{assert(pphead);SLTNode* cur = *pphead;while (cur){SLTNode* next = cur->next;free(cur);cur = next;}*pphead = NULL;
}

3.Test.c

#include "SList.h"
test()
{SLTNode* plist = NULL;SLTPushBack(&plist, 1);SLTPushBack(&plist, 2);SLTPushBack(&plist, 3);SLTPushBack(&plist, 4);SLTPushBack(&plist, 5);//测试尾插SLTPrint(plist);SLTPushFront(&plist, 10);SLTPushFront(&plist, 20);SLTPushFront(&plist, 30);SLTPushFront(&plist, 40);//测试头插SLTPrint(plist);SLTPopBack(&plist);SLTPopBack(&plist);SLTPopBack(&plist);//测试尾删SLTPrint(plist);SLTPopFront(&plist);SLTPopFront(&plist);//测试头删SLTPrint(plist);SLTNode* pos1 = SLTFind(plist, 2);//测试查找+修改if (pos1 != NULL){printf("找到了\n");pos1->data *= 100;SLTPrint(plist);}else{printf("找不到\n");}SLTNode* pos2 = SLTFind(plist, 10);//测试pos位置前插入if (pos2){SLTInsert(&plist, pos2, 66);SLTPrint(plist);}SLTNode* pos3 = SLTFind(plist, 20);//测试pos位置后插入if (pos3){SLTInsertAfter(pos3, 77);SLTPrint(plist);}SLTNode* pos4 = SLTFind(plist, 1);//测试删除pos位置if (pos4){SLTErase(&plist, pos4);SLTPrint(plist);}SLTNode* pos5 = SLTFind(plist, 66);//测试删除pos位置的后一个节点if (pos5){SLTEraseAfter(pos5);SLTPrint(plist);}SLTDestroy(&plist);
}
int main()
{test();return 0;
}

在这里插入图片描述
总算把最费劲的写完了,感谢铁子们的观看,期待大家的支持~

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

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

相关文章

第十五章:联邦学习攻防实战

代码 联邦学习的后门攻击案例 联邦学习的模型压缩案例 联邦学习的差分隐私案例 联邦学习的同态加密案例 联邦学习的参数稀疏化案例

EndNote-文献管理工具【安装篇】

下载&#xff1a;&#xff08;文末附安装包&#xff0c;建议使用这一个&#xff0c;官网都需要付费&#xff09; 打开安装包&#xff0c;双击&#xff1a; 安装完了之后不要直接运行&#xff0c;因为EndNote软件少了一个类型的软件&#xff1a;GB/T17714。 因此我们需要把这个…

VBA技术资料MF45:VBA_在Excel中自定义行高

【分享成果&#xff0c;随喜正能量】可以不光芒万丈&#xff0c;但不要停止发光。有的人陷入困境&#xff0c;不是被人所困&#xff0c;而是自己束缚自己&#xff0c;这时"解铃还须系铃人"&#xff0c;如果自己无法放下&#xff0c;如何能脱困&#xff1f; 。 我给V…

Liunx系统编程:进程信号的概念及产生方式

目录 一. 进程信号概述 1.1 生活中的信号 1.2 进程信号 1.3 信号的查看 二. 信号发送的本质 三. 信号产生的四种方式 3.1 按键产生信号 3.2 通过系统接口发送信号 3.2.1 kill -- 向指定进程发送信号 3.2.2 raise -- 当自身发送信号 3.2.3 abort -- 向自身发送进程终止…

verilog学习笔记6——锁存器和触发器

文章目录 前言一、锁存器1、基本SR锁存器——或非门实现2、基本SR锁存器——与非门实现3、门控SR锁存器4、门控D锁存器 二、触发器1、 电平触发的RS触发器/同步SR触发器2、电平触发的D触发器/D型锁存器3、边沿触发的D触发器4、脉冲触发的RS触发器 三、边沿触发、脉冲触发、电平…

【C# 基础精讲】LINQ to XML查询

LINQ to XML 是 C# 中用于查询和操作 XML 数据的强大工具。它允许您使用 LINQ 查询语法对 XML 文档进行查询、过滤、投影等操作&#xff0c;从而更加方便地处理 XML 数据。本文将详细介绍 LINQ to XML 的基本概念、常见操作以及示例&#xff0c;帮助您了解如何在 C# 中使用 LIN…

智能数据建模软件DTEmpower 2023R2新版本功能介绍

DTEmpower是由天洑软件自主研发的一款通用的智能数据建模软件&#xff0c;致力于帮助工程师及工科专业学生&#xff0c;利用工业领域中的仿真、试验、测量等各类数据进行挖掘分析&#xff0c;建立高质量的数据模型&#xff0c;实现快速设计评估、实时仿真预测、系统参数预警、设…

Verilog同步FIFO设计

同步FIFO(synchronous)的写时钟和读时钟为同一个时钟&#xff0c;FIFO内部所有逻辑都是同步逻辑&#xff0c;常常用于交互数据缓冲。 异步FIFO&#xff1a;数据写入FIFO的时钟和数据读出FIFO的时钟是异步的(asynchronous) 典型同步FIFO有三部分组成: &#xff08;1&#xff0…

arm:day4

1. 实现三盏灯的点亮 .text .global _start_start: led1初始化函数LED_INIT: 1 通过RCC_AHB4_ENSETR寄存器&#xff0c;设置GPIOE F组控制器使能 0x50000A28[5:4]1ldr r0,0X50000A28ldr r1,[r0]orr r1,r1,#(0X3<<4)str r1,[r0] 2.1 通过GPIOE_MODER寄存器&#xff0c;…

史上最简洁实用人工神经元网络c++编写202301

这是史上最简单、清晰…… C语言编写的 带正向传播、反向传播(Forward ……和Back Propagation&#xff09;……任意Nodes数的人工神经元神经网络……。 大一学生、甚至中学生可以读懂。 适合于&#xff0c;没学过高数的程序员……照猫画虎编写人工智能、深度学习之神经网络……

plt绘制箱型图+散点图

import numpy as np import matplotlib.pyplot as plt# 创建示例数据 np.random.seed(1) data [np.random.normal(0, std, 100) for std in range(1, 4)]# 绘制箱型图 plt.boxplot(data, patch_artistTrue,zorder0)# 添加数据点的散点图&#xff0c;并设置参数以避免重叠 for …

6.Web后端开发【SpringBoot入门】

文章目录 1 SpringBoot快速入门1.1 Web分析 2. HTTP协议2.1 HTTP-概述2.1.1 介绍2.2.2 特点 2.2 HTTP-请求协议2.3 HTTP-响应协议2.3.1 格式介绍2.3.2 响应状态码 常见的相应状态码 3 WEB服务器3.1 服务器概述 1 SpringBoot快速入门 Spring的官网Spring Boot 可以帮助我们非常…

探秘Maven神奇力量:使用systemPath加载外部JAR包

&#x1f60a; 作者&#xff1a; 一恍过去 &#x1f496; 主页&#xff1a; https://blog.csdn.net/zhuocailing3390 &#x1f38a; 社区&#xff1a; Java技术栈交流 &#x1f389; 主题&#xff1a; 探秘Maven神奇力量&#xff1a;使用systemPath加载外部JAR包 ⏱️ 创作…

明星都偏爱的多燕瘦活酵素,不含非法添加事件更健康

不少瘦身人士信奉“运动就能瘦”的准则&#xff0c;每天坚持高强度运动&#xff0c;一段时间后却发现&#xff0c;不仅体重没有下降&#xff0c;甚至整个人看起来都变得更加壮实了&#xff0c;这是为什么&#xff1f; 首先&#xff0c;运动是分为减脂和增肌两种类型的&#xff…

拒绝摆烂!C语言练习打卡第四天

&#x1f525;博客主页&#xff1a;小王又困了 &#x1f4da;系列专栏&#xff1a;每日一练 &#x1f31f;人之为学&#xff0c;不日近则日退 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、选择题 &#x1f4dd;1.第一题 &#x1f4dd;2.第二题 &#x1f4d…

远程线程注入(简单样例以及原理)

远程线程注入(简单样例以及原理) 注入的目标是将我们的代码注入到目标进程的地址空间中 注入通常可以根据注入的内容分为两种类型&#xff1a; shellcode注入 &#xff1a;这种注入是将我们的代码直接注入到目标内存中&#xff0c;这就要保证我们的代码在贴到其他地址上后仍…

【AI】文心一言的使用

一、获得内测资格&#xff1a; 1、点击网页链接申请&#xff1a;https://yiyan.baidu.com/ 2、点击加入体验&#xff0c;等待通过 二、获得AI伙伴内测名额 1、收到短信通知&#xff0c;点击链接 网页Link&#xff1a;https://chat.baidu.com/page/launch.html?fa&sourc…

SpringBoot + Vue 微人事权限组管理模块 (十四)

权限组前端页面制作 权限组管理角色和菜单之间关系&#xff0c;操作员管理着用户和角色之间的关系。 英文的输入框要有个前缀&#xff0c;SpringSecurity里角色英文名需要加一个ROLE_的前缀 上代码 <div><div class"permissManaTool"><el-input pla…

通过 kk 创建 k8s 集群和 kubesphere

官方文档&#xff1a;多节点安装 确保从正确的区域下载 KubeKey export KKZONEcn下载 KubeKey curl -sfL https://get-kk.kubesphere.io | VERSIONv3.0.7 sh -为 kk 添加可执行权限&#xff1a; chmod x kk创建 config 文件 KubeSphere 版本&#xff1a;v3.3 支持的 Kuber…

go语言中channel类型

目录 什么是channel 为什么要有channel channel操作使用 初始化&#xff1a; 操作&#xff1a; 单向channel 双向channel&#xff0c;可读可写&#xff1a; close下什么场景会出现panic 总结 什么是channel Channels are a typed conduit through which you can send …