链表中dummy与tail同步更新的底层内存原理问询
近期开始学习链表,采用LeetCode链表题目中的ListNode实现方案,参考现有解法完成了「合并两个有序链表」的代码实现。为便于理解,额外定义了t2变量,调试时发现dummy节点与tail指针会同步更新——修改tail的next属性时,dummy的后续节点也随之变化。对此存在疑惑:dummy与tail不是不同的变量吗?为何会出现同步更新的情况?希望了解该操作在底层内存中的具体运行机制。
ListNode定义
# Definition for singly-linked list. # class ListNode: # def __init__(self, x): # self.val = x # self.next = None
合并链表代码
class Solution: def mergeTwoLists(self, list1: Optional[ListNode], list2: Optional[ListNode]) -> Optional[ListNode]: # 创建dummy节点存储结果链表 dummy = ListNode() tail = dummy t2 = tail print('PREV') print(dummy, tail, t2) print('------------------') while list1 and list2: # 循环直到其中一个链表遍历完毕 # 如果list1当前节点值更小 if list1.val<list2.val: # 将list1节点接在tail后面,并移动list1指针 tail.next = list1 list1 = list1.next print('if', tail, '||' ,dummy) else: # 否则接list2节点,并移动list2指针 tail.next = list2 list2 = list2.next print('else', tail, '||', dummy) # 移动tail指针到新的尾部 tail = tail.next print("UPDATED TAIL:", tail) print(dummy) print('###########') # 处理剩余未遍历的节点 if list1: tail.next = list1 elif list2: tail.next = list2 print('------------------------') print(list1, list2) print(tail) print(dummy) print(t2) # 返回合并后的链表(跳过dummy头节点) return dummy.next
调试日志
PREV ListNode{val: 0, next: None} ListNode{val: 0, next: None} ListNode{val: 0, next: None} ------------------ else ListNode{val: 0, next: ListNode{val: 1, next: ListNode{val: 3, next: ListNode{val: 4, next: None}}}} || ListNode{val: 0, next: ListNode{val: 1, next: ListNode{val: 3, next: ListNode{val: 4, next: None}}}} UPDATED TAIL: ListNode{val: 1, next: ListNode{val: 3, next: ListNode{val: 4, next: None}}} ListNode{val: 0, next: ListNode{val: 1, next: ListNode{val: 3, next: ListNode{val: 4, next: None}}}} ########### if ListNode{val: 1, next: ListNode{val: 1, next: ListNode{val: 2, next: ListNode{val: 4, next: None}}}} || ListNode{val: 0, next: ListNode{val: 1, next: ListNode{val: 1, next: ListNode{val: 2, next: ListNode{val: 4, next: None}}}}} UPDATED TAIL: ListNode{val: 1, next: ListNode{val: 2, next: ListNode{val: 4, next: None}}} ListNode{val: 0, next: ListNode{val: 1, next: ListNode{val: 1, next: ListNode{val: 2, next: ListNode{val: 4, next: None}}}}} ########### if ListNode{val: 1, next: ListNode{val: 2, next: ListNode{val: 4, next: None}}} || ListNode{val: 0, next: ListNode{val: 1, next: ListNode{val: 1, next: ListNode{val: 2, next: ListNode{val: 4, next: None}}}}} UPDATED TAIL: ListNode{val: 2, next: ListNode{val: 4, next: None}} ListNode{val: 0, next: ListNode{val: 1, next: ListNode{val: 1, next: ListNode{val: 2, next: ListNode{val: 4, next: None}}}}} ########### else ListNode{val: 2, next: ListNode{val: 3, next: ListNode{val: 4, next: None}}} || ListNode{val: 0, next: ListNode{val: 1, next: ListNode{val: 1, next: ListNode{val: 2, next: ListNode{val: 3, next: ListNode{val: 4, next: None}}}}}} UPDATED TAIL: ListNode{val: 3, next: ListNode{val: 4, next: None}} ListNode{val: 0, next: ListNode{val: 1, next: ListNode{val: 1, next: ListNode{val: 2, next: ListNode{val: 3, next: ListNode{val: 4, next: None}}}}} ########### else ListNode{val: 3, next: ListNode{val: 4, next: None}} || ListNode{val: 0, next: ListNode{val: 1, next: ListNode{val: 1, next: ListNode{val: 2, next: ListNode{val: 3, next: ListNode{val: 4, next: None}}}}} UPDATED TAIL: ListNode{val: 4, next: None} ListNode{val: 0, next: ListNode{val: 1, next: ListNode{val: 1, next: ListNode{val: 2, next: ListNode{val: 3, next: ListNode{val: 4, next: None}}}}} ########### ------------------------ After loop ListNode{val: 4, next: None} None ListNode{val: 4, next: ListNode{val: 4, next: None}} ListNode{val: 0, next: ListNode{val: 1, next: ListNode{val: 1, next: ListNode{val: 2, next: ListNode{val: 3, next: ListNode{val: 4, next: ListNode{val: 4, next: None}}}}}} ListNode{val: 0, next: ListNode{val: 1, next: ListNode{val: 1, next: ListNode{val: 2, next: ListNode{val: 3, next: ListNode{val: 4, next: ListNode{val: 4, next: None}}}}}
核心原因是Python中对象和变量的关系是「引用」而非「拷贝」,具体拆解如下:
初始阶段的引用指向
当执行dummy = ListNode()时,内存中会创建一个ListNode对象(假设内存地址为0x123),dummy这个变量其实是存储了该对象的内存地址,相当于一个指针指向0x123处的对象。
紧接着tail = dummy和t2 = tail,这两个操作只是把dummy里存储的内存地址(0x123)拷贝给了tail和t2,此时三个变量都指向同一个内存地址的ListNode对象。修改对象属性的影响
当执行tail.next = list1或tail.next = list2时,并不是修改tail变量本身,而是通过tail的引用,找到它指向的内存地址(0x123)处的ListNode对象,修改该对象的next属性。
因为dummy同样指向这个0x123的对象,所以当你查看dummy的next时,自然会看到和tail修改后的结果一致——本质是同一个对象的属性被修改,所有指向它的变量都会感知到这个变化。tail = tail.next的本质
这个操作是改变tail变量的引用指向:从原来的0x123对象,改为指向0x123对象的next属性对应的那个ListNode对象(比如0x456)。此时dummy仍然指向0x123的头节点,而tail已经指向链表的下一个节点,但0x123对象的next链已经被之前的操作修改,所以dummy的后续节点会跟着整个链表的构建过程更新。
简单来说,dummy、tail、t2一开始是「共享同一个对象的不同指针」,修改对象的属性会影响所有指针;而tail = tail.next是让tail这个指针换一个对象指向,但原来的对象已经被修改过,dummy作为头指针自然能看到完整的链表结构。
内容的提问来源于stack exchange,提问作者Aman Savaria

