尝试为双向链表编写交换位置的函数

1 投票
1 回答
732 浏览
提问于 2025-04-18 12:20

我一直在尝试用Python实现一个简单的链表作为代码练习,虽然我大部分功能都已经实现了(比如插入、删除、漂亮打印、交换两个节点的内容),但我在交换两个节点的部分卡了好几天。

我在网上查了一下,大多数人似乎建议要么删除再插入节点,要么交换节点的数据。这两种方法都很好用,但我想挑战自己,看看能不能用“正确”的方式来交换节点。

理想情况下,我希望能有一个通用的函数,能够处理所有边界情况(比如移动到开头、结尾和随机交换节点)。这比我想象的要难得多。

我用纸和笔做了一些实验,也在网上找了一些资料,发现了以下讨论和示例实现:

我遇到的问题是我的 node1.nextnode2.prev 被交换了,而且我的 node2.next 指向了自己,而不是链表中的下一个节点。

示例实现页面上的评论专门提到了这个问题,并说在他的实现中不应该发生这种情况。

我似乎搞不清楚自己哪里出错了。我想我可以“作弊”,强制它们在最后取正确的值,但这样在节点是第一个或最后一个时会出现很多问题。

__author__ = 'laurens'
from django.core.exceptions import ObjectDoesNotExist
import logging
import copy

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

class DoublyLinkedList(object):
    """This class implements a basic doubly linked list in Django, it depends
    on a Django model with the following field:

    id : PK
    prev: integer previous node
    data_field: Foreign key
    next: integer next node

    the prev and next fields don't need to be self-referencing

    When instantiating the class you have to link this class to a Django model
     and specify a data field. The data field can link to a foreign key
     or contain data
    """

    def __init__(self, doubly_linked_list_model, data_field):
        self.doubly_linked_list_model = doubly_linked_list_model
        self.data_field = data_field

    def get_node_from_node_id(self, node_id=None):
        """This function returns the node associated with a certain node_id"""
        if node_id is None:
            node = None
        else:
            try:
                node = self.doubly_linked_list_model.get(id=node_id)
            except ObjectDoesNotExist:
                node = None

        return node

    @staticmethod
    def _update_node(node, prev=None, next=None):

        node.prev = prev
        node.next = next

        logger.debug('updating node: %s', node.id)
        logger.debug('node.prev = %s', node.prev)
        logger.debug('node.next = %s', node.next)

        try:
            node.save()
        except Exception as e: #Todo: specify this
            logger.debug('Error saving node: %s', node.id)

    def move_node(self, node1=None, node2=None):
        """
        This function swaps the position of node1 with the position of node2
        """

        #swapping two nodes!
        logger.debug('Swapping two random nodes!: %s, %s', node1.id, node2.id)

        # Swapping next nodes
        logger.debug('Swapping next node')
        tmp = copy.deepcopy(node1.next)

        self._update_node(node=node1,
                          prev=node1.prev,
                          next=node2.next)

        #Todo: Check if tmp changes or is stored as a copy
        self._update_node(node=node2,
                          prev=node2.prev,
                          next=tmp)

        if node1.next is not None:
            logger.debug('Connect the next node to node 1')
            node_next = self.get_node_from_node_id(node1.next)
            self._update_node(node=node_next,
                              prev=node1.id,
                              next=node_next.next)

        if node2.next is not None:
            logger.debug('Connect the next node to node 2')
            node_next = self.get_node_from_node_id(node2.next)
            self._update_node(node=node_next,
                              prev=node2.id,
                              next=node_next.next)

        logger.debug('Swap prev nodes')
        tmp = copy.deepcopy(node1.prev)

        self._update_node(node=node1,
                          prev=node2.prev,
                          next=node1.next)

        self._update_node(node=node2,
                          prev=tmp,
                          next=node2.next)

        # Connect the node before node1 to node 1
        if node1.prev is not None:
            logger.debug('Connect the prev to node 1')
            node_prev = self.get_node_from_node_id(node1.prev)
            self._update_node(node=node_prev,
                              prev=node_prev.prev,
                              next=node1.id)

        # Connect the node before node2 to node 2
        if node2.prev is not None:
            logger.debug('Connect the prev to node 2')
            node_prev = self.get_node_from_node_id(node2.prev)
            self._update_node(node=node_prev,
                              prev=node_prev.prev,
                              next=node2.id)

这个 _update_node function 只是把我的输入保存到数据库中,它可以处理 None 值。

get_node_from_node_id 接受一个整数作为输入,并返回与之关联的节点对象。我这样做是为了避免在数据库中使用自引用的外键(这个说法对吗?),目前我想继续这样工作。一旦这个部分正常工作了,我会再去修正数据库中的相关内容。

1 个回答

1

小提示:当我提供一个最小、完整、可验证的例子(MCVE)时,我能更快得到答案,这个也叫简短、自包含、可编译的例子(SSCCE)

你的示例代码没有展示出问题,这让我们无法帮助你。请尽量让我们更容易帮助你。

我运行了你的示例代码,但没有看到任何输出。

我遇到的问题是...我的node2.next指向了它自己,而不是列表中的下一个节点。

为什么node2.next指向node2会是个问题呢?

就我所知,你给我们的代码部分运行得很好。

我经历过的一些最困难的调试过程,最后都只是在我意识到其实一切都正常,原本以为的“bug”根本不存在时才结束。

是的,在算法的中间步骤中,确实有一个节点指向了它自己,这看起来显然是错的。

但算法的后半部分会进行进一步的修改。等算法完成时,

  • “next”链条从头到尾正确连接,
  • “forward”链条则是从尾到头正确连接,顺序和“next”链条相反,
  • 这两条链的顺序和最初的顺序相似,只是node1和node2的逻辑位置互换了(但它们仍然在同一个物理位置),
  • 每个节点指向2个其他节点(或者指向NULL节点),绝不会指向自己。

这不是你想要的结果吗?

当我运行以下测试脚本时,得到了很多输出——但在输出中没有看到任何问题。 (我使用了一个简单的数组,带有整数索引,而不是使用真正的指针或引用,这样代码更简短,也更容易调试,相比于SQL数据库。)

你能指出哪一行输出不是你预期的,并详细说明你希望那一行输出实际应该是什么吗?

#!/usr/bin/env python
# https://stackoverflow.com/questions/24610889/trying-to-write-a-swap-position-function-for-a-doubly-linked-list
# Is this the shortest possible program that exhibits the bug?
# Before running this probram, you may need to install
#     sudo apt-get install python-django
# 2015-03-12: David added some test scaffolding
# 2014-07-07: Laurens posted to StackOverflow

__author__ = 'laurens'
from django.core.exceptions import ObjectDoesNotExist
import logging
import copy

logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)

class DoublyLinkedList(object):
    """This class implements a basic doubly linked list in Django, it depends
    on a Django model with the following field:

    id : PK
    prev: integer previous node
    data_field: Foreign key
    next: integer next node

    the prev and next fields don't need to be self-referencing

    When instantiating the class you have to link this class to a Django model
     and specify a data field. The data field can link to a foreign key
     or contain data
    """

    def __init__(self, doubly_linked_list_model, data_field):
        self.doubly_linked_list_model = doubly_linked_list_model
        self.data_field = data_field

    def get_node_from_node_id(self, node_id=None):
        """This function returns the node associated with a certain node_id"""
        if node_id is None:
            node = None
        else:
            try:
                node = self.doubly_linked_list_model.get(id=node_id)
            except ObjectDoesNotExist:
                node = None

        return node

    @staticmethod
    def _update_node(node, prev=None, next=None):

        node.prev = prev
        node.next = next

        logger.debug('updating node: %s', node.id)
        logger.debug('node.prev = %s', node.prev)
        logger.debug('node.next = %s', node.next)

        try:
            node.save()
        except Exception as e: #Todo: specify this
            logger.debug('Error saving node: %s', node.id)

    def move_node(self, node1=None, node2=None):
        """
        This function swaps the position of node1 with the position of node2
        """

        #swapping two nodes!
        logger.debug('Swapping two random nodes!: %s, %s', node1.id, node2.id)

        # Swapping next nodes
        logger.debug('Swapping next node')
        tmp = copy.deepcopy(node1.next)

        self._update_node(node=node1,
                          prev=node1.prev,
                          next=node2.next)

        #Todo: Check if tmp changes or is stored as a copy
        self._update_node(node=node2,
                          prev=node2.prev,
                          next=tmp)

        if node1.next is not None:
            logger.debug('Connect the next node to node 1')
            node_next = self.get_node_from_node_id(node1.next)
            self._update_node(node=node_next,
                              prev=node1.id,
                              next=node_next.next)

        if node2.next is not None:
            logger.debug('Connect the next node to node 2')
            node_next = self.get_node_from_node_id(node2.next)
            self._update_node(node=node_next,
                              prev=node2.id,
                              next=node_next.next)

        logger.debug('Swap prev nodes')
        tmp = copy.deepcopy(node1.prev)

        self._update_node(node=node1,
                          prev=node2.prev,
                          next=node1.next)

        self._update_node(node=node2,
                          prev=tmp,
                          next=node2.next)

        # Connect the node before node1 to node 1
        if node1.prev is not None:
            logger.debug('Connect the prev to node 1')
            node_prev = self.get_node_from_node_id(node1.prev)
            self._update_node(node=node_prev,
                              prev=node_prev.prev,
                              next=node1.id)

        # Connect the node before node2 to node 2
        if node2.prev is not None:
            logger.debug('Connect the prev to node 2')
            node_prev = self.get_node_from_node_id(node2.prev)
            self._update_node(node=node_prev,
                              prev=node_prev.prev,
                              next=node2.id)


global_test_array = []
obfuscation = 0xaa

class trivial_test_node_class(object):
    def __init__(self, prev, id, next, data):
        print "initializing test class."
        # self.stuff = [ ["first", 0, 1], ["second", 0, 1] ]
        self.id = id
        self.prev = prev
        self.next = next
        self.data = data
    def something(self):
        print "something"
    def save(self):
        id = self.id
        global_test_array[id] = self
    def __repr__(self):
        # print self.prev, self.id, self.next, self.data
        the_string = "%s(%r)\n" % (self.__class__, self.__dict__)
        return the_string

class trivial_test_list_model_class(object):
    def __init__(self):
        print "initializing test class."
        #self.stuff = [ ["first", 0, 1], ["second", 0, 1] ]
        self.stuff = [ 0 for i in xrange(0,10) ]
        data = 'a'
        for i in xrange(1,10):
            self.stuff[i] = trivial_test_node_class(i-1,i,i+1,data);
            data += 'r' # talk like a pirate day
        self.stuff[-1].next = 0 # use 0 as NULL id.
        global global_test_array
        global_test_array = self.stuff
    def something(self):
        print "something"
    def get(self,id):
        return self.stuff[id]
    def __repr__(self):
        # for i in xrange(1,10):
        #     print self.stuff[i]
        the_string = "%s(%r)" % (self.__class__, self.__dict__)
        return the_string

if __name__ == '__main__':
    # test code that only gets run when this file is run directly,
    # not when this file is imported from some other python script.
    print "Hello, world."
    trivial_test_model = trivial_test_list_model_class()
    print trivial_test_model
    testll = DoublyLinkedList( trivial_test_model, "data" )
    left_node = trivial_test_model.get(3)
    right_node = trivial_test_model.get(4)
    testll.move_node( left_node, right_node )
    print trivial_test_model


# recommended by http://wiki.python.org/moin/vim
# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 :

撰写回答