【问题标题】:Weird Python behavior when setting an object's variable from another object's variable从另一个对象的变量设置对象的变量时出现奇怪的 Python 行为
【发布时间】:2019-01-23 13:08:55
【问题描述】:

我创建了一个可以反转链接列表的函数,但是发生了一些奇怪的事情,我一直无法弄清楚。

我正在尝试编辑列表以节省空间,因此该方法更改了原始列表对象并且不返回任何内容。这意味着如果 reverse_list 方法是最后几行(为清楚起见,此处重命名变量):

original_first_node.val = new_first_node.val
original_first_node.next = new_first_node.next

但由于某种原因,original_first_node.next 上的节点链看起来与new_first_node.next 上的不同,而且现在也是循环的。

这是一些单元测试失败的可运行代码(请参阅reverse_list 函数中的 cmets):

import unittest


class Node(object):
    def __init__(self, x):
        self.val = x
        self.next = None


def create_list(list):
    if not list:
        return None
    sentinel = Node(None)
    current = sentinel
    for item in list:
        current.next = Node(item)
        current = current.next
    return sentinel.next


def convert_list(head):
    ret = []
    if head:
        current = head
        while current:
            ret.append(current.val)
            current = current.next
    return ret


def is_list_cyclic(head):
    if not head:
        return False
    tortoise = hare = head
    while hare.next and hare.next.next:
        tortoise = tortoise.next
        hare = hare.next.next
        if tortoise == hare:
            return True
    return False


def reverse_list(head):
    if not head or not head.next:
        return

    current = head
    prev = None
    while current:
        static_next = current.next
        current.next = prev
        prev = current
        current = static_next

    # At this point, prev.next looks great

    head.val = prev.val
    head.next = prev.next

    # head.next is cyclical now for some reason ??


class TestSuite(unittest.TestCase):

    def test_reverse_list(self):
        head = create_list([1, 2, 3, 4])

        reverse_list(head)

        self.assertFalse(is_list_cyclic(head))
        self.assertEqual([4, 3, 2, 1], convert_list(head))


if __name__ == "__main__":
    unittest.main()

【问题讨论】:

    标签: python python-3.x linked-list pass-by-reference variable-assignment


    【解决方案1】:

    这篇 Stackoverflow 帖子包含有关在 Python 中传递参数的详细信息:How do I pass a variable by reference?

    reverse_list 函数中的以下两行是问题所在:

    head.val = prev.val
    head.next = prev.next
    

    这是我认为正在发生的事情:

    # Marker 1
    head.val = prev.val
    head.next = prev.next
    # Marker 2
    

    Marker 1,列表如下所示:

    None  <---  1  <---  2  <---  3  <---  4
    
                ^                          ^
                |                          |
              head                       prev
    

    Marker 2,列表如下所示:

            ----------------------
           |                      |
           |                      |
           |                      v
           ---  4  <---  2  <---  3  <---  4
    
                ^                          ^
                |                          |
              head                       prev
    

    因此,在reverse_list 的末尾,head 仍然指向第一个节点,但它的值为4。而head.next 指向包含3 的节点,所以你得到了如图所示的循环。

    我的建议是您返回对反向列表的第一个节点的引用。修改后的reversed_list 如下所示:

    def reverse_list(head):
        if not head or not head.next:
            return
    
        current = head
        prev = None
        while current:
            static_next = current.next
            current.next = prev
            prev = current
            current = static_next
    
        return prev
    

    您的测试可以修改为:

    class TestSuite(unittest.TestCase):
    
        def test_reverse_list(self):
            head = create_list([1, 2, 3, 4])
    
            rev = reverse_list(head)
    
            self.assertFalse(is_list_cyclic(rev))
            self.assertEqual([4, 3, 2, 1], convert_list(rev))
    

    编辑

    @mattalxndr,在阅读您的 cmets 时,主要问题似乎是如何在不返回值的情况下“就地”反转列表。我能想到的最简单的解决方案是:

    • 制作列表副本(保存到copied_list
    • 反向copied_list
    • 开始从左到右遍历原始列表
    • 开始从右到左遍历copied_list
    • valcopied_list复制到原始列表

    这种技术会创建另一个列表副本,因此使用 O(n) 空间。可能存在更好的算法,但我目前想不出任何算法。

    【讨论】:

    • 我是否正确理解这样做会绕过就地编辑列表的初衷?
    • 列表仍在原地编辑中。 reverse_list 中没有创建新节点,因此原始节点保持不变,除了它们的 next 变量正在更新以指向列表中的不同 Node 实例。
    • 可能是我对“就地”有错误的想法; reverse_list 正在返回一个新对象。事实证明,为了测试反转,我们必须查看 rev 而不是 head,不是吗?
    • 由于 python 的传递值,在reverse_list 中对head 本身所做的任何修改都不会修改headtest_reverse_list 中所指的内容。例如,我们可以在reverse_list 中添加语句head = "some_value",在test_reverse_list 中添加head 仍然会引用列表的第一个节点。在我的理解中,“就地”意味着我们创建的新节点数量与原始列表中的节点数量不同 - 要使用 big-o 表示法,reverse_list 函数必须使用 O(1) 额外内存。
    • 我在某个时候返回它,但该函数正在对节点对象链进行更改,这些节点对象链被带回外部范围。与其在方法中清理它,我认为不返回任何东西而只是改变输入更干净,就像Java中的Collections.sort(al)一样。是的,我想坚持使用 O(n) 内存,O(1) 空间。
    猜你喜欢
    • 2010-09-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多