【问题标题】:What is the pointer-to-pointer technique for the simpler traversal of linked lists? [duplicate]更简单的链表遍历的指针对指针技术是什么? [复制]
【发布时间】:2011-03-12 02:19:06
【问题描述】:

十年前,我看到了一种遍历链表的技术:您使用的是双指针(指针对指针),而不是使用单指针。

通过消除检查某些边界/边缘情况的需要,该技术产生了更小、更优雅的代码。

有人知道这种技术到底是什么吗?

【问题讨论】:

  • 在 C++ 中,您应该使用 std::list 和迭代器。请记住:list 是最后的容器。
  • “双指针”到底是什么意思?
  • a **node -- 指向节点的指针。

标签: c++ c linked-list


【解决方案1】:

我认为您的意思是“指向指针的指针”中的双指针,这对于插入单链表树结构的末尾非常有效。这个想法是,一旦找到结尾(NULL 指针),您就不需要特殊情况或“尾随指针”来跟随您的遍历指针。因为您可以将指针取消引用到要插入的指针(它指向 到最后一个节点的下一个指针!)。像这样的:

T **p = &list_start;
while (*p) {
   p = &(*p)->next;
}
*p = new T;

而不是这样的:

T *p = list_start;
if (p == NULL) {
    list_start = new T;
} else {
    while (p->next) {
        p = p->next;
    }
    p->next = new T;
}

注意:它对于为单链表制作有效的删除代码也很有用。在任何时候执行*p = (*p)->next 都会删除您正在“查看”的节点(当然您仍然需要清理节点的存储)。

【讨论】:

  • 一个后续问题:您能否“简化”“p = &(*p)->next”背后的逻辑?我想当你取消引用 p 时,你会得到实际的链接。那么为什么要再次引用它呢?
【解决方案2】:

我同意 cmets 关于使用 STL 容器处理您的列表脏工作的观点。然而,这就是 Stack Overflow,我们都在这里学习一些东西。

以下是您通常插入列表的方式:

typedef struct _Node {
    void * data;
    Node * next;
} Node;

Node * insert( Node * root, void * data ) {
    Node * list = root;
    Node * listSave = root;

    while ( list != null ) {
        if ( data < list->data ) {
            break;
        }

        listSave = list;
        list = list->next;
    }

    Node * newNode = (Node*)malloc( sizeof(Node) );
    newNode->data = data;
    /* Insert at the beginning of the list */
    if ( listSave == list ) {
        newNode->next = list;
        list = newNode;
    }
    /* Insert at the end of the list */
    else if ( list == null ) {
        listSave->next = newNode;
        newNode->next = null;
        list = root;
    }
    /* Insert at the middle of the list */
    else {
        listSave->next = newNode;
        newNode->next = list;
        list = root;
    }

    return list;
}

请注意,根据插入是发生在列表的开头、结尾还是中间,您必须执行的所有额外检查。将此与双指针方法进行对比:

void insert( Node ** proot, void * data ) {
    Node ** plist = proot;

    while ( *plist != null ) {
        if ( data < (*plist)->data ) {
            break;
        }

        plist = &(*plist)->next;
    }

    Node * newNode = (Node *)malloc( sizeof(Node) );
    newNode->data = data;
    newNode->next = *plist;
    *plist = newNode;
}

正如 Evan Teran 所指出的,这对于单链表很有效,但是当它是双链表时,你最终会经历与单指针情况一样多的操作,如果不是更多的话。另一个缺点是每次遍历都要进行两次指针取消引用。虽然代码看起来更干净,但运行速度可能不如单指针代码。

【讨论】:

    【解决方案3】:

    “双指针”是指“指针对指针”。这很有用,因为它允许您消除头指针或尾指针的特殊情况。例如,给定这个列表:

    struct node {
        struct node *next;
        int key;
        /* ... */
    };
    
    struct node *head;
    

    如果要搜索节点并将其从列表中删除,单指针方法如下所示:

    if (head->key == search_key)
    {
        removed = head;
        head = head->next;
    }
    else
    {
        struct node *cur;
    
        for (cur = head; cur->next != NULL; cur = cur->next)
        {
            if (cur->next->key == search_key)
            {
                removed = cur->next;
                cur->next = cur->next->next;
                break;
             }
        }
    }
    

    而指针到指针的方法要简单得多:

    struct node **cur;
    
    for (cur = &head; *cur != NULL; cur = &(*cur)->next)
    {
        if ((*cur)->key == search_key)
        {
            removed = *cur;
            *cur = (*cur)->next;
            break;
        }
    }
    

    【讨论】:

      【解决方案4】:

      您可能是指一个双向链表,其中一个指针向前,另一个指针向后。这使您可以访问给定节点的下一个和上一个节点,而无需记住最后遇到的一个或两个节点(如在单链表中)。

      但我发现让代码更加更加优雅的一件事是在列表中始终有两个虚拟元素,第一个和最后一个。这消除了插入和删除的边缘情况,因为您总是作用于列表中间的节点。

      例如,创建一个空列表:

      first = new node
      last = new node
      first.next = last
      first.prev = null
      last.next = null
      last.prev = first
      // null <- first <-> last -> null
      

      显然,遍历列表略有修改(仅显示正向版本):

      curr = first.next
      while curr <> last:
          do something with curr
          curr = curr.next
      

      插入要简单得多,因为您不必关心是在列表的开头还是结尾插入。在当前点之前插入:

      if curr = first:
          raise error
      add = new node
      add.next = curr
      add.prev = curr.prev
      curr.prev.next = add
      curr.prev = add
      

      删除也更简单,避免了边缘情况:

      if curr = first or curr = last:
          raise error
      curr.prev.next = curr.next
      curr.next.prev = curr.prev
      delete curr
      

      所有的代码都非常简洁,而且每个列表只需维护两个额外的节点,这在当今巨大的内存空间环境中并不是一个很大的负担。

      警告 1:如果您在空间仍然很重要的情况下进行嵌入式编程,那么这可能不是一个可行的解决方案(尽管现在一些嵌入式环境也很糟糕)。

      警告 2:如果您使用的语言已经提供了链表功能,那么最好这样做而不是自己动手(除非在非常特殊的情况下)。

      【讨论】:

        【解决方案5】:

        我认为您的意思是 doubly-linked lists 一个节点类似于:

        struct Node {
        (..) data // The data being stored in the node, it can be of any data type
        Node *next; // A pointer to the next node; null for last node
        Node *prev; // A pointer to the previous node; null for first node
        }
        

        【讨论】:

        • 为了避免检查边界,您可以将其设为圆形。
        • 循环列表可能是避免边界检查的最糟糕方法之一。与其在某处进行简单的空检查,不如记住从哪里开始,回到那里时停止。
        • 我认为他的意思是指向单链表插入的指针。看我的回答。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2010-09-14
        • 1970-01-01
        • 2013-08-07
        • 2014-08-19
        • 2016-09-25
        • 2013-06-27
        相关资源
        最近更新 更多