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 进行递归合并。

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

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

相关推荐
尘心cx3 分钟前
数据结构-栈
数据结构
学无止境\n7 分钟前
[C语言]指针和数组
c语言·数据结构·算法
黄俊懿11 分钟前
【深入理解SpringCloud微服务】手写实现各种限流算法——固定时间窗、滑动时间窗、令牌桶算法、漏桶算法
java·后端·算法·spring cloud·微服务·架构
新缸中之脑11 分钟前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法
夜雨翦春韭31 分钟前
【代码随想录Day29】贪心算法Part03
java·数据结构·算法·leetcode·贪心算法
Curry_Math1 小时前
Acwing 区间DP & 计数类DP
算法
Tisfy1 小时前
LeetCode 1928.规定时间内到达终点的最小花费:动态规划
算法·leetcode·动态规划·
ん贤1 小时前
贪心算法.
算法·贪心算法
cndes1 小时前
大数据算法的思维
大数据·算法·支持向量机