【数据结构-初阶】详解线性表(3)---双链表

【数据结构-初阶】详解线性表(3)---双链表

🎈主页传送门:良木生香

🔥个人专栏:《C语言》 《数据结构-初阶》 《程序设计》

🌟人为善,福随未至,祸已远行;人为恶,祸虽未至,福已远离


目录

1、双链表的概念

2、双链表的基本实现

2.1、双向链表节点的创建

2.2、双向链表的初始化

2.3、双向链表长度的计算

2.4、双向链表的插入操作:

2.4.1、头部插入:

2.4.2、尾部插入:

2.4.3、查找pos位置的元素:

2.4.4、pos位置插入:

2.4.4.1、pos位置之前插入:

2.4.4.2、pos位置之后插入:

2.5、双向链表的删除操作:

2.5.1、头部删除:

2.5.2、尾部删除:

2.5.3、pos位置删除:

2.6、双向链表的元素查找:

2.7、双向链表的修改:

2.8、双向链表的打印:

2.9、双向链表的销毁:

3、代码总和:


上期回顾:在上一篇文章中,我们了解了链表是由一个一个节点通过指针连接在一起而组成的,每个节点是由数据域和指针域组成的.基于这个基础,我们实现了单链表(单向不带头结点不循环链表)的增删查改四个基本操作.

那么今天我们来实现一下另外一个链表---双向带头结点循环链表(双链表)

1、双链表的概念

双链表,根据类比单链表得出,双链表是由两个指针的,因为单链表只有一个指针嘛,嘻嘻。确实,双链表的结构与单链表极其的相似,双链表同样由数据域和指针域组成,只不过在指针域中,双链表有两个指针,一个指针是指向有一个节点,一个指针是指向前一个节点,如下图:

因为今天要讲的是双向带头结点循环链表,所以整个链表的就应该为下图所示:

头结点相当于哨兵位,不会因为链表的操作而改变,有了这些前备知识之后,我们就可以开始对双链表进行实现了~~~

2、双链表的基本实现

2.1、双向链表节点的创建

双向链表节点的创建与单链表的相似,都是先向内存申请一个节点的空间,然后对立面的元素先进行初始化,但因为双向链表里面有两个指针,所以在指针的指向上会有所不同

下面是图示:

在初始化指针指向时候,要将两个指针指向自己,下面是具体的代码:

DListNode* Buy_Node(Elemtype data) { DListNode* newNode = (DListNode*)malloc(sizeof(DListNode)); if (newNode == NULL) { printf("新节点创建失败...\n"); return NULL; } newNode->next = newNode; newNode->data = data; newNode->front = newNode; return newNode; } 

2.2、双向链表的初始化

今天我们要讲的是带头节点的双向链表,那么就不用像不带头结点的单链表那样子对第一个节点初始化,所以我们只用对哨兵位进行初始化,即创建一个空节点,随便给节点进行赋值,在这里我将哨兵位赋值上-1,代码如下:

DListNode* Init_DListNode() { DListNode* phead = Buy_Node(-1); //现在是创建头节点 if (phead == NULL) { printf("头节点创建失败...\n"); return NULL; } return phead; } 

2.3、双向链表长度的计算

计算双向链表的长度,只用遍历一遍链表并用计数器技术即可,下面是代码

int Get_DListlength(DListNode* phead) { DListNode* pcur = phead; int count = 0; if (pcur->next == phead) { return 0; } while (pcur->next != phead) { pcur = pcur->next; count++; } return count; } 

我们将用于遍历的指针放在头节点(哨兵位)之后,即正式节点的位置,这样就不会将哨兵位计算进去

2.4、双向链表的插入操作:

与之前的链表一样,插入都可以分为头插,尾插,pos位置插入

2.4.1、头部插入:

对于头部插入,我们是将新节点插入到哨兵位与第一个正式节点之间的,下面图是示意图:

下面是具体实现的代码:

void Push_Front(DListNode* phead, Elemtype data) { assert(phead); //先创建新节点 DListNode* newNode = Buy_Node(data); if (newNode == NULL) { printf("新的头插节点创建失败...\n"); return; } //phead newNode phead->next newNode->next = phead->next; newNode->front = phead; phead->next->front = newNode; phead->next = newNode; }
在写代码的时候,要注意先将新节点的next与front指针与旧节点进行连接,再断开旧节点之间的连接

2.4.2、尾部插入:

尾部插入就是在双向链表中的最后一个节点与头结点之间插入新的节点,在单链表中,我们要遍历整个链表才能找到尾结点,但是在循环链表中就不用这么麻烦,因为哨兵位与尾结点是相连接的,我们可以通过哨兵位的front指针直接找到尾结点,这样一来就显得非常简单了,其实图示可以参考头部插入的图示,只用将哨兵位改成尾结点,第一个正式节点改成哨兵位即可,现在我们直接上代码:

void Push_Back(DListNode* phead, Elemtype data) { assert(phead); DListNode* newNode = Buy_Node(data); if (newNode == NULL) { printf("尾插节点创建失败...\n"); return; } //这时候就要创建尾巴节点,用于指向最后一个节点 DListNode* ptail = phead->front; //ptail newNode phead // newNode->front = ptail; newNode->next = phead; ptail->next = newNode; phead->front = newNode; }
要注意的依旧是,要优先处理新节点的两个指针与旧节点的关系,再将旧节点的相关指针指向新节点

2.4.3、查找pos位置的元素:

在进行写一个操作之前,我们要先做一个铺垫,先用一个函数来查找pos位置上的元素,看看pos位置上是否存在元素.这里我们用遍历的方法进行查找:

DListNode* Search_elem_for_pos(DListNode* phead, int pos) { assert(phead); int len = Get_DListlength(phead); if (pos<1 || pos>len) { printf("pos值不合法...\n"); return NULL; } DListNode* pcur = phead->next; for (int i = 1; i < pos; i++) { if (pcur->next == phead) { printf("pos位置没有元素...\n"); return NULL; } pcur = pcur->next; } return pcur; } 
这里我们将返回的值设计为指针类型,也就是如果找到了,可以直接获得这个元素的指针,以便后续操作的开展

2.4.4、pos位置插入:

2.4.4.1、pos位置之前插入:

在进行pos位置的相关操作之前,我们都是要进行一遍老步骤:判断pos值,判断pos位置是不是存在元素,如果存在元素,那就么pos位置之前插入就相当于头插,示意图与头插几乎一样,所以我们直接上代码:

void Push_pos_Front(DListNode* phead, Elemtype data, int pos) { assert(phead); int len = Get_DListlength(phead); if (pos<1 || pos>len) { printf("pos值不合法...\n"); return; } if (pos == 1) { Push_Front(phead, data); return; } DListNode* ppos = Search_elem_for_pos(phead, pos); if (ppos == NULL) { //Push_Front(phead, data); return; } //ppos->front newNode ppos else { DListNode* newNode = Buy_Node(data); if (newNode == NULL) return; newNode->next = ppos; newNode->front = ppos->front; ppos->front->next = newNode; ppos->front = newNode; } } 
要注意的是,如果pos值为1,那么就相当于头插,直接调用头插的函数即可

2.4.4.2、pos位置之后插入:

步骤与pos位置之前插入相同,如果pos位置存在元素,那么示意图就与尾尾插相似,那就直接上代码吧~~~~

void Push_pos_Back(DListNode* phead, Elemtype data, int pos) { assert(phead); int len = Get_DListlength(phead); if (pos<1 || pos>len) { printf("pos值不合法...\n"); return; } if (pos == 1) { Push_Back(phead, data); return; } DListNode* newNode = Buy_Node(data); if (newNode == NULL) { return; } DListNode* ppos = Search_elem_for_pos(phead, pos); if (ppos == NULL) { //Push_Back(phead, data); return; } //ppos newNode ppos->next newNode->next = ppos->next; newNode->front = ppos; ppos->next->front = newNode; ppos->next = newNode; }
如果pos值为1,那么就相当于尾插,像pos位置之前一样,直接调用尾插函数即可.

2.5、双向链表的删除操作:

删除操作与插入操作也一样,依旧是分头删,尾删,pos位置删除

2.5.1、头部删除:

头部删除指的是删除第一个正式节点,让哨兵位与第二个正式节点相连接,下面是是示意图:

在了解了示意图之后,我们直接上代码:

void Pop_Front(DListNode* phead) { assert(phead); int len = Get_DListlength(phead); if (len == 0) { printf("链表为空,无法删除...\n"); return; } //phead phead->next phead->next->next DListNode* delet = phead->next; phead->next = phead->next->next; phead->next->next->front = phead; free(delet); delet = NULL; }
要注意的是,在删除时对于指针的操作与增加时有所不同,在删除时是要先将不删除的两个节点先相连接,再free掉要删除的节点,最后要记得将delet指针置为NULL哦~~~

2.5.2、尾部删除:

与头部删除相类似,依旧是处理被删除节点与不删除节点之间的关系,我们直接上代码:

void Pop_Back(DListNode* phead) { assert(phead); int len = Get_DListlength(phead); if (len == 0) { printf("链表为空,无法删除...\n"); return; } DListNode* delet = phead->front; //delet->front delet phead; delet->front->next = phead; phead->front = delet->front; free(delet); delet = NULL; }
再三提醒,再free掉delet指针的内容后,要将delet指针置为NULL才行,不然会成为野指针

2.5.3、pos位置删除:

对链表而言,就没有pos之前或者之后删除了,直接就是指定的pos位置删除,删除的方式与头删尾删一样,无非就是为位置的不同罢了,话不多说,我们直接上代码:

void Pop_Pos(DListNode* phead, int pos) { assert(phead); int len = Get_DListlength(phead); if (pos<1 || pos>len) { printf("pos值不合法...\n"); return; } if (pos == 1) { Pop_Front(phead); return; } if (pos == len) { Pop_Back(phead); return; } //pos->front pos pos->next DListNode* delet = Search_elem_for_pos(phead, pos); if (delet == NULL) { return; } delet->front->next = delet->next; delet->next->front = delet->front; free(delet); delet = NULL; }
要注意的是,pos系列的操作一定要记得对pos值进行判断,以及特殊情况的处理,像pos==1或者pos==尾结点,这就要直接调用头删或者尾删了

2.6、双向链表的元素查找:

想要查找链表里面的元素,就要先求出当前链表的长度,看看长度是否为0,然后再用第一个正式节点开始遍历,查找链表中是否存在这个元素,有的话就输出"找到了",没有就输出"没找到",下面上代码:

void Search_elem(DListNode* phead, Elemtype data) { assert(phead); int len = Get_DListlength(phead); if (len == 0) { printf("当前链表为0,没有元素可以查找...\n"); return; } DListNode* pcur = phead->next; int pos = 1; while (pcur != phead) { if (pcur->data == data) { printf("找到了!!!\n"); return; } pcur = pcur->next; pos++; } printf("找不到...\n"); return; }

2.7、双向链表的修改:

想要修改某个位置的元素的值,就要先输入想修改的位置,记忆修改之后的数值,所以参数藜麦那就要含有pos以及data两个参数,然后再判断pos位置是否存在元素,不存在则无法进行修改:

void Change_elem(DListNode* phead, Elemtype data, int pos) { assert(phead); int len = Get_DListlength(phead); if (pos<1 || pos>len) { printf("pos值不合法...\n"); return; } DListNode* ppos = Search_elem_for_pos(phead, pos); if (ppos == NULL) { printf("想要修改的元素不存在\n"); return; } ppos->data = data; }

如果存在,直接修改就行.

2.8、双向链表的打印:

打印就更加简单了,直接遍历打印:

//现在是打印双向带头结点循环链表 void my_printf(DListNode* phead) { DListNode* pcur = phead->next; if (pcur == phead) { printf("当前链表内容为空...\n"); return; } while (pcur != phead) { printf("%d ", pcur->data); pcur = pcur->next; } } 

2.9、双向链表的销毁:

在做完一系列的操作之后,肯定是要对链表进行销毁的,我们就采用与打印相同的方法---遍历,逐个节点逐个节点进行销毁:

//现在是销毁链表 void Destory_list(DListNode* phead) { DListNode* pcur = phead->next; while (pcur != phead) { DListNode* delet = pcur; pcur = pcur->next; free(delet); delet = NULL; } free(phead); phead = NULL; } 

要注意的是,销毁完所有的节点之后,要记得将遍历的指针和哨兵位置为NULL.

以上就是关于带头循环双向链表基本操作的详解了,下面是所有代码的综合,有兴趣的朋友们可以看看~~~~~~

3、代码总和:

话不多说,直接上代码:

#define _CRT_SECURE_NO_WARNINGS 520 #define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<stdlib.h> #include<windows.h> #include<assert.h> //现在说的双向链表指的是双向带头循环链表 typedef int Elemtype; //现在定义双链表结构体 typedef struct DListNode { Elemtype data; //数据域 struct DListNode* front; struct DListNode* next; //指针域 }DListNode; //现在实现双向链表的基本操作: DListNode* Init_DListNode(DListNode* phead); int Get_DListlength(DListNode* phead); void Push_Front(DListNode* phead, Elemtype data); void Push_Back(DListNode* phead, Elemtype data); void Push_pos_Front(DListNode* phead, Elemtype data, int pos); void Push_pos_Back(DListNode* phead, Elemtype data, int pos); void Pop_Front(DListNode* phead); void Pop_Back(DListNode* phead); void Pop_Pos(DListNode* phead, int pos); DListNode* Search_elem_for_pos(DListNode* phead, int pos); void Search_elem(DListNode* phead, Elemtype data); void Change_elem(DListNode* phead, Elemtype data, int pos); //计算链表长度 int Get_DListlength(DListNode* phead) { DListNode* pcur = phead; int count = 0; if (pcur->next == phead) { return 0; } while (pcur->next != phead) { pcur = pcur->next; count++; } return count; } DListNode* Buy_Node(Elemtype data) { DListNode* newNode = (DListNode*)malloc(sizeof(DListNode)); if (newNode == NULL) { printf("新节点创建失败...\n"); return NULL; } newNode->next = newNode; newNode->data = data; newNode->front = newNode; return newNode; } DListNode* Init_DListNode() { DListNode* phead = Buy_Node(-1); //现在是创建头节点 if (phead == NULL) { printf("头节点创建失败...\n"); return NULL; } return phead; } void Push_Front(DListNode* phead, Elemtype data) { assert(phead); //先创建新节点 DListNode* newNode = Buy_Node(data); if (newNode == NULL) { printf("新的头插节点创建失败...\n"); return; } //phead newNode phead->next newNode->next = phead->next; newNode->front = phead; phead->next->front = newNode; phead->next = newNode; } void Push_Back(DListNode* phead, Elemtype data) { assert(phead); DListNode* newNode = Buy_Node(data); if (newNode == NULL) { printf("尾插节点创建失败...\n"); return; } //这时候就要创建尾巴节点,用于指向最后一个节点 DListNode* ptail = phead->front; //ptail newNode phead // newNode->front = ptail; newNode->next = phead; ptail->next = newNode; phead->front = newNode; } DListNode* Search_elem_for_pos(DListNode* phead, int pos) { assert(phead); int len = Get_DListlength(phead); if (pos<1 || pos>len) { printf("pos值不合法...\n"); return NULL; } DListNode* pcur = phead->next; for (int i = 1; i < pos; i++) { if (pcur->next == phead) { printf("pos位置没有元素...\n"); return NULL; } pcur = pcur->next; } return pcur; } void Push_pos_Front(DListNode* phead, Elemtype data, int pos) { assert(phead); int len = Get_DListlength(phead); if (pos<1 || pos>len) { printf("pos值不合法...\n"); return; } if (pos == 1) { Push_Front(phead, data); return; } DListNode* ppos = Search_elem_for_pos(phead, pos); if (ppos == NULL) { //Push_Front(phead, data); return; } //ppos->front newNode ppos else { DListNode* newNode = Buy_Node(data); if (newNode == NULL) return; newNode->next = ppos; newNode->front = ppos->front; ppos->front->next = newNode; ppos->front = newNode; } } void Push_pos_Back(DListNode* phead, Elemtype data, int pos) { assert(phead); int len = Get_DListlength(phead); if (pos<1 || pos>len) { printf("pos值不合法...\n"); return; } if (pos == 1) { Push_Back(phead, data); return; } DListNode* newNode = Buy_Node(data); if (newNode == NULL) { return; } DListNode* ppos = Search_elem_for_pos(phead, pos); if (ppos == NULL) { //Push_Back(phead, data); return; } //ppos newNode ppos->next newNode->next = ppos->next; newNode->front = ppos; ppos->next->front = newNode; ppos->next = newNode; } void Pop_Front(DListNode* phead) { assert(phead); int len = Get_DListlength(phead); if (len == 0) { printf("链表为空,无法删除...\n"); return; } //phead phead->next phead->next->next DListNode* delet = phead->next; phead->next = phead->next->next; phead->next->next->front = phead; free(delet); delet = NULL; } void Pop_Back(DListNode* phead) { assert(phead); int len = Get_DListlength(phead); if (len == 0) { printf("链表为空,无法删除...\n"); return; } DListNode* delet = phead->front; //delet->front delet phead; delet->front->next = phead; phead->front = delet->front; free(delet); delet = NULL; } void Pop_Pos(DListNode* phead, int pos) { assert(phead); int len = Get_DListlength(phead); if (pos<1 || pos>len) { printf("pos值不合法...\n"); return; } if (pos == 1) { Pop_Front(phead); return; } if (pos == len) { Pop_Back(phead); return; } //pos->front pos pos->next DListNode* delet = Search_elem_for_pos(phead, pos); if (delet == NULL) { return; } delet->front->next = delet->next; delet->next->front = delet->front; free(delet); delet = NULL; } void Change_elem(DListNode* phead, Elemtype data, int pos) { assert(phead); int len = Get_DListlength(phead); if (pos<1 || pos>len) { printf("pos值不合法...\n"); return; } DListNode* ppos = Search_elem_for_pos(phead, pos); if (ppos == NULL) { printf("想要修改的元素不存在\n"); return; } ppos->data = data; } //现在是打印菜单 void printf_menu() { printf("================================================================\n"); printf(" ***带头节点双向循环链表*** \n"); printf("插入:\n"); printf("1.头插 2.尾插 3.Pos位置之前插入 4.pos位置之后插入\n"); printf("删除:\n"); printf("5.头删 6.尾删 7.pos位置删除\n"); printf("其他:\n"); printf("8.查找 9.修改\n"); printf("================================================================\n"); printf("\n"); } void Search_elem(DListNode* phead, Elemtype data) { assert(phead); int len = Get_DListlength(phead); if (len == 0) { printf("当前链表为0,没有元素可以查找...\n"); return; } DListNode* pcur = phead->next; int pos = 1; while (pcur != phead) { if (pcur->data == data) { printf("找到了!!!\n"); return; } pcur = pcur->next; pos++; } printf("找不到...\n"); return; } //现在是打印双向带头结点循环链表 void my_printf(DListNode* phead) { DListNode* pcur = phead->next; if (pcur == phead) { printf("当前链表内容为空...\n"); return; } while (pcur != phead) { printf("%d ", pcur->data); pcur = pcur->next; } } //现在是销毁链表 void Destory_list(DListNode* phead) { DListNode* pcur = phead->next; while (pcur != phead) { DListNode* delet = pcur; pcur = pcur->next; free(delet); delet = NULL; } free(phead); phead = NULL; } int main() { //DListNode head; DListNode* phead = Init_DListNode(); int choose = 0; do { system("cls"); printf_menu(); printf("当前的链表为:\n"); my_printf(phead); printf("\n"); printf("请输入你的选择(按-1结束程序):\n"); scanf("%d", &choose); switch (choose) { case 1: { printf("请输入你想输入元素的个数:\n"); int num = 0; Elemtype data = 0; scanf("%d", &num); printf("请输入你想输入的元素:\n"); for (int i = 0; i < num; i++) { scanf("%d", &data); Push_Front(phead, data); } Sleep(1000); printf("插入成功!!!\n"); Sleep(2000); break; } case 2: { printf("请输入你想输入元素的个数:\n"); int num = 0; Elemtype data = 0; scanf("%d", &num); printf("请输入你想输入的元素:\n"); for (int i = 0; i < num; i++) { scanf("%d", &data); Push_Back(phead, data); } Sleep(1000); printf("插入成功!!!\n"); Sleep(2000); break; } case 3: { printf("请输入pos:\n"); int pos = 0; scanf("%d", &pos); Elemtype data = 0; printf("请输入你想输入的元素:\n"); scanf("%d", &data); Push_pos_Front(phead, data, pos); Sleep(1000); printf("插入成功!!!\n"); Sleep(2000); break; } case 4: { printf("请输入pos:\n"); int pos = 0; scanf("%d", &pos); Elemtype data = 0; printf("请输入你想输入的元素:\n"); scanf("%d", &data); Push_pos_Back(phead, data, pos); Sleep(1000); printf("插入成功!!!\n"); Sleep(2000); break; } case 5: { Pop_Front(phead); printf("删除成功!\n"); Sleep(2000); break; } case 6: { Pop_Back(phead); printf("删除成功!\n"); Sleep(2000); break; } case 7: { printf("请输入你想删除的位置:\n"); int pos = 0; scanf("%d", &pos); Pop_Pos(phead, pos); printf("删除成功!\n"); Sleep(2000); break; } case 8: { printf("请输入你想查找的元素:\n"); Elemtype data = 0; scanf("%d", &data); Search_elem(phead, data); Sleep(2000); break; } case 9: { printf("请输入你向修改之后的元素:\n"); Elemtype data = 0; scanf("%d", &data); printf("请输入你想修改的位置:\n"); int pos = 0; scanf("%d", &pos); Change_elem(phead, data, pos); printf("修改成功!\n"); Sleep(2000); break; } case -1: { printf("正在退出程序...\n"); Sleep(2000); printf("退出成功!!!\n"); Sleep(2000); break; } } } while (choose != -1); Destory_list(phead); return 0; }

以上就是我对单链表所有内容的分享了,感谢大佬们的阅读~~~

文章是自己写的哈,有啥描述不对的、不恰当的地方,恳请大佬指正,看到后会第一时间修改,感谢您的阅读。

Read more

2G 内存云服务器部署 Spring Boot + MySQL 实战:从踩坑到上线

2G 内存云服务器部署 Spring Boot + MySQL 实战:从踩坑到上线

2G 内存云服务器部署 Spring Boot + MySQL 实战:从踩坑到上线 前言 最近把自己的全栈博客项目部署到了腾讯云的入门级服务器(2核2G),过程中踩了不少坑。本文记录完整的部署过程和问题排查思路,希望对同样在小规格服务器上部署 Java 项目的同学有所帮助。 项目技术栈: * 后端:Java 17 + Spring Boot 3.2.3 + Spring Security + JPA * 数据库:MySQL 8.0 * 前端:Flutter Web * 反向代理:Nginx 1.26 * 容器:Docker 28.4 服务器配置: * 腾讯云轻量应用服务器 * 2 核 CPU / 2GB 内存 / 50GB

By Ne0inhk
告别适配难题:Oracle 迁移 KingbaseES SQL 语法快速兼容方案

告别适配难题:Oracle 迁移 KingbaseES SQL 语法快速兼容方案

引言 在数据库国产化替代的浪潮中,Oracle 迁移到 KingbaseES(金仓数据库)已经成为很多企业数字化转型的核心任务。而 SQL 语法适配是迁移过程中最关键的技术环节,直接影响项目效率、成本和系统稳定性。 KingbaseES 以内核级兼容为基础,Oracle 常用 SQL 语法的兼容度能达到 100%,就算有少量差异化场景,也有清晰可落地的适配方案,能帮企业实现“应用无感、平滑迁移”。下面结合官方兼容性文档和实际迁移案例,拆解 SQL 语法适配的核心要点、差异化场景解决方案和批量落地技巧,给数据库管理员和开发人员提供实用参考。 文章目录 * 引言 * 一、迁移前必懂:SQL 兼容性整体情况 * 二、核心适配场景:差异化语法解决方案(含代码示例) * (一)数据类型映射:大多零代码,特殊场景稍调整 * (二)函数差异:精准适配,语法大多兼容(含对比代码) * 1.

By Ne0inhk
如何利用简单的浏览器插件Web Scraper爬取知乎评论数据

如何利用简单的浏览器插件Web Scraper爬取知乎评论数据

一、简单介绍: Web Scraper 的优点就是对新手友好,在最初抓取数据时,把底层的编程知识和网页知识都屏蔽了,可以非常快的入门,只需要鼠标点选几下,几分钟就可以搭建一个自定义的爬虫。 我在过去的半年里,写了很多篇关于 Web Scraper 的教程,本文类似于一篇导航文章,把爬虫的注意要点和我的教程连接起来。最快一个小时,最多一个下午,就可以掌握 Web Scraper 的使用,轻松应对日常生活中的数据爬取需求。 像这样的网页数据,想要通过网页爬虫的方式获取数据,可以下载web scraper进行爬虫 这是常见的网页类型: 1.单页 单页是最常见的网页类型。 我们日常阅读的文章,推文的详情页都可以归于这种类型。作为网页里最简单最常见的类型,Web Scraper 教程里就拿豆瓣电影作为案例,入门 Web Scraper 的基础使用。 2.分页列表 分页列表也是非常常见的网页类型。 互联网的资源可以说是无限的,当我们访问一个网站时,不可能一次性把所有的资源都加载到浏览器里。现在的主流做法是先加载一部分数据,随着用户的交互操作(

By Ne0inhk

Flutter for OpenHarmony: Flutter 三方库 ntp 精准同步鸿蒙设备系统时间(分布式协同授时利器)

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 在进行 OpenHarmony 分布式开发、金融交易或具有严格时效性的业务(如:秒杀倒计时、双因素认证 OTP)时,开发者不能完全信任设备本地的系统时间。用户可能为了某种目的手动篡改时间,或者由于网络同步问题导致时间存在偏差。 ntp 软件包提供了一种直接与互联网授时中心(NTP 服务器)通信的能力。它能绕过本地系统时钟,获取绝对精准的 UTC 时间,并计算出本地时间与真实时间的“偏移量(Offset)”。 一、核心授时原理 ntp 通过测量往返网络延迟来消除误差。 发送 NTP 请求 (UDP) 返回高精度时间戳 鸿蒙 App 全球授时中枢 (pool.ntp.org) 计算网络往返耗时 (RTT) 得出绝对时间偏移量 生成鸿蒙业务专用准时 二、

By Ne0inhk