一、链表基础知识
- 单向链表的访问特点:只能从前向后依次访问
- 与访问数组不同的是数组可以直接访问中间位置的任意元素,而链表必须从头乖乖的一步一步走到访问节点。(数组给出下标我们可以直接定位到元素,而链表需要从头一步步查找)
- 链表结构只是通过指针域控制的,我们可以通过改变指针域的指向来实现插入/删除的目的;
- 单向链表再加一个指针可以构造成双向链表,二叉树也是在单向链表的基础上多一个指针的;看似相同,但深究其中相差还是很大的;
- 链表思维就是唯一指向的思维
- 链表中的每个节点至少包含两个成分:数据域和指针域;
- 链表中的每个节点,通过指针域的值形成一个线性结构;
- 查找节点O(n),插入节点O(1),删除节点O(1);
- 不适合快速定位数据,适合动态的插入和删除数据的应用场景
二、链表的典型应用场景
1、操作系统内的动态内存分配
2、LRU缓存淘汰算法
LRU (Least Recently Used) 意思就是近期最少使用算法,它的核心思想就是会优先淘汰那些近期最少使用的缓存对象。
缓存是高速设备之于低速设备的称呼
链表与数组结合在一起使用的叫块状链表,适用于那种动态扩容的线性表
三、经典面试题
1、链表的访问
leetcode 141.环形链表
- 题目简述
- 实现目标:判断如果链表中存在环,则返回 true 。 否则,返回 false 。
示例:
输入: head = [3,2,0,-4], pos = 1
输出: true
解释: 链表中有一个环,其尾部连接到第二个节点。
- javascript实现 两种方法实现,1、通过给遍历过的节点添加flag标志位。 2、通过快慢指针方法
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {boolean}
*/
var hasCycle = function(head) {
// 对遍历过的节点,添加flag标志
// if(!head || !head.next) return false
// let p = head;
// while(p && p.next) {
// if(p.flag){
// return true
// }else {
// p.flag = '1'
// p = p.next
// }
// }
// return false
// 快慢指针
if(!head || !head.next) return false
let p = q = head;
while(q && q.next) {
p = p.next;
q = q.next.next;
if(p == q) return true
}
return false
};
- 视频讲解:链接
leetcode 142.环形链表II
- 题目简述
- 实现目标:给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null 。 示例:
输入: head = [3,2,0,-4], pos = 1
输出: 返回索引为 1 的链表节点
解释: 链表中有一个环,其尾部连接到第二个节点。
- javascript实现 解题思路:快慢指针法
- 1⃣️ 首先确定是否有环
- 2⃣️ 快慢指针焦点到环起始点 == 头节点到环起始点 ?
我们假设链表头到环起始点的距离为a,从环起始点到相遇点的距离为b,从相遇点到环起始点的 距离为c。
慢指针走过a+b的距离,快指针走过a+n(b+c)+b的距离。由于快指针是慢指针的二倍,所 以:2(a+b)= a+n(b+c)+b,而我们实际上并不用关心n是多少,有可能是10,也有可能是1,因此 上述公式可以简化为:a=c;
a+b = n(b+c),n = 1; a = c;
另外一种理解是:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
/**
* @param {ListNode} head
* @return {ListNode}
*/
var detectCycle = function(head) {
// 添加flag标志位,一旦发现节点有flag,说明是再次到达,这时是环的起始点;
// if(head == null) return null;
// let p = head;
// while (p.next) {
// if(p.flag) {
// return p;
// }else {
// p.flag = true;
// p = p.next;
// }
// }
// return null
// 快慢指针
if(!head || !head.next) return null
let p = q = head;
while(q && q.next) {
p = p.next;
q = q.next.next;
if(p == q) {
// 是有环的
// 在pq的交点到环起始点 == 头到环起始点
// 1、定义一个变量记录从头开始向后走
// 2、慢指针p继续往后走
let start = head;
while(start != p) {
start = start.next;
p = p.next;
}
return start
}
}
return null
};
leetcode 202.快乐数
- 题目简述
- 编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」定义为:
- 对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
- 然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
- 如果 可以变为 1,那么这个数就是快乐数。
如果 n 是快乐数就返回 true ;不是,则返回 false 。
示例:
输入: n = 19
输出: true
解释: 12 + 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02 + 02 = 1
- javascript实现
示例分析: 19 -> 82 -> 68 -> 100 -> 1
- 链表思维就是唯一指向的思维;
- 把19,82,68,100看成链表中的节点;
- 把他们之间的转换规则看成链表的指针;
- 把 1 看成链表的空地址null;
- 链表中如果有环,链表就不会有空地址null;
//链表判环-快慢指针法
function getNext(num) {
let sum = 0;
while(num) {
// 求平方和
sum += (num%10) * (num%10)
num = parseInt(num/10)
}
return sum;
}
var isHappy = function(n) {
if(n==1 || getNext(n)==1) return true
let p = q = n;
while(q && getNext(q) && q != 1) {
p = getNext(p)
q = getNext(getNext(q))
if(p == q) {
return false
}
}
return true
};
2、链表的反转
如: 一个链表 1 -> 2 -> 3 -> 4 -> null; 反转之后就是 4 -> 3 -> 2 -> 1 -> null
- 如何实现链表反转
- 定义一个空指针 pre;
- 再设置一个cur和n指针,cur->1, n -> 2;
- 让cur.next -> pre, pre -> cur; cur = n, n = n.next;
- cur 是未反转链表的头节点,
- pre 是反转后的头节点,
- n 是未反转的头节点(cur)的下一位;
leetcode206.反转链表
- 题目简述
- 给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
示例:
输入: head = [1,2,3,4,5]
输出: [5,4,3,2,1]
- javascript实现
/**
* 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) {
// 法1
// if(head == null) return head;
// // pre: 反转后的头节点
// // cur: 未反转的头节点
// // n: 未反转的头节点的下一个节点
// let pre = null, cur = head, n = head.next;
// while(cur) {
// cur.next = pre;
// pre = cur;
// // 避免当n 为空时错误
// (cur = n) && (n = n.next)
// }
// //单链表可以用头指针的名字来命名
// return pre
// 法2
// 基于递归的回溯过程来反转单链表
// 解题思路。递归的回溯过程就是逆序的遍历链表
// 例: 链表 1 -> 2 -> 3 -> 4 -> 5 -> null
// 为方便理解这里节点直接用数字代替
// 1、递归到链表的最后一个节点 5 后,5.next == null,直接返回 5;
// 2、然后执行节点4,期望5.next = 4, 就是相当于 4.next.next = 4 5->4反转完成;让4节点指向null
// 往后一次类推,最后 5 -> 4 -> 3 -> 2 -> 1 -> null
if(head == null || head.next == null) return head;
let pre = head, cur = reverseList(head.next);
pre.next.next = head;
head.next = null
return cur
};
leetcode92. 反转链表II
- 题目简述
- 给你单链表的头指针 head 和两个整数left 和 right ,其中left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。 示例:
输入: head = [1,2,3,4,5], left = 2, right = 4
输出: [1,4,3,2,5]
- 解题思路
- 先找到待反转区域的前一位 (记作 pre)
这时我们需要创建一个虚拟头节点(是自己创建的一个真实的链表节点,它不是指针也不是引用)。
为什么要虚拟头节点呢?
因为如果待反转区域是从链表的头节点开始时,这时的待反转区域就没有前一位了,为了方便操作,我们可以在原链表头节点之前创建一个虚拟头节点;
通常什么时候需要虚拟头节点?
链表头地址有可能改变时,借助虚拟头节点可以方便我们操作链表。
- 然后把 pre 之后的所有节点看成一个单独的链表, 并反转这个链表的前n个节点(n = right - left + 1);
- 最后再把 pre 指向 反转链表的头节点;
- javascript 实现
/**
* 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} left
* @param {number} right
* @return {ListNode}
*/
function reverseN(head, num) {
if(num == 1) return head;
// p暂存下一节点
let p = head.next, c = reverseN(head.next, num-1);
// 让当前节点与后一节点的指针对调
head.next = p.next;
p.next = head
return c
}
var reverseBetween = function(head, left, right) {
let ret = new ListNode(0, head);
let pre = ret; //初始pre从虚拟头节点开始
let count = right - left + 1
while(--left) pre = pre.next; // 找到待反转区域的前一位
pre.next = reverseN(pre.next, count); // 让pre 指向反转完成的头节点
return ret.next
};
leetcode25.k个一组翻转链表
- 题目简述 给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
进阶:
你可以设计一个只使用常数额外空间的算法来解决此问题吗? 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
示例:
输入: head = [1,2,3,4,5], k = 2
输出: [2,1,4,3,5]
- javascript实现
/**
* 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} k
* @return {ListNode}
*/
// 每k个节点一组进行反转; 首先看链表是否有k个节点
// 如果够k个节点,每次反转前k个节点
// 设 p = 待反转区域的前一个节点,那么p.next 就是下一组待反转区域的前一个节点
function __reverseN(head, n) {
if(n == 1) return head;
let p = head.next, q = __reverseN(head.next, n-1);
head.next = p.next
p.next = head
return q
}
function reverseN(head, k) {
let p = head;
let n = k
while(--k && p) p = p.next;
if(p == null) return head;
return __reverseN(head, n)
}
var reverseKGroup = function(head, k) {
// 设 p = 待反转区域的前一个节点,那么p.next 就是下一组待反转区域的前一个节点q
let ret = new ListNode(-1, head), p = ret, q = p.next;
// 如果反转后的头节点 == q 的话就意味着没有发生反转;
while((p.next = reverseN(q,k)) != q) {
p = q;
q = p.next;
}
return ret.next
};
leetcode61.旋转链表
- 题目简述 给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
示例:
输入: head = [1,2,3,4,5], k = 2
输出: [4,5,1,2,3]
- javascript实现
/**
* 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} k
* @return {ListNode}
*/
var rotateRight = function(head, k) {
// 记录链表的长度,找到链表最后一个节点让它指向头节点,这就是一个圈啊
if(head == null) return head;
let n = 1, p = head;
while(p.next) p = p.next, n += 1;
p.next = head;
// 当k == n时,相当于转一圈又回到原位了;
k %= n; // 忽略整圈数
// 关键:找尾节点!
// 向左移动k==》把前k个节点移动到后面
// 向右移动k==》把后k个节点移动到前面,相当于把前 n-k 个节点移到后面
k = n - k;
while(k--) p = p.next;
head = p.next;
p.next = null
return head
};
3、链表的节点删除
leetcode19.删除链表的倒数第N个节点
- 给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
示例:
输入: head = [1,2,3,4,5], n = 2
输出: [1,2,3,5]
- javascript实现
/**
* 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}
*/
// 1 -> 2 -> 3 -> 4 -> 5 -> null
// 找到待删除节点的前一个节点p
// 从p 到最后null,间隔n 个节点
// 设置虚拟节点,p;
// 头节点 q,
// 先让q 向后走n 步;
// 这时 p与q 间隔n个节点,
// 然后让p,q 同步向后走,当q为null时,p就是待删除节点的前一个节点
var removeNthFromEnd = function(head, n) {
let ret = new ListNode(0, head),p = ret, q = head;
while(n--) q= q.next;
while(q) {
p = p.next;
q= q.next;
}
p.next = p.next.next;
return ret.next
};
leetcode83.删除排序链表中的重复元素
- 存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除所有重复的元素,使每个元素 只出现一次 。返回同样按升序排列的结果链表。
示例:
输入: head = [1,1,2,3,3]
输出: [1,2,3]
- javascript实现
/**
* 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 deleteDuplicates = function(head) {
if(head ==null) return head;
let p = head;
while(p.next) {
if(p.val == p.next.val) {
p.next = p.next.next
}else {
p = p.next
}
}
return head;
};
leetcode82.删除排序链表中的重复元素II
- 存在一个按升序排列的链表,给你这个链表的头节点 head ,请你删除链表中所有存在数字重复情况的节点,只保留原始链表中没有重复出现的数字。返回同样按升序排列的结果链表。
示例:
输入: head = [1,2,3,3,4,4,5]
输出: [1,2,5]
- javascript实现
/**
* 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 deleteDuplicates = function(head) {
// 头节点可能重复,需要借助虚拟节点
let ret = new ListNode(0, head), p = ret, q;
while(p.next) {
// 当p.next与它下一节点值重复
if(p.next.next && p.next.val == p.next.next.val) {
//先找到不等于p.next的节点
q = p.next.next;
while(q && q.val == p.next.val) q = q.next;
p.next = q;
}else {
p = p.next
}
}
return ret.next
};