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

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负责找目标元素。 声明头结点会让操作变得更简单。
    在这里插入图片描述
    该题剑指offer里面是有一个 O ( 1 ) O(1) O(1)的时间复杂度要求的, 这个操作在王道上的数据结构上也见到过,是针对那种指针指向的节点来说的,传入的是一个指针, 我们要在链表中删除指针指向的那个节点。 对于python来讲,应该是行不通的。 下面说一下如果传入的是个指针的话, 这个题怎么在 O ( 1 ) O(1) O(1)的时间复杂度下搞定。来自剑指offer

    当我们想删除一个节点的时候,并不一定要删除这个节点本身。可以先把下一个节点的内容复制出来覆盖被删除点的内容,然后把下一个节点删除

  • LeetCode19: 删除链表的第K个节点: 这个需要用到快慢指针fast, slow, 让fast先走n步, 然后fast和slow齐头并进,当fast走到头,slow就是倒数第n个节点, 但我们这里是要删除倒数第n个节点,所以在并进的同时,还得有个记录slow的前面一个节点。还有就是为了让删除第一个和中间的统一起来,这里还需要一个头结点。
    在这里插入图片描述
  • 剑指offer22: 链表中的倒数第K个节点: 这个题目比上面那个还简单,直接不用删除了,而是直接找就可以了。看上面代码改就可以啦。
  • LeetCode83: 删除排序链表中的重复元素: 这个使用一个指针即可,如果发现他后面的元素和它相等,循环删除,然后往后移动即可。
    在这里插入图片描述
  • 牛客Top200高频: 删除有序链表中重复出现的元素: 注意,这个题和上面这个不同的是,只要是元素重复了,直接把该元素删掉(上面那个是保留其中的一个)。这样对应的,处理的方式也会不太一样。这里需要pre,cur两个指针。
    在这里插入图片描述
    这个题对应着LeetCodeLeetCode82: 删除排序链表中的重复

2.2 链表的反转

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

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

      在这里插入图片描述
  • 剑指offer06: 从尾到头打印链表: 这个题目常规思路可能想着先基于上面的这种操作把链表反转,然后进行打印即可。 但这样的话就改变了链表结构了,通常打印是一个只读操作,我们是不希望改变内部数据的, 那么这时候如何做呢? 第一个思路,栈来存储,由于栈有后进先出的特点,所以可以使用栈存储节点值,然后输出栈里面的值即可。 第二个思路,递归,由于递归本质上是一个栈结构,所以这个题目可以用尾递归来完成。具体代码是这样:
    在这里插入图片描述

  • LeetCode92: 反转链表II: 这个题在上面的基础上要翻转任意给定区间的链表了, 这里包括下面一个题用到的关键思想反转给定区间链表+ 尾插重建。 首先写一个翻转任意区间的链表函数,这个是左开右闭的,然后找到左右区间,把这个反转,然后尾插到新链表即可。
    在这里插入图片描述

  • 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 环形链表

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

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

在这里插入图片描述

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

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

  • LeetCode160: 相交链表: 这个题目是找两个链表的交点, 两种思路,一种是哈希表的方式,这个依然会非常简单,先遍历一个链表,然后把节点加入到哈希表,然后遍历另一个,如果发现在哈希表里,说明相交了,返回。

    def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode:
        if not headA or not headB: return 
        p, q = headA, headB
        s = set()
        while p:
            s.add(p)
            p = p.next
        while q:
            if q in s: return q
            q = q.next
            
        return None
    

    所以判断环或者相交的题目, 哈希表也是一种不错的思路,另一个就是双指针追着跑了, 两个指针p和q分别指向这两个链表, 然后一步一步的开始跑,如果到了自己的链表尾部之后,去另一个链表中跑。这样当两个指针相遇的时候,就是交点,因为它们走的路程发现会一样的。这个思路确实有点巧妙了。
    在这里插入图片描述
    这个题的变形或者应用,是可以迁移到在树中找两个节点的最近公共祖先的(前提是节点中有指向父节点的指针), 那么这时候,树中找最近公共祖先就变成了两个链表找公共节点问题。 基本思路是先分别找到这两个节点,然后沿着它们各自的父节点指针就得到了两条链表。然后就直接找两条链表的相交节点即可。 所以对于树里面找公共祖先也是一组题目, 首先先问清楚是不是二叉树, 如果是二叉树,再问下是不是二叉搜索树,这俩会不一样。 如果不是二叉树,那么问下是不是树节点里面有指向父节点的指针,如果有进行上面的转换,如果没有,难度就又高了,需要想办法借助辅助空间存储两个节点的父节点,再转成找公共节点的问题。所以面试的时候一定要善于和面试官沟通

  • LeetCode234: 回文链表: 这个题目第一种思路,就是用一个栈, 先快慢指针找中点,然后中点前面的元素入栈, 然后等快指针走到头之后, slow指向了中点处,此时再slow右移,然后依次出栈的与元素比较,看看是否回文。奇数点和偶数点的情况会不一样。
    在这里插入图片描述
    第二种优化思路,就是空间复杂度为O(1),这个不是用个栈保存中点之前的元素了,而是在遍历过程中,把中点之前的元素逆序。这样等fast走到头之后,中点前面的也逆序完了,slow边走边和前面的比较即可。

    在这里插入图片描述

  • LeetCode138: 复制带随机指针的链表: 这个题目的思路就是先复制一遍链表,但是在赋值的同时, 要用哈希表把每个链表的随机指针指向的节点存储下来, 这样再遍历新链表的时候, 对于每个节点,修改其random指针的指向即可。
    在这里插入图片描述

2.5 链表合并

  • LeetCode2: 两数相加:这个和数组的两数相加思路是一样的,无非就是遍历的时候有点麻烦, 先相加,然后再考虑进位即可。
    在这里插入图片描述

  • LeetCode445: 两数相加II: 这个题和上面不太一样的就是正序存储数了。然后执行两数相加,那么思路就是先逆序,然后再和上面一样,然后在逆序回去。
    在这里插入图片描述

  • LeetCode21: 合并两个有序链表: 两个指针遍历,两两比较,小的接到新数组上去。
    在这里插入图片描述
    这个or操作比较骚气啊还是。

  • LeetCode23: 合并K个排序链表: 这个题目把上面那个代码拿过来, 上面是两两合并, 而这里合并K个,其实就可以等价成很多个链表两两合并。所以self.merge是两两链表合并的函数。
    在这里插入图片描述
    But, 这个时间复杂度是 O ( k 2 n ) O(k^2n) O(k2n),不是好的解法,而这个题目是很多大厂喜欢问的面试题,只会上面这个应该是hold不住,所以又学了一种思路,就是优先级队列的思路。 听这个名字仿佛好难,但其实上一说思路,就会发现不是很复杂:

    K个有序链表排序,我们可以用K个指针分别指向每个链表, 和两个链表的一样,每次相当于K个元素找最小值, 找到之后, 加入到合并后的链表,然后对应链表的指针后移。再K个元素找最小,重复即可。

    只不过这里找最小值,如何更加高效点的实现呢? 很自然的可以想到堆排序, 也就是如果把K个链表的首元素先建立一个最小堆,这样堆顶元素就是最小值, 拿下来,放入最终链表中,然后看看这个最小值来自哪个链表, 把该链表接下来的一个元素加入到堆里面去即可。 这样,每次都会在 l o g k logk logk的时间内找到最小值。等堆空了, 也就合并完了,时间复杂度是 O ( n l o g k ) O(nlogk) O(nlogk) n n n是假设的每个链表的最大长度。

    这个过程翻译过来就是优先级队列做的事情, 这个东西原来就是Python里面的heapq模块实现的,具体代码如下:

    在这里插入图片描述

2.6 链表重排

  • LeetCode86: 分割链表: 这个题有点类似于插入排序的性质了,首先先找到比x大的或者等于x的节点的前面一个节点, 让rear指向它,然后在rear后面找比x小的元素,找到之后,在原位置删除,然后插入到rear后面,rear后移即可。 其实考察的是链表的删除和插入等操作。
    在这里插入图片描述
  • LeetCode148: 排序链表: 链表的排序,并且还要求 O ( n l o g n ) O(nlogn) O(nlogn)的时间复杂度, 那么采用归并排序的递归方式会比较好做。 归并排序的思路就是先中点(快慢指针),然后将链表一分为2, 然后先把这两个子链表排序好,然后进行合并,这是个递归的过程,而合并就是两个有序链表合并的代码。所以这个题用到了找链表中点和两个有序链表合并的知识
    在这里插入图片描述
    self.merge函数依然是合并两个有序链表的函数。切开的这行代码注意一下。这个代码里面还有一个注意点就是找链表中点的时候, 初始化slow=head, fast=head.next, 而之前找中点的时候slow=head, fast=head。 这两种写法也是有讲究和区别的, 如果上面的代码换成后面这个就会陷入死循环之中。 具体区别是: 前者这种写法是下取整,而后面这种写法是上取整, 当链表节点数是偶数的时候,就一目了然了,一个是取到前面那个数,而一个是取到后面的数。 如果是两个节点的链表,如果此时使用后面这种写法,这两个节点是分不开的, 所以只有采用前面的那种写法,才能把这两个再一分为二,然后排序。
  • LeetCode143: 重排链表: 这个题目首先还是找中点, 找到之后,切断,中点后面的逆序,然后交叉插入到新数组即可。

    在这里插入图片描述
  • LeetCode328: 奇偶链表: 这个题的思路就是奇数位置的数从后面删除,然后往前面插入即可。 用到的知识点: 链表删除,链表节点尾插。 这里用pre指向偶数位置,然后cur指向奇数位置, 每次执行删除,插入,归位操作即可。
    在这里插入图片描述
  • LeetCode138: 复制带随机指针的链表: 这个题目是链表的复制, 不过每个链表节点还有一个random指针,所以复制的时候,怎么在新链表上把这个东西复制过来是这个问题的关键。 这里的第一个思路,就是空间换时间,第一步, 遍历复制链表,但是在复制的同时,也要把原链表节点和新链表节点一一对应放入哈希表,这样遍历完毕,就建立了一个新的链表。 第二步,调新链表的random指针, 这个就可以根据原链表的对应节点找到新链表的random节点了。空间复杂度O(n)
    在这里插入图片描述
    面试的时候有这款代码勉强过关, 但是还可以优化, 非常巧妙,把空间复杂度降到O(1),时间复杂度依然是O(n), 具体可以看剑指offer189页。这里直接给出代码:

    在这里插入图片描述

2.7 LRU缓存

  • 面试题16.25: LRU缓存: 这个题目考察的是数据结构的使用, “最近最少使用”原则去模拟缓存的存和取, 这里会用到哈希表和双向链表。 首先先梳理下让实现的这两个操作是个什么样的流程,然后再看看这样的数据结构设计。

    1. get(key): 这个操作是用户输入一个key,然后从缓存中要取到对应的值,同时,还需要把这个key和值放到缓存的最头部位置去。
    2. put(key,value): 这个操作是要往缓存里面写数据, 这时候,如果缓存达到最大容量了,此时要删除缓存最尾部的元素,然后再把这个新的(key,value)插入到缓存中。

    所以分析完了之后,就是这个缓存类似于一个队列的样子,并且get操作要实现o(1)的复杂度, 插入删除也得O(1)的时间复杂度, 这时候,就需要哈希表+双向链表一块的数据结构, 哈希表负责访问链表中的值,而具体的插入删除要从链表中实现。 之所以双向,是因为插入删除都是O(1)。 而之所以链表中存取键值, 是因为当删除队尾元素的时候, 要同时从哈希表中删除相应的键。所以这个题一点点的写就是这样。 首先,我们先建立一个双向链表节点:

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

    然后我们建立一个双向链表的结构,里面要写双向链表的插入和删除操作,这个也需要练习一下,双向链表的插入和删除修改指针指向是有顺序的。

    class DoubleList:
    	def __init__(self):
    		# 这里需要建立两个虚拟节点
    		self.dummyHead = ListNode(-1, -1)
    		self.dummyTail = ListNode(-1, -1)
    		self.dummyHead.next = self.dummyTail
    		self.dummyTail.prev = self.dummyHead
    		self.size = 0
    	
    	# 链表的首部插入操作,这里主要指针的修改顺序
    	def addFirst(self, x):
    		x.next = self.dummyHead.next
    		x.prev = self.dummyHead
    		self.dummyHead.next.prev = x
    		self.dummyHead.next = x
    		self.size += 1
    	
    	# 链表的节点删除操作, 这里是节点一定存在
    	def remove(self,x):
    		x.prev.next = x.next
    		x.next.prev = x.prev
    		self.size -= 1
    	
    	# 删除尾部的节点
    	def removeLast(self):
    		if self.size == 0:
    			return None
    		
    		last_node = self.dummyTail.prev
    		self.remove(last_node)
    		return last_node
    	
    	# 得到节点个数
    	def getSize(self):
    		return self.size
    

    这样,双向链表的相关操作已经设置好了,接下来,就是可以模拟LRU的缓存机制了。

    class LRUCache:
    	def __init__(self, capacity):
    		self.capacity = capacity
    		self.map = {}
    		self.cache = DoubleList()
    	
    	# get()函数的设计:首先如果缓存里面有,那么就取出来,同时把这个节点从原链表中删除,然后从最前面插入
    	def get(self, key):
    		if key not in self.map: return -1
    		val = self.map[key].val
    		self.put(key, val)
    		return val
    	
    	# 这里就是先从双向链表中把当前的节点删除掉,然后再从最前面插入
    	def put(self, key, val):
    		new_item = ListNode(key, val)
    		if key in self.map:
    			self.cache.remove(self.map[key])
    			self.cache.addFirst(new_item)
    			self.map[key] = new_item
    		# 如果没在原先的里面,就看看是否达到了缓存容量,如果到了,需要删除最后一个
            # 注意,这里也得从哈希表删除,所以链表中才记录了键和值
    		else:
    			if self.capacity == self.cache.getSize():
    				last_node = self.cache.removeLast()
    				self.map.pop(last_node.key)
    			# 插入新节点
    			self.cache.addFirst(new_item)
    			self.map[key] = new_item	
    

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两个指针(滑动窗口 — > 子数组,子串的相关题目)

这块的重要思想:

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

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

链表元素的删除:

链表的反转

链表节点的交换

快慢指针

链表合并

链表重排

LRU缓存模拟

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Pintos是一个操作系统的教学项目,它里面有许多基础数据结构。其中之一就是通用链表(generic list)。 链表是一个常见的数据结构,用于存储一系列的元素。在Pintos中,通用链表是一种灵活且可扩展的数据结构,可以存储任意类型的元素。 Pintos中的通用链表由两个主要的结构组成:链表节点和链表本身。每个链表节点包含了一个指向前一个节点的指针和一个指向后一个节点的指针,以及一个用于存储元素的指针。链表本身则包含了一个指向头节点和尾节点的指针,以及用于记录链表长度的变量。 通过使用这些结构,Pintos的通用链表提供了一系列的操作来管理链表中的元素。比如,它可以实现在链表头部和尾部插入元素、删除元素以及在指定位置插入或删除元素等功能。此外,通过遍历链表,我们可以对链表中的每个元素进行操作,比如查找、更新和打印。 Pintos的通用链表使用简单而高效的方法来处理链表操作。通过使用指针连接节点,我们可以轻松地插入和删除元素,并且时间复杂度为O(1)。而遍历链表的操作也只需要O(n)的时间复杂度,其中n是链表的长度。 总之,Pintos的通用链表为操作系统的开发提供了一个方便和高效的数据结构。它可以存储任意类型的元素,并提供了丰富的操作来管理和操作这些元素。无论是在Pintos项目中还是在实际操作系统的开发中,通用链表都是一个非常有用的工具。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值