iOS必知必会链表常考算法题总结(附代码实现)

如何判断链表有环?

要判断一个链表是否有环,可以使用快慢指针的方法。

快慢指针是指两个指针,一个指针每次移动两步,称为快指针,另一个指针每次移动一步,称为慢指针。如果链表中存在环,那么快指针最终会在某个时刻追上慢指针,形成一个循环。

下面是使用快慢指针判断链表是否有环的算法:

  1. 定义两个指针,一个快指针 fast 和一个慢指针 slow,初始时都指向链表的头节点。

  2. 使用一个循环,每次循环中,快指针 fast 移动两步,慢指针 slow 移动一步。

  3. 在每次移动后,判断快指针 fast 和慢指针 slow 是否相等。如果相等,说明链表中存在环,返回 true

  4. 如果快指针 fast 移动到链表的末尾(即指向 null),则说明链表没有环,返回 false

下面是使用 Swift 语言实现的示例代码:

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
}

在上述代码中,使用两个指针 slowfast,它们都指向链表的头节点 head。在每次循环中,slow 指针移动一步,fast 指针移动两步。如果链表中存在环,那么 fast 指针最终会追上 slow 指针,返回 true。如果 fast 指针移动到链表末尾(即指向 null),则说明链表没有环,返回 false

可以通过调用 hasCycle 函数来判断给定链表是否有环。

如何判断两个单向链表是否相交,如果相交,求出交点

要判断两个单向链表是否相交,并找出它们的交点,可以使用以下方法:

  1. 遍历第一个链表,将每个节点的引用存储到一个集合(如哈希集合)中。

  2. 遍历第二个链表,对于每个节点,检查集合中是否存在相同的节点引用。如果存在,则说明两个链表相交,交点即为当前节点。

下面是使用 Swift 语言实现的示例代码:

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

以下是一个测试用例的示例:

swift 复制代码
// 创建链表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"。

在一个有环链表中,如何找出链表的入环点?

要找出一个有环链表的入环点,可以使用快慢指针的方法。

以下是找出链表入环点的算法步骤:

  1. 使用快慢指针,同时从链表的头部出发,快指针每次前进两步,慢指针每次前进一步,直到它们相遇在某个节点上。

  2. 当快慢指针相遇时,将其中任一个指针移回链表的头部,另一个指针保持在相遇的节点上。

  3. 然后,两个指针同时以每次前进一步的速度移动,直到它们再次相遇。相遇的节点即为链表的入环点。

下面是使用 Swift 语言实现的示例代码:

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

以下是一个测试用例的示例:

swift 复制代码
// 创建链表
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。

单链表反转

要将一个单链表进行反转,可以使用迭代或递归两种方法。

迭代法

迭代法是通过遍历链表,逐个修改节点的指针指向来实现反转。

以下是使用迭代法反转单链表的示例代码:

swift 复制代码
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

递归法

递归法是通过递归地反转子链表来实现整个链表的反转。

以下是使用递归法反转单链表的示例代码:

swift 复制代码
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,即整个链表反转后的头节点。

这是两种常用的方法来反转单链表。

合并两个有序链表

要合并两个有序链表,可以使用迭代或递归两种方法。

迭代法

迭代法是通过遍历两个有序链表的节点,逐个比较节点的值,并将较小值的节点链接到新链表中,最终得到合并后的有序链表。

以下是使用迭代法合并两个有序链表的示例代码:

swift 复制代码
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。同时,使用 p1p2 分别指向两个有序链表的当前比较节点。

然后,通过比较 p1p2 节点的值,将较小值的节点链接到新链表的尾部,并将相应指针向后移动。直到其中一个链表遍历完毕。

最后,如果还有剩余节点未处理完,将剩余节点链接到新链表的尾部。

最后,返回 dummyHead.next,即合并后的有序链表的头节点。

递归法

递归法是通过递归地合并两个有序链表的子问题,最终得到合并后的有序链表。

以下是使用递归法合并两个有序链表的示例代码:

swift 复制代码
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.nextl2 进行递归合并;如果 l2 的头节点较小,则将 l1l2.next 进行递归合并。

最后,返回递归合并后的结果。

这是两种常用的方法来合并两个有序链表。

相关推荐
绝无仅有10 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫11 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫11 小时前
Handler基本概念
面试
Gorway12 小时前
解析残差网络 (ResNet)
算法
Wect12 小时前
浏览器缓存机制
前端·面试·浏览器
拖拉斯旋风12 小时前
LeetCode 经典算法题解析:优先队列与广度优先搜索的巧妙应用
算法
Wect12 小时前
LeetCode 207. 课程表:两种解法(BFS+DFS)详细解析
前端·算法·typescript
掘金安东尼13 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼13 小时前
Next.js 企业级落地
前端·javascript·面试
掘金安东尼13 小时前
React 性能优化完全指南 2026
前端·javascript·面试