DS线性表之链表

前言

我们上一期介绍了顺序表,它的底层就是数组,我们也分别对顺序表的动态版本和静态版本进行了实现!并且分析了顺序表的优缺点,优点是:尾插、尾删效率很高,其时间复杂度是O(1);缺点是:在头部插入、删除的时候效率低,其时间复杂度是O(N);而且即使是动态版本的扩容也是会浪费空间的(这里在动态内存管理介绍realloc时专门介绍过)!!这个上期的最后也介绍了!我们想有没有一种数据结构,用一个添加一个,做到最起码的空间不浪费呢?答案是:有的~!他就是我们本期介绍的链表!

本期内容介绍

什么是链表

链表的分类

单链表的实现(不带头)

单链表的实现(带头)

带头双向循环链表的实现

链表和顺序表的区别

目录

前言

本期内容介绍

一、什么是链表

二、链表的分类

三、单链表的实现(不带头)

链表节点类型申明

动态申请一个节点

链表的打印

链表的销毁

单链表尾插

单链表尾删

单链表头插

单链表头删

单链表查找

在pos前插入x

在pos后插入x

删除pos位置

删除pos位置的下一个

全部源码:

四、单链表的实现(带头)

链表结点类型声明

链表初始化

链表的销毁

链表的打印

动态开辟一个节点

单链表尾插

单链表的尾删

单链表头插

单链表的头删

单链表查找

单链表修改

在pos前插入

在pos后插入

删除pos位置

删除pos后一个

链表的长度

总结:

全部源码:

五、带头双向循环链表的实现

结点类型声明

初始化

销毁

打印

动态开辟新节点

尾插

尾删

头插

头删 

查找

修改

在pos前插入

在pos后插入

删除pos

删除pos的下一个

获取长度

六、链表和顺序表的区别

一、什么是链表

链表是一种物理存储结构上连续(非顺序存储的结构),逻辑结构上连续的一种存储数据的结构!它的逻辑连续是由他里面的指针实现的~!

我们在结构体那一期介绍过一个东西叫结构体的自引用,他实质上就是链表的声明!OK,我们来个栗子回忆一下:

struct Node
{int data;struct Node* next;
};

这就是一个链表的申明,也就是结构体的自引用~!他这里一般把data叫做数据域,next叫做指针域!链表的指针域是指向下一个同类型的节点~!什么意思呢?我们再来画个图理解一下:

这个图是不是很清楚的反映了上了上面的所有的点,物理结构非连续的(因为是malloc的节点在堆上,它的内存时操作系统随机分配的可能是连续的但大概率是不连续的),他们每个节点的地址都不连续,逻辑结构连续是因为当前节点的next存的是下一个节点的地址,这样只要第一个节点的地址就可以访问链表的所有节点了~!

这就和火车一样一节连一节:

二、链表的分类

链表的结构有8种,是否带头节点,单双向,是否循环~的组合结果!

在这里我们先介绍不带头的单链表,再介绍带头的单链表,最后在实现链表中最优秀的结构---》带头双向循环链表!这三种如果熟练掌握其他就都是小卡拉米~!OK,我们还是画一下,认识一下:

不带头:

带头:

这就是链表的8种结构的图!其实常见的或常用的就两种无头单链表和双向循环链表!但单链表不会单独做什么存储工作,他会配合其他的去实现更高阶的数据结构例如就是实现哈希桶和图的邻接表等;

而双向循环链表是会单独存储数据的,例如你每天刷的抖音是不是有访客呀~!它实际上底层就是双向循环链表,每次来一个方可头插一个,若一个人多次访问你的主页,也不过是改一下指针的事~!

三、单链表的实现(不带头)

OK,我们还是和往期一样采用多文件编程,申明等都放在.h文件里, 实现放在.c的文件, 测试在test.c中。

一开始我们得先声明一下链表的每个节点的类型吧!

链表节点类型申明

typedef int SLDataType;typedef struct SListNode
{SLDataType data;struct SListNode* next;
}SListNode;

动态申请一个节点

这里由于是不带头的所以就不用初始化的,链表是1个或多个节点组成的,我们只要在申请的时候处理好它的next就OK!我们把next 一开始申请成功后置为NULL,这样即使是最后一个也不用单独处理为NULL了!

//动态申请一个节点
SListNode* BuySListNode(SLDataType x)
{SListNode* newnode = (SListNode*)malloc (sizeof(SListNode));if (newnode == NULL){perror("malloc failed");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}

链表的打印

//打印链表
void SListPrint(SListNode* head)
{SListNode* cur = head;while (cur){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}

链表的销毁

为了使程序的健壮性更高,以及防止程序出现内存泄漏,我们要在操作完链表后记得销毁~!

这里从头开始,把每一个节点free即可,但这里由于是不带头的,所以得用二级指针!!!这里head本质是形参,改变形参不改变实参,要改变形参影响实参,我们得传实参的地址!

最后free完了记得把头置空!否则就是野指针!

//单链表的销毁
void SListDestory(SListNode** head)
{assert(head);SListNode* cur = *head;while (cur){SListNode* next = cur->next;free(cur);cur = next;}*head = NULL;
}

单链表尾插

这里我们先来个硬菜不带头节点的单链表的尾插!这里还是得用二级指针,因为一开始如果链表为空第一个插入的就得是头,head就得指向他,所以改变指针的指向要用二级指针。这里head是二级指针,即使外面的链表为空他也不可能为空,因此断言一下!然后新开一个节点,判断是否是第一个插入。如果是,直接让head指向他,否则找到尾节点后插入即可~!

//尾插
void SListPushBack(SListNode** head, SLDataType x)
{assert(head);SListNode* node = BuySListNode(x);if (*head == NULL){*head = node;}else{SListNode* cur = *head;while (cur->next){cur = cur->next;}cur->next = node;}}

单链表尾删

删除这里一开始想到的就是判断当前链表是否为空!如果为空了就不要删了,判断的方式还是和以前一样,“温柔型”和“暴力型”,小编就比较喜欢暴力型~!然后这里也要考虑删完的情况,也即是head的处理,如果把最后一个节点都删了就把head置为空,而这里的head是形参,要改变它得用它的指针也即是二级~!

//尾删
void SListPopBack(SListNode** head)
{assert(head);assert(*head);//没有节点可删if ((*head)->next == NULL){free(*head);*head = NULL;}else{SListNode* cur = *head;while (cur->next->next){cur = cur->next;}free(cur->next);cur->next = NULL;}
}

当然这里常规的删除还有一种写法,即快慢指针法:用个prv前驱指针记录它的前面的一个节点。当cur->next不等于NULL时把cur赋值给prv,当cur->next为空时free掉cur,prv->next 置空即可!

//尾删
void SListPopBack(SListNode** head)
{assert(head);assert(*head);//没有节点可删if ((*head)->next == NULL){free(*head);*head = NULL;}else{/*SListNode* cur = *head;while (cur->next->next){cur = cur->next;}free(cur->next);cur->next = NULL;*/SListNode* cur = *head;SListNode* prv = NULL;while (cur->next){prv = cur;cur = cur->next;}free(cur);prv->next = NULL;}
}

OK,我们还是养成好习惯写点测试一点:

void testBack()
{SListNode* s = NULL;//这里不置空就是野指针,会直接奔溃掉SListPushBack(&s, 1);SListPushBack(&s, 2);SListPushBack(&s, 3);SListPushBack(&s, 4);SListPushBack(&s, 5);SListPrint(s);SListPopBack(&s);SListPrint(s);SListPopBack(&s);SListPrint(s);SListPopBack(&s);SListPrint(s);SListPopBack(&s);SListPrint(s);SListPopBack(&s);SListPrint(s);//SListPopBack(&s);//测试尾删是否有Bug//SListPrint(s);SListDestory(&s);
}

OK,尾插尾删没有问题!我们下来在来搞一下头插头删!

单链表头插

头插相对简单,新开的节点next连接head在让head指向新节点。但是还时要注意,一开始为空链表的情况,所以得用二级指针搞~!

//头插
void SListPushFront(SListNode** head, SLDataType x)
{assert(head);SListNode* node = BuySListNode(x);if (*head == NULL){*head = node;}else{node->next = *head;*head = node;}
}

单链表头删

这里删除首先要注意为空的情况,其次只有一个节点的情况,删掉这一个节点把head置空,这里也要改变head的值所以要用二级指针。正常删除就是保存当前节点的下一个节点,free当前节点在把刚保存的下一个节点赋值给当前节点~!

//头删
void SListPopFront(SListNode** head)
{assert(head);assert(*head);//没有节点可删if ((*head)->next == NULL){free(*head);*head = NULL;}else{SListNode* tail = (*head)->next;free(*head);*head = tail;}
}

OK,还是来测试一下:

void testFront()
{SListNode* s = NULL;SListPushFront(&s, 1);SListPushFront(&s, 2);SListPushFront(&s, 3);SListPushFront(&s, 4);SListPushFront(&s, 5);SListPrint(s);SListPopFront(&s);SListPrint(s);SListPopFront(&s);SListPrint(s);SListPopFront(&s);SListPrint(s);SListPopFront(&s);SListPrint(s);SListPopFront(&s);SListPrint(s);//SListPopFront(&s);//测试头删是是否有Bug//SListPrint(s);SListDestory(&s);
}

OK,没有问题,我们在来实现一下查找和在pos前插入和pos后插入!

单链表查找

这里就很简单了,找到了返回该节点的地址,否则返回NULL即可!这里仅仅是查找不改变内容所以一级指针即可!

//单链表的查找
SListNode* SListFind(SListNode* head, SLDataType x)
{SListNode* cur = head;while (cur){if (x == cur->data)return cur;cur = cur->next;}return NULL;
}

在pos前插入x

这里是在pos位置前插入一个x,有两种写法。这里也得注意第一个插入的情况!所以得用二级指针!第一种是直接手动实现没有借助一点外力,第二种调用了头插解决第一次插入的情况~!而且注意的是第一种你必须加break插入后跳出否则就是死循环,第二种改变不用担心这种情况~!

//在pos位置前插入
void SListInsert(SListNode** head, SListNode* pos, SLDataType x)
{assert(head);assert(pos);SListNode* node = BuySListNode(x);if (*head == NULL){node->next = *head;*head = node;}else{SListNode* cur = *head;while (cur){if (cur->next == pos){node->next = pos;cur->next = node;break;}cur = cur->next;}}}
//在pos位置前插入
void SListInsert(SListNode** head, SListNode* pos, SLDataType x)
{assert(head);assert(pos);if (*head == NULL){SListPushFront(head, x);}else{SListNode* cur = *head;while (cur->next != pos){cur = cur->next;}SListNode* node = BuySListNode(x);node->next = pos;cur->next = node;}}

在pos后插入x

这个接口就更简单了,只需要让pos->next 连接到node->next然后再让pos->next 链接上node即可~!也不用担心pos->next 为空的情况!

//在pos位置后插入
void SListInsertAfter(SListNode* pos, SLDataType x)
{assert(pos);SListNode* node = BuySListNode(x);node->next = pos->next;pos->next = node;
}

OK,还是来测试一下:

testInsert()
{SListNode* s = NULL;SListPushFront(&s, 1);SListPushFront(&s, 2);SListPushFront(&s, 3);SListPushFront(&s, 4);SListPushFront(&s, 5);SListPrint(s);SListInsert(&s, SListFind(s, 1), 10);SListPrint(s);SListInsert(&s, SListFind(s, 1), 100);SListPrint(s);SListInsertAfter(SListFind(s, 4), 40);SListPrint(s);SListDestory(&s);
}

OK,没有问题,我们在来实现一下删除pos位置和删除pos位置的后一个节点~!

删除pos位置

这个接口就是当头和pos相同时,可以直接调用头删不用自己多判断。否则就遍历找pos然后删除~~!

//删除pos位置
void SListErase(SListNode** head, SListNode* pos)
{assert(head);assert(*head);//空了就不要删了assert(pos);if (*head == pos){SListPopFront(head);}else{SListNode* cur = *head;while (cur->next != pos){cur = cur->next;}cur->next = pos->next;free(pos);}}

删除pos位置的下一个

这个得保证有pos和有pos的下一个,然后保存pos的下一个,pos的next 连接上pos->next->next 然后删掉保存的pos的下一个即可~!

//删除pos后一个位置
void SListEraseAfter(SListNode* pos)
{assert(pos);assert(pos->next);SListNode* Pnext = pos->next;pos->next = Pnext->next;free(Pnext);
}

OK,测试一下:

void testErase()
{SListNode* s = NULL;//这里不置空就是野指针,会直接奔溃掉SListPushBack(&s, 1);SListPushBack(&s, 2);SListPushBack(&s, 3);SListPushBack(&s, 4);SListPushBack(&s, 5);SListPrint(s);SListErase(&s, SListFind(s, 1));SListPrint(s);SListErase(&s, SListFind(s, 2));SListPrint(s);SListEraseAfter(SListFind(s, 3));SListPrint(s);SListDestory(&s);
}

OK,没有问题~!

全部源码:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>typedef int SLDataType;typedef struct SListNode
{SLDataType data;struct SListNode* next;
}SListNode;//动态申请一个节点
SListNode* BuySListNode(SLDataType x);//打印链表
void SListPrint(SListNode* head);//单链表的销毁
void SListDestory(SListNode** head);//尾插
void SListPushBack(SListNode** head, SLDataType x);//尾删
void SListPopBack(SListNode** head);//头插
void SListPushFront(SListNode** head, SLDataType x);//头删
void SListPopFront(SListNode** head);//单链表的查找
SListNode* SListFind(SListNode* head, SLDataType x);//在pos位置前插入
void SListInsert(SListNode** head, SListNode* pos, SLDataType x);//在pos位置后插入
void SListInsertAfter(SListNode* pos, SLDataType x);//删除pos位置
void SListErase(SListNode** head, SListNode* pos);//删除pos后一个位置
void SListEraseAfter(SListNode* pos);
#include "SListNode.h"//动态申请一个节点
SListNode* BuySListNode(SLDataType x)
{SListNode* newnode = (SListNode*)malloc (sizeof(SListNode));if (newnode == NULL){perror("malloc failed");exit(-1);}newnode->data = x;newnode->next = NULL;return newnode;
}//打印链表
void SListPrint(SListNode* head)
{SListNode* cur = head;while (cur){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}//单链表的销毁
void SListDestory(SListNode** head)
{assert(head);SListNode* cur = *head;while (cur){SListNode* next = cur->next;free(cur);cur = next;}*head = NULL;
}//尾插
void SListPushBack(SListNode** head, SLDataType x)
{assert(head);SListNode* node = BuySListNode(x);if (*head == NULL){*head = node;}else{SListNode* cur = *head;while (cur->next){cur = cur->next;}cur->next = node;}}//尾删
void SListPopBack(SListNode** head)
{assert(head);assert(*head);//没有节点可删if ((*head)->next == NULL){free(*head);*head = NULL;}else{/*SListNode* cur = *head;while (cur->next->next){cur = cur->next;}free(cur->next);cur->next = NULL;*/SListNode* cur = *head;SListNode* prv = NULL;while (cur->next){prv = cur;cur = cur->next;}free(cur);prv->next = NULL;}
}//头插
void SListPushFront(SListNode** head, SLDataType x)
{assert(head);SListNode* node = BuySListNode(x);if (*head == NULL){*head = node;}else{node->next = *head;*head = node;}
}//头删
void SListPopFront(SListNode** head)
{assert(head);assert(*head);//没有节点可删if ((*head)->next == NULL){free(*head);*head = NULL;}else{SListNode* tail = (*head)->next;free(*head);*head = tail;}
}//单链表的查找
SListNode* SListFind(SListNode* head, SLDataType x)
{SListNode* cur = head;while (cur){if (x == cur->data)return  cur;cur = cur->next;}return NULL;
}//在pos位置前插入
void SListInsert(SListNode** head, SListNode* pos, SLDataType x)
{assert(head);assert(pos);SListNode* node = BuySListNode(x);if (*head == NULL){node->next = *head;*head = node;}else{SListNode* cur = *head;while (cur){if (cur->next == pos){node->next = pos;cur->next = node;break;}cur = cur->next;}}}//在pos位置后插入
void SListInsertAfter(SListNode* pos, SLDataType x)
{assert(pos);SListNode* node = BuySListNode(x);node->next = pos->next;pos->next = node;
}//删除pos位置
void SListErase(SListNode** head, SListNode* pos)
{assert(head);assert(*head);//空了就不要删了assert(pos);if (*head == pos){SListPopFront(head);}else{SListNode* cur = *head;while (cur->next != pos){cur = cur->next;}cur->next = pos->next;free(pos);}}//删除pos后一个位置
void SListEraseAfter(SListNode* pos)
{assert(pos);assert(pos->next);SListNode* Pnext = pos->next;pos->next = Pnext->next;free(Pnext);
}
#include "SListNode.h"void testBack()
{SListNode* s = NULL;//这里不置空就是野指针,会直接奔溃掉SListPushBack(&s, 1);SListPushBack(&s, 2);SListPushBack(&s, 3);SListPushBack(&s, 4);SListPushBack(&s, 5);SListPrint(s);SListPopBack(&s);SListPrint(s);SListPopBack(&s);SListPrint(s);SListPopBack(&s);SListPrint(s);SListPopBack(&s);SListPrint(s);SListPopBack(&s);SListPrint(s);//SListPopBack(&s);//测试尾删是否有Bug//SListPrint(s);SListDestory(&s);
}void testFront()
{SListNode* s = NULL;SListPushFront(&s, 1);SListPushFront(&s, 2);SListPushFront(&s, 3);SListPushFront(&s, 4);SListPushFront(&s, 5);SListPrint(s);SListPopFront(&s);SListPrint(s);SListPopFront(&s);SListPrint(s);SListPopFront(&s);SListPrint(s);SListPopFront(&s);SListPrint(s);SListPopFront(&s);SListPrint(s);//SListPopFront(&s);//测试头删是是否有Bug//SListPrint(s);SListDestory(&s);
}testInsert()
{SListNode* s = NULL;SListPushFront(&s, 1);SListPushFront(&s, 2);SListPushFront(&s, 3);SListPushFront(&s, 4);SListPushFront(&s, 5);SListPrint(s);SListInsert(&s, SListFind(s, 1), 10);SListPrint(s);SListInsert(&s, SListFind(s, 1), 100);SListPrint(s);SListInsertAfter(SListFind(s, 4), 40);SListPrint(s);SListDestory(&s);
}void testErase()
{SListNode* s = NULL;//这里不置空就是野指针,会直接奔溃掉SListPushBack(&s, 1);SListPushBack(&s, 2);SListPushBack(&s, 3);SListPushBack(&s, 4);SListPushBack(&s, 5);SListPrint(s);SListErase(&s, SListFind(s, 1));SListPrint(s);SListErase(&s, SListFind(s, 2));SListPrint(s);SListEraseAfter(SListFind(s, 3));SListPrint(s);SListDestory(&s);
}int main()
{//testBack();//testFront();//testInsert();testErase();return 0;
}

OK,无头(没有哨兵位的头结点)单链表就到这里~!这应该是我们介绍的三个里面最复杂的一个~!如果这个掌握了那其他的很容易~!下面我们来看看带哨兵位的头结点的单链表~!

四、单链表的实现(带头)

还是和没有头的一样先声明每个节点的类型:

链表结点类型声明

typedef int SLDataType;typedef struct SListNode
{SLDataType data;struct SListNode* next;
}SLNode;

链表初始化

由于前面是不带头的,所以不需要进行初始化,而这里现在是带哨兵位头结点的~!一开始得要处理好头结点~!处理的方式有两种,一种是在初始的时候无返回值需要传一次二级,其他地方都不用,这显得有点突兀,所以我们采用第二种返回值,将处理好的头结点的地址返回外面接收此时就是带头的了。其实还有一种就是再定义一个结构体,里面是专门维护这个链表的指针,传一级指针也是可以的~~!当然这里采用返回值的方式!

//链表的初始化
SLNode* SLInit()
{SLNode* head = (SLNode*)malloc(sizeof(SLNode));if (head == NULL){perror("head malloc failed");exit(-1);}head->next = NULL;return head;
}

链表的销毁

这里销毁也是一样有两种方式一种是传二级啥都不管了,内部会处理好但这里人家都是一级你一个二级显得有些突兀,还有一种就是不传二级外面最后记得把头置空即可!我们这里就采用第二种!

//链表的销毁
void SLDestory(SLNode* head)
{assert(head);//头结点不可能为空SLNode* cur = head;while (cur){SLNode* tail = cur->next;free(cur);cur = tail;}
}

链表的打印

//链表的打印
void SLPrint(SLNode* head)
{assert(head);SLNode* cur = head->next;while (cur){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}

动态开辟一个节点

由于后面会有大量的插入,每次插入都要开新节点,所以我们直接把他封装成一个函数,后续调用起来方便,代码简洁~!

//动态开辟一个节点
SLNode* BuyNode(SLDataType x)
{SLNode* node = (SLNode*)malloc(sizeof(SLNode));if (node == NULL){perror("malloc failed");exit(-1);}node->data = x;node->next = NULL;return node;
}

单链表尾插

由于有头结点,可以不因考虑第一个插入的情况,另外这里的head是头结点,即使链表为空他也不可能为空!因此为了代码的健壮性断言一下还是好的~!


//尾插
void SLPushBack(SLNode* head, SLDataType x)
{assert(head);SLNode* newnode = BuyNode(x);SLNode* cur = head;while (cur->next){cur = cur->next;}cur->next = newnode;
}

单链表的尾删

尾删也不许担心删完后置头结点为空,这里带了哨兵位的头不需要担心直接删即可,另外这里还是有两种方法上面已经说过,这里采用快慢指针法~!

//尾删
void SLPopBack(SLNode* head)
{assert(head);assert(head->next);//空链表SLNode* cur = head;while (cur->next->next){cur = cur->next;}free(cur->next);cur->next = NULL;
}

OK,来测试一把:

void testBack()
{SLNode* s = SLInit();SLPushBack(s, 1);SLPushBack(s, 2);SLPushBack(s, 3);SLPrint(s);SLPopBack(s);SLPrint(s);SLPopBack(s);SLPrint(s);SLPopBack(s);SLPrint(s);//SLPopBack(s);//检测尾删是否有Bug//SLPrint(s);SLDestory(s);s = NULL;//记得把头置空,避免野指针
}

OK,没有问题,这个是不是相较于上面没带哨兵位的头结点的那个简单多了~!哈哈,OK,我们再来实现一下,头插、头删~!

单链表头插

这相比上面的就很简单了,直接插在哨兵位头结点的后面即可!不用考虑第一个插入改变头的情况~!

//头插
void SLPushFront(SLNode* head, SLDataType x)
{assert(head);SLNode* newnode = BuyNode(x);newnode->next = head->next;head->next = newnode;
}

单链表的头删

头删也是比以前的简单,只需判空即可,不需要考虑改变头的问题~!

//头删
void SLPopFront(SLNode* head)
{assert(head);assert(head->next);//无节点可删SLNode* del = head->next;head->next = del->next;free(del);
}

OK,还是来测试一波:

void testFront()
{SLNode* s = SLInit();SLPushFront(s, 1);SLPushFront(s, 2);SLPushFront(s, 3);SLPushFront(s, 4);SLPrint(s);SLPopFront(s);SLPrint(s);SLPopFront(s);SLPrint(s);SLPopFront(s);SLPrint(s);SLPopFront(s);SLPrint(s);//SLPopFront(s);//测试头删是否有Bug//SLPrint(s);SLDestory(s);s = NULL;//记得把头置空,避免野指针
}

单链表查找

还是和前面一样找到了返回当前节点的地址,否则返回NULL。

//单链表的查找
SLNode* SLFind(SLNode* head, SLDataType x)
{assert(head);SLNode* cur = head->next;while (cur){if (x == cur->data)return cur;cur = cur->next;}return NULL;
}

这里肯定有小伙伴问这个函数不就查找吗,除了配合insert和erase插入删除还能干啥?其实他还能修改信息~!数据结构学的是数据的物理存储结构、逻辑顺序结构和运算~!这里的运算简单点就是增删查改。上面无头的我们已经进行了增删查,没有实现改(其实是我忘了,刚刚想起哈哈),这里我们可以补在这个上,上面无头的也是一样的~!!!

单链表修改

//单链表的修改
void SLModif(SLNode* head, SLDataType x, SLDataType y)
{assert(head);SLNode* ret = SLFind(head, x);assert(ret);ret->data = y;
}

在pos前插入

逻辑和上面的一样,这里也不用考虑第一个插入要改变头的情况~!

//在pos前插入
void SLInsert(SLNode* head, SLNode* pos, SLDataType x)
{assert(head);assert(pos);SLNode* newnode = BuyNode(x);SLNode* cur = head;while (cur->next != pos){cur = cur->next;}newnode->next = pos;cur->next = newnode;
}

在pos后插入

//在pos后插入
void SLInsertAfter(SLNode* pos, SLDataType x)
{assert(pos);SLNode* newnode = BuyNode(x);newnode->next = pos->next;pos->next = newnode;
}

OK,则是一波走起:(这里就把修改顺便测了)

void testInsert()
{SLNode* s = SLInit();SLPushBack(s, 1);SLPushBack(s, 2);SLPushBack(s, 3);SLPrint(s);SLInsert(s, SLFind(s, 1), 10);SLPrint(s);SLInsertAfter(SLFind(s, 2), 200);SLPrint(s);SLModif(s, 3, 300);SLPrint(s);SLDestory(s);s = NULL;//记得把头置空,避免野指针
}

OK,没有问题~!再来实现两个erase的接口!

删除pos位置

这里由于是SLFind配合删除的,所以不因担心删完多删的情况!

//删除pos位置
void SLErase(SLNode* head, SLNode* pos)
{assert(head);assert(pos);SLNode* cur = head;while (cur->next != pos){cur = cur->next;}cur->next = pos->next;free(pos);
}

删除pos后一个

唯一要注意的是pos得有下一个才让删除,否则结束~!

//删除pos位置的后一个
void SLEraseAfter(SLNode* pos)
{assert(pos);assert(pos->next);SLNode* del = pos->next;pos->next = del->next;free(del);
}

链表的长度

这里多加的一个接口是获取链表的长度~!这个不难很容易~!

//获取链表的长度
int SLSize(SLNode* head)
{assert(head);int size = 0;SLNode* cur = head->next;while (cur){++size;cur = cur->next;}return size;
}

OK,测试一把:

void testErase()
{SLNode* s = SLInit();SLPushBack(s, 1);SLPushBack(s, 2);SLPushBack(s, 3);SLPushBack(s, 4);SLPrint(s);SLErase(s, SLFind(s, 1));SLPrint(s);SLEraseAfter(SLFind(s, 2));SLPrint(s);printf("size = %d\n", SLSize(s));SLDestory(s);s = NULL;//记得把头置空,避免野指针
}

OK,没有问题~!

总结:

带哨兵位的头结点的确比不带头的要简单很多!这两种都很重要以后工作中常用的就是这两种和下面要介绍的带头双向循环链表~!而且他这里插入和删除可以用Insert和Erase简化!

全部源码:

#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>typedef int SLDataType;
typedef struct SListNode
{SLDataType data;struct SListNode* next;
}SLNode;//链表的初始化
SLNode* SLInit();//链表的销毁
void SLDestory(SLNode* head);//链表的打印
void SLPrint(SLNode* head);//动态开辟一个节点
SLNode* BuyNode(SLDataType x);//尾插
void SLPushBack(SLNode* head, SLDataType x);//尾删
void SLPopBack(SLNode* head);//头插
void SLPushFront(SLNode* head, SLDataType x);//头删
void SLPopFront(SLNode* head);//单链表的查找
SLNode* SLFind(SLNode* head, SLDataType x);//单链表的修改
void SLModif(SLNode* head, SLDataType x, SLDataType y);//在pos前插入
void SLInsert(SLNode* head, SLNode* pos, SLDataType x);//在pos后插入
void SLInsertAfter(SLNode* pos, SLDataType x);//删除pos位置
void SLErase(SLNode* head, SLNode* pos);//删除pos位置的后一个
void SLEraseAfter(SLNode* pos);//获取链表的长度
int SLSize(SLNode* head);
#include "SLT.h"//链表的初始化
SLNode* SLInit()
{SLNode* head = (SLNode*)malloc(sizeof(SLNode));if (head == NULL){perror("head malloc failed");exit(-1);}head->next = NULL;return head;
}//链表的销毁
void SLDestory(SLNode* head)
{assert(head);//头结点不可能为空SLNode* cur = head;while (cur){SLNode* tail = cur->next;free(cur);cur = tail;}
}//链表的打印
void SLPrint(SLNode* head)
{assert(head);SLNode* cur = head->next;while (cur){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}//动态开辟一个节点
SLNode* BuyNode(SLDataType x)
{SLNode* node = (SLNode*)malloc(sizeof(SLNode));if (node == NULL){perror("malloc failed");exit(-1);}node->data = x;node->next = NULL;return node;
}//尾插
void SLPushBack(SLNode* head, SLDataType x)
{assert(head);SLNode* newnode = BuyNode(x);SLNode* cur = head;while (cur->next){cur = cur->next;}cur->next = newnode;
}//尾删
void SLPopBack(SLNode* head)
{assert(head);assert(head->next);//空链表SLNode* cur = head;while (cur->next->next){cur = cur->next;}free(cur->next);cur->next = NULL;
}//头插
void SLPushFront(SLNode* head, SLDataType x)
{assert(head);SLNode* newnode = BuyNode(x);newnode->next = head->next;head->next = newnode;
}//头删
void SLPopFront(SLNode* head)
{assert(head);assert(head->next);//无节点可删SLNode* del = head->next;head->next = del->next;free(del);
}//单链表的查找
SLNode* SLFind(SLNode* head, SLDataType x)
{assert(head);SLNode* cur = head->next;while (cur){if (x == cur->data)return cur;cur = cur->next;}return NULL;
}//单链表的修改
void SLModif(SLNode* head, SLDataType x, SLDataType y)
{assert(head);SLNode* ret = SLFind(head, x);assert(ret);ret->data = y;
}//在pos前插入
void SLInsert(SLNode* head, SLNode* pos, SLDataType x)
{assert(head);assert(pos);SLNode* newnode = BuyNode(x);SLNode* cur = head;while (cur->next != pos){cur = cur->next;}newnode->next = pos;cur->next = newnode;
}//在pos后插入
void SLInsertAfter(SLNode* pos, SLDataType x)
{assert(pos);SLNode* newnode = BuyNode(x);newnode->next = pos->next;pos->next = newnode;
}//删除pos位置
void SLErase(SLNode* head, SLNode* pos)
{assert(head);assert(pos);SLNode* cur = head;while (cur->next != pos){cur = cur->next;}cur->next = pos->next;free(pos);
}//删除pos位置的后一个
void SLEraseAfter(SLNode* pos)
{assert(pos);assert(pos->next);SLNode* del = pos->next;pos->next = del->next;free(del);
}//获取链表的长度
int SLSize(SLNode* head)
{assert(head);int size = 0;SLNode* cur = head->next;while (cur){++size;cur = cur->next;}return size;
}
#include "SLT.h"void testBack()
{SLNode* s = SLInit();SLPushBack(s, 1);SLPushBack(s, 2);SLPushBack(s, 3);SLPrint(s);SLPopBack(s);SLPrint(s);SLPopBack(s);SLPrint(s);SLPopBack(s);SLPrint(s);//SLPopBack(s);//检测尾删是否有Bug//SLPrint(s);SLDestory(s);s = NULL;//记得把头置空,避免野指针
}void testFront()
{SLNode* s = SLInit();SLPushFront(s, 1);SLPushFront(s, 2);SLPushFront(s, 3);SLPushFront(s, 4);SLPrint(s);SLPopFront(s);SLPrint(s);SLPopFront(s);SLPrint(s);SLPopFront(s);SLPrint(s);SLPopFront(s);SLPrint(s);//SLPopFront(s);//测试头删是否有Bug//SLPrint(s);SLDestory(s);s = NULL;//记得把头置空,避免野指针
}void testInsert()
{SLNode* s = SLInit();SLPushBack(s, 1);SLPushBack(s, 2);SLPushBack(s, 3);SLPrint(s);SLInsert(s, SLFind(s, 1), 10);SLPrint(s);SLInsertAfter(SLFind(s, 2), 200);SLPrint(s);SLModif(s, 3, 300);SLPrint(s);SLDestory(s);s = NULL;//记得把头置空,避免野指针
}void testErase()
{SLNode* s = SLInit();SLPushBack(s, 1);SLPushBack(s, 2);SLPushBack(s, 3);SLPushBack(s, 4);SLPrint(s);SLErase(s, SLFind(s, 1));SLPrint(s);SLEraseAfter(SLFind(s, 2));SLPrint(s);printf("size = %d\n", SLSize(s));SLDestory(s);s = NULL;//记得把头置空,避免野指针
}int main()
{//testBack();//testFront();//testInsert();testErase();return 0;
}

五、带头双向循环链表的实现

我们上面实现的两种都是单链表,他们头插头删的效率很高,但在中间或尾部插入删除时的效率就很低,为了解决这个问题我们在定义节点类型时在加入一个前驱指针来记录当前节点的前一个节点的地址,这样插入删除就很OK了~!当最后一个时让他的后继指针指向头结点,第一个的前驱指向最后一个节点,这样就实现了循环而且任何节点的插入删除都很高~!OK,下面是图:

OK,下面我们就来实现一下~!

结点类型声明

typedef int LTDataType;
typedef struct ListNode
{LTDataType data;struct ListNode* prv;struct ListNode* next;
}ListNode;

初始化

//初始化
ListNode* ListInit()
{ListNode* head = (ListNode*)malloc(sizeof(ListNode));if (!head){perror("malloc failed");exit(-1);}head->prv = head;head->next = head;return head;
}

销毁

这里与上面单向带有不一样的是!这里因为是循环的最后的next指向哨兵位的头,所以不能和上面一样直接从head开始销毁,应该从head->next开始等所有的销毁完了,在把head置空即可~!这是因为是一级指针所以外面的调用者在最后记得置空head~!

//销毁
void ListDestory(ListNode* head)
{assert(head);ListNode* cur = head->next;while (cur != head){ListNode* tail = cur->next;free(cur);cur = tail;}free(head);
}

打印

//链表的打印
void ListPrint(ListNode* head)
{assert(head);ListNode* cur = head->next;while (cur != head){printf("%d->", cur->data);cur = cur->next;}
}

动态开辟新节点

//动态开辟一个节点
ListNode* BuyNode(LTDataType x)
{ListNode* node = (ListNode*)malloc(sizeof(ListNode));if (!node){perror("malloc failed");exit(-1);}node->data = x;node->prv = NULL;node->next = NULL;return node;
}

尾插

这里比单链表好的就是不用遍历找尾直接就可以找到,但注意的是要把prv前驱指针处理好~!如果不熟练可以多定义个指针,如果熟悉了就可以多个->next->prv哈哈~!下面是两种写法!

//尾插
void ListPushBack(ListNode* head, LTDataType x)
{assert(head);ListNode* newnode = BuyNode(x);ListNode* tail = head->prv;tail->next = newnode;newnode->prv = tail;newnode->next = head;head->prv = newnode;/*head->prv->next = newnode;newnode->prv = head->prv;newnode->next = head;head->prv = newnode;*/
}

尾删

尾删也是一样可以多定义指针也可以直接来!也要注意处理好前驱~!下面是两种方式!

//尾删
void ListPopBack(ListNode* head)
{assert(head);assert(head->next != head);//链表为空ListNode* tail = head->prv;ListNode* tailPrv = tail->prv;tailPrv->next = head;head->prv = tailPrv;free(tail);/*ListNode* del = head->prv;head->prv->prv->next = head;head->prv = head->prv->prv;free(del);*/
}

OK还是测试一波:

void testBack()
{ListNode* L = ListInit();ListPushBack(L, 1);ListPushBack(L, 2);ListPushBack(L, 3);ListPrint(L);ListPopBack(L);ListPrint(L);ListPopBack(L);ListPrint(L);ListPopBack(L);ListPrint(L);//ListPopBack(L);//测试尾删是否有Bug//ListPrint(L);ListDestory(L);L = NULL;
}

OK,没有问题,再来搞两个头插和头删~!

头插

//头插
void ListPushFront(ListNode* head, LTDataType x)
{assert(head);ListNode* newnode = BuyNode(x);ListNode* tail = head->next;newnode->next = tail;tail->prv = newnode;head->next = newnode;newnode->prv = head;/*newnode->next = head->next;head->next->prv = newnode;head->next = newnode;newnode->prv = head;*/
}

头删 

//头删
void ListPopFront(ListNode* head)
{assert(head);assert(head->next != head);ListNode* del = head->next;head->next = del->next;del->next->prv = head;free(del);
}

OK,来测试一波:

void testFront()
{ListNode* L = ListInit();ListPushFront(L, 1);ListPushFront(L, 2);ListPushFront(L, 3);ListPrint(L);ListPopFront(L);ListPrint(L);ListPopFront(L);ListPrint(L);ListPopFront(L);ListPrint(L);//ListPopFront(L);//测试头删是否有Bug//ListPrint(L);ListDestory(L);L = NULL;
}

OK,没有bug!我们继续:

查找

这里还是和前面一样找到了返回当前节点的地址否则返回NULL,实际上在很多常见下就是这样的~!

//查找
ListNode* ListFind(ListNode* head, LTDataType x)
{assert(head);ListNode* cur = head->next;while (cur != head){if (x == cur->data)return cur;cur = cur->next;}return NULL;
}

修改

记得判断找不到的情况,我这里采用的是暴力检查!!!

//修改
void ListModif(ListNode* head, LTDataType x, LTDataType y)
{assert(head);ListNode* ret = ListFind(head, x);assert(ret);ret->data = y;
}

在pos前插入

这里注意的是由于查找函数已经把pos查找好了,且有前驱指针所以不需要遍历找pos,直接链接即可~!

//在pos前插入
void ListInsert( ListNode* pos, LTDataType x)
{assert(pos);ListNode* newnode = BuyNode(x);ListNode* Prv = pos->prv;newnode->next = pos;pos->prv = newnode;Prv->next = newnode;newnode->prv = Prv;
}

在pos后插入

//在pos后插入
void ListInsertAfter(ListNode* pos, LTDataType x)
{assert(pos);ListNode* newnode = BuyNode(x);ListNode* tail = pos->next;newnode->next = tail;tail->prv = newnode;pos->next = newnode;newnode->prv = pos;
}

OK,测试一波:

void testInsert()
{ListNode* L = ListInit();ListPushBack(L, 1);ListPushBack(L, 2);ListPushBack(L, 3);ListPrint(L);ListInsert(ListFind(L, 1), 10);ListPrint(L);ListInsertAfter(ListFind(L, 3), 300);ListPrint(L);ListDestory(L);L = NULL;
}

OK,没有问题~!

删除pos

//删除pos位置
void ListErase(ListNode* pos)
{assert(pos);ListNode* Prv = pos->prv;ListNode* tail = pos->next;Prv->next = tail;tail->prv = Prv;free(pos);
}

删除pos的下一个

这里得注意的是又是是循环的。如果pos没有下一个就指向head,所以判断有没有pos的下一个必须有头~!!!

//删除pos位置的下一个
void ListEraseAfter(ListNode* head, ListNode* pos)
{assert(pos);assert(pos->next != head);//没有下一个节点可删ListNode* tail = pos->next;pos->next = tail->next;tail->prv = pos;free(tail);
}

OK,测试一把~!

void testErase()
{ListNode* L = ListInit();ListPushBack(L, 1);ListPushBack(L, 2);ListPushBack(L, 3);ListPrint(L);ListErase(ListFind(L, 1));ListPrint(L);ListEraseAfter(L, ListFind(L, 2));ListPrint(L);ListDestory(L);L = NULL;
}

没有问题,上面有个修改函数忘了测试了,等在写一个长度一起在这个里面测试了:

获取长度

//获取链表的长度
int ListSize(ListNode* head)
{assert(head);int size = 0;ListNode* cur = head->next;while (cur != head){++size;cur = cur->next;}return size;
}

OK,测试一下:

void testErase()
{ListNode* L = ListInit();ListPushBack(L, 1);ListPushBack(L, 2);ListPushBack(L, 3);ListPrint(L);printf("size = %d\n", ListSize(L));ListErase(ListFind(L, 1));ListPrint(L);ListEraseAfter(L, ListFind(L, 2));ListPrint(L);printf("size = %d\n", ListSize(L));ListModif(L, 2, 200);ListPrint(L);ListDestory(L);L = NULL;
}

OK,这就是带头双向循环链表!链表的实现也就到此结束了,下面我们来对顺序表和链表做一个全面的对比!

六、链表和顺序表的区别

这里我们先来用单链表和顺序表对比,然后再用链表的最优结构---->带头双向循环链表来对比~!

OK,我们再来对比一下顺序表和带头双向循环链表的区别~!

这就是他们的区别,这里可以看出如果你需要大量查找和排序等操作时顺序表是优选,如果说是频繁的插入删除的话链表优选~!不能说他们谁好谁不好,知识应用的场景不同,他们其实是互补的数据结构~!OK,这里提到了一个东西叫缓存命中率~!这个是计算机存成原理里面的概念,这里实际上说的是存储体系和局部性原理~!OK我们大概谈谈:缓存利用率全称:CPU高速缓存利用率

我们一般的计算机中的三大件是:cpu、内存、和本地磁盘~!我们写的数据结构的代码都在内存中。计组中有个存储器层次体系,就是把每个存储器按快慢给分层了,我们来看看:

这里它实际上分为四层(按照价格以及快慢):

寄存器和缓存实际上作用差不多,都是加载数据给cpu处理的,小的数据直接由寄存器来例如一个变量,大的数据由缓存去内存加载到cpu处理,例如数组!为什么CPU不直接到内存中去访问呢?原因很简单,CPU太快了内存和他不是一个等级,那内存以下的就更不要说了。

为了解决这个问题电脑厂商会在CPU周围多弄几个寄存器和多级缓存;他们的速度是跟的上CPU的。这里你可能还会问既然有内存了为什么要磁盘呢?在这里有两方面的原因:一方面最直接的原因贵!另一方面是内存是带电存储而磁盘不是。内存直接能和上级交流磁盘则不行需要先加载到内存在于上级交流!

另外还有一个是局部性原理,他分为局部性时间原理和局部性空间原理:下面是我找的一本计组上的解释:

为什么说顺序表的缓存利用率高呢?根据局部性原理,CPU在访问万当前数据后它的下一个很有可能会被访问到,所以缓存会一次加载不止一个数据而是这个数据和后面的几个,具体几个得看硬件!这样cpu每次访问时缓存已经把顺序表的内容加载过去了,效率会高很多,原因是他们的地址是连续的!链表之所以低得原因是它的每个节点的内存地址大概率是不连续的,每一次访问一个节点的内容都要加载,而且他有可能会加载错造成缓存污染~!

OK,好兄弟,链表就分享到这里!我们下期再见~~!

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

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

相关文章

使用 ClassFinal 对 java class 文件进行加密防止反编译

ClassFinal 是一款 java class文件安全加密工具&#xff0c;支持直接加密 jar 包或 war 包&#xff0c;无需修改任何项目代码&#xff0c;兼容 spring-framework&#xff1b;可避免源码泄漏或字节码被反编译 特点 无需修改原项目代码&#xff0c;只要把编译好的jar/war包用本工…

Nginx简介与Docker Compose部署指南

Nginx是一款高性能的开源Web服务器和反向代理服务器&#xff0c;以其卓越的性能、可伸缩性和灵活性而闻名。它在全球范围内广泛用于托管Web应用程序、负载均衡、反向代理和更多场景中。在本文中&#xff0c;我们将首先介绍Nginx的基本概念&#xff0c;然后演示如何使用Docker C…

tortoiseSVN树冲突解决方案

方案一&#xff1a; 手动导出 trunk 上的文件(夹)&#xff0c;把本地目录文件(夹)删了并替换成 trunk上的&#xff0c;再点击测试合并方案二&#xff1a; 如果执行了方案一还是冲突&#xff0c;确认本地和trunk文件一致后&#xff0c;可以跳过冲突的revision

【NLP的python库(03/4) 】: 全面概述

一、说明 Python 对自然语言处理库有丰富的支持。从文本处理、标记化文本并确定其引理开始&#xff0c;到句法分析、解析文本并分配句法角色&#xff0c;再到语义处理&#xff0c;例如识别命名实体、情感分析和文档分类&#xff0c;一切都由至少一个库提供。那么&#xff0c;你…

Linux--socket编程

socket套接字编程 一、服务器和客户端的开发步骤&#xff1a; 1、创建套接字 2、为套接字添加信息&#xff08;ip地址和端口号&#xff09; 3、监听网络连接 4、监听到有客户端接入&#xff0c;接受连接&#xff08;如没有接入&#xff0c;会发生阻塞到&#xff09; 5、数据…

【机器学习】熵和概率分布,图像生成中的量化评估IS与FID

详解机器学习中的熵、条件熵、相对熵、交叉熵 图像生成中常用的量化评估指标通常有Inception Score (IS)和Frchet Inception Distance (FID) Inception Score (IS) 与 Frchet Inception Distance (FID) GAN的量化评估方法——IS和FID&#xff0c;及其pytorch代码

Unity 鼠标悬浮时文本滚动(Text Mesh Pro)

效果 直接将脚本挂载在Text Mesh Pro上&#xff0c;但是需要滚动的文本必须在Scroll View中&#xff0c;否侧会定位错误&#xff0c;还需要给Scroll View中看需求添加垂直或者水平布局的组件 代码 using System.Collections; using System.Collections.Generic; using UnityE…

思科:iOS和iOSXe软件存在漏洞

思科警告说,有人试图利用iOS软件和iOSXe软件中的一个安全缺陷,这些缺陷可能会让一个经过认证的远程攻击者在受影响的系统上实现远程代码执行。 中严重程度的脆弱性被追踪为 CVE-2023-20109 ,并以6.6分得分。它会影响启用Gdoi或G-Ikev2协议的软件的所有版本。 国际知名白帽黑客…

世界前沿技术发展报告2023《世界航天技术发展报告》(二)卫星技术

&#xff08;二&#xff09;卫星技术 1.概述2. 通信卫星2.1 美国太空发展局推进“国防太空体系架构”&#xff0c;持续部署“传输层”卫星2.2 美国军方在近地轨道成功演示验证星间激光通信2.3 DARPA启动“天基自适应通信节点”项目&#xff0c;为增强太空通信在轨互操作能力提供…

c#设计模式-结构型模式 之 组合模式

&#x1f680;简介 组合模式又名部分整体模式&#xff0c;是一种 结构型设计模式 &#xff0c;是用于把一组相似的对象当作一个 单一的对象 。组合模式 依据树形结构来组合对象 &#xff0c;用来表示部分以及整体层&#xff0c;它可以让你将对象组合成树形结构&#xff0c;并且…

【AI视野·今日CV 计算机视觉论文速览 第258期】Mon, 2 Oct 2023

AI视野今日CS.CV 计算机视觉论文速览 Mon, 2 Oct 2023 (showing first 100 of 112 entries) Totally 100 papers &#x1f449;上期速览✈更多精彩请移步主页 Interesting: &#x1f4da;HAvatar,基于神经辐射场的头部合成重建。(from 清华大学) &#x1f4da;GAIA-1, 用于自…

线上Vue项目访问其他服务器接口(宝塔平台配置解决)

前端本地解决跨域问题非常简单&#xff0c;配置代理即可&#xff0c;线上需要配置nginx&#xff0c;宝塔给我们更简单的配置方式&#xff1a;反向代理。 登录进宝塔页面&#xff0c;选择网站&#xff0c;点击网站名&#xff0c;选择反向代理 点击添加反向代理 注意&#xff…

分类预测 | Matlab实现SSA-CNN-SVM麻雀算法优化卷积支持向量机分类预测

分类预测 | Matlab实现SSA-CNN-SVM麻雀算法优化卷积支持向量机分类预测 目录 分类预测 | Matlab实现SSA-CNN-SVM麻雀算法优化卷积支持向量机分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.Matlab实现SSA-CNN-SVM麻雀算法优化卷积支持向量机分类预测&#xff0…

华为乾坤区县教育安全云服务解决方案(2)

本文承接&#xff1a; https://blog.csdn.net/qq_37633855/article/details/133276200?spm1001.2014.3001.5501 重点讲解华为乾坤区县教育安全云服务解决方案的部署流程。 华为乾坤区县教育安全云服务解决方案&#xff08;2&#xff09; 课程地址解决方案部署整体流程组网规划…

数字IC前端学习笔记:数字乘法器的优化设计(进位保留乘法器)

相关阅读 数字IC前端https://blog.csdn.net/weixin_45791458/category_12173698.html?spm1001.2014.3001.5482 阵列乘法器设计中限制乘法器速度的是随着数据位宽而迅速增大的串行进位链&#xff0c;如果使用进位保留加法器&#xff0c;则可以避免在设计中引入较长时间的等待&…

springmvc-JSR303进行服务端校验分组验证SpringMVC定义Restfull接口异常处理流程RestController异常处理

目录& 1. JSR303 2. JSR303中含有的注解 3. spring中使用JSR303进行服务端校验 3.1 导入依赖包 3.2 添加验证规则 3.3 执行校验 4. 分组验证 4.1 定义分组验证规则 4.2 验证时通过参数指定验证规则 4.3 验证信息的显示 5. SpringMVC定义Restfull接口 5.1 增加s…

mysql约束

约束 概念&#xff1a;约束是作用于表中字段上的规则&#xff0c;用于限制存储在表中的数据。 目的&#xff1a;保证数据的正确、有效性和完整性。 分类&#xff1a; 非空约束&#xff1a;限制该字段的数据不能为null。 not null 唯一约束&#xff1a;保证该字段所…

【面试经典150 | 矩阵】生命游戏

文章目录 写在前面Tag题目来源题目解读解题思路方法一&#xff1a; O ( m n ) O(mn) O(mn) 额外空间方法二&#xff1a; O ( 1 ) O(1) O(1) 额外空间 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专…

计算机,软件工程,网络工程,大数据专业毕业设计选题有哪些(附源码获取)

计算机&#xff0c;软件工程&#xff0c;网络工程&#xff0c;大数据专业毕业设计选题有哪些?&#xff08;附源码获取&#xff09; ✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于J…

Java并发-满老师

Java并发 一级目录栈与栈帧线程上下文切换三级目录 一级目录 栈与栈帧 满老师视频链接 我们都知道 JVM 中由堆、栈、方法区所组成&#xff0c;其中栈内存是给谁用的呢&#xff1f;其实就是线程&#xff0c;每个线程启动后&#xff0c;虚拟机就会为其分配一块栈内存 每个栈由…