【数据结构与算法】最适合新手小白的教程——链表多种结构题目讲解(包你看懂!)

hello大家好~这篇文章接着之前的步伐,继续更新数据结构这一块的内容,对前面链表初步的文章进行补充,主要是对链表结构做出讲解。另外,本篇文章大多数为单链表,也就是说,只有一个next,大家注意分辨。

1、复制含有随机指针节点的链表

这也是一道比较经典的链表问题了,我给大家大致描述一下题目

有这样的一个链表结构:

class Node:
    def __init__(self, val: int):
        self.value = val  # 节点的值
        self.next = None  # 指向下一个节点的指针
        self.rand = None  # 随机指针,可能指向链表中的任意节点或None

rand指针是单链表节点结构中新增加的指针,rand可能指向链表中的任意一个节点,也可能指向null。给定一个由Node节点类型组成的无环单链表的头节点head,要求实现一个函数完成这个链表的复制,并返回复制的新链表的头节点。

给大家来张图,便于理解

这题实际上有两种解法,考虑了不同的空间复杂度,我先来说说爆破法

也就是使用哈希表的map(有不懂哈希表的朋友可以看看我之前的文章),用额外空间来操作。首先第一步,设置map,key值为老链表节点的地址假设是1,value的值为克隆出来的节点,设置为1'。节点设置完成后,开始考虑指针。先设置next指针。1的next指针指向2,那么对应的,重复步骤一,找出2的克隆节点2',让1'指向2',这里就完成了第一个克隆节点next指针的设置。再来看rand指针,假设1的rand指针指向3,那么,重复步骤一,找到3',然后重复步骤二,将1'的rand指针指向3',完成rand指针的设置。接下来的节点就重复上述步骤,即可完成新链表的复制,只额外使用了一个map空间。

图示:

代码示例:
 

class Node:
    def __init__(self, val: int):
        self.value = val  # 节点的值
        self.next = None  # 指向下一个节点的指针
        self.rand = None  # 随机指针,可能指向链表中的任意节点或None

def copy_random_list(head: Node) -> Node:
    if not head:
        return None  # 如果链表为空,直接返回None
    
    # 第一步:使用字典(哈希表)来保存原链表节点与新克隆节点的映射
    # key 是原节点,value 是对应的克隆节点
    node_map = {}
    
    # 创建一个指针,用于遍历原链表
    current = head
    
    # 复制每个节点,并将新节点存入 map 中,完成第一步
    while current:
        # 创建新节点,值与原节点相同
        node_map[current] = Node(current.value)
        current = current.next
    
    # 第二步:设置新节点的 next 和 rand 指针
    current = head
    
    while current:
        # 先设置 next 指针
        if current.next:
            node_map[current].next = node_map[current.next]
        
        # 设置 rand 指针
        if current.rand:
            node_map[current].rand = node_map[current.rand]
        
        # 移动到下一个节点
        current = current.next
    
    # 第三步:返回复制的链表的头节点
    return node_map[head]

#def copy_random_list(head: Node) -> Node:这里的node是代码注解的意思,告诉我们参数和返回值的类型,去掉也是对的

接着来看第二种,不用而外空间的做法。

这种方法的基本思想就是将复制的节点通过特定的位置关系,用老节点来确定新节点的位置,而每个节点的位置确定,就代表了每个指针也能确定下来。最后只要将新旧节点分离就OK。

首先,在原来每一个节点的后面添加其克隆节点,这样就可以通过老节点来找到新节点的位置,然后将rand指针,模仿老节点,把新的节点连起来,最后通过移动next指针,将新老节点分开。这样就可以做到不使用额外的空间,在原有的链表上面操作。

图示:

代码示例:


def copy_random_list(head: Node) -> Node:
    if not head:
        return None  # 如果链表为空,直接返回None

    # 第一步:在每个原节点后插入一个新节点
    current = head
    while current:
        # 创建新节点,值与当前节点相同
        new_node = Node(current.value)
        # 将新节点插入到当前节点后面
        new_node.next = current.next
        current.next = new_node
        # 移动到下一个原节点(跳过新插入的节点)
        current = new_node.next
    
    # 第二步:设置新节点的 rand 指针
    current = head
    while current:
        if current.rand:
            # 新节点的rand指向的是老节点rand指针所指节点的克隆节点
            current.next.rand = current.rand.next
        # 移动到下一个原节点(跳过新插入的节点)
        current = current.next.next

    # 第三步:将新旧链表分离
    old_list = head  # 原链表的指针
    new_list = head.next  # 新链表的指针
    new_head = head.next  # 保存新链表的头节点

    while old_list:
        # 将原链表的next指向原链表的下一个节点
        old_list.next = old_list.next.next
        # 将新链表的next指向新链表的下一个节点
        if new_list.next:
            new_list.next = new_list.next.next
        # 移动到下一个节点
        old_list = old_list.next
        new_list = new_list.next

    return new_head  # 返回新链表的头节点

#这个代码其实不难,不懂的多看两遍就会了,主要难在想到这个方法

2、两个链表相交的一系列问题

这是一个链表里面难度较大的题目了,没见过或者不知道方法的人,确实是很难想到思路

题目要求大概是这样的:给定两个可能有环也可能无环的单链表,头节点head1和head2。请实现一个函数,如果两个链表相交,请返回相交的第一个节点。如果不相交,返回null。

这题目的可能性确实是非常非常多,需要考虑到很多种情况,列如链表到底有无环,哪一个有环 ,是否相交。有很多种的结构,下面就给大家讲解一下。

首先先说一下,怎么判断链表是否有环。

这里同样也是两种方法,一种是哈希表,一种是快慢指针的方法。哈希表的做法就是建立一个set,遍历每一个节点,把节点记录在set里面, 每次新记录一个就查一次set,如果查到重复,就说明有环,如果遍历完都没查到,就说明没有环。

当然这个方法很慢,时间复杂度和空间复杂度都比较高。所以建议用快慢指针的方法。

这里先说明一点,如果一个链表有环,那他在遍历的时候一定停不下来,就是没办法走到null,如果停下来了,那他一定是在哪个节点多了一个next指针,就不是单链表结构了。

再来说说快慢指针。将两个指针从头节点开始移动,慢指针一次走一个,快指针一次走两个,如果最后他们在走完之前相遇,说明链表上有环。这里说明一点,因为快指针比慢指针快一倍,所以如果有环的话,那他们一定会在两圈之内相遇,这个是数学问题,这里就不证了,大家有兴趣的可以自己画图看看。

如果快慢指针相遇,那么在相遇处,快指针返回头节点,慢指针停留在原地,然后按照原本的步幅前进,他们下一次在环上相遇的节点一定是第一个入环节点(这个也是结论,记住就行)。

接着就是如何判断两个链表是否相交了

我们可以通过判断两个链表的结尾节点的地址是否一致,如果一致则一定相交了,反之则一定不相交。并且,通过在遍历链表的同时,用一个变量记录两个链表长度的差值,然后让长的链表先走差值步,再让两个指针一起同步幅出发(短的链表从头出发),最后一定会在第一个入环节点处相遇。

我们再来讨论一下其他情况。如果两个链表的入环节点都为空,那说明两个链表可相交,且没有环。如果一个链表入环节点为空,一个不为空,那说明两个链表不可能相交。如果两个入环节点都不为空,那么两个链表也可相交。我画图给大家看一下。

OK,思路差不多讲完了,接下来就来看看代码实现吧

class Node:
    def __init__(self, val: int):
        self.value = val
        self.next = None

# 判断链表是否有环,如果有返回第一个入环节点,否则返回None
def detect_cycle(head: Node) -> Node:
    if not head or not head.next:
        return None
    
    slow = head
    fast = head
    
    # 使用快慢指针判断链表是否有环
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        
        # 如果快慢指针相遇,说明链表有环
        if slow == fast:
            # 快指针回到头节点,慢指针不动,继续移动直到相遇
            fast = head
            while fast != slow:
                fast = fast.next
                slow = slow.next
            # 相遇点即为第一个入环节点
            return slow
    
    return None

# 判断两个链表是否相交
def get_intersection_node(head1: Node, head2: Node) -> Node:
    # 先判断两个链表是否有环,并得到入环节点
    cycle_node1 = detect_cycle(head1)
    cycle_node2 = detect_cycle(head2)
    
    # 情况1:如果一个链表有环,另一个链表无环,必然不相交
    if (cycle_node1 and not cycle_node2) or (not cycle_node1 and cycle_node2):
        return None
    
    # 情况2:如果两个链表都无环,判断是否相交
    if not cycle_node1 and not cycle_node2:
        return get_intersection_without_cycle(head1, head2)
    
    # 情况3:如果两个链表都有环
    return get_intersection_with_cycle(head1, head2, cycle_node1, cycle_node2)

# 情况2:无环的链表相交判断
def get_intersection_without_cycle(head1: Node, head2: Node) -> Node:
    # 遍历链表,获取两个链表的长度
    len1, len2 = 0, 0
    cur1, cur2 = head1, head2
    
    while cur1:
        len1 += 1
        cur1 = cur1.next
    
    while cur2:
        len2 += 1
        cur2 = cur2.next
    
    # 让较长的链表先走 |len1 - len2| 步
    cur1, cur2 = head1, head2
    if len1 > len2:
        for _ in range(len1 - len2):
            cur1 = cur1.next
    else:
        for _ in range(len2 - len1):
            cur2 = cur2.next
    
    # 同时移动两个指针,直到找到相交节点
    while cur1 and cur2:
        if cur1 == cur2:
            return cur1
        cur1 = cur1.next
        cur2 = cur2.next
    
    return None

# 情况3:有环的链表相交判断
def get_intersection_with_cycle(head1: Node, head2: Node, cycle_node1: Node, cycle_node2: Node) -> Node:
    # 如果两个链表的入环节点相同
    if cycle_node1 == cycle_node2:
        # 链表在入环前相交,按无环链表的方法处理
        return get_intersection_without_cycle_until_cycle(head1, head2, cycle_node1)
    
    # 如果两个链表的入环节点不同,判断它们是否在同一个环中
    cur = cycle_node1.next
    while cur != cycle_node1:
        if cur == cycle_node2:
            return cycle_node1  # 相交于环内的某个节点
        cur = cur.next
    
    return None  # 不相交

# 处理有环链表在入环前的相交情况
def get_intersection_without_cycle_until_cycle(head1: Node, head2: Node, cycle_node: Node) -> Node:
    len1, len2 = 0, 0
    cur1, cur2 = head1, head2
    
    # 计算入环前的长度
    while cur1 != cycle_node:
        len1 += 1
        cur1 = cur1.next
    
    while cur2 != cycle_node:
        len2 += 1
        cur2 = cur2.next
    
    # 让较长的链表先走 |len1 - len2| 步
    cur1, cur2 = head1, head2
    if len1 > len2:
        for _ in range(len1 - len2):
            cur1 = cur1.next
    else:
        for _ in range(len2 - len1):
            cur2 = cur2.next
    
    # 同时移动两个指针,直到找到相交节点
    while cur1 != cur2:
        cur1 = cur1.next
        cur2 = cur2.next
    
    return cur1

#前面讲的每一部分,代码都分成模块写出来了,根据情况调用不同模块就行。

3.一些小tip

本期文章内容也不少,但还挺好理解的我觉得。就是这些题目如果之前没看过的话,那可能其确实想不出来,所以还是建议大家多看几眼,万一以后面试或者笔试的时候遇到了呢

下一期可能更新机器学习的内容吧,论文终于是要写完了,但感觉开学之后事情是真的多,希望我们很快就能再见吧~

本期文章就到这里啦,祝大家学习生活顺利,拜拜~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值