链表篇:203.移除链表元素 、707.设计链表、206.反转链表

提示:努力生活,开心、快乐的一天


链表的基础知识

一、 什么是链表
链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思),链表的入口节点称为链表的头结点也就是head
二、链表的几种类型
单链表 :如上解释
在这里插入图片描述
双链表 :每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。双链表 既可以向前查询也可以向后查询
在这里插入图片描述
循环链表 :顾名思义,就是链表首尾相连
在这里插入图片描述
三、链表的存储方式
数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。
链表是通过指针域的指针链接在内存中各个节点。
四、链表的操作
删除链表
在这里插入图片描述
添加链表
在这里插入图片描述
五、性能对比
在这里插入图片描述

203.移除链表元素

题目链接:203.移除链表元素

💡解题思路

  • 移除元素可能为链表的头节点,需要特殊处理
  • 在原链表进行操作分两种情况
    删除的元素是头节点:将头节点向后移动一位即可
    删除的元素非头节点:记住一个原则,删除的元素一定是当前元素的下一个元素,所以,当前指针与下一个指针都不能为空,定义一个当前元素cur,当cur.next.val===val时,将cur.next指向cur.next.next;否则,当前指针后移一位。最后return head
  • 设置虚拟节点进行操作:该方法是在头节点之前再添加一个节点,将头节点当成普通节点对待,无需特殊处理。该方法的重点在于设置虚拟节点let newHead = new ListNode(0, head),然后从该虚拟节点开始遍历。最后return newHead.next,为什么此处不return出head,因为,有可能head是被删除的那一个,而newHead.next却是一定存在的

🤔遇到的问题

  • 在原链表进行操作时,没有结束循环的操作

💻代码实现

在原链表操作

var removeElements = function (head, val) {
    //原链表进行操作
    //删除的是头节点
    while (head !== null && head.val === val) {
        head = head.next
    }
    //删除非头节点
    let cur = head
    while (cur !== null && cur.next !== null) {
        if (cur.next.val === val) {
            cur.next = cur.next.next
            continue //错误点,忘记结束单次循环
        } else {
            cur = cur.next
        }
    }
    return head
};

设置虚拟节点

var removeElements = function (head, val) {
    //设置虚拟节点
    let newHead = new ListNode(0, head);
    let cur = newHead;
    while (cur.next) {
        if (cur.next.val === val) {
            cur.next = cur.next.next
            continue
        }
        cur = cur.next
    }
    return newHead.next
};

🎯题目总结

  • 无论哪种方式,最终删除的都是cur.next,然后cur.next = cur.next.next

707.设计链表

题目链接:707.设计链表

💡解题思路

  • 初始化列表对象:定义三个值,分别为头节点head尾节点tail列表长度size
  • 创建节点用类,节点中包含2个参数:valnext
  • getNode(index)方法获取输入的值在链表中的节点
    index非法(小于0或者大于列表长度)时,return null;index合法时,创建一个虚拟节点,指向head,然后0到index循环,获取index在列表中的节点
  • get(index)方法:考虑边界index无效的情况返回-1;其他情况,可掉getNode(index)方法,获取index在列表中的节点后,取该节点的数据(val)
  • addAtHead(val)方法:定义新节点,新节点指向head,将head移到该节点,size++,边界情况,原链表为空链表,在原来操作的基础上添加尾节点的赋值,赋值为新节点
  • addAtTail(val)方法:创建新添加的节点后,处理边界,如果原链表为空链表新添加的节点也会是头节点,否则原链表的尾节点就是新添加的节点。另外新的尾节点是新添加的节点,size++
  • addAtIndex(index,val)方法:先处理边界,边界一index > size,直接return;边界二index <= 0,相当于添加头节点;边界三index === size,相当于添加尾节点;其他正常情况获取需要插入位置的前一个节点,创建需要插入的新节点,保存cur的下一个节点,cur的下个节点指向新节点,新节点的下一个节点指向之前保存的原来的cur的下一个节点,size++
  • deleteAtIndex(index)方法:先处理边界,边界一index合法性,如上;边界二列表只有一个节点,头尾节点均为null边界三index为0,head指向head.next边界四index为size-1(尾节点),获取期望删除节点的上一个节点并指向null,修改尾节点其他情况(index>0&&index<size-1)获取删除节点的上一个节点后,将其指向下一个节点的下一个节点。最后size--

🤔遇到的问题

  • 边界值处理问题,基本都是针对空列表的情况或者只有1个节点的列表的增加及删除操作的边界处理,调试很麻烦

💻代码实现

class LinkNode {
    constructor(val, next = null) {
        this.val = val;
        this.next = next;
    }
}

var MyLinkedList = function () {
    this.size = 0;//节点数量
    this.tail = null;//尾节点
    this.head = null;//头节点
};

/** 
 * @param {number} index
 * @return {number} 获取输入的值在链表中的节点
 */
MyLinkedList.prototype.getNode = function (index) {
    if (index < 0 || index >= this.size) return null
    //创建虚拟节点
    let cur = new LinkNode(0, this.head)
    //从0(虚拟节点)-index遍历
    while (index >= 0) {
        cur = cur.next;
        index--
    }
    return cur
};

/** 
 * @param {number} index
 * @return {number}
 */
MyLinkedList.prototype.get = function (index) {
    if (index < 0 || index >= this.size) return -1;
    return this.getNode(index).val
};

/** 
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtHead = function (val) {
    //创建新添加的节点
    let node = new LinkNode(val, this.head)
    this.head = node;
    //原链表为空时
    if (!this.size) {
        this.tail = node;
    }
    this.size++
};

/** 
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtTail = function (val) {
    //创建新添加的节点
    let node = new LinkNode(val)
    if (!this.size) {
        //原链表为空链表,新添加的节点也会是头节点
        this.head = node
    } else {
        //原链表的尾节点就是新添加的节点
        this.tail.next = node
    }
    //新的尾节点是新添加的节点
    this.tail = node
    this.size++
};

/** 
 * @param {number} index 
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtIndex = function (index, val) {
    if (index > this.size) return;
    if (index <= 0) {
        this.addAtHead(val)
        return
    }
    if (index === this.size) {
        this.addAtTail(val)
        return
    }
    //获取需要插入位置的前一个节点
    let cur = this.getNode(index - 1)
    //创建需要插入的新节点
    const node = new LinkNode(val)
    //保存cur的下一个节点
    let temp = cur.next;
    //cur的下个节点指向新节点
    cur.next = node
    //新节点的下一个节点指向之前保存的原来的cur的下一个节点
    node.next = temp
    this.size++
};

/** 
 * @param {number} index
 * @return {void}
 */
MyLinkedList.prototype.deleteAtIndex = function (index) {
    if (index < 0 || index >= this.size) return
    if (this.size === 1) {
        this.head = null
        this.tail = null
        this.size--
        return
    }
    //如果删除的是头节点
    if (index === 0) {
        this.head = this.head.next
    }
    //如果删除的是尾节点
    if (index === this.size - 1) {
        //获取删除节点的上一个节点
        const node = this.getNode(index - 1)
        //将该节点指向null
        node.next = null
        //修改尾节点
        this.tail = node
    }
    //正常
    if (index > 0 && index < this.size - 1) {
        //获取删除节点的上一个节点
        const node = this.getNode(index - 1)
        //将该节点指向下一个节点的下一个节点
        const temp = node.next.next
        node.next = temp
    }
    this.size--;
};

🎯题目总结

  • 设置虚拟节点能解决很多问题,非常有用
  • 边界处理需要认真小心,尤其对于空链表或者只有1个节点的列表
  • 这道题值得反复刷,不断回味

206.反转链表

题目链接:206.反转链表

💡解题思路

  1. 双指针解法:反转链表是将每个指针的指向调转一下,这样原本最后一个元素指向null的,变成第一个元素指向null。
    定义两个指针,一个pre为null,另一个cur(当前节点)为head,结束条件是cur=null,也就是原链表的最后一个元素的指向。接下来的4个反转步骤不可调换顺序,否则就会报错
    1、将当前节点的下一个节点指向先保存temp = cur.next
    2、反转,将当前节点的指向下一个节点改为指向pre(此时原本的cur.next已断开,这就是为什么先保存的原因)cur.next = pre
    3、pre前移一位,来到cur所在的位置pre = cur
    4、cur前移一位,来到原本cur.next的位置,即保存的temp的位置cur = temp
  2. 递归解法:与双指针的逻辑一致,只是在pre与cur移动时采用递归的方式

🤔遇到的问题

  1. 反转步骤会搞错,需要画图慢慢来
  2. 递归解法的跳出递归的条件判断错误

💻代码实现

双指针解法

var reverseList = function (head) {
    //双指针
    if (!head || !head.next) return head;
    let cur = head//当前节点
    let pre = null//前节点
    let temp = null//当前节点的后一个节点
    while (cur) {
        //下面的顺序不可更改
        temp = cur.next;//将当前节点的下一个节点先保存
        cur.next = pre;//反转,将当前节点的指向下一个节点改为指向pre(此时原本的cur.next已断开,这就是为什么先保存的原因)
        pre = cur;//pre前移一位,来到cur所在的位置
        cur = temp;//cur前移一位,来到原本cur.next的位置,即保存的temp的位置
    }
    return pre;
};

递归解法

var reverse = function (pre, cur) {
    if (!cur) return pre;
    const temp = cur.next;
    cur.next = pre;
    return reverse(cur, temp)
}
var reverseList = function (head) {
    //递归的写法
    return reverse(null, head)
};

🎯题目总结

链表的双指针:分为precur两个,在移动的时候也需要注意,需要先将cur.next保存下来,将指向反转后,先移动pre,再移动cur
递归很容易进入死循环,需要谨慎写明跳出循环的条件

🎈今日心得

链表基本忘光啦,现在重现捡起来,有种熟悉又陌生的感觉,今天很充实,加油

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值