算法(2):链表

  终于开始提笔写算法(2)了,我先为自己鼓个小手,然后决定,这一波写个常见的数据结构,链表,还望各位小伙伴多多支持,有号的就帮忙捧个人场,点个赞留个言啥的,你们的关注就是我继续写下去的动力~
  上一篇递归写了三天,这一波链表,估计时间也不会短,,,


  • 目录:
    算法:附录
    算法(1):递归
    算法(2):链表
    算法(3):数组
    算法(4):字符串
    算法(5):二叉树
    算法(6):二叉查找树
    算法(7):队列和堆栈(附赠BFS和DFS)
    算法(8):动态规划
    算法(9):哈希表
    算法(10):排序
    算法(11):回溯法
    算法(12):位操作

  数组我想大家都很熟悉,链表和数组都属于线性数据结构,但是长相差别很大,具体长啥样请看下图:


算法(2):链表_第1张图片
单向链表

  链表的每一个元素存储空间不是相邻的,而数组则是存放在同一块相邻的空间下。如上图所示,链表每一个元素(后面会称之为节点)分两部分,一部分存放值(value),另一部分存放下一个元素的地址(reference field)。
  上图为单向链表(singly linked list),还有一种链表,叫双向链表(doubly linked list),其结构如下:


双向链表

单向链表(Singly Linked List):

  首先,我先给大家看一段用 python 定义链表的代码,很简单,简单到你可能只会瞟一眼便略过:

class ListNode:   #定义节点类
    def __init__(self, x):
        self.val = x
        self.next = None

  在大部分情况里,我们一般使用头节点(head node)来表示整个链表。也正是由于链表的这个特性,我们不可能在常数时间里访问链表当中的某个随机元素,因为我们要从头节点遍历下来才行,所以查找元素所用时间复杂度为。虽然查找的时候和数组相比(数组时间复杂度为)弱爆了,但是链表自有其可取之处,比如当你插入和删除元素时,你会看到,链表正在将数组按在地上摩擦。

插入元素:

  插入操作主要分为三步:首先,构建好一个节点,其次,将当前节点指向下一个节点,最后,将先前的节点指向新构建的节点。


算法(2):链表_第2张图片
1.png
算法(2):链表_第3张图片
2.png
算法(2):链表_第4张图片
3.png

  插入一个元素时间复杂度只需要,数组则是,毕竟他要让排在后面的元素一个个给新元素挪位置。但是这里其实有一个问题,一般情况下,我们要找到这个要插入的位置再执行插入操作,而该查找操作,时间复杂度却是。比如,我给一个要求,让你在头节点新增一个元素,那很简单,直接新建节点,指向头节点,再将该新节点设置为头节点即可,很明显,这是。我看你完成的这么轻松,就使坏了,让你在链表末端插入一个节点。这个时候,你只能从头节点开始,不停的 next 到下一节点,在经过了 时间后,你终于来到了链表的尾端,感叹道:“今天晚上就要去看惊奇队长电影了!”
  以免大家看了我上面的分析陷入迷糊,这里再说一下,一般我们说插入的时候,不考虑那么多,是默认已经给出插入位置了的,所以记住这句话,单向链表插入一个元素的时间复杂度为。

删除操作:

  删除的时候只需要一步,被删节点的前一个节点,指向被删节点的后一个节点即可~


1.png
算法(2):链表_第5张图片
2.png

  那么删除一个元素的时间复杂度是多少呢?。假设我们现在知道被删节点 (cur node),那么找下一个节点(next node)很简单,但是如何找它之前的那个节点(prev node)呢?答案是从头遍历。。。。这样子,虽然删除操作简单,但是为了找到 pred node,平均时间复杂度就又到了级别。
  所以再记住这句话,单向链表删除一个元素的时间复杂度为。


  当然还是老样子,没有 coding 的算法讲解都是耍流氓!我虽然是个流氓,例题是肯定得放上它三五个,才像回事。
  那么,接下来便是代码时刻(coding time)!


问题1:判断一个链表是否有环
  何为环?首尾相连即为环。请看下图:

算法(2):链表_第6张图片

  调皮的同学可能会说,你这玩意儿不是首尾相连, 我一个大嘴巴子......,这时我会温柔的告诉他,上图链表不是环结构(多了个小尾巴),但是它有环结构。好,闲话少说,接下来让你们见识一个厉害的家伙,它叫 双指针技术(two-pointer technique),常用来解决快慢指针问题以及滑窗问题。大家可以通过这道题来跟它搞好关系~
  首先,我跟大家讲个龟兔赛跑的故事。话说有一天,小乌龟和小白兔在一条笔直的公路上赛跑, 结局是小白兔被车撞死了,那么小白兔会先到达目的地;如果他们两个在操场绕圈跑步, 结局是小白兔被操场散步的大爷带回家吃了,那么小白兔会超小乌龟一圈又一圈。
  双指针,便是如此,有两个指针,一个运动速度快,一个运动速度慢,如果链表无环,则快指针先到达链表尾端;如果链表有环,则两个指针必会碰头,快指针超慢指针一圈或N圈。在一般情况下,我们设定慢指针一次一步, slow_pointer = slow_pointer.next ;快指针一次两步, fast_pointer = fast_pointer.next.next
  那么,了解了双指针技术后,习题走起!

输入:链表
输出:True(有环) or False(无环)
代码如下:

class ListNode(object):
    def __init__(self, x):
        self.val = x
        self.next = None

def hasCycle(head):
    """
    :type head: ListNode
    :rtype: bool
    """
    if head == None: return False

    fast = slow = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            return True
    else: return False

if __name__ == '__main__':
    head1 = node1 = ListNode(1)  # 无环链表头节点
    head2 = node2 = ListNode(1)  # 有环链表头节点
    for i in range(2,5):
        node1.next = ListNode(i)
        node2.next = ListNode(i)
        node1 = node1.next
        node2 = node2.next

    node2.next = head2.next   # 加个环
    
    ans1 = hasCycle(head1)
    ans2 = hasCycle(head2)
    print(ans1,ans2)


问题2:该问题为问题1的延申,如果链表有环,则返回环开始的那个节点。
例子:
输入:1->2->3->4(->2>3->4......)
输出: ListNode(2)
解决思路:
  该问题我们仍继续用双指针的方法来做,判断有没有环很简单,但是如何找到环开始的节点呢(突然想到了万恶之源这个词...)?操作很简单,当快慢两指针相遇时,再让一个指针从头节点开始,和慢指针一同以相同步伐运动(这是快指针已经没用了),当新指针和慢指针相遇时,该位置便为所求之点!
  那么到底是为啥呢?各位看官请准备好瓜子花生小板凳,听我慢慢道来:
  当快慢指针碰头时,咱假设慢指针走了K步,那么快指针便走了2K步。此时快指针比慢指针多运动n个环的距离,我们假设一个环有节点s个,那么可以得到式子:,即。啥意思呢,也就是说,这个时候,慢指针走的步数刚好也是该环的整数倍!这个时候,我们搞一个和慢指针一样速度的指针(暂且称之为小白兔把)从头节点开始走,最终小白兔在走了K步时,和慢指针一定会又在该地相遇(加上之前的,此时慢指针共走了2K步)。但这不是它们首次相遇之地,它们因为速度一样,所以是在环开始的地方相遇,然后肩并肩手挽手的走到了第K个节点这里~(不信你让它们在这个第K个节点开始原路返回,你发现它们先是一起倒着走,在某个节点分道扬镳,小白兔继续朝头节点方向倒着走,而慢指针则回到了该链表尾部,继续它的绕圈圈)。
  so,got it?懂了的话就来看代码把~

class ListNode(object):
    def __init__(self, x):
        self.val = x
        self.next = None


def detectCycle( head):
    """
    :type head: ListNode
    :rtype: ListNode
    """
    fast = slow = head
    while fast and fast.next:
        fast = fast.next.next
        slow = slow.next
        if fast == slow:
            break
    else:
        return None

    while head != slow:
        head = head.next
        slow = slow.next

    return slow

if __name__ == '__main__':
    head1 = node1 = ListNode(1)  # 无环链表头节点
    head2 = node2 = ListNode(1)  # 有环链表头节点
    for i in range(2,5):
        node1.next = ListNode(i)
        node2.next = ListNode(i)
        node1 = node1.next
        node2 = node2.next

    node2.next = head2.next   # 加个环

    ans1 = detectCycle(head1)
    ans2 = detectCycle(head2)
    print(ans1)
    print(ans2.val)


问题3:这个问题还需要我们的双指针大侠出手。如下图所示,给两个连在一起的链表,我们需要找到这个连接点,即c1。

算法(2):链表_第7张图片
问题三

  看到这个问题,客官老爷不知道有没有想法? 啥?你说你觉得红烧兔头更好吃?我问的不是你对兔子的想法...... 其实很简单,我们继续让指针绕圈圈即可。不过两个指针此时不分快慢,步伐一致,一个沿着A链表绕圈,另一个沿着B绕。当他们两个指针最终首次相遇,那么相遇节点一定是c1(嗑瓜子的你吐了口中瓜子皮,说不一定?那好,假设在c2处首次相遇,那么他们因为步伐一致,所以在c1处一定也已经见面,故在c2处首次相遇的假设不成立)。
  但是,它们会绕多少圈才能首次相遇呢?有可能是很多圈。那么,有没有其他优化方法呢?(这时你又要跃跃欲试,想要发言,我赶快打断了你,继续讲下去)当然有!方法便是指针a在遍历完A链表之后,不从a1节点继续绕圈,而是跳到B链表头部b1,作为新的征程!(指针b亦然,第二圈从a1开始)这时你会发现,在第二次遍历时,他们必定会在c1处相遇。这时,每个指针都刚好把A和B所有不重复的节点走了一遍。
  接下来,上代码!(功夫不负有心人,你看到机会终于来了,憋了半天的话脱口而出:“瓜子吃完了,再给我端一盘。”)

class ListNode(object):
    def __init__(self, x):
        self.val = x
        self.next = None

def printListNode(node):   #辅助函数,打印链表
    while node:
        print(node.val, end=' ')
        if node.next:
            print('->', end=' ')
        node = node.next
    print()

def getIntersectionNode( headA, headB):
    """
    :type headA, headB: ListNode
    :rtype: ListNode
    """
    pa = headA
    pb = headB
    while pa != pb:
        pa = pa.next if pa != None else headB
        pb = pb.next if pb != None else headA

    return pa

if __name__ == '__main__':
    head1 = node1 = ListNode(1)
    head2 = node2 = ListNode(1)

    node1.next = ListNode(2);node1 = node1.next
    node2.next = ListNode(2);node2 = node2.next
    node2.next = ListNode(3);node2 = node2.next
    node1.next = node2.next = ListNode(5);node1 = node1.next;node2 = node2.next
    for i in range(10,12):
        node1.next = ListNode(i)
        node1 = node1.next
        # node2 = node2.next

    printListNode(head1)
    printListNode(head2)
    ans1 = getIntersectionNode(head1,head2)

    print(ans1.val)


问题4:翻转链表。上一章我们翻转了字符串,这一章,链表羡慕的紧,也想翻一翻。那咱就给它个机会~
  翻转列表有多种方法,遍历列表,把每个节点装入列表当中,然后再反向拼接这些节点便可轻松做到。但是这里的空间复杂度应该是(如果小编没理解错的话,设链表长度为n,装入第一个节点时相当于装了整个链表,占用空间n,第二个n-1......最后1,共占用个空间,但是不确定这些中间节点是否是共享的,如果共享,则为复杂度),但这里说一个复杂度没那么高的,空间复杂度为、时间复杂度为的方法,供大家参考。
  如下图所示,我们的做法是从头节点开始,依次取出头节点后面的节点,放到头节点位置。第一步,先将头节点23放到头节点位置(其实就是不做任何操作......),第二步,将节点6扣下来,然后连接23与15,最后将6放在头节点位置;第三步,将15扣下来,然后连接23与None,最后将15放到头节点位置。就这么循环到链表尾部,就结束啦~

1.png

2.png
3.png

  当然,代码还是得拉出来溜溜才行~

class ListNode(object):
    def __init__(self, x):
        self.val = x
        self.next = None

def printListNode(node):   #辅助函数,打印链表
    while node:
        print(node.val, end=' ')
        if node.next:
            print('->', end=' ')
        node = node.next
    print()

def reverseList(head):
    """
    :type head: ListNode
    :rtype: ListNode
    """
    if head == None: return
    node = head
    while node.next:
        cur = node.next   # cur 即为被抠出来的节点 
        node.next = node.next.next   # 将 node 的下一个节点指向更后面的那个
        cur.next = head   # 将 cur 跟链表拼接起来
        head = cur   # 保存头节点信息

    return head

if __name__ == '__main__':
    head1 = node1 = ListNode(1)

    for i in range(2,6):
        node1.next = ListNode(i)
        node1 = node1.next

    printListNode(head1)
    ans = reverseList(head1)
    printListNode(ans)

问题5:奇偶链表
输入一个链表,将第奇数个节点取出放在一起,第偶数个节点取出放在一起,然后偶数组接在奇数组后面,返回该合成后的链表。
例子:
输入:1 -> 2 -> 4 -> 6 -> 8
输出:1 -> 4 -> 8 -> 2 -> 6

class ListNode(object):
    def __init__(self, x):
        self.val = x
        self.next = None

def printListNode(node):   #辅助函数,打印链表
    while node:
        print(node.val, end=' ')
        if node.next:
            print('->', end=' ')
        node = node.next
    print()


def oddEvenList(head: ListNode) -> ListNode:
    if head:
        odd = head
        even = evenHead = odd.next
        while even and even.next:
            odd.next = odd.next.next;
            odd = odd.next;
            even.next = even.next.next;
            even = even.next;
            # odd.next = odd = even.next   #也可以试试把上面四句换成下面两句,结果是一样的呦~
            # even.next = even = odd.next

        odd.next = evenHead
        return head

if __name__ == '__main__':
    head1 = node1 = ListNode(1)

    for i in range(2,10,2):
        node1.next = ListNode(i)
        node1 = node1.next

    printListNode(head1)
    ans = oddEvenList(head1)
    printListNode(ans)


小贴士:

  1. 在你调用一个节点的 next 操作时,要首先确认该节点是不是空节点。即if node: node = node.next,或者if node and node.next: node = node.next.next
  2. 要考虑循环的边界条件,不要让调用陷入死循环当中。诸如一个有环链表,当你遍历打印该链表的节点时,要写好跳出循环的条件。
  3. 可以尝试同时使用多个指针(pointer)来追踪节点。
  4. 很多场景下,可能需要追踪当前节点的前一个节点(previous node)。
  5. 有时候指针来回交换几次之后比较迷,可以自己准备几个链表样本来测试,查看指针变换后的结果。(如上面程序中的ListNode()函数以及printListNode()函数,都可以方便我进行测试)

(还没写完,大家可以再准备点瓜子,我才刚刚说完单向链表,,,)


双向链表(Doubly Linked List):

  大家还记得单向链表的定义吗?你说我没讲?我一套组合升龙拳......跳个舞给你们看......双向链表跟它相同,但是每个节点都多了一部分,一个储存前一个节点位置的空间(reference field)。具体案例如下图所示:

双向链表

  具体代码定义如下所示:

class ListNode(object):
    def __init__(self, x):
        self.val = x
        self.next = None
        self.prev = None   # 其实没啥,就多了行这(请不要质疑我的注释水平)

  双向和单向链表在遍历、查找、添加节点时,时空复杂度都是一样的,唯独删除节点,时间复杂度由 降低至 (因为可以直接找到 prev node)。内容较为简单,后面不会细说,大家如果有什么不懂的,你自己解决啊!, 热烈欢迎留言讨论~ 小编可是极其期待有人给我留言的(苦逼小编写了快二十篇文章,至今还未打破 0 评论惨案)~

添加节点
  双向链表添加节点操作和单向链表很相似,只不过原来是断开一个连接、新增两个连接,变成了断开两个连接、新增四个连接罢了~ 来,让小编我找个例子逗大伙乐乐~
  下图所示,共执行两步操作:Step1.新增节点9,将该节点的 next 和 prev分别连上节点15 和节点6。Step2.断开节点6和节点15的连接,让这两个节点分别与节点9相恋。

算法(2):链表_第8张图片
1.png
算法(2):链表_第9张图片
2.png

删除节点
  那啥,删除操作非常简答,cur.prev.next = cur.next; cur.next.prev = cur.prev,只需要更改两个连接即可~

算法(2):链表_第10张图片
1.png


*例题:展平多级双向链表
  该双向链表多了一个功能,叫子节点(child node)。而本题的目标便是让子链表插入主链表,将链表平铺。如下例子所示,节点3和8都有自己的子节点,而我们要做的就是将节点11、12插入节点8和9之间,再将7、8、11、12、9、10插入节点2和3之间,得到一个展平的双向链表。

输入:
 1---2---3---4---5---6--NULL
         |
         7---8---9---10--NULL
             |
             11--12--NULL

输出:
1-2-3-7-8-11-12-9-10-4-5-6-NULL

  实现代码如下(这里使用了深度优先搜素思想(DFS),我会在后面的算法系列讲到该知识点,如果我不坑的话,后面DFS、BFS、回溯、动规等系列算法应该都会给各位看官一一奉上):

class ListNode(object):
    def __init__(self, x):
        self.val = x
        self.next = None
        self.prev = None
        self.child = None

def printListNode(node):   #辅助函数,打印链表
    while node:
        print(node.val, end=' <-> ')
        if node.child:
            print('(', end=' ')
            tmp = node.child
            while tmp:
                print(tmp.val, end=' <-> ')
                tmp = tmp.next
            print('None) <->', end=' ')
        node = node.next
    print('None')

def flatten(head):
    def dfs(node):
        if not node:
            return None, None
        trav = node
        head = node
        tail = node
        while trav:
            if trav.child:
                nex = trav.next
                insert, last = dfs(trav.child)
                trav.next = insert
                insert.prev = trav
                if nex:
                    last.next = nex
                    nex.prev = last
                trav.child = None
                trav = last
            tail = trav
            trav = trav.next
        return head, tail

    return dfs(head)

if __name__ == '__main__':
    head1 = node = ListNode(1)

    for i in range(5,15,2):
        node.next = ListNode(i)
        node.next.prev = node
        if i % 3 == 0:
            node.child = ListNode(i + 0.1)
            node.child.prev = node
            node.child.next = ListNode(i + 0.2)
            node.child.next.prev = node.child
        node = node.next

    printListNode(head1)
    head,tail = flatten(head1)
    printListNode(head)

附录:在LeetCode上找到了一张好图,贴在这里,侵删。

算法(2):链表_第11张图片
时间复杂度对比图

从上图可以看出,如果你频繁添加或者删除节点,那么可以使用链表结构;如果你倾向于通过索引(index)查找元素,那么数组效果会更好~


震惊!二十来岁妙龄小伙竟然在电脑前做那种事!
小伙竟然在电脑前不停求赞求赞求赞!

你可能感兴趣的:(算法(2):链表)