【问题标题】:How do I insert a new node before first node of a doubly-linked list?如何在双向链表的第一个节点之前插入一个新节点?
【发布时间】:2011-03-15 12:42:49
【问题描述】:

我正在研究如何在双向链表的第一个节点之前插入一个新节点。我对此操作所需的辅助节点以及执行该操作的步骤顺序感到困惑。我将不胜感激有关如何解决此问题的提示,即我的 insertBeforeFirst 方法有什么问题。就目前而言,该方法会导致我发现很难解决的 nullPointerException。 (注意:这是之前post 的后续。)

public DLL()
{
    header = null ;
    tail = null ;
}

...
DLL myList = new DLL() ;
DLLNode A = new DLLNode("Hello", null, null) ;
DLLNode B = new DLLNode("Hi", null, null) ;
...

myList.addFirst(A) ;
myList.insertBeforeFirst(B)

...
public void addFirst(DLLNode v)
{
    v.pred = header ; 
    v.succ = tail ; 
    header = v ;
    tail = v ;
}
...

public void insertBeforeFirst(DLLNode v)
{
    DLLNode aux = v ;
    aux.succ = header.succ ;
    header = aux ;
    DLLNode aux2 = aux.succ ;
    aux2.pred = v ;
}

[编辑]

我听从了 Aaron 的建议并画了一张图,我有一点改进,因为我不再收到 nullPointerException,但是新模式插入到第一个节点之后而不是之前。所以我认为我的绘画技巧也需要一些打磨:)

public void insertBeforeFirst(DLLNode v)
{
    v.succ = header.succ ; // point new node succ to current first node
    header.succ = v ;  //point header to new node
    DLLNode aux = header.succ ; // auxiliary node for backward insertion
    aux.pred = v ; // point auxiliary's pred backward to new node
}

[EDIT2]

看看 MahlerFive 的帖子,我现在明白了为什么你们中的一些人可能会对我的头尾谈话感到困惑。这是我从这里得到的:“为了简化编程,在双向链表的两端添加特殊节点很方便:在列表头部之前添加一个 header 节点,和一个 trailer 节点就在列表的尾部。这些“虚拟”节点不存储任何元素” source

因此,对于初学者来说,我似乎需要找到一种方法来正确实现这些虚拟节点,然后才能添加任何内容并进行正确的引用。这些 DUMMY 节点似乎需要不同的 Node 构造函数?它们可以由 DLL 默认构造函数实例化吗?

[EDIT3]

@MahlerFive,DLL 构造函数将如下所示:

public DLL()
{
    DLLNode Header = new DLLNode(null, null, null) ;
    DLLNode Tail = new DLLNode(null, Header, null) ;
    Header.succ = Tail ;
}

我的方法是这样的,虽然我现在得到一个 nullPointerException:

// insert z before v
public void addBeforeFirst(DLLNode v, DLLNode z)
{
    DLLNode aux = v.pred ;
    z.pred = aux ;
    z.succ = v ;
    v.pred = z ;
    aux.succ = z ;
}

[EDIT4]

我正在进步。 (感觉很棒!)我同意 MahlerFive 的观点,即 DUMMY Header 和 Tail 节点不是解决这个问题的好方法。但正如在一本出版的书中提到的那样,它至少值得探索。这是我的新代码(不使用虚拟节点):

...

// DLL Constructor
public DLL()
{
    first = null ;
    last = null ;
}
...
// example insert call
// B is the node in front of which i want to insert
l.insert("Ciao", B) ;
...

public void insert(String elem, DLLNode pred)
{
    // make ins a link to a newly-created node with element elem,
    // predecessor null, and successor null.
    DLLNode ins = new DLLNode(elem, null, null) ;
    // Insert ins at the insertion point in the
    // forward SLL headed by first.
    ins.pred = first ;
    ins.succ = first ;
    // let first be the the new node
    first = ins ;
}

这需要微调,因为我还没有设置任何反向链接,但这是一个很好的起点。为确保它正常工作(至少以正向方式),我添加了打印语句以在添加节点时打印出第一个和最后一个元素。事实上,它们已正确更新:

Hi
first: hi
last: hi

Ciao Hi
first: Ciao
last: hi

Moin Ciao Hi
first: Moin
last: hi

【问题讨论】:

  • “虚拟节点”是解释列表头部和尾部的一种非常糟糕的方式。您引用的虚拟节点的定义基本上是“没有数据的普通节点”。但是这是错误的。头节点应该有pred吗?尾巴有成功吗?您可以像使用普通节点一样添加或删除虚拟节点吗?没有。实际上,您应该将头部和尾部视为列表头部和尾部的实际节点的引用或“指针”。您有一个带有头和尾引用的 DLL 对象,这是处理此问题的标准方法。
  • 我编辑了我的答案以响应您的编辑#3
  • 你已经接近了。现在,在每一步之后准确地画出你列出的样子。你应该注意到一些问题。如果没有,请尝试从尾部向后打印列表(在 pred 引用之后),您应该会注意到一个问题。

标签: java linked-list


【解决方案1】:

与其在大脑中做这一切,不如坐下来五分钟,在纸上画出数据结构(DLL 和 2-3 对节点)。

留出空隙,在其下方画出已插入节点后的样子。

用记号笔标记您需要进行的所有更改。给每个更改一个编号。

现在坐下来实施每个更改。

这听起来很乏味,但它会帮助您深入了解正在发生的事情。这样,您只需执行一次。

如果您更喜欢纸和剪刀类型,请获取一些绳子,剪下节点并将便笺粘贴到绳子的末端。现在您可以将字符串用作模型元素之间的“引用”。

【讨论】:

  • 嗨亚伦,我完全同意你的看法。但是即使我尝试这个我也会感到困惑......问题是标题和尾部不包含任何数据而只是一个指针然后当我添加一个辅助节点也只是一个没有数据的指针时,我最终得到很多箭头和盒子,我迷路了。但是是的,我同意,我会再试一次,希望有突破:)任何其他提示也很感激:)
  • 如果绘制结构,您会注意到 DLL 结构(包含头部和尾部)实际上并不是列表的一部分。这只是一个快速查找列表开头和结尾的“快捷方式”。所以这些事情是两个不同的问题。
  • 嗨亚伦,辅助节点呢?绘制框和箭头的问题在于,很容易将指针指向另一个节点,而不会意识到需要辅助节点。因此,我的帖子也在寻找对这些辅助节点以及何时使用它们的更好理解。
  • 只需对插入节点所需进行的更改进行编号。当您为每个数字编写代码时,您最终将需要一个辅助节点。到那时,你为什么需要它就会很清楚了。
  • 我还建议去掉虚拟头尾节点或将insertBefore()/insertAfter() 方法移至节点类。要添加一个节点,您可以使用list.head.insertBefore(newNode)
【解决方案2】:

看起来您的大部分困惑都在于header 的工作原理。它只是对列表中第一个节点的引用。此外,不需要“辅助”节点。

让我们看看您需要按顺序执行的步骤:

第一步:设置你要插入的节点的pred

由于节点会成为第一个节点,所以后面没有节点,所以可以说v.pred = null;

第 2 步:设置您要插入的节点的succ

我们的新节点需要指向旧的第一个节点。这就是混乱的地方。您执行以下操作:

v.succ = header.succ ; // point new node succ to current first node

但是header 是什么?它是对第一个节点的引用。通过说header.succ,您是说您想要第一个节点的后继节点,即第二个节点。当您看到header 时,只需想到“第一个节点”,您就会想到:

v.succ = header;

第 3 步:将旧的第一个节点指向新节点

你已经正确地执行了这一步(记住,认为标题是“第一个节点”):

header.succ = v ; //point header to new node

第 4 步:现在将新节点设为标头

header = v;

编辑(响应您的编辑 #3): 关于整个方法存在一些问题,我将在最后提出,但暂时搁置这些问题并假设您需要以这种方式设置列表...

假设您将列表的第一个节点 (Header.succ) 作为 v 传递,让我们采取相同的步骤:

第一步:设置你要插入的节点的pred

您希望新节点指向 Header。 v.pred = Header;

第 2 步:设置您要插入的节点的succ

您希望新节点指向旧的第一个节点,即Header.succ

v.succ = Header.succ;

第 3 步:将旧的第一个节点指向新节点

确保首先检查第一个节点是否存在(在我的第一篇文章中忘记了)!

if (Header.succ != null) {
    Header.succ.pred = v; // Header.succ is the first node, and you want to set its pred reference
}

第 4 步:现在将新节点设为第一个节点

Header.succ = v;

请注意您甚至不需要z,因为您可以使用Header.succ 到达第一个节点。这意味着您不应该需要它作为参数。

现在,除了所有这些之外,您还应该考虑一些问题:

具有 pred 引用和数据字段的 Header 有什么意义?具有 succ 引用和数据字段的 Tail 有什么意义?

如果你想使用这个链表并插入项目,如果你只需要为每个项目调用 add() 而不是 addFirst() 和 addBeforeFirst() 不是更好吗?如果你可以使用 remove() 方法,那么你必须跟踪列表是否为空,以确定调用哪个方法,addFirst() 或 addBeforeFirst(),这有点难看。最好编写一个更通用的 add() 来为要使用列表的用户处理这个问题。

【讨论】:

  • 谢谢。请参阅我的最新更新,它解释了我对 Header 节点的想法。我试图这样做的方式实际上意味着 Header 与第一个节点不同,它们是独立的实体。
猜你喜欢
  • 1970-01-01
  • 2011-12-23
  • 1970-01-01
  • 2021-08-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-01-18
  • 2014-10-13
相关资源
最近更新 更多