LeetCode 24.两两交换链表中的元素:
看到题目后的想法:
① 交换两个节点不是单纯改变两个节点的值,需要进行实际的节点交换
② 如果只用到待交换的两个节点cur1, cur2,那么这样交换完成之后cur1 的pre节点实际上还是指向cur1的,没有指向cur2。因此交换时一共需要三个指针pre,cur1 和 cur2。为了方便统一操作,采取添加虚拟头节点的方式实现。
③ 交换节点的方式如下:其中①必须在②前
交换完成后:
代码实现
- 迭代
使用虚拟头节点的迭代
需要注意的是先对pre.next和pre.next.next进行是否为空,再使cur1和cur2指向要交换的两个节点和交换节点。先指向的话,会出现求None.next的情况
# class ListNode(object):
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution(object):
def swapPairs(self, head):
"""
:type head: ListNode
:rtype: ListNode
"""
dummy_head = ListNode(next = head) # 创建虚拟头节点
pre = dummy_head
while pre.next != None and pre.next.next != None: # 先判断交换的节点是否存在,再进行交换
cur1, cur2 = pre.next, pre.next.next
# 交换
cur1.next = cur2.next
cur2.next = cur1
pre.next = cur2
# 更新pre
pre = cur1
return dummy_head.next
- 递归
不使用虚拟头节点的递归实现。
递归中的节点交换:首先让cur2.next指向cur1,再对以cnext为头节点的链表进行两两交换,得到交换后链表的新头节点cnext1, 从而cur1.next = cnext1
class Solution(object):
def swapPairs(self, head):
"""
:type head: ListNode
:rtype: ListNode
"""
if head == None or head.next == None:
return head
cur1, cur2 = head, head.next # 不带头节点的链表的交换
cnext = cur2.next # 下一次递归要交换的链表的第一个节点
# 交换cur1和cur2
cur2.next = cur1
cur1.next = self.swapPairs(cnext) # 将以cnext为头节点的链表进行两两交换
return cur2
实现过程中遇到的困难:
① 要先在纸上对节点交换进行模拟,分析清楚要交换的节点以及指针操作的先后顺序
② 在具体代码实现中,要先对交换的两个节点进行判断是否为空,再使得指针指向两个节点以及交换节点
LeetCode 19.删除链表的倒数第N个节点:
看到题目的想法
- 暴力求解
考虑到会出现删除第一个元素的情况,所以添加虚拟头节点dummy_head。
① 遍历链表,统计链表长度size。
② 从dummy_head开始遍历size - n 次,n从1开始,得到待删除节点的的pre。
③ 删除节点 - 双指针解法
① 采用快慢指针求解,因为题目要求是倒数第k个节点,即快慢指针之间的距离是相同的。因此采用初始位置不同,步长均为1的快慢指针。
② 考虑到会出现删除第一个节点的情况,对原链表添加虚拟头节点。
③ 快慢指针之间的距离:
采用删除最后一个节点测试是比较好的方法:
1)如果最后fast停在了尾节点的位置,slow和fast之间的距离为1,此时n为1。因此slow和fast之间的距离为n
2)如果最后fast停在了NULL,slow和fast之间的距离为2,此时n为1。从而slow和fast之间的距离为n + 1
代码部分(fast到尾节点的方式)
# class ListNode(object):
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution(object):
def removeNthFromEnd(self, head, n):
"""
:type head: ListNode
:type n: int
:rtype: ListNode
"""
dummy_head = ListNode(next = head)
slow = fast = dummy_head
for i in range(n):
fast = fast.next # 快指针比慢指针快n步
# 移动两个指针,直到快指针到尾节点
while fast.next != None:
slow = slow.next
fast = fast.next
# 通过更新倒数第(n - 1)个节点的next来删除第n个节点
slow.next = slow.next.next
return dummy_head.next
实现过程中遇到的困难:
对于快慢指针的距离,可以采用举例的方式来进行确定(因为一定是n ± k)。
假定n = 1,那么遍历完成后slow为倒数第2个节点,此时根据fast的位置确定slow和fast之间的距离
面试题 02.07.链表相交:
看到题目后的想法:
使用两个指针curA和curB遍历A链表和B链表。当curA==curB时,表明找到了相交的节点。问题出现再curA != curB时,两者怎么移动的问题。
假定A链表比B链表短,解决办法如下
- 从对齐的节点开始遍历
先让curB移动 len(B) - len(A)的距离,使得curA和curB对齐,从而遍历过程中当curA != curB时,curA和curB可以同时向后移动。
因为移动出现问题的原因在于A和B的长度不相同,那么移动curB后即在A和curB为第一个节点的链表中寻找相交节点,此时这两个链表的长度是相同的。
# class ListNode(object):
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution(object):
def getIntersectionNode(self, headA, headB):
"""
:type head1, head1: ListNode
:rtype: ListNode
"""
dis = self.getListLen(headA) - self.getListLen(headB) # 求距离差
# 通过移动较长的链表,使两个链表相等
if dis < 0:
curB = self.movHead(headB, abs(dis))
curA = headA
else:
curA = self.movHead(headA, dis)
curB = headB
while curA != curB:
curA = curA.next
curB = curB.next
return curA
def getListLen(self, head): # 求链表长度
cur, l = head, 0
while cur != None:
l += 1
cur = cur.next
return l
def movHead(self, head, dis): # 将链表节点移动指定长度
cur = head
for _ in range(dis):
cur = cur.next
return cur
- 均从第一个节点开始遍历
与上面的思路相似,不同在于思路1是先得到两个链表长度的差,再移动curB。而思路2在于,同时从A和B的头节点开始遍历,当curA(curB)遍历到链表A(B)末尾后,切换到另一个链表B(A)的头节点开始遍历。
这种思路的原理在于,第一遍遍历完成之后curA和curB已经相差了len(B) - len(A),从而在第二遍遍历时curA和curB变成了对齐的。(暂且先这么解释)
class Solution(object):
def getIntersectionNode(self, headA, headB):
"""
:type head1, head1: ListNode
:rtype: ListNode
"""
curA, curB = headA, headB
# 遍历两个链表直到指针相交
while curA != curB:
# 将指针向前移动一个节点
curA = curA.next if curA else headB # 只有一个到达末尾才切换链表
curB = curB.next if curB else headA
# 如果相交,curA为相交节点;否则为None
return curA
实现过程中遇到的困难:
对于第二种思路还需要更细致深入的理解
LeetCode 142.环形链表Ⅱ:
看到题目后的想法
- 暴力。
① 因为比较节点是否相等需要同时用到val和next,而新链表对已经遍历过的节点进行记录的顺序和原链表相同,因此新链表采用的方法是:创建虚拟头节点指向head,dsize为新链表的节点个数,加入节点的方式为dsize + 1
② 对原链表进行遍历,遍历过程中将节点加入新链表中,每次遍历到一个节点,都将其与新链表中的节点进行对比,查看在之前是否出现过。
③ 但是时间复杂度O(n^2), 空间复杂度O(1)
# 力扣上面可以过,时间很长
# 简单写一下代码的结构
# class ListNode(object):
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution(object):
def detectCycle(self, head):
dummy_head = ListNode(next = head)
dsize = 0
cur = head
while cur != None:
dcur = dummy_head
for i in range(dsize):
dcur = dcur.next
if dcur == cur:
return cur
dsize += 1
cur = cur.next
return None
不知道算不算优化方法:使用python中的集合来降低时间复杂度。每次遍历判断节点是否在集合中,不在将其加入集合中
class Solution(object):
def detectCycle(self, head):
"""
:type head: ListNode
:rtype: ListNode
"""
visited = set()
cur = head
while cur != None:
if cur in visited:
return cur
visited.add(cur)
cur = cur.next
return None
- 快慢指针
首先清楚我们要解决的问题:① 是否存在环 ② 找到环的入口
① 是否存在环: fast和slow指针从同一个位置开始,fast的步长为2,slow的步长为1。如果存在环,那么fast和slow会在环中相遇。
直观解释:
假定链表中存在环,那么slow进入环时,fast一定已经在环的某个位置。为了便于理解,我们假定slow的位置比fast快 k 步, k < len(环)。
在每个时间间隔中,fast相对于slow多走的步数为1,那么经过k个时间间隔,fast一定会赶上slow。(类似物理中的相对速度和相对位移)
② 怎么找到环的入口:入口与head到entry的距离有关
相遇时,slow走过的节点数为x + y;fast走过的节点数为x + y + n * (y + z)。其中n >= 1(fast至少经过了两次entry)。
而slow和fast的速度比为1:2,因此2 *(x + y) = x + y + n * (y + z),从而得到 x = (n - 1) * (y + z) + z。
直观上理解就是,如果有两个指针p1和p2分别从head和相遇的节点出发,那么p1走x步到达entry;p2也走x = (n - 1) * (y + z) + z,先走 n - 1个环的长度回到相遇的节点(周期),再走z步也到达entry。
所以p1和p2同时出发,步长为1,最终一定会相遇且相遇的节点为入口节点。从而找到了入口节点的位置
class Solution(object):
def detectCycle(self, head):
"""
:type head: ListNode
:rtype: ListNode
"""
fast = slow = head
while fast != None and fast.next != None:
fast = fast.next.next
slow = slow.next
# 如果存在环的话,那么fast和slow一定会相遇
if fast == slow:
# 将其中一个移动到链表开始位置
fast = head
while fast != slow: # 新的相遇位置为entry
fast = fast.next
slow = slow.next
return fast # 返回入口节点
# 不存在环,返回None
return None
实现过程中遇到的问题:
步长不相同的快慢指针在移动时,判断条件为fast != None和fast.next != None(第一个是fast为循环中判断的对象,因此需要保证fast存在;第二个保证fast.next存在,从而不会出现fast.next.next = None.next的情况)
学习收获:
① 对于步长不相同的快慢指针有了进一步的了解。自己实现代码的过程中明确了一些技术细节
② 对于比较复杂的指针/模拟,先在纸上手动模拟再写代码是更好的选择,可以降低代码出错的可能性。