libevent源码学习(5):TAILQ_QUEUE解析

2023-10-27

目录

前言

结点定义

链表初始化

链表查询及遍历

链表查询

链表遍历

插入结点

头插法

尾插法

前插法

后插法

删除结点

替换结点

总结


前言

        在libevent中使用到了TAILQ数据结构,看了一下其他资料,发现TAILQ这一数据结构不仅仅用于libevent中,在很多其他地方像linux内核中也有使用。它的内部实际上就是一个双向链表,可以实现结点的插入(头插、尾插、指定位置插入)、删除、替换和遍历等功能,不过所有功能都是通过宏函数来实现的,有的地方还是比较难以理解的,下面就来分析一下这一数据结构。

结点定义

         TAILQ中涉及到了两个很关键的结构体,如下所示:(queue.h)

#define TAILQ_HEAD(name, type)                        \
struct name {                                \
    struct type *tqh_first;    /* first element */            \
    struct type **tqh_last;    /* addr of last next element */        \
}
#define TAILQ_ENTRY(type)                        \
struct {                                \
    struct type *tqe_next;    /* next element */            \
    struct type **tqe_prev;    /* address of previous next element */    \
}

        先来猜测一下这两个宏定义的作用。在宏定义TAILQ_HEAD下的结构体中,包含了两个结构体成员tqh_first和tqh_last,先不管它们是几级指针,从成员名就能推测,tqh_first应当是和链表第一个元素有关,而tqh_last则和链表最后一个元素相关。再来看宏定义TAILQ_ENTRY,其中也包含了两个结构体成员tqe_next和tqe_prev,从变量名就会发现,二者应当与前后元素相关,这实际上和双向链表中的结点定义是非常相似的。因此,就可以推断,宏定义中所需输入的参数type实际上就是结点类型,这个结点类型应该包含但不限于TAILQ_ENTRY所定义的结构,而TAILQ_HEAD则是对于整个双向链表而言的,用于找到首尾结点元素,因此TAILQ_HEAD中的type也应该是与TAILQ_ENTRY中相同的结点类型。

        那么这里为什么TAILQ_HEAD还需要一个参数name呢?前面说了,TAILQ_ENTRY应当包含在结点类型的定义中,结点类型一旦定义好了并定义了一个结点,那么自然而然tqe_next和tqe_prev就都包含在该结点中了,此时TAILQ_ENTRY结构体作为一个匿名结构体即可,因此无需指定name来定义TAILQ_ENTRY结构体的名称;而对于TAILQ_HEAD来说,它是独立的数据类型,用来描述了双向链表的首尾结点,需要用TAILQ_HEAD来定义一个具体的结构体来存放首尾结点指针,因此这里必须指明结构体名name。

       根据前面的推测,现在来正式分析一下TAILQ_HEAD与TAILQ_ENTRY中各成员的含义。

       对于TAILQ_HEAD宏定义,其中的tqh_first为一级指针,tqh_last为二级指针,也就是说,tqh_first指向一个struct type类型的结点,而tqh_last则是指向一个指向一个struct type类型的结点的指针。TAILQ_ENTRY中的tqe_next和tqe_prev也是类似,这里就不多说了。那么到底各自指向什么呢?如果光是通过代码来推测一级、二级指针各自指向什么,我觉得太麻烦,因此我直接写一个程序先来看看结果如何:

#include <iostream>
#include "queue.h"

using namespace std;

struct Entry    //结点类型
{
	int val;
	TAILQ_ENTRY(Entry)entry;
};

TAILQ_HEAD(Head, Entry);   //名为Head的结构体,指向首尾Entry类型的结点

int _tmain(int argc, _TCHAR* argv[])
{
	Head Head_h;
	TAILQ_INIT(&Head_h); 

	for (int i = 0; i < 3; i++)
	{
		Entry * new_item = (Entry *)malloc(sizeof(Entry));
		new_item->val = i;
		TAILQ_INSERT_HEAD(&Head_h, new_item, entry); //头插法插入新结点
	}
	Entry* p;  //用于遍历时保存当前结点
	int i = 0;

	cout << "first : " << Head_h.tqh_first << "     first addr : " << &Head_h.tqh_first << endl << endl;   //打印first的值以及first的地址
	TAILQ_FOREACH(p, &Head_h, entry)  //遍历链表
	{
		cout << "Node " << i++ << "  addr : " << p << endl;  //打印结点地址
		cout << "prev : " << p->entry.tqe_prev << "     prev addr : " << &p->entry.tqe_prev << endl;   //打印prev的值以及prev地址
		cout << "next : " << p->entry.tqe_next << "     next addr : " << &p->entry.tqe_next << endl << endl;   //打印next的值以及next的地址
	}
	cout << "last : " << Head_h.tqh_last << "     last addr : " << &Head_h.tqh_last << endl;  //打印last的值以及last的地址

 	system("pause");
	return 0;
}

      在该程序中,定义了结点类型为Entry类型,其中包含了一个int型的val变量以及TAILQ_ENTRY所定义的结构体。可以看到,调用TAILQ_HEAD宏函数时,传入的name参数Head最终就成为了TAILQ_HEAD下结构体类型名。然后用Head来定义一个Head_h变量,其中保存的即是双向链表中的首尾结点信息了。接着就是以头插法形式插入三个结点,然后遍历输出各个结点中关键成员的值与地址,结果如下:

        通过程序结果显示,可以得出以下结论:

        对于每个结点,其prev的值等于前一个结点的next的地址,而next的值则等于下一个结点的地址,换句话说,每个结点的prev二级指针实际上是指向前一个结点的next一级指针变量,而next一级指针则是指向下一个结点;

        类似的,first一级指针指向第一个结点,第一个结点的prev二级指针指向first一级指针变量;

        last二级指针则是指向最后一个结点的next一级指针变量。

        通过一幅图来表达即是:(紫色框表示一个结点,蓝色线表示prev指针指向,绿色线表示next指针指向,黑色线表示first、last指针指向)

        从图中可以看出,我们可以将first和last所组成的结构体看做‘头结点’,它与第一个结点相连的同时也指向了最后一个结点的next指针。到此,也就搞清楚了每个指针的指向,接下来看一下TAILQ_QUEUE是如何进行链表操作的。

        注:以下将含first以及last指针的变量称为'头结点',将实际意义上的第一个结点称为'首结点'

链表初始化

        链表初始化使用的宏函数为TAILQ_INIT,其定义如下:

#define	TAILQ_INIT(head) do {						\   //初始化,先将头结点置为NULL,尾结点指向头结点
	(head)->tqh_first = NULL;					\
	(head)->tqh_last = &(head)->tqh_first;				\
} while (0)

        链表的初始化实际上只是初始化了‘头结点’,由于头结点的first与首结点相连,而此时链表为空,因此将头结点的first置为NULL,然后将last指针指向了first。这样初始化可以避免尾插结点时对特殊情况进行处理。多次使用的‘->’表明该宏函数传入的参数应当为指向头结点的指针

链表查询及遍历

链表查询

       TAILQ中关于结点的查询的宏定义有以下几种:

#define	TAILQ_FIRST(head)		((head)->tqh_first)   //首结点地址
#define	TAILQ_END(head)			NULL        //末尾以NULL结尾
#define	TAILQ_NEXT(elm, field)		((elm)->field.tqe_next)   //下一个结点地址
#define TAILQ_LAST(head, headname)					\    //尾结点的地址
	(*(((struct headname *)((head)->tqh_last))->tqh_last))
	
/* XXX */
#define TAILQ_PREV(elm, headname, field)				\    //前一个结点地址
	(*(((struct headname *)((elm)->field.tqe_prev))->tqh_last))
#define	TAILQ_EMPTY(head)						\    //判断链表是否为空
	(TAILQ_FIRST(head) == TAILQ_END(head))

      TAILQ_FIRST以及TAILQ_END就不用多说了。TAILQ_NEXT中涉及到了一个参数field,它的意义实际上就像前文例程中定义结点类型Entry中TAILQ_ENTRY型的变量名entry一样,用来访问匿名子结构体成员。如下所示:

struct Entry    //结点类型
{
    int val;
    TAILQ_ENTRY(Entry)entry;
};

       这里表示entry是一个拥有first和last两个成员变量的结构体变量,如果这里不定义一个变量entry,那么也就无法访问到结点中的first和last指针,而定义一个entry后,则可以根据entry来访问first和last指针了。因此TAILQ_NEXT中的field参数应当为定义结点结构体时,TAILQ_ENTRY结构体类型的变量。由此可见,一旦需要用到first和last指针,那么就应当传入field参数。
       TAILQ_LAST宏函数用于返回尾结点的地址。但是现在关于尾结点只有一个last指针,如何通过last指针获得尾结点的地址呢?回过头继续看这副图:

       由图可知,通过last指针只能获得尾结点的next指针的地址,并非是尾结点的地址,而指向尾结点的指针只有前一个结点的next指针,而尾结点的prev指针又刚好指向前一个结点的next指针,也就是说,对于尾结点,prev存放的是前一个结点的next指针的地址,那么(*prev)即是前一个结点的next指针,而前一个结点的next指针值就是当前结点的地址,因此,(*prev)就是尾结点的地址了,因此现在的问题变成了如何通过last来找到prev

       这里采用的方法是先将last强制转换为头结点类型,由于在内存中next的后面放的是prev,两个指针变量都占8个字节(64位),同样的头结点中的first也是放在last的前面,各自也是占8个字节,因此如果将next和prev看做一个整体,那么其在内存中的布局必定与头结点类型中的first和last内存布局一致。因此通过(struct headname*)last将last指针强制转换为头结点类型后,(struct headname*)last->first实际上还是next,而(struct headname*)last->last则是prev,这样也就通过last找到了prev。

        不得不说这种方法很巧妙,我个人一开始想到的办法是直接通过next的地址偏移sizeof(struct headname*)来找到prev,不过这样的话就可能受到内存对齐的影响(比如内存按16字节对齐,那么偏移值应当为16,但是sizeof的大小为4(32bit)或8(64bit),这样就是错误的,并且如果不同的编译器下结果都可能不一样),而这里的方法是直接强制转换为另一个内存布局相同的类型,这样即使在不同环境下内存对齐情况不同,对强转前后两种类型的影响也必定是相同的,二者的内存布局依然相同。

       因此现在要根据last来得到尾结点的地址就很简单了,(*prev)找到尾结点地址,为(struct headname*)last->last则是prev的值,替换一下就是*(struct headname*)last->last,将其写规范,即为(*(((struct headname *)((head)->tqh_last))->tqh_last))

       TAILQ_PREV宏函数用于找到前一个结点的地址,其原理与TAILQ_LAST类似,不过需要注意的是,这里传入的参数是当前结点地址,如上图所示,要找到前一个结点的地址,也就是要找到前一个结点的前一个结点的next指针地址,因此先用当前结点的prev找到前一个结点的next指针地址,强转后就可以找到前一个结点的prev指针,通过前一个结点的prev也就能找到前一个结点的前一个结点的next指针了,这样前一个结点的地址也就出来了。

        TAILQ_EMPTY用于判断链表是否为空,由于first和last分别为链表的首结点地址以及尾结点的next地址,因此当first为NULL时也就表示整个链表为空了。

链表遍历

         链表遍历分为正向遍历和反向遍历,有了上面对链表查询的分析,以下的代码应当非常容易理解了。

#define TAILQ_FOREACH(var, head, field)					\    //遍历
	for((var) = TAILQ_FIRST(head);					\
	    (var) != TAILQ_END(head);					\
	    (var) = TAILQ_NEXT(var, field))

#define TAILQ_FOREACH_REVERSE(var, head, headname, field)		\   //反向遍历
	for((var) = TAILQ_LAST(head, headname);				\
	    (var) != TAILQ_END(head);					\
	    (var) = TAILQ_PREV(var, headname, field))

         在500万个结点的情况下分别正向、反向遍历,遍历用时如下:

        可以发现正向遍历的效率更高,原因在于逆向遍历中的TAILQ_PREV需要进行两次寻址,而正向遍历中的TAILQ_NEXT则只需要进行一次寻址,因此对于数据量大的时候,TAILQ_PREV会明显比TAILQ_NEXT更慢。

插入结点

        TAILQ插入结点的方式有4种,分别为头插法TAILQ_INSERT_HEAD、尾插法TAILQ_INSERT_TAIL、前插法TAILQ_INSERT_BEFORE和后插法TAILQ_INSERT_AFTER。

头插法

        在分析TAILQ_INSERT_HEAD之前,先来思考一下当在链表头部插入结点时会发生什么:

        如下图所示,4号结点是新插入的结点,虚线为插入新结点时需要发生变化的线。

        首先第一步是新结点的next应当指向原来的首结点,如图中的绿色虚线;

        第二步是将原来的首结点的prev从指向first改为指向新结点的next指针,如图中的蓝色虚线;

        第三步是将first指针从指向原来首结点改为指向新结点,如图中黑色虚线;

        第四步是将新结点的prev指针指向first,如图蓝色虚线。

        在这四步,必须保证第一步在第三步之前,因为第一步中找到原来的首结点时是需要first指向首结点来找到首结点。

        当然也会有特殊情况,比如当前链表为空,此时插入一个新结点的话,由于不存在“原来的首结点”,因此第二步应该取消,取而代之的应该是将last指针指向新结点的next指针。

         头插法TAILQ_INSERT_HEAD宏函数定义如下:

#define TAILQ_INSERT_HEAD(head, elm, field) do {			\ //头插结点
	if (((elm)->field.tqe_next = (head)->tqh_first) != NULL)	\  //如果头结点不为NULL,说明此时链表不为空,同时将新结点elm的next指向当前的头结点
		(head)->tqh_first->field.tqe_prev =			\  //将原来的首结点的prev指向新结点的next
		    &(elm)->field.tqe_next;				\
	else								\  //如果头结点为NULL,说明此时链表为空
		(head)->tqh_last = &(elm)->field.tqe_next;		\  //last指向新结点的next
	(head)->tqh_first = (elm);					\ //重新将first指向新结点
	(elm)->field.tqe_prev = &(head)->tqh_first;			\ //新结点的prev指向first
} while (0)

         该函数的执行逻辑与前面所说的四步完全一样,这里就不多说了。

尾插法

         再来看看从链表尾部插入一个结点时会发生什么:

         如下图所示,第一步是先将新结点的next置为NULL;

         第二步是将新结点的prev通过last指针指向原来的尾结点的next;

         第三步是将原来的尾结点的next由原来的NULL值变为指向新结点

         第四步是将last指针由原来指向原尾结点的next改为指向新结点的next

         再来考虑特殊情况:如果链表本身为空,那么就不存在“原来的尾结点”了,第三步改为first指针指向新结点即可。

         尾插法TAILQ_INSERT_TAIL宏函数定义如下:

#define TAILQ_INSERT_TAIL(head, elm, field) do {			\ //尾插结点
	(elm)->field.tqe_next = NULL;					\  //将待插入结点的next置为NULL
	(elm)->field.tqe_prev = (head)->tqh_last;			\   //将待插入结点的prev指针指向当前的last结点地址
	*(head)->tqh_last = (elm);					\ //将last指向的结点设置为elm
	(head)->tqh_last = &(elm)->field.tqe_next;			\ 
} while (0)

         在该函数中,基本上是符合前面所说四步的,不过需要注意的是,在第二步中,本身是需要将新结点的prev指向原来尾结点的next,而原来尾结点的next又刚好就是last指针的指向,因此直接将last赋值给prev即可,这样也可以兼容链表为空的情况(链表为空时last是指向first的,此时prev就指向了first);在第三步中,对last进行解引用,由此此时的last指向的是原来尾结点的next,因此*last实际上就是原尾结点的next的值,将新结点的指针(elm)赋值给*last,也就是相当于将原尾结点的next指向了新结点。即使是链表为空,此时的*last也就是first的值,*last = elm即是让first指向了新结点,这样也就兼容了链表为空的情况。

        由此可以看出,保证last二级指针在链表为空的情况下指向first是非常重要的,这样可以巧妙地避免链表为空的特殊情况。如果用一般的一级指针,则需要先对链表是否为空进行判断。

前插法

       前插法TAILQ_INSERT_BEFORE的宏定义如下:

#define	TAILQ_INSERT_BEFORE(listelm, elm, field) do {			\
	(elm)->field.tqe_prev = (listelm)->field.tqe_prev;		\ //将原结点的前一个结点作为新结点的前一个结点
	(elm)->field.tqe_next = (listelm);				\  //新结点的next指向原结点
	*(listelm)->field.tqe_prev = (elm);				\  //让本该指向原结点的指针指向新结点
	(listelm)->field.tqe_prev = &(elm)->field.tqe_next;		\  //原结点的prev指向新结点的next
} while (0)

       可以看到,这里的前插法代码并没有对特殊情况进行特殊处理,前插的特殊情况即是前插的原结点本身就是首结点,此时进行前插就相当于头插。

       第一步将新结点的prev指向原结点prev指向的地方,即使链表中只有一个结点,那么新结点的prev指向头结点的first也是没有问题的;第二步将新结点的next指向原结点;第三步中先对原结点的prev解引用,得到的实际上是指向原结点自身的指针,这也是prev作为二级指针指向前一个结点的next指针的好处:*prev是指向当前结点的指针,将elm赋值给*prev的意义,就相当于是将原本该指向原结点的指针让其指向新结点,这样也就避免了特殊情况的处理;最后一步是让原结点的prev指向新结点的next。从而完成结点的前插。

后插法

       后插法TAILQ_INSERT_AFTER的宏定义如下:其中head为头结点指针,listelm为原结点,elm为插入结点

#define TAILQ_INSERT_AFTER(head, listelm, elm, field) do {		\
	if (((elm)->field.tqe_next = (listelm)->field.tqe_next) != NULL)\  //将原结点的next赋值给新结点的next,即新结点的next指向原结点的下一个结点。如果不为NULL,说明原结点不是尾结点
		(elm)->field.tqe_next->field.tqe_prev =			\  //原结点不是尾结点,就将原结点的后一个结点的prev指向新结点的next
		    &(elm)->field.tqe_next;				\
	else								\   //在尾结点后面插入新结点
		(head)->tqh_last = &(elm)->field.tqe_next;		\  //last指针指向新结点的next
	(listelm)->field.tqe_next = (elm);				\  //原结点的next指向新结点
	(elm)->field.tqe_prev = &(listelm)->field.tqe_next;		\  //新结点的prev指向原结点的next
} while (0)

        后插法需要判断特殊情况,看注释即可。

删除结点

      删除节点TAILQ_REMOVE的宏定义如下:

#define TAILQ_REMOVE(head, elm, field) do {				\
	if (((elm)->field.tqe_next) != NULL)				\  //如果删除的结点不是尾结点
		(elm)->field.tqe_next->field.tqe_prev =			\  //让删除结点的下一个结点的prev指向删除结点的前一个结点
		    (elm)->field.tqe_prev;				\
	else								\   //删除尾结点
		(head)->tqh_last = (elm)->field.tqe_prev;		\  //last指向删除结点的prev
	*(elm)->field.tqe_prev = (elm)->field.tqe_next;			\  //原本应当指向删除结点的指针指向删除结点的next
} while (0)

       需要注意的是,如果链表中只剩一个结点,当删除这个结点后,由于last会重新指向被删除结点的prev,而该结点的prev必定是指向first的,这样又使得删除结点后的空链表回到最初状态last指向first。

替换结点

       替换结点TAILQ_REPLACE的宏定义如下:其中head为头结点指针,elm、elm2分别为被替换结点以及新结点

#define TAILQ_REPLACE(head, elm, elm2, field) do {			\  
	if (((elm2)->field.tqe_next = (elm)->field.tqe_next) != NULL)	\  //将被替换结点的next赋值给新结点的next,如果被替换的结点不是尾结点
		(elm2)->field.tqe_next->field.tqe_prev =		\ //将被替换结点的下一个结点的prev指向新结点的next
		    &(elm2)->field.tqe_next;				\
	else								\   //被替换结点为尾结点
		(head)->tqh_last = &(elm2)->field.tqe_next;		\  //last指向新结点的next
	(elm2)->field.tqe_prev = (elm)->field.tqe_prev;			\ //被替换结点的prev赋值给新结点的prev
	*(elm2)->field.tqe_prev = (elm2);				\  //原本指向被替换结点的指针指向新结点
} while (0)

总结

       TAILQ_QUEUE的本质依然是双向链表,为双向链表定义一个头结点是非常重要的,如果没有头结点,那么在删除或插入结点时还需要去判断结点是否为首结点,以此来处理“当前结点为首结点”的特殊情况;而如果有头结点,那么就完全不用考虑这种特殊情况,因为头结点是必定存在的,即使链表为空它也会在那,如果链表不为空,头结点就会与第一个结点连接起来,逻辑上的第一个结点就称为了物理上的第二个结点,其prev指针是有意义的,这样就可以按照处理普通结点的方式去处理“第一个结点”。头结点的好处在TAILQ_QUEUE中仍然存在,从TAILQ_QUEUE中定义的各个宏函数中可以发现,特殊情况只有链表为空和所处理的结点为尾结点两种情况,完全避免了处理首结点的特殊情况

       实际上,将prev和last定义为一级指针也完全可以避免处理首结点特殊情况,那为什么还要将prev和last定义为二级指针呢?

       在一级指针中之所以可以避免处理头结点的特殊情况,是因为头结点与普通结点的类型是完全一样的,因此第一个结点的prev可以直接指向头结点,而在TAILQ中的头结点类型和普通结点类型不一样。在TAILQ中,头结点只定义了两个变量用来找到第一个和最后一个结点,而对于普通结点而言,不光需要找到前驱结点和后驱结点,还需要有结点自身的一些属性(比如说data等等)(当然这里你也可以为头结点强行加上一个变量让它和普通结点类型保持一致,但是TAILQ中并没有这么做)。

        在这种头结点类型和普通结点类型不一致的情况下,第一个结点的prev是无法直接指向头结点的,因此就只能让第一个结点的prev指向头结点的first指针(first依然保留一级指针),不过这样一来,第一个结点的prev就变成二级指针了,因此普通结点的prev就应当定义为二级指针了。此时对于普通结点而言,prev为二级指针,next是一级指针,那么prev就应当指向前一个结点的next指针。而将头结点的last也定义为二级指针主要是为了方便用于寻找某一个结点的前一个结点时的类型转换。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

libevent源码学习(5):TAILQ_QUEUE解析 的相关文章

  • VMware WorkStation安装CentOS7

    VMware WorkStation安装CentOS7 1 安装 设置bios 检查物理机虚拟化支持是否开启 需要进入到BIOS中设置 因各种电脑型号进入BIOS 方式不同 同学们自行查找对应品牌电脑如何进入BIOS 建议 先安装 如果安装
  • C语言 输出3X3的转置矩阵

    方法一 普通函数调用 include
  • GUN和GPL的大概意思

    广而告之 支持一下阿里云 阿里云ECS服务器 有幸运券了 欢迎大家来领取 https promotion aliyun com ntms act ambassador sharetouser html userCode 5uqvqirt p
  • VS code安装和使用技巧

    VS Code 是微软提供的一款轻量级但功能十分强大的编辑器 内置了对JavaScript TypeScript和Node js语言的支持 并且为其他语言如C C Python PHP等提供了丰富的扩展库和运行时 一 VS Code的安装
  • java nio 基础

    Java NIO 由以下三部分组成 Channels 通道部分 Buffers 数据载体部分 Selects 选择器部分 重点应用于网络开发 基于事件驱动类型 Channel 与Buffers 基本上 所有的NIO 都从Channel 开始
  • POP3 邮件接收 出现乱码了,希望哪位能帮忙看下啥问题!帮我改下,谢谢

    using System using System Collections Generic using System ComponentModel using System Data using System Drawing using S
  • tensorflow运行在gpu还是cpu

    tensorflow在电脑的gpu和cpu上均可运行 cpu 0 机器的 CPU device GPU 0 机器的 GPU 如果有一个 device GPU 1 机器的第二个 GPU 以此类推 当想要知道指令和张量在哪个设备上运行时 可以这
  • QT CREATOR 插件开发:添加新的工程类型

    Qt Creator 中 新的工程类型将出现在 文件 gt 新建 菜单项中 我们可以通过打开的选择工程类型的对话框来找到所需要的工程 在本章中 我们将学习如何向上面所示的对话框中添加新的工程类型 Core IWizard接口 Qt Crea
  • OA 第四天笔记

    权限 控制功能的使用 Web应用中的权限 每个功能都有相应URL地址 对功能的控制就是对URL地址的访问控制 权限方案 用户 角色 role 权限 与权限相关的功能具体有哪些 初始化数据 分配权限 使用权限 insert into 1 权限
  • 用Python怎么多赚钱?6种办法用上 让你过上挣钱的好日子

    编程语言Python特别火 火到几乎所有的程序开发公司都要求自己的员工掌握它 可以说 不懂Python的码农们在整个IT行业是无法想象的 不仅如此 Python除了在编程方面应用广泛 而且还能在业余时间变现 让拥有这方面技能的人员获得丰厚的
  • asp二进制mysql_asp 二进制保存数据库

    C 将image中的显示的图片转换成二进制 原文 C 将image中的显示的图片转换成二进制 1 将Image图像文件存入到数据库中 我们知道数据库里的Image类型的数据是 二进制数据 因此必须将图像文件转换成字节数组才能存入数据库中 V
  • Anaconda: Linux下安装Anaconda

    一 说明 1 Linux CentOS7 2 Anaconda Anaconda3 2018 12 Linux x86 64 3 这只是个人的记录 最终以官网安装步骤为准 见参考资料链接 二 安装步骤 1 软件下载 进入到anaconda官
  • 利用插值算法进行上采样和下采样

    1 最邻近插值 The nearest interpolation 原理 划分四个区域 将某点值赋给区域的所有值 def the nearest interpolation img nh nw h w c img shape h是heigh
  • Java 文字转图片输出,Java 输出透明背景图片,Java文字转图片防爬虫

    近部分页面数据被爬虫疯狂的使用 主要就是采用动态代理IP爬取数据 主要是不控制频率 这个最恶心 因为对方是采用动态代理的方式 所以没什么特别好的防止方式 具体防止抓取数据方案大全 下篇博客我会做一些讲解 本篇也是防爬虫的一个方案 就是部分核

随机推荐

  • Bug处理之ImportError: cannot import name 'HTMLParseError

    操作系统Windows10 0 PythonIDE Pycharm2018 02 Python版本 python3 6 anaconda平台 Packages bs4 beautifulsoup4 问题描述 error ImportErro
  • springboot 查看各种依赖的版本(idea工具):

    说明 跟踪 ctrl mouse left
  • CopyTranslator 翻译神器的安装与使用

    download https github com copytranslator copytranslator releases guide https copytranslator github io guide 问题 第一次安装后就会默
  • macbook打印出现乱码解决方案

    macbook打印出现乱码解决方案 参考文章 1 macbook打印出现乱码解决方案 2 https www cnblogs com wenluren p 11325669 html 3 https www javazxz com thre
  • ddos攻击详解

    分布式拒绝服务攻击 DDoS 攻击 是一种网络攻击 旨在通过向目标系统发送大量的流量或请求 以使其无法正常运行或响应合法用户的请求 这种攻击通常涉及多台被感染的计算机 这些计算机被称为 僵尸 或 肉鸡 并被攻击者控制 以协同发动攻击 DDo
  • 03-java数据结构之链表的学习(单链表、双链表等)

    文章目录 1 链表 1 1 链表的介绍 2 单链表 2 1 单链表的显示 2 2 单链表的添加操作 2 2 1 直接添加到链表的尾部 2 2 2 根据no插入到指定位置 2 3 单链表节点的修改 2 4 单链表节点的删除 3 双向链表 3
  • 大数据从入门到精通(超详细版)之Hadoop详解

    前言 嗨 各位小伙伴 恭喜大家学习到这里 不知道关于大数据前面的知识遗忘程度怎么样了 又或者是对大数据后面的知识是否感兴趣 本文是 大数据从入门到精通 超详细版 的一部分 小伙伴们如果对此感谢兴趣的话 推荐大家按照大数据学习路径开始学习哦
  • Ubuntu 16.04下deb包的安装及常用命令

    如果ubuntu要安装新软件 已有deb安装包 例如 iptux deb 但是无法登录到桌面环境 那该怎么安装 答案是 使用dpkg命令 dpkg命令常用格式如下 sudo dpkg I iptux deb 查看iptux deb软件包的详
  • 数据对象总结

    JavaScript对象 对象属于一种复合的数据类型 在对象中可以存储多个不同数据类型的属性 JavaScript 中的所有事物都是对象 字符串 数值 数组 函数 此外 JavaScript 还允许自定义对象 JavaScript 提供多个
  • C++函数调用那些事

    C 函数调用 C 形参带默认值的函数 带默认值的形参必须从右往左给 给出以下实例 int sum int x int y 无默认值函数 int sum int x int y 0 y有默认值 int sum int x 0 int y 0
  • c#初级

    类 创建一个类 public class A 访问修饰符 public公有 protected 受保护的 private私有的 public 在类内和类外都可以使用 public int a 定义一个字段a protected 他只能在类内
  • 模运算

    http blog csdn net ld326 article details 7880429 模运算即求余运算 模 是 Mod 的音译 模运算多应用于程序编写中 Mod的含义为求余 模运算在数论和程序设计中都有着广泛的应用 从奇偶数的判
  • 【Flutter造轮子】Text组件显示指定行文字,若有超出加...点击查看更多

    效果如上图 如果超出 显示 点击查看更多 正好凑够4行 再添加一个字便超出4行 原理 使用TextPainter逐渐添加字尝试 该组件超出的话 其属性didExceedMaxLines为true 代码如下 文字超出一定行 自动隐藏 并添加入
  • AI绘画指南:在CentOS7中训练Lora模型

    本次训练在centos7中完成 使用的训练脚本是 https github com Akegarasu lora scripts git https github com kohya ss sd scripts git 一 安装GPU环境
  • 【动态规划】合唱队形

    题目描述 n位同学站成一排 音乐老师要请其中的 n K 位同学出列 使得剩下的K位同学排成合唱队形 合唱队形是指这样的一种队形 设K位同学从左到右依次编号为1 2 K 他们的身高分别为T1 T2 TK 则他们的身高满足T1 lt Ti l
  • 关于代码家(干货集中营)共享android端知识点综合整理

    关于代码家 干货集中营 共享android端知识点综合整理 标签 开源项目自定义控件教程特效工具 2016 03 08 13 23 8520人阅读 评论 2 收藏 举报 分类 移动开发 28 版权声明 本文为博主原创文章 未经博主允许不得转
  • 探索MySQL错误: 1241 - Operand should contain 1 column(s)问题解决方案

    AI绘画关于SD MJ GPT SDXL百科全书 面试题分享点我直达 2023Python面试题 2023最新面试合集链接 2023大厂面试题PDF 面试题PDF版本 java python面试题 项目实战 AI文本 OCR识别最佳实践 A
  • Qt中moc问题(qt moc 处理 cpp)

    我用的是QT Designer 一般只有用到信号signals和槽slots时才会用到MOC 因为采用信号signals和槽slots是QT的特性 而C 没有 所以采用了MOC 元对象编译器 把信号signals和槽slots部分编译成C
  • 【华为OD统一考试B卷

    在线OJ 已购买本专栏用户 请私信博主开通账号 在线刷题 运行出现 Runtime Error 0Aborted 请忽略 华为OD统一考试A卷 B卷 新题库说明 2023年5月份 华为官方已经将的 2022 0223Q 1 2 3 4 统一
  • libevent源码学习(5):TAILQ_QUEUE解析

    目录 前言 结点定义 链表初始化 链表查询及遍历 链表查询 链表遍历 插入结点 头插法 尾插法 前插法 后插法 删除结点 替换结点 总结 前言 在libevent中使用到了TAILQ数据结构 看了一下其他资料 发现TAILQ这一数据结构不仅