数据结构学习笔记之链表分析与实现(二)

来源:互联网 发布:云计算与大数据前景 编辑:程序博客网 时间:2024/06/08 18:16

     在上一篇文章中,我已经分析了单链表的结构及其建立。

      接下来,我将进一步分析单链表中的其他操作。

       首先,我们来看看如何实现查找现有链表中的某个元素或结点。很显然,我们可以遍历整个链表,并将遍历到的结点与要查找的结点进行比较,如果结果相同,表示查找成功。代码示例如下:

NameVal *FindSpecificItem( NameVal *SingleListPtr,  char *name )

 {

while( NULL != SingleListPtr )

{

if( 0 == strcmp( name, SingleListPtr->name ) )

{

return SingleListPtr;  //表示查找成功

}

SingleListPtr = SingleListPtr->next;//查找不成功,则继续遍历查找

}

return NULL; //查找失败

}

      算法时间复杂度为O(n). 有没有办法改进呢? 答案是没有。即使链表已经排好序, 我们依然需要遍历才能查找到某个元素。在上篇文章,我们说过:二分查找对链表无效!

  接下来,我们再分析下链表的其他操作。如:打印链表中的所有元素; 计算链表的长度等等。简单的办法是,我们可以逐一写函数实现之。经过分析,我们发现,这些操作都需要对链表进行遍历才能实现。有没有一种通用的办法来实现呢? 答案是:YES!如何实现呢?

  我们已经找到了共同点:遍历。遍历过程中具体要做什么,是千变万化的。可能是要查找某个元素,也可能是要打印元素,等等。我们可以提供一个通用的遍历操作,在操作中具体要做什么,由需求来决定。自然而然,我们想到了callback函数。

        什么是callback函数?遍历函数是用户(上层调用者)调用的,可实现为一个API接口。 在遍历过程中, 遍历函数需要回头调用具体用户(上层调用者)提供的函数来实现特定的需求或功能。这种回头调用调用者所提供的函数的方法,我们称之为回调函数或callback机制。

       实现回调函数,可分四步走:

      (1)定义要回调的函数指针类型

      (2)声明公用接口函数

      (3)实现上步中的接口函数

      (4)用户调用该callback函数

       有点难以理解,是不是?所谓实践出真知,下面我们通过具体的例子来深入理解上述四部曲的意义。

      第一步,定义要回调的函数指针类型: void ( *UserCallbackFunc ) ( void * , NameVal * );

      第二步, 声明公用接口函数:void CommonUtility( NameVal *SingleListPtr, UserCallbackFunc RequireFunc, void *ctx )//ctx 为回调函数的参数

      第三步,实现公用接口函数。代码如下:

void CommonUtility( NameVal *SingleListPtr, UserCallbackFunc RequireFunc, void *ctx )

{

       while( NULL != SingleListPtr )

       {

     RequireFunc( ctx, SingleListPtr );

    SingleListPtr = SingleListPtr->next; //则继续遍历查找 

}

}

          第四步,用户实现自己的需求函数并调用该callback函数。

         例如,我们要实现打印链表中的所有元素,我们可以实现一个自己的需求回调函数:

 void PrintAllElements( void *ctx, NameVal *SingleListPtr)

{

       char *format;

       format = (char * ctx;

       printf( format, SingleListPtr->name, SingleListPtr->value );

}

      之后,我们就可以在公用接口中回头调用该callback 函数。如下所示:

        CommonUtility( SingleListPtr, PrintAllElements,“%s: %x.\n”);

      用类似的办法,我们可以实现计算链表长度的callback函数。

 void CountLength( void *ctx, NameVal *SingleListPtr)

{

       int *ListLength;

       ListLength = (int * )ctx;

      (*ListLength)++;

}

调用方法:

int LengthOfList;

LengthOfList = 0;

 CommonUtility( SingleListPtr, CountLength, &LengthOfList);

             需要注意的是,这种采用一个公用接口函数的方法,虽然巧妙,但并不适用于所有情况。例如,如果需要销毁一个链表,如果继续采用以下方式代码就会出问题: 

void CommonUtility( SingleListPtr, FreeMemory, ctx )

{

       while( NULL != SingleListPtr )

       {

     FreeMemory( ctx, SingleListPtr );//假设这里把该结点释放了,并且它的后继结点在链表中存在

    SingleListPtr = SingleListPtr->next;//由于它的上一个结点内存已经释放了,就无法再指向下一个结点了,Memory can not be used after it has been freed.

}

}

        由此可见,这个公用接口函数已经失效了,或者说是有缺陷的。要修正这个缺陷,我们必须在释放结点内存前,用一个临时变量Buffer保存该结点的后继结点。然后在free内存之后再进行赋值.伪代码示例如下:

      

void CommonUtilityCorrect( SingleListPtr, FreeMemory, ctx )

{

      NameVal *Buffer;

       while( NULL != SingleListPtr )

       {

     Buffer = SingleListPtr->next;

    FreeMemory( ctx, SingleListPtr );//假设这里把该结点释放了,并且它的后继结点在链表中存在

    SingleListPtr = Buffer;//由于它的上一个结点内存已经释放了,就无法再指向下一个结点了,Memory can not be used after it has been freed.

}

}

 

        至此为止,我们已经分析了链表的建立(头插法、尾插法)、查找链表中某个元素、计算链表长度、销毁链表等操作;接下来,我们接着简单的介绍下链表中某个元素的插入、删除操作。

        首先,我们分析下删除操作。要删除链表中某个指定的结点,需要以下几个步骤:

(1)查找到待删除结点的前驱结点(prev)。伪代码示意如下:

                      prev = head;//prev初值设置为头指针

                      while( prev->next != SpecificNotePtr ) prev = prev->next; //查找*SpecificNotePtr 的前驱结点

(2)让前驱结点的后继指向待删除结点(SpecificNotePtr)的后继。这样,待删除结点就从链表中删除了。最后释放删除结点所占内存。

                       prev->next = SpecificNotePtr->next;

                       free(SpecificNotePtr);

         有时候,我们还需要删除指定结点的后继结点。同样可分为2步走:

(1)查找到后继结点(Successor)。实际上不需要查找,根据链表的特性,只需指向下一个就OK了:

                      Successor = SpecificNotePtr ->next

(2)让指定结点(SpecificNotePtr)的后继指向后继结点的后继。这样,待删除结点就从链表中删除了。最后释放删除结点所占内存。

                      SpecificNotePtr->next = Successor ->next;

                       free( Successor );

                  我们看一个实际使用的例子,根据某个指定的元素名称,删除该指定结点。完全依照上述规则,代码如下:       

NameVal *DelSpecificItem( NameVal *SingleListPtr,  char *name )

 {

       Nameval  *prev,*SpecificNotePtr ;

      prev = SingleListPtr;//初始值为链表头结点指针

     SpecificNotePtr = FindSpecificItem( &SingleListPtr,  &name );

    while( 0 != strcmp( name, prev ->next->name ) )

    {

            prev = prev ->next; //寻找“name" 对应结点的前驱结点

    }

      prev ->next = SpecificNotePtr ->next;

      free(SpecificNotePtr );//释放指定结点的空间

}

上述代码需要2个循环才能实现。可不可以一个循环就实现呢?可以的。只需要在FindSpecificItem地循环中保存下前驱结点指针就可实现。:       

      

 

NameVal *DelSpecificItem( NameVal *SingleListPtr,  char *name )

 {

       Nameval  *pBuf, *prev;

      pBuf = SingleListPtr;//初始值为链表头结点指针
      prev = NULL;
      if( NULL == pBuf )
      {
         printf("Error.Null List.\n");
         return;
      }

     while( NULL != pBuf) )

    {
           if( 0 == strcmp(name, pBuf->name ) )
           {
                    break;//找到结点,跳出循环
           }

          prev =  pBuf; //寻找“name" 对应结点的前驱结点
          pBuf=pBuf->next;

      }
   
     if ( prev != NULL )
            prev ->next = pBuf ->next;
     else
              pBuf = SingleListPtr;
      free( pBuf );//释放指定结点的空间

}

 

       我们再来分析下插入操作。在分析插入运算前,我们回顾下查找操作。前面我们分析的查找都是按值查找,实际上也可以进行按序号查找。各自的伪代码简单示例重写如下:

     ListNode *FindNodeByIndex(ListNode *head, int Index)

    {

           ListNode *p = head;//从头指针开始查找

           int j = 0;

          while( p ->next !=NULL &&  j <index)

           {

                      p=p->next;

                      j++;

            }

           if( j == index ) return p;

           else  return NULL;

    }

//按值查找,读者可与前面的实现作一番对比,差别较小

  ListNode *FindNodeByValue(ListNode *head, ElemType x)

    {

           ListNode *p = head->next;//从头指针开始查找

           while( p != NULL && p->data != x)

           {

                      p=p->next;

             }

         return p;

      }

      

                我们回到插入运算这个话题。插入运算可以分为后插和前插。后插,在指定结点和指定结点的后继结点之间插入;前插, 则是在指定结点和指定结点的前驱结点之间插入。插入操作使得链表长度增加了。后插可以按2步走:

(1)待插入的结点的后继指向已经知道的指定结点的后继。

(2)指定结点的后继修改为待插入结点。

           前插操作可用后插实现。即先实行后插,然后交换插入结点的数据与指定结点的数据。

           前插操作也可用以下步骤实现:

(1)先找到指定结点的前驱结点。

(2)然后再前驱结点和指定结点之间进行插入。

           例如,我们按index来进行插入,可用如下示例伪代码实现:(前插)

    ReturnType InsertNodeAfterIndex(ListNode *head, int Index, ElemType UserData)

    {

           ListNode *pSpecificNode, *NodeToInsert;//定义前驱结点和待插入结点

           int j = 0;

          pSpecificNode = FindNodeByIndex(head, Index); //查找第index-1个结点,即前驱结点

          if ( NULL == pSpecificNode  )

          {

                return ERROR;

          }

else

{

          NodeToInsert = ( ListNode * )malloc( sizeof(ListNode ) );

          NodeToInsert  ->data = UserData;

          NodeToInsert->next = pSpecificNode->next;

          pSpecificNode ->next = NodeToInsert;

          return OK;

}

    }

      至此,单链表的基本运算介绍完毕。

原创粉丝点击