数据结构中的链表仍是很重要的,因此这章节把剑指offer 和 LeetCode 中的相关题目作一个汇总,分享给你们🤭前端
说真的,有时候,想要表达清楚本身的想法有点小困难,奈何又是个文笔不是很好的粗汉子,有些概念上问题,仍是引用别处的解释比较好,因此还望你们谅解。node
对于时间复杂度和空间复杂度,不太了解的话,能够看看下面这篇文章git
如何理解算法时间复杂度的表示法,例如 O(n²)、O(n)、O(1)、O(nlogn) 等?github
码字不易,对你有所帮助,点赞个支持一下面试
链表题目将收入GitHub中,思路和代码都有,有兴趣的小伙伴能够来玩👇算法
数据结构-链表数组
一种常见的基础数据结构,也是一种线性表,可是并不会按线性表的顺序存储数据,而是在每个节点里存到下一个节点的指针(Pointer)。浏览器
链表在插入的时候能够达到 O(1) 的复杂度,比另外一种线性表 —— 顺序表快得多,可是查找一个节点或者访问特定编号的节点则须要 O(n)的时间,而顺序表相应的时间复杂度分别是 O(log n) 和 O(1)。数据结构
优缺点:
❝使用链表结构能够克服数组链表须要预先知道数据大小的缺点,链表结构能够充分利用计算机内存空间,实现灵活的内存动态管理。可是链表失去了数组随机读取的优势,同时链表因为增长告终点的指针域,空间开销比较大。
❞
「链表容许插入和移除表上任意位置上的节点,可是不容许随机存取。」
链表有不少种不一样的类型:
链表一般能够衍生出循环链表,静态链表,双链表等。对于链表使用,须要注意「头结点」的使用。
class ListNode {
constructor(val) { this.val = val; this.next = null; } } //单链表插入、删除、查找 class LinkedList { constructor(val) { val = val === undefined ? 'head' : val; this.head = new ListNode(val) } // 找val值节点,没有找到返回-1 findByVal(val) { let current = this.head while (current !== null && current.val !== val) { current = current.next } return current ? current : -1 } // 插入节点,在值为val后面插入 insert(newVal, val) { let current = this.findByVal(val) if (current === -1) return false let newNode = new ListNode(newVal) newNode.next = current.next current.next = newNode } // 获取值为nodeVal的前一个节点,找不到为-1,参数是val // 适用于链表中无重复节点 findNodePreByVal(nodeVal) { let current = this.head; while (current.next !== null && current.next.val !== nodeVal) current = current.next return current !== null ? current : -1 } // 根据index查找当前节点, 参数为index // 能够做为比较链表是否有重复节点 findByIndex(index) { let current = this.head, pos = 1 while (current.next !== null && pos !== index) { current = current.next pos++ } return (current && pos === index) ? current : -1 } // 删除某一个节点,删除失败放回false remove(nodeVal) { if(nodeVal === 'head') return false let needRemoveNode = this.findByVal(nodeVal) if (needRemoveNode === -1) return false let preveNode = this.findNodePreByVal(nodeVal) preveNode.next = needRemoveNode.next } //遍历节点 disPlay() { let res = new Array() let current = this.head while (current !== null) { res.push(current.val) current = current.next } return res } // 在链表末尾插入一个新的节点 push(nodeVal) { let current = this.head let node = new ListNode(nodeVal) while (current.next !== null) current = current.next current.next = node } // 在头部插入 frontPush(nodeVal) { let newNode = new ListNode(nodeVal) this.insert(nodeVal,'head') } } 复制代码
固然了,可能还有一些其余的方法我是没有想到的,剩下的能够自行去完成
「链表类的使用」
let demo = new LinkedList() // LinkedList {head: ListNode}
// console.log((demo.disPlay())) demo.push('1232') demo.insert(123, 'head'); demo.push('last value') demo.frontPush('start') demo.remove('head') // demo.remove('last value') // console.log(demo.remove('head')) // demo.push('2132') // demo.insert('不存在的值', '插入失败') //return -1 console.log(demo.findByIndex(1)) console.log((demo.disPlay())) 复制代码
上面的代码片断是测试用到,测试过了,基本上没有上面大问题,固然了,有些细枝末节的地方仍是得注意的,好比findByIndex
这个函数中pos = 0
仍是 pos = 1
问题,取决于本身,还有的话,remove
函数到底能不能删除'head'头节点,这都是没有准确的标准的,这个能够根据本身状况而定,
必定记住,不是惟一标准,你认为能够删除'head'的话,也没有问题。
「双向链表」
双链表以相似的方式工做,但还有一个引用字段
,称为“prev”
字段。有了这个额外的字段,您就可以知道当前结点的前一个结点。
让咱们看一个例子:
绿色箭头表示咱们的“prev”字段是如何工做的。
「结构相似👇」
class doubleLinkNode {
constructor (val) { this.val = val this.prev = null this.next = null } } 复制代码
与单连接列表相似,咱们将使用头结点
来表示整个列表。
对于插入和删除,相比较单链表而言,会稍微复杂一些,由于咱们还须要处理“prev”字段。
「添加操做-双链表」
举个例子吧,固然了,最好的形式就是画图来解决。
让咱们在现有结点 6 以后添加一个新结点 9:
第一步:连接 cur
(结点 9)与 prev
(结点 6)和 next
(结点 15)
第二步:用 cur
(结点 9)从新连接 prev
(结点 6)和 next
(结点 15)
「因此说,作链表题,画图最重要了,画完图,代码也就出来了」
留下来一个问题,若是咱们想在开头
或结尾
插入一个新结点怎么办?
「删除操做-双链表」
举个例子吧👇
咱们的目标是从双链表中删除结点 6
所以,咱们将它的前一个结点 23 和下一个结点 15 连接起来:
结点 6 如今不在咱们的双链表中
留个问题:若是咱们要删除第一个结点
或最后一个结点
怎么办?
画图🤭
代码就不写了,网上不少均可以代码,能够看看人家怎么写的
让咱们简要回顾一下单链表和双链表的表现。
它们在不少操做中是类似的
在 O(1) 时间内删除第一个结点
。
在 O(1) 时间内在给定结点以后或列表开头添加一个新结点
。
随机访问数据
。
可是删除给定结点(包括最后一个结点)时略有不一样。
O(N)
时间来找出前一结点。
O(1)
时间内删除给定结点。
对比一下链表与其余数据结构(数组,队列,栈)之间时间复杂度
的比较:
通过此次比较,咱们不可贵出结论:
❝若是你须要常常添加或删除结点,链表多是一个不错的选择。
若是你须要常常按索引访问元素,数组多是比链表更好的选择。
❞
接下来也就是本文的重点,从理论到实际出发,看看有哪些题型吧👇
接下来的题型梳理是按照我的刷题顺序的,难易程度,也会作个划分,能够参考一下。
主要作题网站👇
题目描述:将两个升序链表合并为一个新的 「升序」 链表并返回。新链表是经过拼接给定的两个链表的全部节点组成的。
❝连接:[力扣]合并两个有序链表
❞
「示例:」
输入:1->2->4, 1->3->4 输出:1->1->2->3->4->4 复制代码
非递归思路:
模拟题+链表
思路固然简单,重要的是模拟过程,在算法程度上,这种题目能够较为模拟题,模拟你思考的过程,每次比较两个l1.val 与l2.val的大小,取小的值,同时更新小的值指向下一个节点
主要注意的就是循环终止的条件:当二者其中有一个为空时,即指向null
最后须要判断两个链表哪一个非空,在将非空的链表与tmp哨兵节点链接就好。
var mergeTwoLists = function (l1, l2) {
let newNode = new ListNode('start'), // 作题套路,头节点 tmp = newNode; // tmp做为哨兵节点 while (l1 && l2) { // 循环结束的条件就是二者都要为非null if(l1.val >= l2.val) { tmp.next = l2 l2 = l2.next }else{ tmp.next = l1 l1 = l1.next } tmp = tmp.next // 哨兵节点更新指向下一个节点 } // 最后须要判断哪一个链表还存在非null tmp.next = l1 == null ? l2 : l1; return newNode.next; }; 复制代码
递归思路: 「递归解法要注意递归主题里每次返回值较小得节点,这样才能保证咱们最后获得得是链表得最小开头」
一开始的作法就是模拟+链表,可是看见讨论区中有递归写法,绝对仍是好好看一遍。一题多解仍是很重要的,这也在某种程度上发散了思惟,仍是提倡多解。
题目描述:实现一种算法,找出单向链表中倒数第 k 个节点。返回该节点的值。
❝ ❞
双指针写法👇
搞俩个先后指针,先让后指针走k,接着两个指针就相差k步,最后遍历后指针,当后指针为null时,前指针就是答案,由于一开始他们两就是相差k距离
题目描述:反转一个单链表。
❝ ❞
「示例:」
输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL 复制代码
思路:迭代 三个指针 prev curr next 前指针 当前指针 下一个指针
小技巧:一开始把哨兵节点设置为null,curr设置为head
一直迭代下取,知道curr当前节点为尾节点
var reverseList = function (head) {
if(!head) return null let prev = null, curr = head while( curr != null) { let next = curr.next; curr.next = prev prev = curr curr = next } return prev }; 复制代码
递归写法
以前讲过思路了,咱们之间看代码吧
var reverseList = function(head) {
let reverse = (prev,curr) => { if(!curr)return prev; let next = curr.next; curr.next = prev; return reverse(curr,next); } return reverse(null,head); }; 复制代码
题目描述:反转从位置 m 到 n 的链表。请使用一趟扫描完成反转。
「说明:」 1 ≤ m ≤ n ≤ 链表长度。
❝ ❞
「示例:」
输入: 1->2->3->4->5->NULL, m = 2, n = 4 输出: 1->4->3->2->5->NULL 复制代码
跟上一题差很少,换汤不换药,因此咱们仍是能够用迭代的作法来完成。
须要记录两个节点,tail和front节点
两个节点做用就是为了最后区间反转后,好从新链接成一个新的链表。
var reverseBetween = function (head, m, n) {
let count = n-m, newNode = new ListNode('head'); tmp = newNode; tmp.next = head; // 哨兵节点,这样子同时也保证了newNode下一个节点就是head for(let i = 0; i < m -1; i++ ){ tmp = tmp.next; } // 此时循环后,tmp保留的就是反转区间前一个节点,须要用front保留下来 let front, prev, curr,tail; front = tmp; // 保留的是区间首节点 // 同时tail指针的做用是将反转后的连接到最后节点 prev = tail = tmp.next; // 保留反转后的队尾节点 也就是tail curr = prev.next for(let i = 0; i < count; i++ ) { let next = curr.next; curr.next = prev; prev = curr curr = next } // 将本来区间首节点连接到后结点 tail.next = curr // font是区间前面一个节点,须要连接的就是区间反转的最后一个节点 front.next = prev return newNode.next // 最后返回newNode.next就行,一开始咱们指向了head节点 }; 复制代码
题目描述:给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。「你不能只是单纯的改变节点内部的值」,而是须要实际的进行节点交换。
❝ ❞
「示例:」
给定 1->2->3->4, 你应该返回 2->1->4->3.
复制代码
迭代思路,套路,加个tmp哨兵节点就行哒,还不懂的话,画图解决一切,实在看不懂的话,看这个图
var swapPairs = function (head) {
let newNode = new ListNode('start'); newNode.next = head, // 链表头节点套路操做 tmp = newNode; // tmp哨兵节点,这里要从newNode节点开始,并非从head开始的 while( tmp.next !== null && tmp.next.next !== null) { let start = tmp.next, end = start.next; tmp.next = end start.next = end.next end.next = start tmp = start } return newNode.next // 返回的天然就是指向 链表头节点的next指针 }; 复制代码
固然了,面试的时候要真的写,画图应该能够的吧,看着图来写,就轻松了,讲真的,我递归写法✍想不出来,我好蠢🤭
题目描述:给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
说明:k 是一个正整数,它的值小于或等于链表的长度。若是节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
❝连接:[K 个一组翻转链表](https://leetcode-cn.com/problems/swap-nodes-in-pairs/)
❞
示例 :
给定这个链表:1->2->3->4->5 当 k = 2 时,应当返回: 2->1->4->3->5 当 k = 3 时,应当返回: 3->2->1->4->5 复制代码
先看题解,leetcode⭐⭐⭐难题的话,不须要去浪费时间本身去思考,能够看看别人的思路,把别人思路搞明白,最后转换为本身的思路很重要。看完真的就顿悟了,就知道该怎么实现了。
start
指针表明的含义就是
start
记录的信息是当前分组的起始节点位置的前一个节点。
end
指针表明的含义就是要区间翻转的后一个节点。
start
指向翻转后链表, 区间
(start,end)
中的最后一个节点, 返回
start
节点。
front.next = cur
在来举个例子,head=[1,2,3,4,5,6,7,8], k = 3
看不到就本身画个图,而后结合代码多看几遍吧,难题就要多看着写几遍,天然就有感受了。
「关键点分析」
var reverseKGroup = (head, k) => {
let reverseList = (start, end) => { let [pre, cur] = [start, start.next], front = cur; // 终止条件就是cur当前节点不能等于end节点 // 翻转的套路 while( cur !== end) { let next = cur.next cur.next = pre pre = cur cur = next } front.next = end // 新翻转链表须要链接,也就是front指向原来区间后一个节点 start.next = pre // 新翻转的开头须要链接start.next return front // 返回翻转后须要链接链表,也就是front指向 } let newNode = new ListNode('start') newNode.next = head; let [start, end] = [newNode,newNode.next], count = 0; while(end !== null ) { count++ if( count % k === 0) { // k个节点翻转后,又从新开始,返回值就是end节点前面一个 start = reverseList(start, end.next) end = start.next }else{ //不是一个分组就指向下一个节点 end = end.next } } return newNode.next }; 复制代码
好家伙,面试的时候,要我写这个,不让我画图的话,我抽象不出来💢💢
题目描述:合并 k 个排序链表,返回合并后的排序链表。请分析和描述算法的复杂度。
❝连接:[合并K个排序链表](https://leetcode-cn.com/problems/swap-nodes-in-pairs/)
❞
「示例:」
输入: [ 1->4->5, 1->3->4, 2->6 ] 输出: 1->1->2->3->4->4->5->6 复制代码
题目描述:请判断一个链表是否为回文链表。
❝ ❞
「示例:」
输入: [ 1->4->5, 1->3->4, 2->6 ] 输出: 1->1->2->3->4->4->5->6 复制代码
「示例 1:」
输入: 1->2
输出: false 复制代码
「示例 2:」
输入: 1->2->2->1
输出: true 复制代码
解题思路:
找到链表中点,而后将后半部分反转,就能够依次比较得出结论了。
关键就是怎么去找中点呢?
「快慢指针」
这个在链表中应用太普遍了,思路就是:设置一个中间指针 mid,在一次遍历中,head 走两格,mid 走一格,当 head 取到最后一个值或者跳出时,mid 就指向中间的值。
let mid = head
// 循环条件:只要head存在则最少走一次 while(head !== null && head.next !== null) { head = head.next.next // 指针一次走两格 mid = mid.next// 中间指针一次走一格 } 复制代码
遍历的时候经过迭代来反转链表,mid 以前的 node 都会被反转。 使用迭代来反转。
while(head !== null && head.next !== null) {
pre = mid mid = mid.next head = head.next.next pre.next = reversed reversed = pre } 复制代码
例如:
奇数:1 -> 2 -> 3 -> 2 ->1 遍历完成后:mid = 3->2->1 reversed = 2->1 复制代码
偶数:1 -> 2 -> 2 ->1 遍历完成后:mid = 2->1 reversed = 2->1 复制代码
完整代码:
var isPalindrome = function (head) {
if (head === null || head.next === null) return true; let mid = head, pre = null, reversed = null; // reversed翻转的链表 while (head !== null && head.next !== null) { // 常规翻转的套路 pre = mid mid = mid.next head = head.next.next pre.next = reversed reversed = pre } // 判断链表数是否是奇数,是的话mid日后走一位 if (head) mid = mid.next while (mid) { if (reversed.val !== mid.val) return false reversed = reversed.next mid = mid.next } return true }; 复制代码
题目描述:给定两个(单向)链表,断定它们是否相交并返回交点。请注意相交的定义基于节点的引用,而不是基于节点的值。换句话说,若是一个链表的第k个节点与另外一个链表的第j个节点是同一节点(引用彻底相同),则这两个链表相交。
❝ ❞
示例 1:
输入:intersectVal = 8, listA = [4,1,8,4,5], listB = [5,0,1,8,4,5], skipA = 2, skipB = 3 输出:Reference of the node with value = 8 输入解释:相交节点的值为 8 (注意,若是两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [4,1,8,4,5],链表 B 为 [5,0,1,8,4,5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。 复制代码
示例 2:
输入:intersectVal = 2, listA = [0,9,1,2,4], listB = [3,2,4], skipA = 3, skipB = 1 输出:Reference of the node with value = 2 输入解释:相交节点的值为 2 (注意,若是两个列表相交则不能为 0)。从各自的表头开始算起,链表 A 为 [0,9,1,2,4],链表 B 为 [3,2,4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。 复制代码
示例 3:
输入:intersectVal = 0, listA = [2,6,4], listB = [1,5], skipA = 3, skipB = 2 输出:null 输入解释:从各自的表头开始算起,链表 A 为 [2,6,4],链表 B 为 [1,5]。因为这两个链表不相交,因此 intersectVal 必须为 0,而 skipA 和 skipB 能够是任意值。 解释:这两个链表不相交,所以返回 null。 复制代码
思路:
var getIntersectionNode = function (headA, headB) {
let p1 = headA, p2 = headB; while (p1 != p2) { p1 = p1 === null ? headB : p1.next p2 = p2 === null ? headA : p2.next } return p1 }; 复制代码
选一部分题目出来,但愿对你们算是一个抛砖引玉的过程吧,也算是对自个人总结,接下来还会继续刷题的,须要继续跟着我刷题的话,能够看看下面噢👇
若是你以为这篇内容对你挺有有帮助的话:
点赞支持下吧,让更多的人也能看到这篇内容(收藏不点赞,都是耍流氓 -_-)
欢迎在留言区与我分享你的想法,也欢迎你在留言区记录你的思考过程。
以为不错的话,也能够看看往期文章:
[诚意满满👍]Chrome DevTools调试小技巧,效率➡️🚀🚀🚀
[1.1W字]写给女朋友的秘籍-浏览器工做原理(渲染流程)篇