iOS必知必会链表常考算法题总结(附代码实现)
如何判断链表有环?
要判断一个链表是否有环,可以使用快慢指针的方法。
快慢指针是指两个指针,一个指针每次移动两步,称为快指针,另一个指针每次移动一步,称为慢指针。如果链表中存在环,那么快指针最终会在某个时刻追上慢指针,形成一个循环。
下面是使用快慢指针判断链表是否有环的算法:
-
定义两个指针,一个快指针
fast
和一个慢指针slow
,初始时都指向链表的头节点。 -
使用一个循环,每次循环中,快指针
fast
移动两步,慢指针slow
移动一步。 -
在每次移动后,判断快指针
fast
和慢指针slow
是否相等。如果相等,说明链表中存在环,返回true
。 -
如果快指针
fast
移动到链表的末尾(即指向null
),则说明链表没有环,返回false
。
下面是使用 Swift 语言实现的示例代码:
class ListNode {
var val: Int
var next: ListNode?
init(_ val: Int) {
self.val = val
self.next = nil
}
}
func hasCycle(_ head: ListNode?) -> Bool {
var slow = head
var fast = head
while fast != nil && fast?.next != nil {
slow = slow?.next
fast = fast?.next?.next
if slow === fast {
return true
}
}
return false
}
在上述代码中,使用两个指针 slow
和 fast
,它们都指向链表的头节点 head
。在每次循环中,slow
指针移动一步,fast
指针移动两步。如果链表中存在环,那么 fast
指针最终会追上 slow
指针,返回 true
。如果 fast
指针移动到链表末尾(即指向 null
),则说明链表没有环,返回 false
。
可以通过调用 hasCycle
函数来判断给定链表是否有环。
如何判断两个单向链表是否相交,如果相交,求出交点
要判断两个单向链表是否相交,并找出它们的交点,可以使用以下方法:
-
遍历第一个链表,将每个节点的引用存储到一个集合(如哈希集合)中。
-
遍历第二个链表,对于每个节点,检查集合中是否存在相同的节点引用。如果存在,则说明两个链表相交,交点即为当前节点。
下面是使用 Swift 语言实现的示例代码:
class ListNode: Hashable {
var val: Int
var next: ListNode?
init(_ val: Int) {
self.val = val
self.next = nil
}
static func ==(lhs: ListNode, rhs: ListNode) -> Bool {
return lhs === rhs
}
func hash(into hasher: inout Hasher) {
hasher.combine(ObjectIdentifier(self).hashValue)
}
}
func getIntersectionNode(_ headA: ListNode?, _ headB: ListNode?) -> ListNode? {
var set = Set<ListNode>()
// 遍历第一个链表,将节点引用存储到集合中
var nodeA = headA
while nodeA != nil {
set.insert(nodeA!)
nodeA = nodeA?.next
}
// 遍历第二个链表,检查集合中是否存在相同的节点引用
var nodeB = headB
while nodeB != nil {
if set.contains(nodeB!) {
return nodeB
}
nodeB = nodeB?.next
}
// 如果没有相交点,返回空
return nil
}
使用以上代码,可以通过调用 getIntersectionNode
函数来判断两个链表是否相交,并找出它们的交点。如果相交,函数将返回交点所在的节点;如果不相交,函数将返回 nil
。
以下是一个测试用例的示例:
// 创建链表1
let node1 = ListNode(1)
let node2 = ListNode(2)
let node3 = ListNode(3)
node1.next = node2
node2.next = node3
// 创建链表2
let node4 = ListNode(4)
let node5 = ListNode(5)
node4.next = node5
node5.next = node2 // 链表2与链表1相交于节点2
// 测试
if let intersectionNode = getIntersectionNode(node1, node4) {
print("两个链表相交,交点的值为:\(intersectionNode.val)") // 输出:两个链表相交,交点的值为:2
} else {
print("两个链表不相交") // 不会执行到这里
}
在这个测试用例中,我们创建了两个链表,链表1的尾节点与链表2的节点2相交。调用 getIntersectionNode
函数来判断它们是否相交,并找出交点。预期输出为 "两个链表相交,交点的值为:2"。
在一个有环链表中,如何找出链表的入环点?
要找出一个有环链表的入环点,可以使用快慢指针的方法。
以下是找出链表入环点的算法步骤:
-
使用快慢指针,同时从链表的头部出发,快指针每次前进两步,慢指针每次前进一步,直到它们相遇在某个节点上。
-
当快慢指针相遇时,将其中任一个指针移回链表的头部,另一个指针保持在相遇的节点上。
-
然后,两个指针同时以每次前进一步的速度移动,直到它们再次相遇。相遇的节点即为链表的入环点。
下面是使用 Swift 语言实现的示例代码:
class ListNode {
var val: Int
var next: ListNode?
init(_ val: Int) {
self.val = val
self.next = nil
}
}
func detectCycle(_ head: ListNode?) -> ListNode? {
var slow = head
var fast = head
var hasCycle = false
// 判断是否存在环
while fast != nil && fast?.next != nil {
slow = slow?.next
fast = fast?.next?.next
if slow === fast {
hasCycle = true
break
}
}
// 如果存在环,则找到入环点
if hasCycle {
slow = head
while slow !== fast {
slow = slow?.next
fast = fast?.next
}
return slow
}
// 如果不存在环,则返回 nil
return nil
}
使用以上代码,可以通过调用 detectCycle
函数来找出一个有环链表的入环点。如果链表存在环,则返回入环点的节点;如果链表不存在环,则返回 nil
。
以下是一个测试用例的示例:
// 创建链表
let node1 = ListNode(1)
let node2 = ListNode(2)
let node3 = ListNode(3)
let node4 = ListNode(4)
let node5 = ListNode(5)
// 构造有环的链表,环的入口是节点2
node1.next = node2
node2.next = node3
node3.next = node4
node4.next = node5
node5.next = node2
// 测试
if let cycleStartNode = detectCycle(node1) {
print("链表的入环点为:\(cycleStartNode.val)") // 输出:链表的入环点为:2
} else {
print("链表没有环") // 不会执行到这里
}
在这个测试用例中,我们创建了一个有环的链表,环的入口是节点2。调用 detectCycle
函数来找出链表的入环点,并输出其值为2。
单链表反转
要将一个单链表进行反转,可以使用迭代或递归两种方法。
迭代法
迭代法是通过遍历链表,逐个修改节点的指针指向来实现反转。
以下是使用迭代法反转单链表的示例代码:
class ListNode {
var val: Int
var next: ListNode?
init(_ val: Int) {
self.val = val
self.next = nil
}
}
func reverseList(_ head: ListNode?) -> ListNode? {
var prev: ListNode? = nil
var curr = head
while curr != nil {
let nextTemp = curr?.next
curr?.next = prev
prev = curr
curr = nextTemp
}
return prev
}
在上述代码中,我们使用 prev
变量来保存已反转部分的头节点,初始时为 nil
。然后,使用 curr
指针遍历链表,将每个节点的 next
指针指向其前一个节点 prev
。最后,返回反转后的链表的头节点 prev
。
递归法
递归法是通过递归地反转子链表来实现整个链表的反转。
以下是使用递归法反转单链表的示例代码:
class ListNode {
var val: Int
var next: ListNode?
init(_ val: Int) {
self.val = val
self.next = nil
}
}
func reverseList(_ head: ListNode?) -> ListNode? {
if head == nil || head?.next == nil {
return head
}
let reversedHead = reverseList(head?.next)
head?.next?.next = head
head?.next = nil
return reversedHead
}
在上述代码中,我们首先判断链表是否为空或只有一个节点,如果是,则直接返回链表头节点。然后,通过递归调用 reverseList
函数来反转剩余的子链表,并将返回的反转后的子链表的头节点保存在 reversedHead
变量中。接下来,将当前头节点的下一个节点的 next
指针指向当前头节点,然后将当前头节点的 next
指针置为 nil
,完成当前节点的反转。最后,返回 reversedHead
,即整个链表反转后的头节点。
这是两种常用的方法来反转单链表。
合并两个有序链表
要合并两个有序链表,可以使用迭代或递归两种方法。
迭代法
迭代法是通过遍历两个有序链表的节点,逐个比较节点的值,并将较小值的节点链接到新链表中,最终得到合并后的有序链表。
以下是使用迭代法合并两个有序链表的示例代码:
class ListNode {
var val: Int
var next: ListNode?
init(_ val: Int) {
self.val = val
self.next = nil
}
}
func mergeTwoLists(_ l1: ListNode?, _ l2: ListNode?) -> ListNode? {
let dummyHead = ListNode(0)
var tail: ListNode? = dummyHead
var p1 = l1
var p2 = l2
while let node1 = p1, let node2 = p2 {
if node1.val < node2.val {
tail?.next = node1
p1 = node1.next
} else {
tail?.next = node2
p2 = node2.next
}
tail = tail?.next
}
if let remainingNodes = p1 ?? p2 {
tail?.next = remainingNodes
}
return dummyHead.next
}
在上述代码中,我们首先创建一个虚拟头节点 dummyHead
,用于保存合并后的有序链表的头节点。然后,使用 tail
指针来追踪合并后链表的尾部节点,初始时指向 dummyHead
。同时,使用 p1
和 p2
分别指向两个有序链表的当前比较节点。
然后,通过比较 p1
和 p2
节点的值,将较小值的节点链接到新链表的尾部,并将相应指针向后移动。直到其中一个链表遍历完毕。
最后,如果还有剩余节点未处理完,将剩余节点链接到新链表的尾部。
最后,返回 dummyHead.next
,即合并后的有序链表的头节点。
递归法
递归法是通过递归地合并两个有序链表的子问题,最终得到合并后的有序链表。
以下是使用递归法合并两个有序链表的示例代码:
class ListNode {
var val: Int
var next: ListNode?
init(_ val: Int) {
self.val = val
self.next = nil
}
}
func mergeTwoLists(_ l1: ListNode?, _ l2: ListNode?) -> ListNode? {
guard let node1 = l1 else {
return l2
}
guard let node2 = l2 else {
return l1
}
if node1.val < node2.val {
node1.next = mergeTwoLists(node1.next, node2)
return node1
} else {
node2.next = mergeTwoLists(node1, node2.next)
return node2
}
}
在上述代码中,我们首先处理特殊情况,即其中一个链表为空,直接返回另一个链表。
然后,比较两个链表的头节点的值,将较小值的节点作为合并后的链表的头节点,并递归地合并剩余部分。如果 l1
的头节点较小,则将 l1.next
和 l2
进行递归合并;如果 l2
的头节点较小,则将 l1
和 l2.next
进行递归合并。
最后,返回递归合并后的结果。
这是两种常用的方法来合并两个有序链表。
转载自:https://juejin.cn/post/7315224380589916214