算法重温(十一): 回归基础数据结构之链表

1. 写在前面

这篇文章来复习链表,链表这里的操作一般是玩指针了,双指针,三指针齐头并进,快慢指针打破常规, 这里的解题关键无它,先画图,然后找对应的指针进行变换操作即可。这里很容易出bug的地方就是越界,或者指针忘了移动陷入死循环。下面先整理有关链表几个常规操作,然后整理具体的题目和代码,最后小总只整理题目,方便后面过思路用。下面开始。基础知识参考

关于链表, 我们需要知道的知识点:

  1. 常见操作: 元素的增, 删, 改, 查, 比较复杂的就是各种指针

  2. Linklist访问数组的时间复杂度O(n), 插入和删除时间复杂度O(1), 内存不连续

  3. 比较经典的一些题目考察的是链表的前插, 后插, 逆序, 翻转, 合并和移动等。

  4. 这里涉及到的一些思想:

    • 头结点思想, 声明一个头结点可以方便很多事,一般用在要返回新的链表的题目中,比如,给定两个排好序的链表,要求将它们整合在一起并排好序。又比如,将一个链表中的奇数和偶数按照原定的顺序分开后重新组合成一个新的链表,链表的头一半是奇数,后一半是偶数。
    • 头插法逆序思想
    • 双指针遍历, 可以做很多事情, 比如两两交换,逆序, 翻转等
    • 快慢指针的思想, 一般可以用到环里面
    • 递归, 链表这个地方的题目很容易递归起来
  5. 优缺点:

    1. 优点:链表能灵活地分配内存空间; 能在 O ( 1 ) O(1) O(1) 时间内删除或者添加元素,前提是该元素的前一个元素已知,当然也取决于是单链表还是双链表,在双链表中,如果已知该元素的后一个元素,同样可以在 O ( 1 ) O(1) O(1) 时间内删除或者添加该元素。
    2. 缺点:不像数组能通过下标迅速读取元素,每次都要从链表头开始一个一个读取;查询第 k k k 个元素需要 O ( k ) O(k) O(k) 时间。
  6. 应用场景: 如果要解决的问题里面需要很多快速查询,链表可能并不适合;如果遇到的问题中,数据的元素个数不确定,而且需要经常进行数据的添加和删除,那么链表会比较合适。而如果数据元素大小确定,删除插入的操作并不多,那么数组可能更适合。

  7. 建议:在解决链表的题目时,可以在纸上或者白板上画出节点之间的相互关系,然后画出修改的方法,既可以帮助你分析问题,又可以在面试的时候,帮助面试官清楚地看到你的思路。

关于链表, 首先应该掌握好几个常规: 查找,插入,删除的操作。

python里面自己定义链表节点要会,一般在LeetCode刷题上这块是不用自己写的,但是真实面试的时候,是不会给的,需要自己写。

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

下面是链表的常规操作,直接拿一个题来整理:

  • LeetCode707: 设计链表:这个题直接考察了链表的那几大基本操作, 查找节点返回索引,在首位置插入,中间插入, 尾部插入,删除节点等。

这个题目首先需要建立链表节点,上面的代码,然后初始化一个头结点

class MyLinkedList:
    def __init__(self):
        """
        Initialize your data structure here.
        """
        self.dummyHead = ListNode(-1)

链表的查找操作, 给定索引,返回对应索引的值

def get(self, index: int) -> int:
    """
    Get the value of the index-th node in the linked list. If the index is invalid, return -1.
    """
    if index < 0: return -1
    p = self.dummyHead.next
    cou = 0
    while p and cou < index:
        p = p.next
        cou += 1
    # self.printl()   打印当前链表结果
    return p.val if p else -1

链表的首位置插入:

def addAtHead(self, val: int) -> None:
    """
    Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list.
    """
    node = ListNode(val)
    node.next = self.dummyHead.next
    self.dummyHead.next = node
    # self.printl()   # 打印当前链表结果

链表的末尾位置插入:

def addAtTail(self, val: int) -> None:
    """
    Append a node of value val to the last element of the linked list.
    """
    node = ListNode(val)
    # 首先到尾部
    pre, cur = self.dummyHead, self.dummyHead.next
    while cur:
        pre, cur = cur, cur.next
    pre.next = node
    # self.printl()   # 打印当前链表结果

链表的指定位置插入:

def addAtIndex(self, index: int, val: int) -> None:
    """
    Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted.
    """
    node = ListNode(val)
    cou = 0
    pre, cur = self.dummyHead, self.dummyHead.next
    while cur and cou < index:
        pre, cur = cur, cur.next
        cou += 1
    # 插入
    node.next = cur
    pre.next = node
    # self.printl()   打印当前链表结果

链表的删除操作:

def deleteAtIndex(self, index: int) -> None:
    """
    Delete the index-th node in the linked list, if the index is valid.
    """
    cou = 0
    pre, cur = self.dummyHead, self.dummyHead.next
    while cur and cou < index:
        pre, cur = cur, cur.next
        cou += 1
    # 删除
    pre.next = cur.next if cur else None
    # self.printl()   打印当前链表结果

这里为了调试, 还写了个输出链表元素的函数, 因为第一次提交的时候,有个地方出错了,而不知道是哪一步的问题,所以写了个这样的函数,在每一步操作之后,输出一下,错误就瞬间显出原形了。

def printl(self):
    p = self.dummyHead.next
    while p:
        print(p.val, end=',')
        p = p.next
    print()

关于基础代码,还有两个操作,就是前向插入和尾部插入构建链表,这个在具体题目理解会遇见。前插会得到一个逆序的链表, 而尾插会得到一个正常的链表。

下面看具体题目。

2. 题目思路和代码梳理

2.1 链表元素的删除

  • LeetCode203: 移除链表元素: 链表操作最基础的题目, 考察链表的删除, 这时候两个指针pre, cur进行遍历,pre负责指向cur的前面一个节点,负责找到之后删除元素, 而cur负责找目标元素。 声明头结点会让操作变得更简单。
    在这里插入图片描述
  • LeetCode19: 删除链表的第K个节点: 这个需要用到快慢指针fast, slow, 让fast先走n步, 然后fast和slow齐头并进,当fast走到头,slow就是倒数第n个节点, 但我们这里是要删除倒数第n个节点,所以在并进的同时,还得有个记录slow的前面一个节点。还有就是为了让删除第一个和中间的统一起来,这里还需要一个头结点。
    在这里插入图片描述

2.2 链表的反转

  • LeetCode206: 反转链表: 这个有三个方式, 并且都非常基础, 我想在这里都整理下。

    1. 首先还是pre, cur这两个哥们,真的是非常好用,正向遍历的时候,顺便把指针调整了, python这里指针调整的代码可以使用元组解包更加简洁,但是写这种之前,最好是把原型写出来,否则容易出错:
      在这里插入图片描述
      其实根据原型写上面那一句代码非常简单, 原型里面等号左边的按顺序放左边,原型里面等号右边的按顺序放右边即可。 但原型要写对,这个关系着操纵顺序。
    2. 头插法重建数组的思想: 上面说过,头插法是可以建立逆序的链表。
      在这里插入图片描述
    3. 递归的思路:这个题可以采用尾递归的方式,对于当前的传进的节点,如果我获得了它后面节点的逆序,即如果我有了head->next的所有逆序了,是不是只需要把head连接到最后面就行了啊,基于这个思想,可以采用尾递归。

      在这里插入图片描述
  • LeetCode25: K个一组翻转链表: 这个题需要借助链表反转的思路,不过这里不是反转所有的,而是反转一组,所以先把上面链表反转的写成个函数,实现反转从[a, b)的节点。然后就可以进行反转了, 这里采用的是一次尾插K个节点重建链表的思路。 用两个指针k_start, k_endk_end先走 k k k步,然后反转[k_start, k_end)的节点, 把反转的节点尾插到新链表里面(尾插的方式)。 然后再继续执行上面的过程, 当发现后面的不够 k k k个了, 此时把剩下的直接尾插到新链表即可。这里的关键反转给定位置的链表节点和尾插思路
    在这里插入图片描述

  • LeetCode61: 旋转链表:之前玩过旋转数组,这里是选择链表了,思路其实是和数组那里一样的,先整体逆序,然后把前k个逆序,然后把k后面的逆序即可。当时链表这里的操作不能那么直接了,逆序的时候得上面的反转函数非常关键,依然是尾插思路+反转区间节点操作
    在这里插入图片描述

2.3 链表节点的交换

  • 两两交换链表中的节点:这个题目又是交换, 而交换类似于逆序, 只不过这个较简单,是相邻节点的逆序,双指针+尾插建链表的思路
    在这里插入图片描述

2.4 环形链表

  • 环形链表:链表中找环的问题最好用的工具就是快慢指针, 定义两个指针fast, slow, 每一次slow往下走一步, fast往下走两步, 如果在某个时刻它俩相遇了,那就说明有环。

    在这里插入图片描述
  • 环形链表II:这个题直接上思路了, 找环的入口, 就是先找到相遇点,之后, slow回到起始点,fast待在相遇点, 两者一步一步的往前走,当再次相遇的时候,就是环的入口。 为什么呢? 起始这就是个路程的计算问题, 假设一开始相遇的时候, slow走了K步, 那么fast就走了2K步(速度是slow的2倍), 那么就相当于slow从起始点走到相遇点的距离等于fast从相遇点,转一圈回到相遇点的距离, 而起始点的话, 无非是在相遇点前面,比如从相遇点后退个m步到起始点, 那么head离起始点的位置与相遇点转一圈到起始点的距离是一样的,都是K-m步。 所以两者先相遇,然后slow回到起点,再同步走,再相遇就是起始点。

在这里插入图片描述

当然,上面找环的这两个,用哈希表也能够非常easy的搞掉, 就拿第二个找环的入口来看, 只需要一个指针对链表进行遍历, 每遍历一个节点,就存入到set集合中。当发现遍历到某个节点的时候, 哈希表里面已经有这个节点了,说明这里就是环的入口,返回这个节点即可, 同样,这时候也能判断有环。
在这里插入图片描述

  • LeetCode876: 链表的中间节点: 快慢指针的经典题目, 两者同时从头走, fast每次走两步, slow每次走一步, 但fast走到头的时候, 此时slow处就是中间节点。
    在这里插入图片描述

2.5 链表合并

3. 小总

链表这边刷的题目比较少,所以拿出了一天的时间又复习了一遍,一些基础的思想非常重要,画图也非常重要, 链表这里我常用的标配:

  1. 头结点dummyHead
  2. pre, cur两个指针
  3. fast, slow两个指针
  4. 尾插 + 双指针, 尾插+头插

现在双指针的标配:
5. pre, cur两个指针(前后指针 —> 链表节点的插入,删除,反转)
6. fast, slow两个指针(快慢指针 —> 判断环, 找链表中点,找倒数第N个节点)
7. left, right两个指针(左右指针 — > 数组,字符串的相关题目)
8. win_start, win_end两个指针(滑动窗口 — > 子数组,子串的相关题目)

这块的重要思想:

  • 尾插 + 双指针
  • 尾插 + 反转区间链表节点
  • 尾插 + 头插

最后的题目梳理总结如下:

链表元素的删除:

链表的反转

链表节点的交换

环形链表

链表合并

猜你喜欢

转载自blog.csdn.net/wuzhongqiang/article/details/115266111