表、栈和队列及其C语言实现

news2024/12/25 9:13:16

1、抽样数据类型

程序设计的基本法则之一是例程不应该超过一页。这可以通过把程序分割为一些模块(module)来实现。每个模块是一个逻辑单元并执行某个特定的任务,它通过调用其他模块而本身保持很小。模块化有几个优点。首先,调试小程序比调试大程序容易得多。第二,多个人同时对一个模块式编程要更容易。第三,一个写的好的模块化程序把某些依赖关系值局限在一个例程中,这样使得修改起来会更容易。例如,需要以某种格式编写输出,那么重要的是让一个例程去实现它。如果打印语句分散在程序各处,那么修改所费的时间就会明显拖长。全局变量和副作用是有害的观念也正是出于模块化是有益的想法。

抽象数据类型(abstract data type,ADT)是一些操作的集合。抽象数据类型是数学的抽象;在ADT的定义中根本没有涉及如何实现操作的集合。这可以看作是模块化设计的扩充。

对诸如表、集合、图和它们的操作一起可以看作是抽象数据类型,就像整数、实数和布尔量是整数类型一样。整数、实数及布尔量有它们相关的操作,而抽样数据类型也有与它们自己相关的操作。对于集合ADT,我们可以有诸如(union)、交(intersection)、测定大小(size)以及取余(complement)等操作。或者,我们也可以只要两种操作:并和查找(find),这两种操作又在该集合上定义了一种不同的ADT。

我们基本的方法是,这些操作的实现只是在程序中编写一次,而程序中任何其他部分需要在该ADT上运行其中一种操作时可以通过调用适当的函数来进行。如果由于某些原因需要改变操作的细节,那么通过只修改运行这些ADT操作的例程应该更容易实现。在理想的情况下这种改变对于程序的其余部分通常是完全透明的。

对于每种ADT并不存在什么法则来告诉我们必须要有哪些操作。这是一种设计决策。错误处理和关系的重组(在适当的地方)一般也取决于程序设计者。

2.表ADT

将处理一般的形如A1,A2,A3,...,AN的表。我们说,这个表的大小是N。我们称大小为0的表为空表(empty list)。对于除空表外的任何表,我们说Ai+1后继Ai(或继Ai之后)并称Ai-1(i<N)前驱Ai(i>1)。表中的第一个元素时A1,而最后一个元素是AN。我们将不定义A1的前驱元,也不定义AN的后继元。元素Ai在表中的位置为i。为了简单起见,我们在讨论中将假设表中的元素是整数,但一般说来任意的复元素也是允许的。

与这些“定义”相关的是我们要在表ADT上进行的操作的集合。PrintList和MakeEmpty是常见的操作,其功能显而易见;Find返回关键字首次出现的位置;Insert和Delete一般是从表的某个位置插入和删除某个关键字;而FindKth则返回某个位置上(作为参数而被指定)的元素。如果34,12,52,16,12是一个表,则Find(52)会返回3;Insert(X,3)可能把表变成34,12,52,X,16,12(如果我们在给定位置的后面插入的话);而Delete(52)则将该表变为34,12,X,16,12。当然,一个函数的功能怎样才算恰当,完全要由程序设计员来确定,就像对特殊情况的处理那样(例如,上述Find(1)返回什么?)。我们还可以添加一些运算,比如Next和Previous,它们去一个位置作为参数并分别返回其后继元素和前驱元素的位置。

2.1 表的简单数组实现

对表的所有操作都可以通过使用数组来实现。虽然数组是动态指定的,但是还是需要对表的大小的最大值进行估计。通常需要估计得大一些,从而会浪费大量的空间。这是严重的局限,特别是在存在许多未知大小的表的情况下。

数组实现使得PrintList和Find正如所预期的那样以线性时间执行,而FindKth则花费常数时间。然而,插入和删除的花费是昂贵的。例如,在位置0的插入(这实际上是做一个新的第一元素)首先需要将整个数组后移一个位置以空出空间来,而删除第一个元素则需要将表中的所有元素前移一个位置,因此这两种操作的最坏情况为O(N)。平均来看,这两种运算都需要移动表的一般的元素,因此仍需要线性时间。只通过N次相继插入来建立一个表将需要二次时间。

因为插入和删除的运行时间是如此的慢以及表的大小还必须事先已知,所以简单数组一般不能用来实现表这种结构。

2.2 链表

为了避免插入和删除的线性开销,需要允许表可以不连续存储,否则表的部分或全部需要整体移动。下图给出了链表的一般想法。

                              

链表由一系列不必在内存中相连的结构组成。每一个结构均含有表元素和指向包含该元素后继元的结构的指针。我们称之为Next指针。最后一个单元的Next指针指向NULL;该值由C定义并且与其他指针混淆。ANSI C规定NULL为零。

我们回忆一下,指针变量就是包含存储另外某个元数据的地址的变量。因此,如果P被声明为指向一个结构的指针,那么存储在P中的值就被解释为主存中的一个位置,在该位置能够找到一个结构。该结构的一个域可以通过P->FieldName访问,其中FieldName是我们想要考察的域的名字。下图表示表的具体表示。这个表含有五个结构,恰好在内存中分配给它们的位置分别是1000,800,712,992和692。第一个结构的指针含有值800,它提供了第二个结构所在的位置。其余每个结构也都有一个指针哟用于类似的目的。当然,为了访问该表,我们需要知道在哪里能够找到第一个单元。指针变量就能够用于这个目的。重要的是要记住,一个指针就是一个数。

                       

为了执行PrintList(L)或Find(L, Key),我们只要将一个指针传递到该表的第一个元素,然后用一些Next指针穿越该表即可。这种操作显然是线性时间,虽然这个常数可能会比用数组实现时要大。FIndKth操作不如数组实现的效率高;FindKth(L,i)花费O(i)时间以显性方式穿越链表而完成。在实践中这个界是保守的,因为调用FindKth常常是以(按i)排序的方式进行。例如,FindKth(L,2),FindKth(L,3),FIndKth(L,4)以及FindKth(L,6)可以通过对表的一次扫描同时实现。

删除命令可以通过修改一个指针来实现,下图给出了在原表中删除第三个元素的结果。

                     

插入命令需要使用一次malloc调用从系统得到一个新单元,并在此之后执行两次指针调整。其一般想法在下图中给出,虚线表示原来的指针。

                      

2.3 程序设计细节

上面的描述实际上足以使每一部分都能正常工作,但还是有几处地方可能会出问题。首先,并不存在所给定义出发在表的前面插入元素的真正显性的方法。第二,从表的前面实行删除是一个特殊的情况,因为它改变了表的起始端;编程中的疏忽将造成表的丢失。第三个问题涉及一般的删除。虽然上述指针的移动很简单,但是删除算法要求我们记住被删除元素前面的表元。

事实上,少做一个简单的变化就能够解决所有这三个问题。我们将留出一个标志结点,有时候称之为表头(header)或哑结点(dummy node)。这是通常的一种习惯,再后面将会多从看到它。我们约定,表头在位置0处。下图给出了一个带有表头的链表,它表示A1,A2,...,A5。

                    

为避免删除相关的一些问题,我们需要编写例程FindPrevious,它将返回我们要删除的表元的前驱元的位置。如果我们使用表头,那么当我们删除表的第一个元素时,FIndPrevious将返回表头的位置。头结点的使用多少是有些争议的一些人认为,添加假想的单元只是为了避免特殊情形,这样的理由不够充分;他们把头结点的使用看成与老式的随意删除没有多大区别。不过即使那样,我们还是在这里使用它,这完全因为它使我们能够表达基本的指针操作且又不致使特殊情形的代码含混不清。除此之外,要不要使用表头则是属于个人兴趣的问题。

作为例子,我们将把这些表ADT的半数例程编写出来。首先,在以下代码中给出我们要的声明。按照C的约定,作为类型的List(表)和Position(位置)以及函数的原型都列在所谓的.h头文件中。具体的Node(结点)声明则在.c文件中。

# ifndef _List_H
struct Node;
typedef struct Node *PtrToNode;
typedef PtrToNode List;
typedef PtrToNode Position;

List MakeEmpty( List L );
int IsEmpty( List L );
int IsLast( Position P, List L );
Position Find(ElementType X, List L);
void Insert(ElementType X, List L, Position P);
void DeleteList( List L );
Position Header( List L );
Position First( List L );
Position Advance( Position P );
ElememntType Retrieve( Position P );

# endif   /* _List_H */
struct Node
{
   ElementType Element;
   Position Next;
};

一个空表如下图所示,

                                                    

  • 测试一个链表是否为空表
/* Return true if L is empty */
int
IsEmpty( List L )
{ 
    return L->Next == NULL;
}
  • 测试当前位置是否为链表的末尾函数
/* Return true if P is the last position in list L */
/* Parameter L is unused in this implementation */

int
IsLast( Position P, List L )
{
   return P-> == NULL;
}
  • 返回某个元素在表中的位置
/* Return Position of X in L; NULL if not Found */

Position
Find( ElementType X, List L )
{
   Position P;
   
   P = L->Next;
   while(P != NULL && P->Element != X)
      P = P->Next;
   
   return P;
} 
  • 删除表L中的某个元素

如果X出现不止一次或者根本就没有,那么我们通过调用FindPrevious函数找出含有X的表示的前驱元素P。

/* Delete First occurance of X from a list */
/* Assume use of a header node */
void 
Delete( ElementType X, List L )
{
  Position P, TmpCell;
  
  P = FindPrevious( X , L );
  
  if( !IsLast( P , L ) ) /*Assumption of header use*/
  {
     TmpCell = P -> Next;
     P->Next = TmpCell -> Next; /* Bypass deleted cell */
     free(TmpCell);
  }
}

/* If X is not found, then Next field of returned */
/* Position is NULL */
/* Assume a header */

Position
FindPrevious(ElementType X, List L)
{
   Position P;
   
   P = L;
   while(P -> Next != NULL && P -> Next -> Element != X)
         P = P -> Next;
         retrun P;
}

将元素插入列表

/* Insert (after legal position P) */
/* Header implementation assumed */
/* Parameter L is unused in this implementation */

void 
Insert( ElementType X, List L, Position P )
{
    Position TmpCell;
    
    TmpCell = malloc( sizeof( struct Node ) );
    if( TmpCell == NULL )
        FataError("Out of space!!!")
    
    TmpCell->Element = X;
    TmpCell->Next = P->Next;
    P->Next = TmpCEell; 
} 

注意,我们已经把表L传递给Insert例程和IsLast例程,尽管它从未被使用过。之所以这么做,是因为别的方法可能会需要这些信息,因此,若不传递表L有可能使得使用ADT的想法失败。

  我们要写的最后一个例程是插入例程。

将要插入的元素与表L和位置P一起传入。这个Insert例程将一个元素插入到P所示的位置之后。我们这个决定有随意性,它意味着插入操作如何实现并没有完全确定的规则。很有可能将新元素插入到位置P处(即在位置P处当时的元素的前面),但是这么做则需要知道位置P前面的元素。它可以通过调用FindPrevious而得到。因此重要的是要说明你要干什么。

注意,我们已将把表传递给Insert例程和IsLast例程,尽管它从未被使用过。之所以这么做,是因为别的实现方法可能会需要这些信息,因此,若不传递L有可能使得使用ADT的想法失败。除Find和FindPrevious例程外(还有例程Delete,它调用FindPrevious),我们已经编码所有操作均需O(1)时间。这是因为在所有的情况下,不管表有多大都只执行固定数目的指令。对于例程Find和FIndPrevious,在最坏的情况下运行时间是O(N),因为此时若元素未找到或位于表的末尾则可能遍历整个表。平均来看,运行时间是O(N),因为必须平均扫描半个表。

2.4  双链表

有时候以倒序扫描链表很方便。标注实现方法此时无能无能为力,然而解决方法却很简单。只要在数据结构上附加一个域,使它包含指指向前面一个单元的指针即可。其开销是一个附加的链,它增加了空间的需求,同时也使得插入和删除的开销增加一倍,因为有更多的指针需要定位。另一方面,它简化了删除操作,因为你不再被迫使用一个指向前驱元的种子指针来访问一个关键字;这个信息是现成的。下图表示一个双链表(doubly linked list)。

                           

2.5 循环列表

让最后的单元反过来直接第一个单元是一种流行的做法。他可以有表头,也可以没有表头(若有表头,则最后的单元就指向它),并且还可以是双向链表(第一个单元的前驱元指针指向最后的单元)。这无疑会影响某些测试,不过这种结构在某些应用程序中却很流行,下图给出了一个无表头的双向循环列表。

                                     

2 栈ADT

栈(stack)是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫做栈顶(top)。对栈的基本操作有Push(进栈)和PoP(出栈),前者相当于插入,后者则相当于删除最后插入的元素。最后插入的元素可以通过使用Top例程在执行Pop之前进行考察。对空栈进行的Pop或Top一般被认为是栈ADT的错误。另一方面,当运行Push时空间用尽是一个实现错误,但不是ADT错误。

                                 

栈有时又叫做LIFO(后劲先出)表。在上图中描述的模型只象征着Push是输入操作而Pop和Top是输出操作。普通的清空栈的操作和判断是否空栈的测试都是栈的操作指令系统的一部分,但是,对栈所能做的,基本上也就是Push和Pop操作。一般的模型是,存在某个元素位于栈顶,而该元素时唯一可见的元素。

由于栈是一个表,因此任何实现表的方式都能实现栈,下面给出链表实现方法。

通过在表顶端插入来实现Push,通过删除表顶端元素实现Pop。Top操作只是考察表顶端元素并返回它的值。有时Pop操作和Top操作合二为一。创建一个空栈也很简单,只要建立一个头结点;MakeEmpty设置Next指针指向NULL。Push是作为向链表前端进行插入而实现的,其中,表的前端作为栈顶。Top的实施是通过考察表在第一个位置上的元素而完成的。最后,Pop是通过删除表的前端的元素而实现的。

  • 栈ADT链表实现的类型声明
# ifndef _Stack_h

strcuct Node;
typedef struct Node Node *PtrToNode;
typedef PtrToNode Stack;

int IsEmpty(Stack S);
Stack CreateStack( void );
void DisposeStack(Stack S);
void MakeEmpty(Stack S);
ElementType Top( Stack S );
void Pop( Stack S );

# endif  /* _Stack_h */
/*Place in implementation file*/
/*Stack implementation is a linked list with a header*/
struct Node
{
   ElementType Element;
   PtrToNode  Next;
}
  • 测试一个栈是否为空
int
IsEmpty( Stack S )
{
   return S->Next  == NULL;
}
  • 创建一个空栈
Stack
CreateStack( void )
{
    Stack S;
    
    S = malloc(sizeof(stuct Node));
    if(S == NULL)
       FatalError("Out of space!")
    S -> Next == NULL;
    Make Empty(S);
    return S;
}

void
MakeEmpty( Stack S )
{
   if( S == NULL )
       Error("Must use CreateStack First");
   else
      while( !IsEmpty( S ) )
          Pop( S );
}
  •  Push进栈
void
Push( ElementType S, Stack S )
{
    PtrToNode  TmpCell;
    
    TmpCell = malloc(sizeof(struct Node));
    if( TmpCell == NULL )
        FataError( "Out of sapce!!" )
    else
    {
       TmpCell -> Element = X;
       TmpCell -> Next = S -> Next;
       S -> Next = TmpCell;
    }

}
  • 返回栈顶元素
ElementType
Top( Stack S )
{
   if( !IsEmpty( S ) )
       return S->Next->Element;
   Error( "Empty stack" );
   return 0; /* Return value used to avoid warning */
}
  • 从栈弹出元素
void
Pop( Stack S )
{
   PtrToNode FirstCell;
   
   if( IsEmpty( S ) )
       Error("Empty stack");
   else
   {
       FirstCell = S -> Next;
       S -> Next = S -> Next -> Next;
       free( FirstCell );
   }
}

很清楚,所有的操作均花费常数时间,因为这些例程没有任何地方涉及到栈的大小(空栈除外),更不用说依赖于栈大小的循环了。这种实现方法的缺点在于对malloc和free的调用的开销是昂贵的,特别是与指针操作的例程相比尤其如此。有的缺点通过使用第二个栈可以避免。该第二个栈初始时为空栈,当一个单元从第一个栈弹出时,它只是被方法到了第二个栈中。此后,当地一个栈需要更新的单元时,它首先去检查第二个栈。

3、 队列模型

队列的基本操作是Enqueue(入队),它是在表的末端(rear)插入一个元素,还有Dequeue(出队),它是删除(或返回)在表的开头(叫做队头(front))的元素。下图显示一个队列的抽象模型。

                    

对于队列而言任何表的实现都是合法的。像栈一样,对于每一种操作,链表实现和数组实现都给出快速O(1)运行时间。下面讨论队列的数组实现。

对于每一个队列数据结构,保留一个数组Queue[ ]以及位置Front和Rear,它们代表列表的两端。还要记录实际存在与队列中的元素的个数Size。所有这些信息是一个结构的一部分,除队列例程本身外通常不会有例程直接访问它们。下图表示处于某个中间状态的一个队列。顺便指出,图中那些空白单元是有着不确定的值的。特别地,前三个单元含有曾经属于该队列的元素。

                     

操作应该是清楚地。为使一个元素X入队,让Size和Rear增1,然后置Queue[Rear] = X。若使一个元素出队,置返回值为Queue[Front],Size减1,然后使Front增1。也可以有其他的策略。现在讨论错误的检测。这种实现存在一个潜在的问题。经过10次入队后,队列似乎是满了,因为Rear现在是10,而下一次再入队就会是一个不存在的设置。然而,队列中也许只存在几个元素,因为若干元素可能已经出队了。像栈一样,即使在有许多操作的情况下队列也常常不是很大。简单的解决方法是,只要Front或Rear到达数组的尾端,它就又绕回到开头。下图显示在某些操作期间的队列情况。这叫做循环数组(cicular array)实现。实现回绕所需要的附加代码时极小的(虽然它可能使得运行时间加倍)。如果Front或Rear增1使得超越了数组,那么其值就要重置为数组的第一个位置。

                                                                             初始状态

            

                                                                      经过Enqueue(1)后

            

                                                                    经过Enqueue(3)后

            

                                                               经过Dequeue并返回2后

            

                                                                  经过Dequeue并返回4后

            

                                                                  经过Dequeue并返回1后

            

                                                      经过Deuqueue并返回3后同时使队列为空

               

                                  

关于队列的循环实现,有两件事情要警惕。第一,检测队列是否为空是很重要的,因为当队列为空时一次Dequeue操作将不知不觉 地返回一个不确定的值。第二,某些程序设计人员使用不同的方法来表示队列的队头的队尾。例如,有些人并不用一个单元来表示队列的大小,因为它们依靠的是基准情形,即当队列为空时Rear = Front -1。队列的大小是通过比较Rear和Front隐式算出的。这是一种非常隐秘的方法,因为存在某些特殊的情形,因此,如果你需要修改用这种方式编写的代码,那么你就要特别仔细。如果队列的大小不是结构的一部分,那么若数组的大小为ASize,则当存在ASize-1个元素时队列就满了,因为只有ASize个不同的大小值可被区分,而0是其中的一个。采用任意一种你喜欢的风格,但要确保你的所有里程都是一致的。由于实现方法有多种选择,因此如果你不使用表示大小的域,那就很可能有必要进行一些讨论,否则会在一个程序中使用两种选择。

在保证Enqueue的次数不会大于队列的大小的应用中,使用回绕是没有必要的。向栈一样,除非主调例程肯定队列为空,否则Dequeue很少执行。因此对这种操作,只要不是关键的代码,错误的调用常常被跳过。一般来说这并不是无可非议的,因为你可能得到的时间节省量是极小的。

通常编写某些队列的例程来结束本节。首先在给出队列的声明。正如对栈的数组实现所做的那样,添加一个最大大小的域。还需要提供例程CreatQueue和DisposeQueue。此外,还要提供测试一个队列是否为空的例程以及构造一个空队列的例程。可以编写函数IsFull,它完成其名字所指处的功能。注意,Rear在Front之前与初始化为1。将要编写的最后的操作是Enqueue例程。严格遵循上面的描述,实现代码如下所示:

队列类型的声明:

# ifndef _Queue_h

struct QueueRecord;
typedef struct QueueRecord *Queue;

int IsEmpty(Queue Q);
int IsFull(Queue Q);
Queue CreateQueue( int MaxElements );
void DisposedQueue(Queue Q);
void MakeEmpty( Queue Q );
void Enqueue( ElementType X, Queue Q );
ElementType Front( Queue Q );
void Dequeue( Queue Q );
ElementType FrontAndDequeue( Queue Q );

#endif /*_Queue_h*/


/* Place in implementation file */
/* Queue implementation is a dynamically allocated array */
# define MinQueueSize( 5 )

struct QueueRecord
{
   int Capacity;
   int Front;
   int Rear;
   int Size;
   ElementType *Array;
};

测试队列是否为空的例程------数组实现:

int
IsEmpty( Queue Q )
{
   return Q->Size == 0;
}

构造空队列的例程------数组实现:

void
MakeEmpty( Queue Q )
{
   Q->Size = 0;
   Q->Front = 1;
   Q->Rear = 0;
};

入队的例程------数组实现:

satic int
Succ( int Value, Queue Q )
{
    if( ++Value == Q->Capacity )
        Value - 0;
    return Value;

}

void 
Enqueue( ElementType X, Queue Q )
{
    if( IsFull( Q ) )
        Error("Full queue"):
    else
    {
        Q->Size++;
        Q->Rear = Succ( Q-Rear, Q );
        Q->Array[ Q-Rear ] = X;
    }
}

参考文献:

  1. MARKALLENWEISS. (2010). Data structures and algorithm analysis in C =. China Machine Press.

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ALM研发管理中规则库的配置与使用

1.规则库简介 规则库就是描述某领域内知识的产生式规则的集合&#xff0c;而规则往往是由一个具体的业务逻辑具象而来&#xff0c;它通常是很具体的&#xff0c;有着明确的处理逻辑&#xff08;即将输入数据经过一系列逻辑处理&#xff0c;输出处理后的结果&#xff09;。 2.规…

从一个 issue 出发,带你玩图数据库 NebulaGraph 内核开发

如何 build NebulaGraph&#xff1f;如何为 NebulaGraph 内核做贡献&#xff1f;即便是新手也能快速上手&#xff0c;从本文作为切入点就够了。 NebulaGraph 的架构简介 为了方便对 NebulaGraph 尚未了解的读者也能快速直接从贡献代码为起点了解它&#xff0c;我把开发、贡献内…

synchronized详解

什么是线程安全问题? 简单来说就是当多个线程同时访问某个方法时,这个方法无法按照我们预期的行为来执行&#xff0c;那么我们认为这个方法是线程不安全的 导致线程不安全的主要原因 1.原子性 什么是原子性 比如在数据库事务的ACID特性中 当前操作中包含多个数据库事务操…

DolphinDB 诚挚招募实施伙伴

随着 DolphinDB 业务发展&#xff0c;为满足迅速增长的客户需求&#xff0c;我们现正式启动“实施伙伴招募计划”。DolphinDB 客户已经涵盖7家Top 10券商、头部公募及私募基金、知名银行、交易所、世界500强制造业客户、标杆能源企业等&#xff0c;我们非常期待和欢迎实施伙伴们…

毕业设计 基于stm32的智能平衡小车 - 单片机 物联网嵌入式

文章目录0 前言1 项目背景2 设计思路3 硬件设计4 软件设计4.1 核心PID控制4.2 直立控制程序设计4.3 速度控制程序设计4.4 方向控制程序设计4.5 关键代码5 最后0 前言 &#x1f525; 这两年开始毕业设计和毕业答辩的要求和难度不断提升&#xff0c;传统的毕设题目缺少创新和亮点…

32、Java高级特性——日期操作类、Date类、SimpleDateFormat类、Calendar类

目录 一、日期操作类 二、Date类 1、Date类中的构造方法 2、获取系统当前时间 三、SimpleDateFormat类 1、SimpleDateFormat类中的构造方法 2、format(Date date,StringBuffer toAppendTo,FieldPosition pos)方法 四、Calendar类 1、Calendar类中的构造方法 2、Cal…

[附源码]Python计算机毕业设计SSM家政信息管理平台(程序+LW)

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

Removing the Bias of Integral Pose Regression 阅读笔记

消除积分姿态回归的偏差 ICCV2021 论文链接 补充材料链接 参考链接 摘要&#xff1a; 尽管姿态估计的回归法更直观&#xff0c;但由于热图法的优越性能&#xff0c;使其在2D人体姿态估计中占主导地位。积分回归法在架构上使用隐式热图&#xff0c;拉近了热图法与回归法。这就…

GraalVM + Springboot3 + IDEA 在widow10 上完成构建本地化服务

GraalVM是开发人员编写和执行Java代码的工具。具体来说&#xff0c;GraalVM是由Oracle创建的Java虚拟机&#xff08;JVM&#xff09;和Java开发工具包&#xff08;JDK&#xff09;。它是一个高性能的运行时&#xff0c;可以提高应用程序的性能和效率。 GraalVM的目标包括&…

【数据结构】哈希表

目录 一、哈希函数的引入 二、解决哈希冲突的思路 2.1基于闭散列的思路 2.2基于开散列的思路 2.3负载因子 三、关于哈希函数的设计 四、基于拉链法实现哈希表 4.1哈希表的内部构造 4.2插入操作 4.3扩容操作 4.4搜索操作 4.5删除操作 哈希表其实就是基于数组衍生而来…

深度解读面试题:链表中环的入口结点(附代码,可过在线OJ)

在解读“链表中环的入口结点”前&#xff0c;我认为有必要明白关于它的一些用于打基础的问题&#xff08;相交链表、判断链表中是否存在环&#xff09; 相交链表 题目&#xff1a; 给你两个单链表的头节点 headA 和 headB &#xff0c;请你找出并返回两个单链表相交的起始节点…

快收藏!!整理了100个Python小技巧!!

下面&#xff0c;我就给大家分享100个Python小技巧&#xff0c;帮助大家更好的了解和学习Python&#xff0c;欢迎收藏、关注&#xff0c;点赞支持&#xff01; ▍1、for循环中的else条件 这是一个for-else方法&#xff0c;循环遍历列表时使用else语句。下面举个例子&#xff…

根据平均值列出记录

AANSI SQL包括几个聚合函数&#xff0c;使您可以对一组值进行计算以将其结果作为单个值返回。他们包括Count(), Min(), Max(), Sum() and AVG(),以及其他。默认情况下&#xff0c;聚合函数适用于所有行&#xff0c;但是您可以通过将WHERE子句应用于SELECT语句来缩小字段的范围。…

直冲云霄,阿里大牛耗时49天整理12W字面试手册,押题准确率直冲95%

很多人都想进字节做开发&#xff0c;不论是技术还是薪资、福利都算得上TOP级~ 7月底官方再次启动扩招&#xff0c;发布了1200&#xff0b;后端工程师岗位&#xff01; 那么本批有哪些优质岗位可选择&#xff1f;薪资待遇如何&#xff1f; 下面给大家列出几类具体的岗位要求&a…

软件项目管理指南:定义、5大过程、估算及进度管理方法等

本文将分享&#xff1a;1、软件项目管理的定义&#xff1b;2、软件项目管理的过程步骤&#xff1b;3、软件项目管理的内容&#xff1b;4、软件项目估算与进度管理方法&#xff1b;5、软件开发各生命周期阶段与文档、角色间的关系&#xff1b;6、软件开发项目中的各大角色职能&a…

深度学习-第P1周——实现mnist手写数字识别

深度学习-第P1周——实现mnist手写数字识别深度学习-第P1周——实现mnist手写数字识别一、前言二、我的环境三、前期工作1、导入依赖项并设置GPU2、导入数据集3、数据可视化四、构建简单的CNN网络五、训练模型1、设置超参数2、编写训练函数3、编写测试函数4、正式训练六、结果可…

ADSP-21489的图形化编程详解(7:延时、增益、分频、反馈、响度)

延时 21489 可以做延时&#xff0c;音频高手会运用此项算法来增强音效&#xff0c;我们做个最简单的&#xff0c;让大家知道怎么用它&#xff0c;至于怎么样嵌入到自己的系统里实现更好的效果&#xff0c;则需要各位调音师专业的耳朵来判断&#xff0c;调音无上限&#xff01;…

MySQL之索引及其背后的数据结构

✨博客主页: 荣 ✨系列专栏: MySQL ✨一句短话: 难在坚持,贵在坚持,成在坚持! 文章目录一. 索引的介绍1. 什么是索引2. 索引的使用二. 索引背后的数据结构1. 考虑使用哈希表2. 二叉搜索树3. N叉搜索树(B树, B树)4. 注意事项一. 索引的介绍 1. 什么是索引 索引 (Index) 是帮助…

[激光原理与应用-39]:《光电检测技术-6》- 光干涉的原理与基础

目录 第1章 概述 1.1 什么是光干涉 1.2 产生干涉的必要条件 1.3 非相干光 - 自发辐射无法产生干涉 1.4 相干光 - 受激辐射 1.5 时间相干性 1.6 空间相干性 它山之石 第1章 概述 1.1 什么是光干涉 它是指因两束光波相遇而引起光的强度重新分布的现象。 指两列或两列以上…

Verilog入门学习笔记:Verilog基础语法梳理

无论是学IC设计还是FPGA开发&#xff0c;Verilog都是最基本、最重要的必备技能。但任何一门编程语言的掌握都需要长期学习。并不是简简单单的随便读几本书&#xff0c;随便动动脑筋那么简单。Verilog是一门基于硬件的独特语言&#xff0c;由于它最终所实现的数字电路&#xff0…