【代码随想录-刷题学习JavaScript】day2-链表

一 、链表理论基础
二、203.移除链表元素
三、707.设计链表
四、206.反转链表
五、 24. 两两交换链表中的节点
六、19.删除链表的倒数第N个节点
七、 面试题 02.07. 链表相交
八、 142.环形链表II
九、链表总结

一 、链表理论基础

建议:了解一下链接基础,以及链表和数组的区别
文章链接

1.定义

通过指针串联在一起的线性结构;

每个节点包括:数据域、指针域;
(比如单链表的指针域是存放指向下一个节点的指针,单链表最后一个节点的指针域比较特殊,是指向null空指针)

入口节点:链表的头结点(head);

2.类型

(1)单链表

指针域只能指向节点的下一个节点或null(最后一个节点的指针域比较特殊,是指向null空指针)
在这里插入图片描述

(2)双链表

每个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。
在这里插入图片描述

(3)循环链表

链表收尾相连。可以解决“约瑟夫环”问题。
在这里插入图片描述

3.存储方式

非连续;
是通过指针域的指针链接在内存中的各个节点;
分配机制取决于操作系统的内存管理。

4.js代码定义

// JS
class ListNode {
  val;
  next = null;
  constructor (value) {
    this.val = value;
    this.next - null;
  }
}
// TS
class ListNode {
  public val: number;
  public next: ListNode | null = null;
  constructor(value: number) {
    this.val = value;
    this.next = null;
  }
}

5.链表的操作

(1)删除 O(1)

将要删除节点的前一个节点的next指针指向要删除节点的后一个节点即可。
(这其中涉及:查找要删除节点的前一个节点的操作,此查找操作时间复杂度O(n) )
在这里插入图片描述

(2)添加 O(1)

将要添加处的节点的next指针指向要添加的节点,再将添加的节点的next指针指向后一个节点即可。
(这其中涉及:查找要添加节点的前一个节点的操作,此查找操作时间复杂度O(n) )
在这里插入图片描述

6.数据vs链表

插入/删除查询适用
数组O(n)O(1)数据量固定、频繁查询、少增删
链表O(1)O(n)数据量不固定、频繁增删、少查询

二、LeetCode203.移除链表元素

输入输出
[1, 2, 3, 6, 4, 5, 6] 6[1, 2, 3, 4, 5]
[] 1[]
[7, 7, 7 ,7] 7[]

两种思路:
(1)常规法
如果是头结点: 让head = head.next(因为头结点无前一个节点)
如果非头结点:让要删除的这个节点的前一个节点指向这个节点的下一个节点

(2)虚拟头结点法:(重点)
在链表的最前面加一个虚拟头结点,这样就一律当做非头结点来处理

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

手把手带你学会操作链表 | LeetCode:203.移除链表元素

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */
var removeElements = function(head, val) {
    /*(1)常规法
    如果是头结点: 让head = head.next(因为头结点无前一个节点)
    如果非头结点:让要删除的这个节点的前一个节点指向这个节点的下一个节点
    */
    while (head !== null && head.val === val) {
        head = head.next;
    }
    if (head == null) return head;

    let cur = head;
    while (cur !== null && cur.next !== null) {
        if (cur.next.val === val) {
            cur.next = cur.next.next;
        } else {
            cur = cur.next;
        }
    }

    return head;
}; 
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} val
 * @return {ListNode}
 */
var removeElements = function(head, val) {
   /* (2)虚拟头结点法
    在链表的最前面加一个虚拟头结点,这样就一律当做非头结点来处理
    */
    let dummyHead = new ListNode(); // 虚拟头结点
    dummyHead.next = head; // 虚拟头结点与链表连接起来
    let cur = dummyHead; // 临时设定个cur,为了不断遍历链表改变链表,又不影响最后的返回
    
    while (cur !== null && cur.next !== null) {
        if (cur.next.val === val) {
            cur.next = cur.next.next;
        } else {
            cur = cur.next;
        }
    }

    return dummyHead.next;  // head可能已经被删除
}; 

三、LeetCode 707.设计链表

get(index) 获取链接第index个节点的值,若无返回-1
addAtHead(val) 在链表头元素之前添加val节点,插入后的val为第一个节点
addAtTail(val) 将值为val的节点添加到链表最后
addAtTail(val) 将值为val的节点添加到链表最后
addAtIndex(index, val) index=链表长度,加链表末尾;index>链表长度,不加;index<0,加头部;其它时候正常添加到链表中
deleteAtIndex(index, val) 若index有效,删除第index个节点

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

帮你把链表操作学个通透!LeetCode:707.设计链表

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

var MyLinkedList = function() {
    this._size = 0;
    this._tail = null;
    this._head = null;
};


MyLinkedList.prototype.getNode = function(index) {
    if(index < 0 || index >= this._size) return null;
    let dummyHead = new LinkNode(0, this._head);
    let cur = dummyHead.next;
    while(index) {
        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 newHead = new LinkNode(val, this._head);
    this._head = newHead;
    this._size++;
    if(!this._tail) {
        this._tail = newHead;
    }
};

/** 
 * @param {number} val
 * @return {void}
 */
MyLinkedList.prototype.addAtTail = function(val) {
    let newTail = new LinkNode(val, null);
    this._size++;
    if(this._tail) {
        this._tail.next = newTail;
        this._tail = newTail;
        return;
    }
    this._tail = newTail;
    this._head = newTail;


};

/** 
 * @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;
    }

    // 获取目标节点的上一个的节点
    const node = this.getNode(index - 1);
    node.next = new LinkNode(val, node.next);
    this._size++;
};

/** 
 * @param {number} index
 * @return {void}
 */
MyLinkedList.prototype.deleteAtIndex = function(index) {
    if(index < 0 || index >= this._size) return;
    if(index === 0) {
        this._head = this._head.next;
        // 如果删除的这个节点同时是尾节点,要处理尾节点
        if(index === this._size - 1){
            this._tail = this._head
        }
        this._size--;
        return;
    }
    
    // 获取目标节点的上一个的节点
    const node = this.getNode(index - 1);
    node.next = node.next.next;

    // 处理尾节点
    if(index === this._size - 1) {
        this._tail = node;
    }
    this._size--;

};

/**
 * Your MyLinkedList object will be instantiated and called as such:
 * var obj = new MyLinkedList()
 * var param_1 = obj.get(index)
 * obj.addAtHead(val)
 * obj.addAtTail(val)
 * obj.addAtIndex(index,val)
 * obj.deleteAtIndex(index)
 */

四、LeetCode 206.反转链表

要注意的点: 学会双指针法,就明白了递归法(因为递归其实实质还是双指针法)

文章讲解

帮你拿下反转链表 | LeetCode:206.反转链表 | 双指针法 | 递归法

双指针法:cur、pre分别指向head和null,改变cur的指向,2个指针后移。只要cur有值就不断循环

// 双指针法
/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
    let cur = head;
    let pre = null;
    while (cur) { // 当cur===null,自然循环完了
        let temp = cur.next;  // 在改变cur方向之前,使用临时指针保存cur与下一节点之间的连接
        cur.next = pre; // 改变方向
        pre = cur; // 两个指针后移,先移动pre
        cur = temp; // 再移动cur
    }
    return pre; // 最后cur指向null,pre指向新的链表head
};

递归法:其实拆解来看就是双指针法

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var reverseList = function(head) {
 // 自己定义reverse函数用于翻转及递归调用
    var reverse = function(pre, cur) {
        if(!cur) return pre;
        const temp = cur.next;
        cur.next = pre;
        pre = cur;
        cur = temp;
        return reverse(pre, temp);
    }

    return reverse(null, head);
};

五、 LeetCode 24. 两两交换链表中的节点

输入输出
[1, 2, 3, 4][2, 1, 4, 3]
[][]
[1][1]

用虚拟头结点,这样会方便很多。
不是单纯改变节点内部val,是实际的进行节点交换。
为什么需要temp保存临时节点?因为如果不保存,那找1或者3就没有对应的关系了。
文章讲解

帮你把链表细节学清楚! | LeetCode:24. 两两交换链表中的节点

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var swapPairs = function(head) {
    let dummyHead = new ListNode(0, head);
    let cur = dummyHead;

    while(cur.next && cur.next.next) {
        let temp = cur.next; // 保存1
        let temp1 = cur.next.next.next; // 保存3

        cur.next = cur.next.next; // 虚拟头结点的next指向2
        cur.next.next = temp; // 2指向1
        temp.next = temp1; // 1指向3
        cur = cur.next.next; // 当前指针往后移动2位
    }
    return dummyHead.next;
};


六、LeetCode 19.删除链表的倒数第N个节点

双指针的操作,要注意,删除第N个节点,那么我们当前遍历的指针一定要指向 第N个节点的前一个节点。
建议先看视频。
文章讲解

链表遍历学清楚! | LeetCode:19.删除链表倒数第N个节点

/**
 * Definition for singly-linked list.
 * function ListNode(val, next) {
 *     this.val = (val===undefined ? 0 : val)
 *     this.next = (next===undefined ? null : next)
 * }
 */
/**
 * @param {ListNode} head
 * @param {number} n
 * @return {ListNode}
 */
var removeNthFromEnd = function(head, n) {
    let dummyHead = new ListNode(0, head);
    let slow = dummyHead;
    let fast = dummyHead;
    n++; // 虚拟头节点多增了1个节点
    while(n-- && fast !== null) {
        fast = fast.next;  // 快指针先移动n位
    }

    while (fast !== null) {
        // 再快慢指针同时移动,直到fast指针指向null
        fast = fast.next;
        slow = slow.next;
    }

    slow.next = slow.next.next;
    return dummyHead.next;
};

七、 LeetCode 面试题 02.07. 链表相交

输入输出
2个单链表的头结点headA和headB两个单链表相交的起始节点(如果没有返回null)

不存在环。
交点不是数值相等,而是指针相等。

本题没有视频讲解。
注意 数值相同,不代表指针相同。
文章讲解

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

var getListLen = function(head) {
    let len = 0;
    let cur = head;
    while (cur) {
        len++;
        cur = cur.next;
    }
    return len;
}

/**
 * @param {ListNode} headA
 * @param {ListNode} headB
 * @return {ListNode}
 */
var getIntersectionNode = function(headA, headB) {
    let curA = headA;
    let curB = headB;
    let lenA = getListLen(headA);
    let lenB = getListLen(headB);
    if (lenA < lenB) {
        [curA, curB] = [curB, curA];
        [lenA, lenB] = [lenB, lenA];
    }
    let i = lenA - lenB;
    while (i-- > 0) {
        curA = curA.next;
    }
    while (curA && curB !== curA) {
        curA = curA.next;
        curB = curB.next;
    }
    return curA;
};

八、 LeetCode 142.环形链表II

输入输出
一个环形链表,pos并不是输入,只是好表示环形链表链表开始入环的第一个节点
head=[3, 2, 0, -4] pos = 11
head=[1, 2] pos = 00

算是链表比较有难度的题目,需要多花点时间理解 确定环和找环入口,建议先看视频。

文章讲解
如果有环,快指针每次走2个节点,慢指针每次1个节点,快慢指针终会相遇。
视频链接

/**
 * Definition for singly-linked list.
 * function ListNode(val) {
 *     this.val = val;
 *     this.next = null;
 * }
 */

/**
 * @param {ListNode} head
 * @return {ListNode}
 */
var detectCycle = function(head) {
    if(!head || !head.next) return null;
    let fast = head.next.next; // 初始快指针
    let slow = head.next; // 初始慢指针

    while( fast && fast.next) { // 因为快指针每次移动2步
        fast = fast.next.next;
        slow = slow.next;
        if (fast == slow) { // 快慢指针相遇
            let slow = head; // 相遇点
            while (fast !== slow) {
                slow = slow.next; // 一起往后走
                fast  = fast.next; // 一起往后走
            }
            return slow;
        }
    }
    return null;
};

九、链表总结

文章链接

1.种类(略)

2.存储方式(略)

3.如何进行增删查改(略)

4.vs数组(略)

5.经典题目:
(1)移除链表元素
①常规法要判断是不是头结点
②所以虚拟头结点,虚拟后记得使用临时指针cur指向dummyHead,就可以正常cur.next = cur.next.next;
最后return 的是dummyHead.next(因为dummyHead可能被删)

(2)设计链表
①get方法,需要遍历,就需要虚拟头结点
②addAtHead 需要先把新的节点连到旧链表上,再用dummyHead连接到新节点上
③addAtTail 遍历到最后尾部,再cur.next = newNode
④addAtIndex 遍历到n,cur.next = newNode
⑤deleteAtIndex 遍历到n, cur.next = cur.next.next

(3)反转链表
①双指针是递归法的基础,学会了双指针,递归法就会了。
cur指针指向head、pre指向null。temp = cur.next保存关系不丢失。
cur改变方向cur.next = pre后。
两个指针后移:先以pre = cur,cur = temp。
②抽一个函数出来专门做上面的操作。通过递归调用来给cur、pre指针循环赋值。

(4)两两交换链表中的节点
①有循环遍历就有dummyHead。
②while循环的终止条件想清楚:cur.next !== null && cu.next.next ! == null
(因为cur一定指向需要翻转的2个节点的前一个节点,自己可以画下)
③dummyHead指向2,2指向1,1指向3
在dummyHead指向2之前,先保存1、3
④交换完毕后,每次循环cur = cur.next.next 走两位
⑤最后return的是dummyHead.next

两两交换前需要保存节点之间的关系:

(5)删除链表中倒数第N个节点
①虚拟头结点,因为要循环遍历。快慢指针均从dummyHead开始,多了个节点,所以n++
②fast指针先走n步骤,直到n = 0;
③再fast和slow同时走,直到fast走到null
④slow.next = slow.next.next(因为slow指向要删除的节点的前一个节点)
⑤最后return的是dummyHead.next

(6)链表相交
找到相交规律。
①遍历获得A、B长度
②保证A是长的那个,省去分支判断
③求长度差,用于遍历A到与B同一起点的位置
④遍历A、B,遇到相同的直接返回

(7)环形链表
主要找到形成环的条件。快慢指针之间的关系。

6.常用技巧:
(1)虚拟头结点
(2)双指针、递归
(3)while循环


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

倏存

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值