链表基础
- 单链表:data, next pointer
- 双链表:previous pointer, data, next pointer
- 循环链表:首尾相连的链表
- 链表储存在空间上可不连续,使用pointer串联,分配机制取决于操作系统的内存管理
- 链表节点的定义需要写构建函数以方便initialization时同时赋值data (python不需要担心这一点)
class ListNode:
def __init__(self, val, next=None):
self.val = val
self.next = next
- 链表的【添加】与【删除本节点】均通过修改pointer实现,O(1)
- 链表的【查找】O(n),如需查找到某个节点再删除,则O(n)
- 与数组对比
插入/删除 | 查询 | 适用 | |
---|---|---|---|
数组 | O(n) 【长度固定需要重新定义一个】 | O(1) | 数据量固定,频繁查询,较少增删 |
链表 | O(1) | O(n) | 数据量不固定,频繁增删,较少查询 |
203 移除链表元素
初始思路:
为解决head本身可能需要被移除的情况,在head前加一个dummy new_head,new_head.next = head。从new_head开始,用ptr指向当前查看的ListNode,只要ptr.next还存在ListNode x,就查看x.val值,如果需要移除,则直接将ptr.next指向x.next,删除x;如果不需要移除,则移动ptr,指向ptr.next。
def removeElements(self, head, val):
new_head = ListNode(0,head)
ptr = new_head
while ptr.next is not None:
if ptr.next.val == val:
ptr.next = ptr.next.next
else:
ptr = ptr.next
return new_head.next
另一种思路:
单独处理删除head的状况,将head指向head.next。
Complexity
time: O(n)
space: O(1)
707 设计链表
初始思路:
增删操作本身或在抬头和末尾处增删都较为容易,改变pointer的指向即可。
而get(), addAtIndex(), deleteAtIndex(),需要先找到指定index。
继续使用dummy_head,但没有使用size,开始想以逐一递减index的while循环来找到位置,可以自然在:
- index递减到0先于出现None:找到index
- 出现None先于递减到0: invalid index
问题:
边界需要仔细处理,需要很多+1和-1,容易混乱。并且判定ndex invalid时,也需要遍历整个linked list,计算成本过大。
优解参考:
class MyLinkedList(object):
def __init__(self):
self.dummy_head = ListNode()
self.size = 0 # 更快捷的判断invalid index,并且可以追踪长度
def get(self, index):
# 注意:这里index只能小于self.size,因为index的范围
if index < 0 or index >= self.size:
return -1
ptr = self.dummy_head.next
for i in range(index):
ptr = ptr.next
return ptr.val
def addAtHead(self, val):
old_head = self.dummy_head.next
new_head = ListNode(val, old_head)
self.dummy_head.next = new_head
self.size += 1
def addAtTail(self, val):
last = ListNode(val)
ptr = self.dummy_head
while ptr.next is not None:
ptr = ptr.next
ptr.next = last
self.size += 1
def addAtIndex(self, index, val):
# 注意:这里index可以等于size,因为是在添加,可以添加在最后,即为新增的index=size
if index < 0 or self.size < index:
return
just_before = self.dummy_head
for i in range(index):
if just_before.next is not None:
just_before = just_before.next
just_before.next = ListNode(val, just_before.next)
self.size += 1
def deleteAtIndex(self, index):
# 注意:这里index只能小于self.size,因为只是在减少,index只能在原本的范围
if index < 0 or self.size <= index:
return
just_before = self.dummy_head
for i in range(index):
if just_before.next is not None:
just_before = just_before.next
just_before.next = just_before.next.next
self.size -= 1
反思及注意:
- 判断index的validity在get(), addAtIndex(), deleteAtIndex()中有所不同,addAtIndex()因为可能在最后多加一个,所以index可以等于current size,而其他两者还是只能在既有的范围内,必须小于size。
- 使用for loop正向循环搭配dummy head要简单清楚很多。
- 另有双链表的实现方法:size更有意义,取中点判断index从哪边 (head or tail) 遍历更近,就从哪边开始,节省算力。
Complexity
time: 涉及 index 的相关操作为 O(index), 其余为 O(1)
space: O(n)
206 反转链表
初始思路:
遍历中逐个将pointer方向反转,在将ptr的next指向pre时,暂时将真正的ptr.next存储在tmp中。在实现的时候对于循环条件是否涉及.next纠结太久。
没有意识到的是,这俨然也属于双指针方法。
def reverseList(self, head):
pre = None
ptr = head
while ptr is not None:
tmp = ptr.next
ptr.next = pre
pre = ptr
ptr = tmp
return pre
Complexity
time: O(n)
space: O(1)
代码随想录:
另一种方法: 递归
- 与双指针相同,从前向后递归
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
return self.reverse(head, None)
def reverse(self, cur: ListNode, pre: ListNode) -> ListNode:
if cur == None:
return pre
temp = cur.next
cur.next = pre
return self.reverse(temp, cur)
2.从后向前递归
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
# 边缘条件判断
if head is None:
return None
if head.next is None:
return head
# 递归调用,反转第二个节点开始往后的链表
last = reverseList(head.next)
# 反转头节点与第二个节点的指向
head.next.next = head
# 此时的head节点为尾节点,next需要指向None
head.next = None
return last
Complexity
time: O(n), 要递归处理链表的每个节点
space: O(n), 递归调用了 n 层栈空间