当前位置: 首页 > news >正文

线性表,顺序表,链表

线性表

线性表(linear list)是n个具有相同特性的数据元素有限序列

线性表是一种在实际中广泛使 用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...

线性表在逻辑上是线性结构,也就说是连续的一条直线

但是在物理结构上并不一定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储

也就是说,我们可以将线性表当作顺序表与链表的集合体 


顺序表

顺序表的本质是数组

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存 储

数组有一个绝对的优势:下标的随机访问

举个例子:

对于二分查找来说,只能用于数组(还有其他排序一样),就是通过数组的下标访问来实现的,由于数组的数据是连续存储的,可以通过下标按照规定的顺序来访问相对于的元素

在数组上完成数据的增删查改


顺序表一般可以分为:

1. 静态顺序表:使用定长数组存储元素

使用定长数组存储元素

//静态的顺序表#define N 10
typedef int SLDatatype;
//对SLDatatype重新赋予类型,目的是可以随意改变SLDatatype的类型struct SeqList
{SLDatatype a[N];//定义一个数组int size;//评估顺序表有多少数据
};

但是对于静态顺序表而言,它存在两个问题:

1.由于事先给定长度,当想扩容会变得非常麻烦(给小了不够用

2.若给定过大长度,则容易造成内存空间浪费(给多了浪费

所以我们一般使用动态顺序表


2. 动态顺序表:使用动态开辟的数组存储

//动态的顺序表typedef int SLdatatype;struct SeqList
{SLdatatype* a;//定义一个动态开辟的空间int size;//评估顺序表存储的有效数据个数int capacity;//评估顺序表的容量空间
};

对SeqList结构体进行初始化操作

void SLInit(SL* psl)//顺序初始化
{psl->a = (SLDatatype*)malloc(sizeof(SLDatatype) * 4);//开辟4个SLDatatype类型的空间//将返回的无符号类型指针地址强制转换为SLDatatype*类型if (psl->a == NULL){perror("malloc fail");return;}psl->size = 0;psl->capacity = 4;
}

通过上面可以看到,对SeqList的数组a进行了动态开辟内存的操作

这样就可以弥补静态开辟内存的不足

接下来我们研究增删查改的实现

在上面我们进行了开辟空间的操作,接下来我们进行增加元素的操作

分为以下几种:头部增加(头插),尾部增加(尾插),任意位置增加

先来看头插

void SLPushFront(SL* psl, SLDatatype x)//头插
{int end = psl->size - 1;int str = 0;while (end >= str){psl->a[end + 1] = psl->a[end];//令后一项等于前一项(后移前,避免覆盖数据)end--;}psl->a[str] = x;psl->size++;
}

由于我们在最前方进行数据插入的操作,为了让移动数据时不会覆盖掉数据导致bug的出现

我们应该从后往前移动数据,如上代码所示

但是对于这个代码会有什么样的问题出现呢?

若当该空间(psl->a)已全部存放数据,此时再进行插入数据操作时,会出现越界问题

所以在进行插入操作前,我们应该增加一次检查空间容量的操作

如下代码所示即为检查空间容量实现代码

void SLCheckCapacity(SL* psl)检查容量
{if (psl->size == psl->capacity){SLDatatype* tmp = (SLDatatype*)realloc(psl->a, sizeof(SLDatatype) * psl->capacity * 2);//扩容到原来空间的2倍if (tmp == NULL){perror("realloc fail");return;}psl->a = tmp;psl->capacity *= 2;}
}

并需要在插入操作时加入该操作

下面看尾插

void SLPushBack(SL* psl, SLDatatype x)//尾插
{SLCheckCapacity(psl);//检查容量psl->a[psl->size] = x;//psl->a[psl->size++] = x;//先赋值,再自增psl->size++;
}

由于也是插入数据操作,我们依旧需要判断空间是否足够

在这里我们有两种表达方式

一种是psl->a[psl->size] = x;        psl->size++;

另一种是psl->[psl->size++] = x;

这两种等效(由于size++为后置自增,先使用再自增,详细查看操作符详解)

由于下标索引为psl->size指向的位置正为当前空间的最后一个元素空间

所以直接进行赋值操作,再另size++即可

最后看任意位置插入

void SLInsert(SL* psl, int pos, SLDatatype x)//任意位置插入
{SLCheckCapacity(psl);//检查容量int end = psl->size - 1;while (end >= pos){psl->a[end + 1] = psl->a[end];end--;}psl->a[pos] = x;psl->size++;
}

同上,先进行空间检查操作

由于是在任意位置插入,需要进行数据移位操作,为了不覆盖数据,需要从后往前移动

设定指针end,令其指向当前最后有效一个元素的位置,再进行移动赋值操作

直到end的位置等于pos的位置

但是对于这个代码而言,存在一个很严重的问题

当我选择插入的空间大于或者小于这个空间的范围时,会出现越界的情况

所以我们还需要在此之前增加一个判断,检测pos的值是否合法

在这里我们有两种表达方式

void SLInsert(SL* psl, int pos, SLDatatype x)//任意位置插入
{//assert(pos >= 0 && pos <= psl->size);SLCheckCapacity(psl);//检查容量if (pos > psl->size || pos < 0){printf("输入错误!\n");return;}int end = psl->size - 1;while (end >= pos){psl->a[end + 1] = psl->a[end];end--;}psl->a[pos] = x;psl->size++;
}

我么可以使用assert断言或者使用if判断语句,前者是暴力检查,后者是柔性检查

(assert断言的使用方法)

其实,对于头插和尾插,我们都可以通过引用任意位置插入的函数来实现

头插:SLInsert(psl, 0, x);(在起始位置插入)

尾插:SLInsert(psl , psl->size, x);(在结束位置插入)


对于删除操作,我们也有以下三种情况

头部删除(头删),尾部删除(尾删),任意位置删除

首先看头删

void SLPopFront(SL* psl)//头删
{int str = 0;while (str < psl->size - 1){psl->a[str] = psl->a[str + 1];//将后值赋给前值(从前往后避免覆盖)str++;}psl->size--;
}

由于是头部删除,删除之后要进行元素移动操作

为了不使数据被覆盖,这里需要从前往后移动数据避免覆盖

当删除完毕后,size需要自减

对于这里而言,我们仍然需要增加一个判断,来检查空间内是否有元素需要删除,避免越界

完整代码如下

void SLPopFront(SL* psl)//头删
{//assert(psl->size > 0);if (psl->size == 0){printf("表内无数据!\n");return;}int str = 0;while (str < psl->size - 1){psl->a[str] = psl->a[str + 1];//将后值赋给前值(从前往后避免覆盖)str++;}psl->size--;
}

再来看尾部删除

void SLPopBack(SL* psl)//尾删
{//assert(psl->size > 0);if (psl->size == 0){printf("表内无数据!\n");return;}psl->size--;
}

同理,我们仍然需要先判断空间内是否有元素

对于尾删而言,也许我们一开始会选择将最后一个有效元素赋值为0

可是当最后一个有效元素本身就为0的情况下,这种操作未免显得没有意义

所以我们可以直接选择直接令psl->size减少1,直接去除最后一个元素

最后看任意位置删除

void SLErase(SL* psl, int pos)//任意位置删除
{//assert(pos >= 0 && pos < psl->size);if (pos > psl->size || pos < 0){printf("输入错误!\n");return;}while (pos < psl->size - 1){psl->a[pos] = psl->a[pos + 1];pos++;}psl->size--;
}

老规矩,先检查空间

再进行移动赋值操作(需要从后往前移动,避免覆盖)

对于头删和尾删,可以引用任意位置删除函数来进行删除操作

头删:SLErase(psl, 0);(删除第一位元素)

尾删:SLErase(psl, psl->size - 1);(删除最后一位元素)


查 

代码如下

SLDatatype SLFind(SL* psl, SLDatatype x)//查
{for (int i = 0; i < psl->size; i++){if (psl->a[i] == x){return i;}elsereturn -1;}
}

非常简单,只需要一个循环遍历即可


代码如下

void SLModify(SL* psl, int pos, SLDatatype x)//改
{//assert(pos >= 0 && pos < psl->size);if (pos > psl->size || pos < 0){printf("输入错误!\n");return;}psl->a[pos] = x;
}

同样非常简单

只需要通过下标索引值找到想要修改的值,对其重新赋值即可


执行完上面的操作之后,下面还有几个需要注意的点

打印函数

为了可以直观地看到数据,我们需要使用一个打印函数来检查是否满足我们的需求

void SLPrint(SL* psl)//打印
{if (psl->size == 0){printf("表内无数据!\n");}for (int i = 0; i < psl->size; i++){printf("%d ", psl->a[i]);}printf("\n");
}

释放内存

由于使用malloc和relloc函数在堆中开辟空间,结束使用后需要手动释放内存

避免造成内存泄漏

void SLDestroy(SL* psl)//释放内存
{free(psl->a);psl->a = NULL;psl->size = 0;psl->capacity = 0;
}

值得一提的是,我们在进行函数调用时,有时候可能会出现错误传递空指针

为了避免这样的情况,我们应该在每一个函数定义部分都加上assert断言

来判断是否有传递空指针的情况

完整代码如下

SeqList.h#include <stdio.h>
#include <stdlib.h>
#include <assert.h>//静态的顺序表//#define N 10
//typedef int SLDatatype;
对SLDatatype重新赋予类型,目的是可以随意改变SLDatatype的类型
//
//struct SeqList
//{
//	SLDatatype a[N];
//	//定义一个数组
//	int size;
//	//评估顺序表有多少数据
//};//动态的顺序表typedef int SLDatatype;typedef struct SeqList
{SLDatatype* a;//定义一个动态开辟的空间int size;//评估顺序表存储的有效数据个数int capacity;//评估顺序表的容量空间
}SL;
//对结构体SeqList类型重命名void SLInit(SL* psl);//初始化
void SLDestroy(SL* psl);//释放内存void SLCheckCapacity(SL* psl);//检查容量空间是否足够
void SLPrint(SL* psl);//打印void SLPushBack(SL* psl, SLDatatype x);//尾插
void SLPushFront(SL* psl, SLDatatype x);//头插void SLPopBack(SL* psl);//尾删
void SLPopFront(SL* psl);//头删void SLInsert(SL* psl, int pos, SLDatatype x);//任意位置插入
void SLErase(SL* psl, int pos);//任意位置删除SLDatatype SLFind(SL* psl, SLDatatype x);//查
void SLModify(SL* psl, int pos, SLDatatype x);//改

此处是对结构体的定义,函数的声明以及#define操作

SeqList.c#include "SeqList.h"void SLInit(SL* psl)//顺序初始化
{psl->a = (SLDatatype*)malloc(sizeof(SLDatatype) * 4);//开辟4个SLDatatype类型的空间//将返回的无符号类型指针地址强制转换为SLDatatype*类型if (psl->a == NULL){perror("malloc fail");return;}psl->size = 0;psl->capacity = 4;
}void SLCheckCapacity(SL* psl)检查容量
{assert(psl != NULL);if (psl->size == psl->capacity){SLDatatype* tmp = (SLDatatype*)realloc(psl->a, sizeof(SLDatatype) * psl->capacity * 2);//扩容到原来空间的2倍if (tmp == NULL){perror("realloc fail");return;}psl->a = tmp;psl->capacity *= 2;}
}void SLPrint(SL* psl)//打印
{assert(psl != NULL);if (psl->size == 0){printf("表内无数据!\n");}for (int i = 0; i < psl->size; i++){printf("%d ", psl->a[i]);}printf("\n");
}void SLDestroy(SL* psl)//释放内存
{assert(psl != NULL);free(psl->a);psl->a = NULL;psl->size = 0;psl->capacity = 0;
}void SLPushBack(SL* psl, SLDatatype x)//尾插
{assert(psl != NULL);SLCheckCapacity(psl);//检查容量psl->a[psl->size] = x;//psl->a[psl->size++] = x;//先赋值,再自增psl->size++;//SLInsert(psl , psl->size, 3);
}void SLPushFront(SL* psl, SLDatatype x)//头插
{assert(psl != NULL);SLCheckCapacity(psl);//检查容量int end = psl->size - 1;int str = 0;while (end >= str){psl->a[end + 1] = psl->a[end];//令后一项等于前一项(后移前,避免覆盖数据)end--;}psl->a[str] = x;psl->size++;//SLInsert(psl, 0, 3);
}void SLPopBack(SL* psl)//尾删
{assert(psl != NULL);//assert(psl->size > 0);if (psl->size == 0){printf("表内无数据!\n");return;}psl->size--;//SLErase(psl, psl->size - 1);
}void SLPopFront(SL* psl)//头删
{assert(psl != NULL);//assert(psl->size > 0);if (psl->size == 0){printf("表内无数据!\n");return;}int str = 0;while (str < psl->size - 1){psl->a[str] = psl->a[str + 1];//将后值赋给前值(从前往后避免覆盖)str++;}psl->size--;//SLErase(psl, 0);
}void SLInsert(SL* psl, int pos, SLDatatype x)//任意位置插入
{assert(psl != NULL);//assert(pos >= 0 && pos <= psl->size);SLCheckCapacity(psl);//检查容量if (pos > psl->size || pos < 0){printf("输入错误!\n");return;}int end = psl->size - 1;while (end >= pos){psl->a[end + 1] = psl->a[end];end--;}psl->a[pos] = x;psl->size++;
}void SLErase(SL* psl, int pos)//任意位置删除
{assert(psl != NULL);//assert(pos >= 0 && pos < psl->size);if (pos > psl->size || pos < 0){printf("输入错误!\n");return;}while (pos < psl->size - 1){psl->a[pos] = psl->a[pos + 1];pos++;}psl->size--;
}SLDatatype SLFind(SL* psl, SLDatatype x)//查
{assert(psl != NULL);for (int i = 0; i < psl->size; i++){if (psl->a[i] == x){return i;}elsereturn -1;}
}void SLModify(SL* psl, int pos, SLDatatype x)//改
{assert(psl != NULL);//assert(pos >= 0 && pos < psl->size);if (pos > psl->size || pos < 0){printf("输入错误!\n");return;}psl->a[pos] = x;
}

此处是对各个函数的定义

main.c#include "SeqList.h"void test1(SL* s)//头尾插及打印测试
{SLInit(s);SLPushBack(s, 1);SLPushBack(s, 2);SLPushFront(s, 1);SLPushFront(s, 2);SLPushFront(s, 3);SLPushFront(s, 4);SLPrint(s);SLDestroy(s);
}void test2(SL* s)//尾删测试
{SLInit(s);SLPushBack(s, 1);SLPushBack(s, 2);SLPushFront(s, 1);SLPushFront(s, 2);SLPushFront(s, 3);SLPopBack(s);SLPopBack(s);SLPopBack(s);SLPopBack(s);//SLPopBack(s);//SLPopBack(s);//测试是否有越界的问题SLPrint(s);SLDestroy(s);
}void test3(SL* s)//头删测试
{SLInit(s);SLPushBack(s, 1);SLPushBack(s, 2);SLPushFront(s, 1);SLPushFront(s, 2);SLPushFront(s, 3);SLPopFront(s);SLPopFront(s);SLPopFront(s);SLPopFront(s);//SLPopFront(s);//SLPopFront(s);SLPrint(s);SLDestroy(s);
}void test4(SL* s)//任意位置插与任意位置删测试
{SLInit(s);SLPushBack(s, 1);SLPushBack(s, 2);SLInsert(s, 2, 3);SLInsert(s, 3, 4);SLInsert(s, 4, 5);SLInsert(s, 5, 6);SLErase(s, 2);SLErase(s, 2);SLErase(s, 2);SLPrint(s);SLDestroy(s);
}void test5(SL* s)//查找与修改测试
{SLInit(s);SLPushBack(s, 1);SLPushBack(s, 2);SLPushBack(s, 3);SLPushBack(s, 4);SLModify(s, 1, 20);SLPrint(s);printf("%d\n", SLFind(s, 1));SLDestroy(s);
}int main()
{SL s;//test1(&s);//test2(&s);//test3(&s);//test4(&s);test5(&s);return 0;
}

此处是主函数对各个函数的测试


链表

链表是一种物理存储结构上非连续、非顺序的存储结构

数据元素的逻辑顺序是通过链表中的指针链接次序实现的

相比较于顺序表,链表可以做到以下几个点:

1.不需要扩容

2.可以按需求申请释放

3.解决头部/中间插入删除需要解决的移动数据的问题


单链表

画图来理解:

如图所示

黄色区域存放的是链表中每个结点的数据

粉色区域存放的是链表中每个结点指向下一个结点的地址

那么如何管理他们?

首先起始位置需要有一个指针指向第一个结点

通过第一个结点的指针部分找到下一个结点的地址,从而进行访问

以此类推

最后一个结点的指针指向空指针

如何定义?

typedef int SLTDataType;    //将int类型重命名为SLTDataTypetypedef struct SListNode
{SLTDataType data;    //存放数据struct SListNode* next;    //指向下一段的结构体指针
}SLTNode;    //将struct SListNode重命名为SLTNode

对于单链表,我们也需要实现不同的功能:增,删,查

但在实现这些功能之前,我们需要为链表写一个开辟空间的函数方便调用

//开辟空间
SLTNode* BuyLTNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//开辟一个新的SLTNode结构体的空间,将其强制转换为SLTNode*类型,并将地址传给newnodeif (newnode == NULL){perror("malloc fail");return NULL;}//判断是否开辟空间成功newnode->data = x;newnode->next = NULL;//对newnode进行初始化return newnode;
}

接收一个data数据x,用于存放在链表结点内

将开辟的空间存放在newnode结构体指针中,进行判断是否开辟成功

随后将newnode中的data赋值为x,将newnode中的next置空(防止出现悬空指针)

返回这个新开辟的结点newnode


 增:

对于增,有三种方法,分别是头插,尾插与任意位置前插

头插:

//头插
void SLPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);//判断是否接收到plist的地址(pphead是头指针plist的地址)SLTNode* newnode = BuyLTNode(x);assert(newnode != NULL);//判断是否开辟成功newnode->next = *pphead;//令结点指针指向原先的结点(通过pphead的地址修改)*pphead = newnode;//令phead指针指向新结点(通过pphead的地址修改,其实是改变plist)
}

首先使用断言判断是否接收到pphead传送过来的地址

调用开辟空间函数,使用newnode接收新结点的地址

判断是否开辟成功

再另新结点指针指向原先的结点,最后令phead指针指向新结点(将头指向newnode)


尾插(第一种):

//尾插
void SLPushBack(SLTNode* phead, SLTDataType x)
{SLTNode* tail = phead;while (tail != NULL){tail = tail->next;}SLTNode* newnode = BuyLTNode(x);assert(newnode != NULL);//判断是否开辟成功tail = newnode;
}

 上面这种属于错误写法 

首先定义以一个结构体指针tail接收phead的地址(将tail作为头)

判断tail是否为空

之后再使用一个结构体指针newnode接收新结点的地址

判断是否开辟成功

再令tail等于新结点newnode

但是!

对于这个函数而言,其实尾插是不成功的

有两个原因:

1、本质上tail并没有与链表连接起来(局部变量出了函数销毁)

2、链表连接应该使用tail->next = newnode来连接

接下来来改进一下

尾插(第二种):

//尾插
void SLPushBack(SLTNode* phead, SLTDataType x)
{SLTNode* tail = phead;while (tail->next != NULL){tail = tail->next;}SLTNode* newnode = BuyLTNode(x);assert(newnode != NULL);tail->next = newnode;
}

上面这种属于错误写法 

此处与上面第一种不同的点为最后连接方式是使用tail->next = newnode来建立连接的

但是我们仍然忽略了一个最重要的点:

局部变量出了函数自动销毁

此处我们定义了一个tail结构体指针来接收phead,也就是想通过tail来改变链表

但是tail属于局部变量,当尾插函数一结束,tail自动销毁,而他会对phead指向的链表更改吗?

答案是不会的

由于phead本身属于指针(SLTNode* phead),他指向链表的头结点

想要通过传参改变链表,就需要通过二级指针来传参,(SLTNode** pphead)

第一层*访问找到pphead的地址,第二层*通过地址在访问到pphead指向的链表头结点

只有这样才能通过传参改变链表

尾插(第三种):

//尾插(正确写法)
void SLPushBack(SLTNode** pphead, SLTDataType x)//使用二级指针来接收plist的地址
{SLTNode* newnode = BuyLTNode(x);assert(newnode != NULL);//链表为空if (*pphead == NULL)//对pphead进行解引用来访问pphead{*pphead = newnode;//将newnode的地址赋值给pphead}//链表非空else{SLTNode* tail = *pphead;//用结构体指针tail来接收pphead的地址(拷贝)while (tail->next != NULL){tail = tail->next;}tail->next = newnode;}
}

对于这一种写法,我们还加入了一个判断条件(判度链表是否为空)

假如链表没有任何元素时,phead为NULL,使用上面原来的方法将无法进行插入

需要分为两种情况:链表有元素,链表无元素进行讨论

之后再用结构体指针tail来接收指向头结点pphead的地址

通过二级指针来顺利改变链表


任意位置前插:

//任意位置前插入
void SLInsertBefore(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos);//判断目标位置是否为空SLTNode* prev = *pphead;if (*pphead == pos)//若目标位置为起始位置{SLPushFront(pphead, x);//头插return;}while (prev->next != pos)//找到pos节点的上一个节点地址{prev = prev->next;}SLTNode* newnode = BuyLTNode(x);prev->next = newnode;newnode->next = pos;
}

参数为头结点指针pphead,插入位置指针pos和插入元素x

老样子,先进行链表判断是否为空的操作

由于任意位置前插会出现两种情况:目标位置为起始位置、目标位置不为起始位置

对于这两种情况有不同的操作方法

起始位置:

直接进行头插即可

非起始位置:

首先要找到目标位置的上一个结点的地址,定义一个结构体指针prev来接收头结点的地址

通过while循环来自增,当prev的next指向的地址为pos的地址时

停止循环,此时prev指向的结点即为pos的上一个位置的结点

用结构体指针newnode接收开辟空间返回的新结点的地址

随后令prev->next = newnode;
           newnode->next = pos;

即完成任意位置前插的操作

注意:此种方法需要结合查的函数使用


删也分为三种方式:头删、尾删与任意位置删

头删(第一种):

//头删
void SLPopFront(SLTNode** pphead)
{assert(pphead);if (*pphead == NULL){printf("链表内无结点!");}SLTNode* head = *pphead;if (head != NULL){if (head->next == NULL)//只有一个结点{free(head);*pphead = NULL;return;}*pphead = head->next;//有多个结点free(head);}
}

由于是删除操作,需要进行链表是否为空的判断,以免出现越界访问的问题

使用结构体指针head访问*pphead达到二级指针更改结点的目的

若只有一个结点,直接释放,并令*pphead指向空(NULL)并返回

若有多个结点,则令*pphead指向head->next的地址

释放head

对于头删,还存在另一种方法

头删(第二种):

//头删
void SLPopFront(SLTNode** pphead)
{assert(pphead);if (*pphead == NULL){printf("链表内无结点!");}SLTNode* head = *pphead;//令结构体指针head指向起始位置if ((*pphead) != NULL){*pphead = (*pphead)->next;free(head);//释放head}
}

令起始位置指针指向next指向的位置
若只有一个元素,则(*pphead)->next为NULL,令*pphead为NULL,相同效果(妙哉)


尾删(第一种):

//尾删(双指针法)
void SLPopBack(SLTNode** pphead)
{if (*pphead == NULL)//判断链表是否为空{printf("链表内无结点!");}SLTNode* tail = *pphead;SLTNode* flag = NULL;//找倒数第二个结点//while (tail->next != NULL)//{//	flag = tail;//	tail = tail->next;//}while (tail->next)//指针整型可以直接做条件逻辑判断{flag = tail;tail = tail->next;}free(tail);if (flag != NULL){flag->next = NULL;}//此处加上判断是为了防止报警告
}

令结构体指针tail指向*pphead的地址

结构体指针flag指向空

当tail->next指向非空时

令falg指向tail,tail指向tail->next(此循环步骤是为了找到倒数第二个结点)

找到以后释放tail

令flag->next指向NULL(此处加一个判断是为了避免在VS运行的时候出现警告)

但是这种方法可以更加简洁

尾删(第二种)

//尾删(直接找倒数第二个节点)
void SLPopBack(SLTNode** pphead)
{if (*pphead == NULL)//判断链表是否为空{printf("链表内无结点!");}SLTNode* tail = *pphead;while ((tail->next)->next)//两次解引用,找到tail指向的结点的next指向的结点的next{tail = tail->next;}free((tail->next));//更加巧妙tail->next = NULL;
}

相比于第一种用两个指针来操作,这种方法显得更加简洁

使用两次解引用,找到tail指向的结点的next指向的结点的next

显得更加巧妙

但是以上两种尾删的方法都是不完美的,当出现只有一个元素的情况是该如何处理呢?

尾删(第三种):

//尾删(分两种情况)
void SLPopBack(SLTNode** pphead)
{assert(pphead);if (*pphead == NULL)//判断链表是否为空{printf("链表内无结点!");}SLTNode* tail = *pphead;if (tail != NULL){if (tail->next == NULL)//当前结点为唯一结点{free(tail);*pphead = NULL;return;}while ((tail->next)->next)//当前节点非唯一结点{tail = tail->next;}free((tail->next));tail->next = NULL;}//此处的判断是为了防止报警告
}

首先进行判断链表是否为空的操作

令结构体指针tail接收*pphead

进行判断操作:

若当前tail->next指向的为空,则代表链表只有tail这一个结点,直接释放tail,并将*pphead置空,返回

若tail并非当前唯一结点,则令tail指向tail->next,释放tail->next,并将tail->next置空,避免出现悬空指针


任意位置删除:

//任意位置删除
void SLErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(pos);//判断目标位置是否为空if (pos == *pphead)//判断是否为头{SLPopFront(pphead);return;}SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;
}

参数为头结点pphead、目标位置pos

首先判断pos位置是否为头结点

是则使用头删

否则定义一个结构体指针prev接收*pphead

循环令prev指向pos的上一个结点的位置

令prev->next指向pos->next的位置

释放pos,并将pos置空


//查
SLTNode* STFind(SLTNode* phead, SLTDataType x)
{SLTNode* cur = phead;while (cur)//遍历链表查找{if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}

使用遍历链表的方法查找

定义结构体指针cur接收phead

循环遍历链表,当cur不为NULL时

cur指向cur->next的位置

若其中cur->data为目标元素,返回cur,否则返回空


完整代码如下:

//SList.htypedef int SLTDataType;typedef struct SListNode
{SLTDataType data;//存放数据struct SListNode* next;//指向下一段的结构体指针
}SLTNode;void SLTPrint(SLTNode* phead);//打印
SLTNode* BuyLTNode();//开辟空间void SLPushFront(SLTNode** pphead, SLTDataType x);//头插
void SLPushBack(SLTNode** pphead, SLTDataType x);//尾插void SLPopFront(SLTNode** pphead);//头删
void SLPopBack(SLTNode** pphead);//尾删SLTNode* STFind(SLTNode* phead, SLTDataType x);//查找void SLInsertBefore(SLTNode** pphead, SLTNode* pos, SLTDataType x);//任意位置前插入
void SLInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x);//任意位置后插入void SLErase(SLTNode** pphead, SLTNode* pos);//任意位置删除
SList.c
#define _CRT_SECURE_NO_WARNINGS 1#include "SList.h"void SLTPrint(SLTNode* phead)//打印链表(遍历)
{SLTNode* cur = phead;//令cur等于当前指针(拷贝)while (cur != NULL){printf("%d -> ", cur->data);cur = cur->next;//令cur指向下一段的地址}printf("NULL\n");
}//开辟空间
SLTNode* BuyLTNode(SLTDataType x)
{SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));//开辟一个新的SLTNode结构体的空间,将其强制转换为SLTNode*类型,并将地址传给newnodeif (newnode == NULL){perror("malloc fail");return NULL;}//判断是否开辟空间成功newnode->data = x;newnode->next = NULL;//对newnode进行初始化return newnode;
}//头插
void SLPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);//判断是否接收到plist的地址(pphead是头指针plist的地址)SLTNode* newnode = BuyLTNode(x);assert(newnode != NULL);//判断是否开辟成功newnode->next = *pphead;//令结点指针指向原先的结点(通过pphead的地址修改)*pphead = newnode;//令phead指针指向新结点(通过pphead的地址修改,其实是改变plist)
}/*
//尾插(错误写法1)
void SLPushBack(SLTNode* phead, SLTDataType x)
{SLTNode* tail = phead;while (tail != NULL){tail = tail->next;}SLTNode* newnode = BuyLTNode(x);assert(newnode != NULL);//判断是否开辟成功tail = newnode;
}//本质上tail并没有与链表连接起来(局部变量出了函数销毁)
*//*
//尾插(错误写法2)
void SLPushBack(SLTNode* phead, SLTDataType x)
{SLTNode* tail = phead;while (tail->next != NULL){tail = tail->next;}SLTNode* newnode = BuyLTNode(x);assert(newnode != NULL);tail->next = newnode;//假如链表没有任何元素时,phead为NULL,使用上面的方法将无法进行插入//需要分为两种情况:链表有元素,链表无元素进行讨论
}
*//*
//尾插(错误写法3)
void SLPushBack(SLTNode* phead, SLTDataType x)
{SLTNode* newnode = BuyLTNode(x);assert(newnode != NULL);//链表为空if (phead == NULL){phead = newnode;}//链表非空else{SLTNode* tail = phead;while (tail->next != NULL){tail = tail->next;}tail->next = newnode;}//由于plist传参,phead无法改变plist(局部变量),所以需要使用地址传参来改变plist
}
*///尾插(正确写法)
void SLPushBack(SLTNode** pphead, SLTDataType x)//使用二级指针来接收plist的地址
{SLTNode* newnode = BuyLTNode(x);assert(newnode != NULL);//链表为空if (*pphead == NULL)//对pphead进行解引用来访问plist{*pphead = newnode;//将newnode的地址赋值给plist}//链表非空else{SLTNode* tail = *pphead;//用结构体指针tail来接收plist的地址(拷贝)while (tail->next != NULL){tail = tail->next;}tail->next = newnode;}
}/*
//头删(方法1)
void SLPopFront(SLTNode** pphead)
{assert(pphead);if (*pphead == NULL){printf("链表内无结点!");}SLTNode* head = *pphead;if (head != NULL){if (head->next == NULL)//只有一个结点{free(head);*pphead = NULL;return;}*pphead = head->next;//有多个结点free(head);}
}
*///头删(方法2)
void SLPopFront(SLTNode** pphead)
{assert(pphead);if (*pphead == NULL){printf("链表内无结点!");}SLTNode* head = *pphead;//令结构体指针head指向起始位置if ((*pphead) != NULL){*pphead = (*pphead)->next;//令起始位置指针指向next指向的位置//若只有一个元素,则(*pphead)->next为NULL,令*pphead为NULL,相同效果free(head);//释放head}
}/*
//尾删(方法1,双指针)
void SLPopBack(SLTNode** pphead)
{if (*pphead == NULL)//判断链表是否为空{printf("链表内无结点!");}SLTNode* tail = *pphead;SLTNode* flag = NULL;//找倒数第二个结点//while (tail->next != NULL)//{//	flag = tail;//	tail = tail->next;//}while (tail->next)//指针整型可以直接做条件逻辑判断{flag = tail;tail = tail->next;}free(tail);if (flag != NULL){flag->next = NULL;}//此处加上判断是为了防止报警告
}
*//*
//尾删(方法2,直接找倒数第二个节点)
void SLPopBack(SLTNode** pphead)
{if (*pphead == NULL)//判断链表是否为空{printf("链表内无结点!");}SLTNode* tail = *pphead;while ((tail->next)->next)//两次解引用,找到tail指向的结点的next指向的结点的next{tail = tail->next;}free((tail->next));//更加巧妙tail->next = NULL;
}
*///以上两种尾删的方法都是不完美的,当出现只有一个元素的情况是该如何处理呢?//尾删(方法3,分两种情况)
void SLPopBack(SLTNode** pphead)
{assert(pphead);if (*pphead == NULL)//判断链表是否为空{printf("链表内无结点!");}SLTNode* tail = *pphead;if (tail != NULL){if (tail->next == NULL)//当前结点为唯一结点{free(tail);*pphead = NULL;return;}while ((tail->next)->next)//当前节点非唯一结点{tail = tail->next;}free((tail->next));tail->next = NULL;}//此处的判断是为了防止报警告
}//查
SLTNode* STFind(SLTNode* phead, SLTDataType x)
{SLTNode* cur = phead;while (cur)//遍历链表查找{if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}//任意位置前插入
void SLInsertBefore(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos);//判断目标位置是否为空SLTNode* prev = *pphead;if (*pphead == pos)//若目标位置为起始位置{SLPushFront(pphead, x);//头插return;}while (prev->next != pos)//找到pos节点的上一个节点地址{prev = prev->next;}SLTNode* newnode = BuyLTNode(x);prev->next = newnode;newnode->next = pos;
}//任意位置后插入
void SLInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos);SLTNode* newnode = BuyLTNode(x);if (pos->next == NULL)//如果目标位置为最后一个节点{pos->next = newnode;newnode->next = NULL;return;}newnode->next = pos->next;pos->next = newnode;
}//任意位置删除
void SLErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(pos);//判断目标位置是否为空if (pos == *pphead)//判断是否为头{SLPopFront(pphead);return;}SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);pos = NULL;
}
//test.c#define _CRT_SECURE_NO_WARNINGS 1#include "SList.h"void test1()
{SLTNode* pplist = NULL;SLPushFront(&pplist, 1);SLPushFront(&pplist, 2);SLPushFront(&pplist, 3);SLPushFront(&pplist, 4);SLTPrint(pplist);//ps:此处需要通过pphead的地址修改
}void test2()
{SLTNode* pplist = NULL;SLPushFront(&pplist, 1);SLPushFront(&pplist, 2);SLPushFront(&pplist, 3);SLPushFront(&pplist, 4);SLPushBack(&pplist, 5);SLPushBack(&pplist, 6);SLPushBack(&pplist, 7);SLPushBack(&pplist, 8);SLTPrint(pplist);
}void test3()
{SLTNode* plist = NULL;SLPushBack(&plist, 5);SLPushBack(&plist, 6);SLPushBack(&plist, 7);SLPushBack(&plist, 8);SLTPrint(plist);
}void test4()//测试尾插
{SLTNode* pplist = NULL;SLPushBack(&pplist, 5);SLPushBack(&pplist, 6);SLPushBack(&pplist, 7);SLPushBack(&pplist, 8);SLTPrint(pplist);
}void test5()//测试尾删
{SLTNode* pplist = NULL;SLPushBack(&pplist, 5);SLPushBack(&pplist, 6);SLPushBack(&pplist, 7);SLPushBack(&pplist, 8);SLPopBack(&pplist);SLPopBack(&pplist);SLPopBack(&pplist);SLTPrint(pplist);
}void test6()//测试尾删
{SLTNode* pplist = NULL;SLPushBack(&pplist, 5);SLPushBack(&pplist, 6);SLPushBack(&pplist, 7);SLPushBack(&pplist, 8);SLPopBack(&pplist);SLPopBack(&pplist);SLPopBack(&pplist);SLPopBack(&pplist);//SLPopBack(&pplist);SLTPrint(pplist);
}void test7()//测试头删
{SLTNode* pplist = NULL;SLPushBack(&pplist, 5);SLPushBack(&pplist, 6);SLPushBack(&pplist, 7);SLPushBack(&pplist, 8);SLPopFront(&pplist);SLPopFront(&pplist);SLPopFront(&pplist);//SLPopFront(&pplist);//SLPopFront(&pplist);SLTPrint(pplist);
}void test8()//测试查找
{SLTNode* pplist = NULL;SLPushBack(&pplist, 5);SLPushBack(&pplist, 6);SLPushBack(&pplist, 7);SLPushBack(&pplist, 8);SLTNode* pos = STFind(pplist, 10);assert(pos);//判断是否为空printf("%d %p", pos->data, pos->next);
}void test9()//测试任意位置前插入
{SLTNode* pplist = NULL;SLPushBack(&pplist, 5);SLPushBack(&pplist, 6);SLPushBack(&pplist, 7);SLPushBack(&pplist, 8);SLTNode* pos1 = STFind(pplist, 5);SLInsertBefore(&pplist, pos1, 66);//在5前插入SLTNode* pos2 = STFind(pplist, 7);SLInsertBefore(&pplist, pos2, 88);//在7前插入SLTPrint(pplist);
}void test10()//测试任意位置后插入
{SLTNode* pplist = NULL;SLPushBack(&pplist, 5);SLPushBack(&pplist, 6);SLPushBack(&pplist, 7);SLPushBack(&pplist, 8);SLTNode* pos1 = STFind(pplist, 5);SLInsertAfter(&pplist, pos1, 66);//在5后插入SLTNode* pos2 = STFind(pplist, 7);SLInsertAfter(&pplist, pos2, 88);//在7后插入SLTPrint(pplist);
}void test11()//测试任意删除
{SLTNode* pplist = NULL;SLPushBack(&pplist, 5);SLPushBack(&pplist, 6);SLPushBack(&pplist, 7);SLPushBack(&pplist, 8);SLTNode* pos1 = STFind(pplist, 5);SLErase(&pplist, pos1);SLTNode* pos2 = STFind(pplist, 7);SLErase(&pplist, pos2);SLTPrint(pplist);
}//记住你要改变的是什么东西
int main()
{//test1();//test2();//test3();//test4();//test5();//test6();//test7();//test8();//test9();//test10();test11();return 0;
}

相关文章:

线性表,顺序表,链表

线性表 线性表&#xff08;linear list&#xff09;是n个具有相同特性的数据元素的有限序列 线性表是一种在实际中广泛使 用的数据结构&#xff0c;常见的线性表&#xff1a;顺序表、链表、栈、队列、字符串... 线性表在逻辑上是线性结构&#xff0c;也就说是连续的一条直线 …...

洛谷 P2782 友好城市 线性DP 最长上升子序列 二分查找 lower_bound

&#x1f351; 算法题解专栏 &#x1f351; 洛谷&#xff1a;友好城市 题目描述 有一条横贯东西的大河&#xff0c;河有笔直的南北两岸&#xff0c;岸上各有位置各不相同的N个城市。北岸的每个城市有且仅有一个友好城市在南岸&#xff0c;而且不同城市的友好城市不相同。每对…...

easyexcel读取excel合并单元格数据

普通的excel列表&#xff0c;easyexcel读取是没有什么问题的。但是&#xff0c;如果有合并单元格&#xff0c;那么它读取的时候&#xff0c;能获取数据&#xff0c;但是数据是不完整的。如下所示的单元格数据&#xff1a; 我们通过简单的异步读取&#xff0c;最后查看数据内容&…...

2023哪款蓝牙耳机性价比高?200左右高性价比蓝牙耳机推荐

现如今的蓝牙耳机越来越多&#xff0c;人们在选择时不免纠结&#xff0c;不知道选什么蓝牙耳机比较好&#xff1f;针对这个问题&#xff0c;我来给大家推荐几款性价比高的蓝牙耳机&#xff0c;一起来看看吧。 一、南卡小音舱Lite2蓝牙耳机 参考价&#xff1a;299 蓝牙版本&am…...

Java代码弱点与修复之——Masked Field(掩码字段)

弱点描述 MF: Masked Field (FB.MF_CLASS_MASKS_FIELD) 是 FindBugs 代码分析工具的一个警告信息, 属于中风险的代码弱点。 Masked Field,翻译过来是掩码字段, 字段可以理解为属性, 那么掩码是什么意思呢? 掩码是什么? 掩码是一串二进制代码对目标字段进行位与运算,屏…...

C语言编程入门之刷题篇(C语言130题)(8)

&#xff08;题目标题可以直接跳转此题链接&#xff09; BC72 平均身高 描述 从键盘输入5个人的身高&#xff08;米&#xff09;&#xff0c;求他们的平均身高&#xff08;米&#xff09;。 输入描述&#xff1a; 一行&#xff0c;连续输入5个身高&#xff08;范围0.00~2.00…...

QML动画类型总结

目录 一 常用动画 二 特殊场景动画 一 常用动画 有几种类型的动画&#xff0c;每一种都在特定情况下都有最佳的效果&#xff0c;下面列出了一些常用的动画&#xff1a; 1、PropertyAnimation&#xff08;属性动画&#xff09;- 使用属性值改变播放的动画&#xff1b; 2、Num…...

编译一个魔兽世界开源服务端Windows需要安装什么环境

编译一个魔兽世界开源服务端Windows需要安装什么环境 大家好我是艾西&#xff0c;去年十月份左右wy和bx发布了在停服的公告。当时不少小伙伴都在担心如果停服了怎么办&#xff0c;魔兽这游戏伴随着我们渡过了太多的时光。但已经发生的事情我们只能顺其自然的等待GF的消息就好了…...

HTML5字体集合的实践经验

随着互联网的发展&#xff0c;网站已成为人们获取信息和交流的重要平台。而一个好的网站&#xff0c;不仅需要有美观的界面&#xff0c;还需要有良好的用户体验。其中&#xff0c;字体是影响用户体验的一个重要因素。下面就让我们来看看HTML字体集合的相关内容。 HTML字体集合是…...

Mybatis 框架 ( 一 ) 基本步骤

1.概念 1.1.什么是Mybatis框架 &#xff08;1&#xff09;Mybatis是一个半ORM&#xff08;Object Relation Mapping 对象关系映射&#xff09;框架&#xff0c;它内部封装了JDBC&#xff0c;开发时只需要关注SQL语句本身&#xff0c;不需要花费精力去处理加载驱动、创建连接、…...

【华为OD机试真题】We Are A Team(C++javapython)100%通过率 超详细代码注释 代码优化

We Are A Team 题目描述: 总共有n个人在机房,每个人有一个标号(1<=标号<=n) ,他们分成了多个团队,需要你根据收到的m条消息判定指定的两个人是否在 一个团队中,具体的: 1、消息构成为abc,整数a、b分别代表两个人的标号,整数C代表指令 2、c = = 0 代表a和b在一…...

Oracle_Workflow_Builder工作流工具(一)

简介 目标WORKFLOW是oracle 公司的一个标准产品&#xff0c;它通过图形化的方式来表达业务处理过程。用户使用工作流可以灵活地定义或更改流程的结构。WORKFLOW是建立在数据库基础上的一个应用&#xff0c;它由后台的数据对象和前台的客户端程序组成。本文档主要介绍工作流的基…...

JavaWeb学习--RequestResponse

目录 JavaWeb学习--Request&Response 1&#xff0c;Request和Response的概述 request:获取请求数据 response:设置响应数据 **小结** 2&#xff0c;Request对象 **小结** 2.2 Request获取请求数据 **小结** 2.4 请求参数中文乱码问题 URL编码 2.5 Request请求转…...

Linux cat 命令

cat&#xff08;英文全拼&#xff1a;concatenate&#xff09;命令用于连接文件并打印到标准输出设备上。 使用权限 所有使用者 语法格式 cat [-AbeEnstTuv] [--help] [--version] fileName 参数说明&#xff1a; -n 或 --number&#xff1a;由 1 开始对所有输出的行数编…...

力扣sql中等篇练习(十四)

力扣sql中等篇练习(十四) 1 最后一个能进入电梯的人 1.1 题目内容 1.1.1 基本题目信息 1.1.2 示例输入输出 1.2 示例sql语句 # 在表某一个范围内的可以考虑自连接的方式,注意连接的表只需要精准的字段 # 需要排序是因为它需要找到最后一个上车的用户 SELECT q1.person_name…...

什么是Spring FactoryBean?有什么作用?

1、什么是Spring Spring是分层的 Java SE/EE应用 full-stack 轻量级开源框架&#xff0c;以 IOC和AOP为内核。含有七大核心模块 2、Spring的七大模块 (1)Spring Core&#xff1a;核心容器提供了Spring的基本功能。核心容器的核心功能是用IOC 容器来管理类的依赖关系&#xff…...

Python List pop()方法

在Python中&#xff0c;列表&#xff08;list&#xff09;是一种有序的可变集合&#xff0c;可以包含任何数据类型的元素。列表对象提供了许多方法来处理列表中的元素&#xff0c;其中之一是pop()方法。 pop()方法用于从列表中移除并返回指定位置的元素。如果不指定位置&#…...

HJ51 输出单向链表中倒数第k个结点

写在前面&#xff1a; 做题环境如下&#xff1a; 题目渠道&#xff1a;牛客网 HJ51 输出单向链表中倒数第k个结点 华为机试题 编程语言&#xff1a;C 一、题目描述 描述 输入一个单向链表&#xff0c;输出该链表中倒数第k个结点&#xff0c;链表的倒数第1个结点为链表的尾指针…...

c#笔记-内置类型

内置类型 内置类型是一些有关键字表示的类型。关键字具有非常高的优先级&#xff0c;可以让你在没有别的配置的情况下&#xff0c; 只要用的是c#就可以使用。这也意味着这些类型是非常重要&#xff0c;或是基本的东西。 整数&#xff1a;byte, sbyte, short, ushort, int, ui…...

功能齐全的 DIY ESP32 智能手表设计之原理图讲解一

相关设计资料下载ESP32 智能手表带心率、指南针设计资料(包含Arduino源码+原理图+Gerber+3D文件).zip 目录 USB部分原理图讲解 供电部分原理图讲解 USB转串口原理图讲解...

【Oracle APEX开发小技巧12】

有如下需求&#xff1a; 有一个问题反馈页面&#xff0c;要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据&#xff0c;方便管理员及时处理反馈。 我的方法&#xff1a;直接将逻辑写在SQL中&#xff0c;这样可以直接在页面展示 完整代码&#xff1a; SELECTSF.FE…...

iPhone密码忘记了办?iPhoneUnlocker,iPhone解锁工具Aiseesoft iPhone Unlocker 高级注册版​分享

平时用 iPhone 的时候&#xff0c;难免会碰到解锁的麻烦事。比如密码忘了、人脸识别 / 指纹识别突然不灵&#xff0c;或者买了二手 iPhone 却被原来的 iCloud 账号锁住&#xff0c;这时候就需要靠谱的解锁工具来帮忙了。Aiseesoft iPhone Unlocker 就是专门解决这些问题的软件&…...

Nginx server_name 配置说明

Nginx 是一个高性能的反向代理和负载均衡服务器&#xff0c;其核心配置之一是 server 块中的 server_name 指令。server_name 决定了 Nginx 如何根据客户端请求的 Host 头匹配对应的虚拟主机&#xff08;Virtual Host&#xff09;。 1. 简介 Nginx 使用 server_name 指令来确定…...

鸿蒙DevEco Studio HarmonyOS 5跑酷小游戏实现指南

1. 项目概述 本跑酷小游戏基于鸿蒙HarmonyOS 5开发&#xff0c;使用DevEco Studio作为开发工具&#xff0c;采用Java语言实现&#xff0c;包含角色控制、障碍物生成和分数计算系统。 2. 项目结构 /src/main/java/com/example/runner/├── MainAbilitySlice.java // 主界…...

JS设计模式(4):观察者模式

JS设计模式(4):观察者模式 一、引入 在开发中&#xff0c;我们经常会遇到这样的场景&#xff1a;一个对象的状态变化需要自动通知其他对象&#xff0c;比如&#xff1a; 电商平台中&#xff0c;商品库存变化时需要通知所有订阅该商品的用户&#xff1b;新闻网站中&#xff0…...

【C++进阶篇】智能指针

C内存管理终极指南&#xff1a;智能指针从入门到源码剖析 一. 智能指针1.1 auto_ptr1.2 unique_ptr1.3 shared_ptr1.4 make_shared 二. 原理三. shared_ptr循环引用问题三. 线程安全问题四. 内存泄漏4.1 什么是内存泄漏4.2 危害4.3 避免内存泄漏 五. 最后 一. 智能指针 智能指…...

解读《网络安全法》最新修订,把握网络安全新趋势

《网络安全法》自2017年施行以来&#xff0c;在维护网络空间安全方面发挥了重要作用。但随着网络环境的日益复杂&#xff0c;网络攻击、数据泄露等事件频发&#xff0c;现行法律已难以完全适应新的风险挑战。 2025年3月28日&#xff0c;国家网信办会同相关部门起草了《网络安全…...

9-Oracle 23 ai Vector Search 特性 知识准备

很多小伙伴是不是参加了 免费认证课程&#xff08;限时至2025/5/15&#xff09; Oracle AI Vector Search 1Z0-184-25考试&#xff0c;都顺利拿到certified了没。 各行各业的AI 大模型的到来&#xff0c;传统的数据库中的SQL还能不能打&#xff0c;结构化和非结构的话数据如何和…...

Java中栈的多种实现类详解

Java中栈的多种实现类详解&#xff1a;Stack、LinkedList与ArrayDeque全方位对比 前言一、Stack类——Java最早的栈实现1.1 Stack类简介1.2 常用方法1.3 优缺点分析 二、LinkedList类——灵活的双端链表2.1 LinkedList类简介2.2 常用方法2.3 优缺点分析 三、ArrayDeque类——高…...

多模态学习路线(2)——DL基础系列

目录 前言 一、归一化 1. Layer Normalization (LN) 2. Batch Normalization (BN) 3. Instance Normalization (IN) 4. Group Normalization (GN) 5. Root Mean Square Normalization&#xff08;RMSNorm&#xff09; 二、激活函数 1. Sigmoid激活函数&#xff08;二分类&…...