【问题标题】:Is a Linked-List implementation without using pointers possible or not?不使用指针的链表实现是否可行?
【发布时间】:2011-03-01 11:21:44
【问题描述】:

我的问题很简单,可以使用 C++ 实现链接列表数据结构而不使用指针(下一个节点)吗?为了进一步限定我的问题,我的意思是可以仅使用类实例化来创建链接列表数据结构。

一个常见的节点定义可能是这样的:

template<typename T>
struct node
{
   T t;
   node<T>* next;
   node<T>* prev;
};

我知道std::list 等,我只是想知道它是否可能——如果是的话怎么办?代码示例将不胜感激。

更多说明:

  1. 插入应该是 O(1)。
  2. 遍历不应超过 O(n)。
  3. 真实节点和空节点应该是可区分的。
  4. 链接列表的大小应仅受可用内存量的限制。

【问题讨论】:

  • @Mike Atlas:不只是好奇。
  • 我认为使用像auto_ptrshared_ptr 这样的智能指针会作弊。
  • @Ken:他们会破坏问题的目的。
  • 我假设在规则 4 中,受到可用运行时堆栈空间量的限制是有效的。
  • 他只是想出了规则列表,因为他不明白我的回答。 (他在我的解决方案中询问了这两件事他将规则列表添加到问题中。)

标签: c++ pointers linked-list


【解决方案1】:

当然,如果您不介意链表具有最大大小,您可以静态分配列表节点数组,然后使用数组中的整数索引作为每个节点的“上一个”和“下一个”值,而不是指针。我过去这样做是为了节省一点内存(因为整数可以是 2 或 4 字节,而在 64 位系统上,指针将是 8 字节)

【讨论】:

  • 但这不是链表,而是向量。
  • @Jeremy:正如比利所提到的,你所描述的不是一个链表 wrt 结构的真正定义。
  • 不是向量也不是链表。它需要连续的内存分配,但它允许一些链表的好处(在 O(1) 中的任何地方插入和删除)。
  • 是一个链表,所有节点都占用一块连续的内存空间。来自 Wikipedia:“链表是一种数据结构,它由一系列数据记录组成,这样在每条记录中都有一个字段包含对序列中下一条记录的引用(即链接)。” @Jeremy Friesner 描述的内容满足这个定义。
  • @billy:按照您的逻辑,如果池使用连续内存,则使用池分配器的 std::map 也将是一个向量。
【解决方案2】:

是的,这是可能的。使用数组索引而不是指针。

【讨论】:

  • 抱歉,没有代码示例,因为这对我来说就像家庭作业,我不喜欢回答硬件问题,除非发帖人预先说明了这一事实。
  • 你的意思是像 std::vector<:pair>> ?
  • @DVK: 是的,那么你的数组也不会是一个。
  • 它仍然是一个链表,但它仍然使用指针。这方面的术语是“基于指针”。
  • Billy 如果列表中的条目具有指向 hte 列表中“上一个”和“下一个”条目的链接,则它是一个链接列表。最初的“链表”实现是磁盘上的数据库结构,具有“上一个”和“下一个”的逻辑指针
【解决方案3】:

来自Wikipedia

在计算机科学中,链表是 一种数据结构,由 数据记录的序列,使得在 每条记录都有一个字段 包含对的引用(即链接) 序列中的下一条记录。

该定义中没有任何内容指定存储或使用引用的方式。如果你不存储一个引用,它就不是一个链表——它是别的东西。

如果您的目标仅仅是避免使用指针(或对象引用),那么使用带索引的向量是一种常见的实现方式。 (使用向量/索引实现的原因之一是持久性:很难在活动内存空间之外正确持久化指针/对象引用。)

【讨论】:

    【解决方案4】:

    是的:

    class node { 
      std::string filenameOfNextNode;
      std::string filenameOfPrevNode;
      std::string data;
      node nextNode() {
        node retVal;
        std::ifstream file(filenameOfNextNode.c_str());
        retVal.filenameOfNextNode = file.getline();
        retVal.filenameOfPrevNode = file.getline();
        retVal.data = file.getline();
        return retVal;
      }
    };
    

    灵感来自关于链表起源的评论

    【讨论】:

      【解决方案5】:

      可以使用临时、const 引用和继承来创建一个 cons-cells 列表。但是您必须非常小心,不要在其生命周期之外保留对它的任何引用。而且你可能无法摆脱任何可变的东西。

      这大致基于这些列表的 Scala 实现(特别是使用继承和 NilList 子类而不是使用空指针的想法)。

      template<class T>
      struct ConsList{
         virtual T const & car() const=0;
         virtual ConsList<T> const & cdr() const=0;
      }
      
      template<class T>
      struct ConsCell:ConsList{
         ConsCell(T const & data_, ConsList<T> const & next_):
              data(data_),next(next_){}
         virtual T const & car() const{return data;}
         virtual ConstList<T> const & cdr() const{return next;}
      
         private:
           T data;
           ConsList<T> const & next;
      }
      
      template<class T>
      struct NilList:ConsList{  
         // replace std::exception with some other kind of exception class
         virtual T const & car() const{throw std::exception;}
         virtual ConstList<T> const & cdr() const{throw std::exception;}
      }
      
      void foo(ConsList<int> const & l){
         if(l != NilList<int>()){
            //...
            foo(NilList.cdr());
         }
      }
      
      foo(ConsList<int>(1,ConsList(2,ConsList<int>(3,NilList<int>()))));
      // the whole list is destructed here, so you better not have
      // any references to it stored away when you reach this comment.
      

      【讨论】:

      • 实际上,您可以通过给每个节点一个带有名称的变量并引用这些变量来避免临时变量的问题。但这也可能是危险的生活。
      • @Ken:您的解决方案与静态数组没有什么不同,因为必须在编译时知道结构的大小。此外,结构的大小完全取决于编译器的模板深度,而不是您可用的内存量。
      • @sonicoder,这个结构没有使用嵌套模板,所以编译器的模板深度应该不会影响任何东西。此外,如果您使用递归函数来构建列表并使用延续传递样式对其进行操作,那么您可以构建一个动态大小的列表。
      • @Ken:您能否演示一下如何在节点 2 和 3 之间插入一个节点?
      • @sonicoder:那做不到。这是一个 cons-list,就像在函数式语言中可以找到的那样,它们并不打算支持中间的插入。 (这是我可以摆脱这个愚蠢的例子的唯一原因。如果你想要可变性,你将不得不使用指针。)
      【解决方案6】:

      虽然我不确定你的问题背后的背景是什么,但如果你做一些开箱即用的想法,我相信你可以。

      DVK 建议使用数组,这是非常正确的,但数组只是指针算法的简单包装。

      完全不同的东西怎么样:使用文件系统为你做存储!

      例如,文件 /linked-list/1 包含数据:

      数据 1!

      5

      /linked-list/5 是列表中的下一个节点...

      如果你愿意破解,一切皆有可能:-p

      请注意,上述实现的复杂性/速度完全取决于您的文件系统(即它不一定是 O(1))

      【讨论】:

      • 那仍然不是一个链表。插入或删除需要在文件中向上或向下推元素。它更像是一个向量数据结构。
      • @Steven:假设我正在寻找的解决方案不应该基于标准操作系统内存分配和代码执行设施以外的任何东西。
      • @Billy:我看不出这有什么像矢量的东西。如果要在 1 和 5 之间插入,则取 1 并将其引用更改为 6534,然后将 6534 指向 5。不需要“推”或“弹出”。
      • @sonicoder:你的问题很奇怪,我觉得不得不有点傻。显然,这不符合您只使用标准内存分配等的愿望......
      • 除了一个非常非常大的数组之外,什么是主内存呢? :-p
      【解决方案7】:

      我想使用引用是作弊,从技术上讲,这会导致 UB,但是你去吧:

      // Beware, un-compiled code ahead!
      template< typename T >
      struct node;
      
      template< typename T >
      struct links {
        node<T>& prev;
        node<T>& next;
        link(node<T>* prv, node<T>* nxt); // omitted
      };
      
      template< typename T >
      struct node {
        T data;
        links<T> linked_nodes;
        node(const T& d, node* prv, node* nxt); // omitted
      };
      
      // technically, this causes UB...
      template< typename T >
      void my_list<T>::link_nodes(node<T>* prev, node<T>* next)
      {
        node<T>* prev_prev = prev.linked_nodes.prev;
        node<T>* next_next = next.linked_nodes.next;
        prev.linked_nodes.~links<T>();
        new (prev.linked_nodes) links<T>(prev_prev, next);
        next.linked_nodes.~links<T>();
        new (next.linked_nodes) links<T>(next, next_next);
      }
      
      template< typename T >
      void my_list<T>::insert(node<T>* at, const T& data)
      {
        node<T>* prev = at;
        node<T>* next = at.linked_nodes.next;
        node<T>* new_node = new node<T>(data, prev, next);
      
        link_nodes(prev, new_node);
        link_nodes(new_node, next);
      }
      

      【讨论】:

        【解决方案8】:

        是的,您可以,不必为链接列表使用指针。可以在不使用指针的情况下链接列表。您可以为节点静态分配一个数组,而不是使用下一个和前一个指针,您可以只使用索引。您可以这样做以节省一些内存,例如,如果您的链接列表不大于 255,您可以使用 'unsigned char' 作为索引(引用 C),并为下一个和上一个指示保存 6 个字节。

        在嵌入式编程中你可能需要这种数组,因为内存限制有时会很麻烦。

        另外请记住,您的链接列表节点在内存中不一定是连续的。

        假设您的链接列表将有 60000 个节点。使用线性搜索从数组中分配一个空闲节点应该是低效的。相反,您可以每次只保留下一个空闲节点索引:

        只需初始化您的数组,因为每个下一个索引显示当前数组索引 + 1,并且 firstEmptyIndex = 0。

        从数组中分配空闲节点时,获取 firstEmptyIndex 节点,将 firstEmptyIndex 更新为当前数组索引的下一个索引(不要忘记将下一个索引更新为 Null 或空或此后的任何内容)。

        deallocating时,将deallocating node的next index更新为firstEmptyIndex,然后执行firstEmptyIndex = deallocating node index。

        通过这种方式,您可以为自己创建一个从数组中分配空闲节点的快捷方式。

        【讨论】:

          【解决方案9】:

          您可以使用references 创建一个链接列表,但这可能会比必要的更复杂。你必须实现一个immutable 链表,如果没有内置的垃圾收集器,这将是复杂的。

          【讨论】:

          • 不,你不能。引用必须指向分配在 somewhere 的东西,并且如果您使用指针,您不能像使用指针那样让链表管理该内存。您仍然必须以某种方式将分配的对象保存在您的链表中,而这将不是一个链表。
          • @Billy ONeal 我知道,这就是为什么它会很复杂。而不是最后的空指针,您将为空列表创建一个特殊的单例实例。引用将在构造函数中设置。如果您查看 Ken Blooms 的代码,这几乎就是他正在做的事情。
          • @Luke:您仍然会遇到这样一个问题,即当您尝试将参考指向的东西放入列表时,它会死掉。无法确保引用保持有效。如果您查看 Ken Bloom 的代码,请注意在单个语句之后没有任何结构保留。
          • @Billy ONeal 我的 C++ 有点生疏,但你不能在堆上构造对象,然后从指针创建引用,然后将这些“新的”对象传递给下一个构造函数,这将延长列表对象的生命周期。现在显然很难管理所有这些分配。
          • 是的,但不在数据结构本身中
          【解决方案10】:

          不支持任何类型引用的语言仍然可以通过将指针替换为数组索引来创建链接。方法是保留一个记录数组,其中每条记录都有整数字段,指示数组中下一个(也可能是前一个)节点的索引。并非需要使用阵列中的所有节点。如果记录也不支持,通常可以使用并行数组来代替。

          例如,考虑以下使用数组而不是指针的链表记录:

          record Entry {
          integer next; // index of next entry in array
          string data; // can be anything a struct also. }
          

          创建一个具有高数字的数组。并将 listHead 指向数组的第一个索引元素

          integer listHead;
          Entry Records[10000];
          

          查看 wiki 页面:http://en.wikipedia.org/wiki/Linked_list 了解详情,搜索“使用节点数组的链表”

          【讨论】:

            【解决方案11】:

            哇,不是吗?你们确定不是认真的吗?

            链表只需要一个链接。没有什么说它必须是一个指针。考虑想要将链表存储在共享内存中的情况,其中基地址是动态的?答案很简单,将链接存储为从 mem 块开始的偏移量(或其他常量)并重新定义迭代器以执行添加基本地址的实际逻辑。显然,插入等也必须更改。

            但相当琐碎!

            艾伦

            【讨论】:

              【解决方案12】:

              一种可能的方法是使用Nodes 的数组,其中每个节点存储prevnextNode 的(数组)索引。因此,您的 Node 看起来像:

              struct Node 
              {
                  T data;
                  int prev;    // index of the previous Node of the list
                  int next;    // index of the next Node of the list
              }
              

              此外,您可能必须动态分配 Node 数组,对数组中的 getfree 空间进行一些记账:例如 bool将未占用索引存储在Node 数组中的数组,以及两个函数,每次添加或删除新的Node / 索引时都会更新它(它将被分段,因为节点并不总是连续的);在数组中找到Node 的索引:例如,从数组的第一个地址中减去Node 的地址。

              下面是使用上述技术的双向链表的可能接口如下所示:

              template <typename T>                          // T type Node in your case
              class DList
              {
                  T** head;                                  // array of pointers of type Node
                  int first;                                 // index of first Node
                  int last;                                  // index of last Node
              
                  bool* available;                           // array of available indexes 
                  int list_size;                             // size of list
              
                  int get_index();                           // search for index with value true in bool available
                  void return_index(int i);                  // mark index as free in bool available
              
                  std::ptrdiff_t link_index(T*& l) const;    // get index of Node
              
                  void init();                               // initialize data members
                  void create();                             // allocate arrays: head and available
              
                  void clear();                              // delete array elements
                  void destroy();                            // delete head
              
              public:
                  DList();                                   // call create() and init()
                  ~DList();                                  // call clear() and destroy()
              
                  void push_back(T* l);
                  void push_front(T* l);
                  void insert(T*& ref, T* l);                // insert l before ref
              
                  T* erase(T* l);                            // erase current (i.e. l) and return next
                  T* advance(T* l, int n);                   // return n-th link before or after currnet
              
                  std::size_t size() const;
                  int capacity () const { return list_size; }
              };
              

              您可以将其用作基准并自行实施。

              template <typename T>
              void DList<T>::push_back(T* l)
              {
                  if (l == nullptr)
                  {
                      throw std::invalid_argument("Dlist::push_back()::Null pointer as argument!\n");
                  }
              
                  int index = get_index();
                  head[index] = l;
              
                  if (last != -1)
                  {
                      head[last]->next = index;
                      head[index]->prev = last;
                  }
                  else
                  {
                      first = index;
                      head[index]->prev = -1;
                  }
              
                  last = index;
                  head[index]->next = -1;
              }
              

              【讨论】:

                【解决方案13】:

                作为使用previousnext 向量/数组的现有答案的补充,我们可以在更动态调整大小的结构之上构建,即失去调整大小操作的摊销。

                为什么我认为这是合适的?好吧,我们通过使用向量/数组获得了一些优势,但我们得到了摊销的大小调整作为回报。如果我们能摆脱后者,我们可能会完全扭转局面!

                具体来说,我指的是Resizable Arrays in Optimal Time and Space。这是一种非常有趣的数据结构,尤其是作为其他数据结构的基础,例如我们正在讨论的数据结构。

                请注意,我已链接到技术报告,与常规论文不同,该报告还包括(非常有趣的)关于如何实现 双重可调整大小的数组的解释。

                【讨论】:

                  【解决方案14】:

                  可以使用 C++ 实现链接列表数据结构而不使用指针(下一个节点)吗?
                  没有。

                  【讨论】:

                  • 链接不必是绝对内存地址,它可以是相对于指针的,也就是数组索引。查看其他答案。
                  • @Ramónster:我已经回答了其他答案,为什么我认为它们不正确。
                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2015-09-09
                  • 2013-10-29
                  • 1970-01-01
                  • 1970-01-01
                  • 2021-08-25
                  • 2012-04-20
                  • 1970-01-01
                  相关资源
                  最近更新 更多