算法--链表相关套路
链表
链表题一般常考
定义
单链表:一个节点 + 指向下一个节点的指针
头指针:第一个节点,head
尾指针:最后一个节点,tail
双向链表:单链表增加指向前继结点的指针
特点
增加、删除特别方便,复杂度:O(1)
查找、获得第k个元素,复杂度: O(n)
实现
参考之前的文章: 用最容易的方式学会单链表(Python实现)
class ListNode:
"""链表结点定义
"""
def __init__(self, data=0, next_node=None):
self.data = data
self.next = next_node
操作
查找:
def search_list(L, key):
cur = L.head
while cur and cur.data != key:
cur = cur.next
return True # 做需要的操作
插入一个节点(后插法):
def insert_after(node, new_node):
new_node.next = node.next
node.next = new_node
# 做需要的操作
return ""
删除节点:
def delete_after(node):
node.next = node.next.next
# 做需要的操作
return ""
链表问题常见套路:
通常来说,链表的问题从概念上讲很简单,更多时单纯的考察编码能力,而不是设计和解决算法。
套路一:设置虚拟头节点(也称哨兵节点)dummy head
。可以避免检查空链表,极大简化代码,减少错误的发生。可参见下面的题目。
套路二:双指针。单链表的快慢指针,要么设置两个指针指向不同的位置,要么设置两个指针走的步数不一样。
链表常考题目
1. 合并两个有序链表
* 例如:
* 输入:1->2->4, 1->3->4->5
* 输出:1->1->2->3->4->4->5
一个超级暴力解法的解法,把两个链表append在一起,然后排序。但是此方法没有利用到链表有序的特点。
更有效的方法是:遍历两个链表,总是选择拥有最小元素的节点,并一直进行
问: 如果其中一个链表已经走完,另一个怎么处理?
不能忘记把剩下的链表直接添加到末尾
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
dummy_head = cur = ListNode() # 虚拟头结点
while l1 and l2:
if l1.val < l2.val:
cur.next, l1 = l1, l1.next
else:
cur.next, l2 = l2, l2.next
cur = cur.next
# 添加剩下的链表,l1 或者 l2
cur.next = l1 or l2
return dummy_head.next
时间复杂度: O((n + m), n和m分别为两个链表的长度
空间复杂度: O(1) , 无额外空间
更多解法参考:合并两个有序链表
2. 反转链表
"""
# 反转一个单链表。
#
# 示例:
#
# 输入: 1->2->3->4->5->NULL
# 输出: 5->4->3->2->1->NULL
#
# 进阶:
# 你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
"""
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
cur = head
reverseHead = None
while cur:
temp = cur.next
cur.next = reverseHead
reverseHead = cur
cur = temp
return reverseHead
变体1:反转链表2
反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
说明:1 ≤ m ≤ n ≤ 链表长度。
示例:
输入: 1->2->3->4->5->NULL, m = 2, n = 4
输出: 1->4->3->2->5->NULL
解题思路:分离[m,n]的子链表,对子链表反转,然后分割放回去。缺点是需要两次处理子链表。
找到子链表[m, n],反转它们,然后连接m和n+1, 连接n和m-1。
class Solution:
def reverseBetween(self, head: ListNode, m: int, n: int) -> ListNode:
if m == n:
return head
dummy_head = pre = ListNode(0)
dummy_head.next = head
for _ in range(m - 1):
pre = pre.next
# Reverse sublist[m,n]
cur = pre.next
reverse = None
for _ in range(n - m):
temp = cur.next
cur.next, reverse, cur = (reverse, cur, temp)
pre.next.next = cur
pre.next = reverse
return dummy_head.next
变体2: k个一组反转链表
简单版:两两反转链表
3. 环形链表
- 空间换时间:哈希表法
这个问题有几种解决方案。 如果空间不是问题,最简单的方法是从头开始通过下一个字段探索节点,并将访问的节点存储在哈希表中-仅当我们访问哈希表中已经存在的节点时,存在一个循环。 如果不存在循环,则搜索在结尾处结束(通常通过将下一个字段设置为null来表示)。 此解决方案需要O(n)空间,其中n是列表中的节点数。
- 暴力解法
不使用额外存储空间且不修改列表的暴力方法是在两个循环中遍历该列表-外循环一遍遍遍历节点,而内循环从头开始并遍历为 到目前为止,由于外循环已经经历了许多节点。 如果外部循环访问的节点被访问两次,则检测到循环。 (如果外部循环遇到列表的末尾,则不存在循环。)此方法的复杂度为。
- 快慢指针
可以使这种想法在线性时间内工作-使用慢指针和快指针遍历列表。 在每次迭代中,将慢指针加1,将快指针加2。 当且仅当两个指针相遇时,列表才具有循环。
原因如下:如果快指针跳过了慢指针,则在下一步中,慢指针将等于快指针。
class Solution:
def hasCycle(self, head: ListNode) -> bool:
# hash_set = set()
# while head:
# if head in hash_set:
# return True
# hash_set.add(head)
# head = head.next
# return False
fast = slow = head
while slow and fast and fast.next:
slow = slow.next
fast = fast.next.next
if slow is fast:
return True
return False
扩展:找到环的入口节点