代码随想录Day 3 | 【链表理论基础】203.移除链表元素、707.设计链表、206.反转链表

了解链表基础,以及链表和数组的区别

文章链接:代码随想录

一、链表理论基础

1. 链表的类型

(1)单链表:链表是由通过一些指针串连起来的线性结构。主要有数据域和指针域(指针域存放的指针指向下一个节点),链表入口节点称为头节点head,最后一个节点的指针域指向NULL。

(2)双链表:单链表的指针只能指向下一个节点,而双链表有两个指针,前面一个指针指向上一个节点,下一个指针指向下一个节点;双链表既可以向前查询,也可以向后查询。

(3)循环链表:即链表首尾相连,可以用来解决约瑟夫环问题。

2. 链表的存储方式

        数组是内存中连续分布的;而链表是通过指针链接在内存中的各个节点,所以链表的内存空间不是连续分布的,而是散乱地分布在内存的某地址上,分配机制取决于操作系统的内存管理。

3. 链表的定义

        注意:平时在刷 leetcode 时,链表定义是默认给出的,但在面试中一旦要求手写链表,是需要自己进行链表定义的。

        以下是python定义链表节点的方式:

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

4. 链表的操作

(1)删除节点:

        不是直接删除,而是将C节点的指针指向E节点,即移动指针由C-->D到C-->E。C++需要手动释放这块内存,而其他语言有自己的回收机制。

(2)添加节点:要添加节点F,需要先让F的指针指向D节点,然后让C节点的指针指向F节点。

        删除和添加操作的时间复杂度都是O(1),但如果要删除最后一个节点,需要通过头指针查找到第N-1个节点进行删除,那么查找的时间复杂度是O(n)。

5.性能分析

插入/删除(时间复杂度)查找(时间复杂度)适用场景
数组O(n)O(1)数据量固定,频繁查找,增删较少
链表O(1)O(n)数据量不固定,频繁增删,查找较少

        数组在定义的时候长度就是固定的,如果想改动数组长度,需要重新定义数组。链表长度不固定,并且动态增删,适合数据量不固定、频繁增删、较少查询的应用场景。

本题最关键是要理解 虚拟头结点的使用技巧,这个对链表题目很重要。

题目链接/文章讲解/视频讲解::代码随想录

二、203.移除链表元素

1. 看到这道题的第一想法

        首先定义一个虚拟头节点dummy_head,然后让它指向头节点head,因为如果需要删除的节点是头节点,那么需要分类讨论,而采用虚拟头节点的方式可以统一算法规则。

        接下来遍历所以的节点,当节点数据域等于给定的整数时,则将上一个节点的指针指向下下个节点,就可以完成删除值等于val的节点操作。

2. 看完代码随想录后的想法

        (1)定义虚拟头节点及初始化目前指针指向的节点。

        注意目前指针也是指向虚拟头节点,而不是头节点。因为如果current = head,那么当val等于头节点时(即删除的元素是头节点)那么无法进行删除;

        首先明确我们需要删除的元素必须是current.next,删除的操作才会是current 指向 current.next.next,也就是把current.next.next赋值给current.next(current.next = current.next.next)。

        其次需要注意的是初始化定义虚拟头节点的代码如下:

dummy_head = ListNode(next = head)

        (2)循环遍历每一个节点,寻找数据域等于val的节点,并将其删除:首先为了防止空指针的出现(指针异常)所以将current.next非空作为循环条件,接下来判断current.next的值是否为val,如果是的话,则current的下一个节点变更为下下个节点,即current.next = current.next.next,如果不是则继续遍历下一个节点。

        (3)最后返回dummy_nead.next,因为如果删除的是头节点的话,那么这个链表就没有头节点了,所以引入虚拟头节点,那么进行删除操作后,返回dummy_head的下一个节点仍然是这个链表的头节点,无论新旧。

这是一道考察链表综合操作的题目,不算容易,可以练一练 使用虚拟头结点

题目链接/文章讲解/视频讲解:代码随想录

三、707.设计链表

1. 看到这道题的第一想法

(1)获取第n个节点的数值:

        从头节点开始依次遍历一直到下标为n的节点,返回其数值。

(2)头部插入节点:

        定义虚拟头节点dummy_head,然后定义一个临时指针指向当前的head头节点(即暂时存放虚拟头节点,因为一旦dummy_head指向新的头节点,那么原始头节点就找不到了,于是先将原始头节点存放起来),然后将dummy_head.next指向新的头节点,再让新的头节点指向临时指针(也就是暂时存放原始头节点的指针)。

(3)尾部插入节点:

        从头节点开始遍历所有节点,一直到最后一个节点的next指向的是NULL,然后定义一个新节点,将新节点的指针指向NULL,再让最后一个节点的next指向新节点。

(4)前n个节点前插入:

        定义新节点,然后从头节点遍历到第N个节点,将新节点的next指向第n个节点的Next,再移动第n个节点的指针指向新节点。

(5)删除第n个节点:

        定义临时指针进行循环遍历链表,直到第n个节点的前一个节点就跳出循环,然后使得current.next指向current.next.next,最后size-1。

2. 看完代码随想录后的想法

        这道题目统一使用虚拟头节点的方式进行操作,可以保证操作的一致性。

(1)获取第n个节点的数值:

        A.明确链表是从0开始,到n-1结束的;B.要对节点下标做限制,超出合法范围则下标无效,也就是n < 0 或者 n > size-1 则无效;C.需要定义临时指针来遍历链表,并且让临时指针current指向dunmmy_head.next(即头指针),而不能直接操作头指针,否则最后返回头指针而头指针的值已被更改,无法返回完整链表;D.依次遍历链表节点(从0到n-1)就是第n个节点,最后返回第n个节点的数值即可。

(2)头部插入节点:

        A.定义一个值为val的新节点NewNode;B.先将新节点的指针NewNode.next指向head(必须写成dummy_head.next),然后再将dummy_head.next指向新节点。注意写代码在操作指针的时候要将self写上,比如self.head而不是head。C. 最后size加1。

(3)尾部插入节点:

        A.定义一个新节点NewNode,默认它的next指向NULL;B.定义一个临时指针current用于遍历链表所有节点,一直到尾节点(尾节点的判定条件:next指向的是null);C.让尾节点current直接指向NewNode; D.最后size加1

(4)前n个节点前插入:

        注意:是在第n个节点前插入,而不是在第n个节点后插入!!A.判断index是否有效;B.遍历节点直到第n个节点的前一个节点,就结束循环;C.定义新节点,并且新节点指向第n个节点;.然后将第n个节点的前一个节点指向新节点;D.最后size加1

(5)删除第n个节点:

A.判断index是否有效;B.遍历节点直到第n个节点的前一个节点,就结束循环;C.current.next = current.next.next;D.最后size-1。

3. 实现过程中遇到的困难及解决

(1)关于界定index下标有没有效的问题:

        一般情况下例如删除、查询的时候,index应该满足>=0或者<=self.size-1只有在添加的时候,index是可以等于self.size的,因为最终它的size+=1

(2)关于初始化节点的时候:init前后一定是双下划线!!__init__(下划线是两根连起来)

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

(3)定义新节点时,应该写定义函数,而不是直接写newnode = listnode(...),在这道题目中直接调用ListNode类即可。self.dummy_head.next =ListNode(val,self.dummy_head.next)。

(4)本题目中使用虚拟头节点时,应该调用对象self才可以,因为虚拟头节点被定义在了初始化函数中。self.dummy_head.next。

(5)在进行插入节点后,需要最后size加1;删除节点就size-1。

题目链接/文章讲解/视频讲解: 代码随想录

四、206.反转链表(面试高频)

1. 看到这道题的第一想法

        首先,定义一个current指针用于遍历链表;然后定义一个临时指针用于存放current指向的下一个节点,也就是即将改变方向的节点;接下来反转指针由指向下一个节点转变为指向上一个节点;注意结束循环的条件及边界点。

2. 看完代码随想录后的想法

(1)双指针法

        A. 定义一个current用于遍历链表的每一个节点;定义一个pre指针,代表current反转后的next,用于反转链表,初始化为None因为反转后的链表pre是尾节点。

        B. 进入循环遍历,以current不为NULL为循环条件;

        C. 定义一个临时指针temp用于存放原来的current.next,因为方向一旦改变就无法查找到这个节点了,也就无法进行反转链表;然后反转链表操作,将current.next指向pre,然后移动pre和current进行下一轮循环。

        D.当current指向NULL时停止遍历,此时pre是链表的头节点,所以返回pre即可。

(2)递归法

        A. 主函数直接调用我们即将定义的反转函数reverse;

        B.定义reverse函数:同双指针法

3. 实现过程中遇到的困难及解决

(1)双指针法:pre指针的初始化为None,而不是NULL

(2)递归法:注意定义方式和调用方式。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值