LeetCode 160:相交链表
先补充数组和动态数组的区别,再比较动态数组和链表的区别,最后讲解本题题解。
数组和动态数组
1. 普通数组(静态数组,Static Array)
-
内存分配:一旦定义,大小固定(如 C 语言中的
int arr[5]
)。 -
缺点:如果满了就不能再加元素,想扩容必须手动申请新数组并复制数据。
-
优点:结构简单,内存连续,访问下标 O(1)。
2. 动态数组(Dynamic Array)
-
内存分配:底层还是数组,但容量不足时会自动申请更大空间并复制元素,对用户是透明的。
-
扩容策略:常见是扩容为当前容量的 1.5~2 倍,以减少频繁复制。
-
典型实现:
-
Python 的
list
-
Java 的
ArrayList
-
C++ 的
std::vector
-
动态数组(Dynamic Array)和链表(Linked List)
两种典型的线性数据结构,它们在内存分配、访问方式、插入删除效率等方面有明显区别:
1. 内存分配方式
-
动态数组(如 Python 的
list
):-
在内存中分配一块连续的空间来存放元素。
-
如果容量不够,会一次性申请更大的空间(通常是扩容 1.5~2 倍),然后把旧数据拷贝过去。
-
-
链表:
-
每个元素是一个节点,包含数据和指向下一个节点的指针(
next
)。 -
节点分散在内存中,通过指针链接,不要求连续内存。
-
2. 元素访问效率
-
动态数组:支持随机访问,
arr[i]
时间复杂度 O(1),因为可以直接通过偏移量定位。 -
链表:只能从头节点开始依次遍历,查找第
i
个元素的时间复杂度 O(n)。
3. 插入与删除效率
-
动态数组:
-
在尾部插入/删除:O(1)(摊销)
-
在中间插入/删除:O(n),因为需要移动大量元素。
-
-
链表:
-
已知节点的前驱节点时,插入/删除节点:O(1)
-
但如果要先找到插入位置,查找过程是 O(n)。
-
4. 空间利用率
-
动态数组:可能存在额外的预留空间(扩容时会浪费部分内存)。
-
链表:每个节点需要额外的指针域,指针占用额外内存,尤其数据量小的时候浪费更明显。
5. 适用场景总结
-
如果访问多、随机读取多:用动态数组更高效(例如 Python 的
list
)。 -
如果频繁插入删除,尤其在中间或两端:链表更适合(如
collections.deque
用的是双向链表+块结构)。
直观对比表
特性 | 动态数组(Python list) | 链表(Linked List) |
---|---|---|
内存结构 | 连续内存 | 分散内存 |
随机访问效率 | O(1) | O(n) |
插入/删除效率 | 中间 O(n),尾部 O(1) | 已知节点 O(1),查找 O(n) |
空间开销 | 可能有预留空间 | 每个节点多一个指针 |
扩容成本 | 有扩容成本(复制数据) | 无扩容成本 |
LeetCode 160:相交链表
思路
在单链表题里,传进来的两个“头节点”headA、headB 就分别代表两条链表:每个头节点是第一个实际节点的引用,你可以从它开始沿着 .next 指针一直走到结尾把整条链表遍历出来。相交判定是看节点是否同一对象(p is q),不是看 val 是否相等
解题过程
把两个头节点headA,headB先赋值分别赋值给p,q,为了不改变头节点信息,当p的val和q的val不为null时,p走一步,q走一步,相同时返回,不相同一直遍历下去,A遍历完遍历B,B遍历完遍历A
复杂度
-
时间复杂度: O(m+n)O(m+n)O(m+n)
-
空间复杂度: O(1) O(1) O(1)