数组在上一篇的专栏,中咱们进行了回顾和刷题。javascript
趁热打铁,咱们来对比数组来学习链表。前端
首先要明确的是,链表和数组的底层存储结构不一样
,数组要求存储在一块连续的内存中,而链表是经过指针将一组零散的内存块串联起来。java
可见链表对内存的要求下降了,可是随机访问的性能就没有数组好了,须要 O(n) 的时间复杂度。
node
下图中展现了单链表及单链表的添加和删除操做,其实链表操做的本质就是处理链表结点之间的指针。
git
在删除链表结点的操做中,咱们只须要将须要删除结点的前驱结点的 next 指针,指向其后继便可。这样,当前被删除的结点就被丢弃在内存中,等待着它的是被垃圾回收器清除。github
为了更便于你理解,链表能够类比现实生活中的火车,火车的每节车箱就是链表的一个个结点。车箱之间相互链接,能够添加或者移除掉。春运时,客运量比较大,列车通常会加挂车箱。
面试
链表的结点结构由数据域
和指针域
组成,在 JavaScript 中,以嵌套的对象形式实现。数组
{ // 数据域 val: 1, // 指针域 next: { val:2, next: ... } }
年初立了一个 flag,上面这个仓库在 2021 年写满 100 道前端面试高频题解,目前进度已经完成了 50%。数据结构
若是你也准备刷或者正在刷 LeetCode,不妨加入前端食堂,一块儿并肩做战,刷个痛快。性能
了解了链表的基础知识后,立刻开启咱们愉快的刷题之旅,我整理了 6 道高频的 LeetCode 链表题及题解以下。
先明确,删除倒数第 n 个结点,咱们须要找到倒数第 n+1 个结点,删除其后继结点便可。
const removeNthFromEnd = function(head, n) { let prev = new ListNode(0), fast = prev, slow = prev; prev.next = head; while (n--) { fast = fast.next; } while (fast && fast.next) { fast = fast.next; slow = slow.next; } slow.next = slow.next.next; return prev.next; }
n + m 是两条链表的长度
const mergeTwoLists = function (l1, l2) { if (l1 === null) { return l2; } if (l2 === null) { return l1; } if (l1.val < l2.val) { l1.next = mergeTwoLists(l1.next, l2); return l1; } else { l2.next = mergeTwoLists(l1, l2.next); return l2; } }
先明确想要交换节点共须要有三个指针进行改变。
const swapPairs = (head) => { const dummy = new ListNode(0); dummy.next = head; // 头部添加哨兵节点 let prev = dummy; while (head && head.next) { const next = head.next; // 保存 head.next head.next = next.next; next.next = head; prev.next = next; // 下面两个操做将指针更新 prev = head; head = head.next; } return dummy.next; };
若是你对递归还以为掌握的不够透彻,能够移步个人这篇专栏
回到本题的递归解法:
const swapPairs = function (head) { // 递归终止条件 if (head === null || head.next === null) { return head; } // 得到第 2 个节点 let newHead = head.next; // 将第 1 个节点指向第 3 个节点,并从第 3 个节点开始递归 head.next = swapPairs(newHead.next); // 将第 2 个节点指向第 1 个节点 newHead.next = head; return newHead; }
const hasCycle = function(head) { if (!head || !head.next) return false; let fast = head.next; let slow = head; while (fast !== slow) { if (!fast || !fast.next) { return false; } fast = fast.next.next; slow = slow.next; } return true; };
遍历链表,经过 flag 标记判断是否有环,若是标记存在则有环。(走过的地方插个旗子作标记)
const hasCycle = function(head) { while (head) { if (head.flag) { return true; } else { head.flag = true; head = head.next; } } return false; }
const reverseList = function(head) { let prev = null; let curr = head; while (curr !== null) { // 记录 next 节点 let next = curr.next; // 反转指针 curr.next = prev; // 推动指针 prev = curr; curr = next; } // 返回翻转后的头节点 return prev; };
const reverseList = function(head) { if (!head || !head.next) return head; // 记录当前节点的下一个节点 let next = head.next; let reverseHead = reverseList(next); // 操做指针进行反转 head.next = null; next.next = head; return reverseHead; };
老套路,借助快慢指针,fast 一次走两步,slow 一次走一步,当 fast 到达链表末尾时,slow 就处于链表的中间点了。
const middleNode = function(head) { let fast = head, slow = head; while (fast && fast.next) { slow = slow.next; fast = fast.next.next; } return slow; };