【问题标题】:Linked Lists: Inserting at the end using last pointer链表:使用最后一个指针插入末尾
【发布时间】:2010-08-24 12:30:19
【问题描述】:

我正在学习链表,想知道我为在链表末尾插入元素而制作的以下程序(基本上是InsertAtEnd函数)是否正确。

基本思想是*HEAD指向列表的第一个元素,*LAST指向最后一个元素。这样可以节省遍历到列表的最后一个元素然后添加元素的时间和计算量。

#include<stdio.h>
#include<stdlib.h>

// Structure for the list element/node
struct node 
{
 int data; // Stores the data
 struct node *next; // Points to the next element in the list.
};

int InsertAtEnd(struct node **, struct node **, int); /*Declaration of the function which 
                                                    inserts elements at the end.*/

int main()
{
 struct node *HEAD=NULL; //Points to the first element in the list.
 struct node *LAST=NULL; //Points to the last element in the list.
 int i=1;

 for(i=1;i<11;i++)
   {
     InsertAtEnd(&HEAD,&LAST,i);
   }
 }

// Function to insert element at the end.
int InsertAtEnd(struct node **headref,struct node **lastref,int i)
 {
   struct node *newnode=malloc(sizeof(struct node)); /*Allocates memory for the newnode 
                                                     and store the address in pointer 
                                                     newnode*/
   newnode->data=i; // Assign value to the data variable of the newnode.
   newnode->next=NULL; // Assign NULL to the next pointer of the newnode.

   if(*headref==NULL) //Checks if the list is empty.
    {
      *headref=newnode; // Places the address of the new node in HEAD pointer.
      *lastref=newnode; // Places the address of the new node in LAST pointer.

       return 0; //Exit function
    }

/* If the list is not empty, then make the next pointer of the present last node point to the new node*/

   (*lastref)->next=newnode;

   *lastref=(*lastref)->next; // Increment LAST to point to the new last node.

    return 0;
}

我想具体问的问题是:

a) 上述用于在末尾添加元素的代码(即 InsertAtEnd 函数)是否正确? (注意:我在我的机器上测试了它,它按预期工作。但我仍然想向你们确认)

b)代码(InsertAtEnd 函数)是否高效?

c)如果我尝试制作更长的列表,是否会影响代码(InsertAtEnd 函数)的效率。

d) 有没有更高效、更简单的算法在最后插入元素?你能引导我去找他们吗?

【问题讨论】:

  • 谢谢你们。您的回答非常有帮助。你们给了很多我什至没有考虑过的品脱。我很难选择一个被接受的答案:) 所以我只会选择票数最多的答案。再次感谢。
  • 我还想问的一件事是。你说我不需要退货声明。那么我将如何退出“if”语句?我的意思是,如果我传递一个空列表,则执行 if 语句,但如果我不返回 0,那么接下来的两个语句也将被执行。不是吗?我想我应该把最后两行放在一个 else 块中。这样就够了吗?还是我犯了一个逻辑错误?
  • 将该函数的剩余部分放在 else{} 分支中,这样 if 确实可以工作。
  • 你应该做一个结构来结合开始和结束。

标签: c linked-list


【解决方案1】:

a) 看起来是正确的

b) 是的。你可以让函数返回 void,因为你不需要那个 int 返回值

c) 不。换句话说,执行时间是恒定的。

d) malloc 需要时间。您可以使用缓冲技术,提前 malloc 一大块节点,然后从缓冲区中取出下一个节点。当缓冲区变空时,分配另一个块。可以使用统计算法配置甚至(复杂)计算块尺寸。

此外,您不检查 malloc 的 NULL 返回,但它可能会失败。

【讨论】:

  • 我从未想过拥有一个 malloced 节点的缓冲区。有趣的想法。
  • @Colin:过早的优化是万恶之源。当 malloc 时间成为问题时执行此操作,如果它成为问题。
  • @ijw:我同意。这只是我以前没有想到的。我怀疑我是否会编写足够关键的软件,以至于我很快就需要担心 malloc 时间。
【解决方案2】:

此代码不受列表大小的影响(除了空的特殊情况),即它是一个恒定时间算法。所以想出一个更有效的插入算法可能是相当困难的。

但是,唯一可以改进的部分是使用malloc()。看起来您正在单独分配每个节点。如果您准备使用比实际需要更多的内存,您可以考虑一次分配多个节点,这样您对malloc() 的调用就更少了。因此,当您尝试创建节点时,它首先会检查池中是否还有空闲节点。如果是,那么只需连接指针。如果没有,您将需要分配一个新池。显然,这将需要更复杂的代码。

【讨论】:

    【解决方案3】:

    a) 不检查 malloc 的结果。它可以在内存不足的情况下返回 NULL,从而导致崩溃。我相信算法的其余部分是正确的。

    B+C+D) 效率很高; O(1) 意味着插入 LAST 元素所花费的时间是恒定的。其实我想不出更简单更高效的算法。

    【讨论】:

      【解决方案4】:

      a) 是的,它应该可以工作(假设您的列表永远不会损坏并且具有 headref != NULL 但 lastref == NULL)。

      如果返回值始终为 0,那么它的意义何在?

      b) 除了为每个节点分配内存之外,当然。这是您自己的设计决定,但不同的解决方案会更加复杂,超出本练习的范围。

      至于函数本身 - 链表的好处是它们是 O(1)。现在删除一个元素是另一回事,除非你有一个双链表,否则它会更慢。

      c) 不,它是 O(1)。如您所见,没有循环或任何涉及的内容,无论列表中有 5 个还是 5000 个元素,您都执行完全相同的步骤。

      d) 您可以通过使用哨兵来避免检查列表是否为空,即使用虚拟节点代替 headref。您只需在内存中的某处放置一个节点并将 lastref 设置为它。现在您不必将空列表作为特殊情况处理。

      【讨论】:

        【解决方案5】:

        a) 我认为您的代码是正确的,因为它可以按照您的预期进行。您可能需要检查malloc 的返回值。

        b) 我认为您的代码也很有效。我唯一能想到的是*lastref=(*lastref)-&gt;next; 行,我将用*lastref=newnode; 替换它。但我认为这将被几乎每个编译器自动优化。

        c) 您的方法具有恒定的运行时间 (O(1)),因此添加到更大的列表不会改变运行时间。但是,如果元素连续存储在内存中,遍历列表可能会更快。

        d) 我不认为insertAtEnd 可以更快地实现。您可以尝试将元素连续存储在内存中,并检查 malloc 的返回。

        我个人在实现这些东西时唯一做的事情是:

        • 为整个链表数据结构创建一个自己的结构(包含sizehead- 和lastElement-指针)

        • 我不会限制列表包含整数而是任意元素(因此 void*s)

        【讨论】:

          【解决方案6】:

          抱歉,这并不能回答问题,但我认为您可能会发现它很有用。我稍微改变一下方法:

          int InsertAtEnd(struct node ***, int); /*Declaration of the function which 
                                                   inserts elements at the end.*/
          
          struct node *HEAD=NULL; //Points to the first element in the list.
          struct node **LAST=&HEAD; //Points to the last (NULL) *pointer* in the list.
          
          // Function to insert element at the end.
          int InsertAtEnd(struct node ***lastrefref,int i) // no need to pass HEAD
          {
              struct node *newnode=malloc(sizeof(struct node)); /*Allocates memory for the newnode 
                                                                  and store the address in pointer 
                                                                  newnode*/
          
              // And here we should be checking for errors...
          
              newnode->data=i; // Assign value to the data variable of the newnode.
              newnode->next=NULL; // Assign NULL to the next pointer of the newnode.
          
              **lastrefref = newnode; // Put new element at end of list
              *lastrefref=&(newnode->next); // Update last pointer location
          }
          

          调用约定会丢失一个参数,并且不需要条件。如果您想快速访问最后一个列表元素,这会丢失一点,因为它不是那么简单。

          struct <anything> ***
          

          开始变得愚蠢了,请注意。

          【讨论】:

          • 谢谢!很有帮助。除了在 InsertAtEnd 函数中创建指向 LAST 指针的指针之外,您不能不只是将 LAST 指针作为按值调用传递,然后在 InsertAtEnd 函数中:InsertAtEnd(struct node** lastref,int i)。这使指针指向节点的下一个指针,而不是指向 LAST 指针/变量。现在,我没有足够的经验来提出这个解决方案,但是我找到了我刚刚在 Nick Parlante 的一篇题为“链接列表基础”的论文中告诉你的解决方案。查看本文的第 20 页。
          • 我更改了 lastref 指向的 ->next(或 HEAD)指针的内容,然后我更改了 lastref 本身的内容。他也这样做——这是相同的解决方案。不同之处在于他没有更新函数中的 lastref 指针,他只是在循环体中更新。参数需要额外的“*”,以便您可以从函数内部更新 lastref。
          • 对于普通读者,我在cslibrary.stanford.edu/103/LinkedListBasics.pdf找到了这篇论文