与链表相关的概念
链表定义
- 链表是一种线性表数据结构;
- 从底层存储结构上看,链表不需要一整块连续的存储空间,而是通过“指针”将一组零散的内存块串联起来使用;
- 链表中的每个内存块被称为链表的“结点”,每个结点除了要存储数据外,还需要记录上(下)一个结点的地址。
链表特点
- 插入、删除数据效率高,只需要考虑相邻结点的指针改变,不需要搬移数据,时间复杂度是 O(1);
- 随机查找效率低,需要根据指针一个结点一个结点的遍历查找,时间复杂度为O(n);
- 与内存相比,链表的空间消耗大,因为每个结点除了要存储数据本身,还要储存上(下)结点的地址。
单链表(本文只说这种类型)
如图所示:
- 单链表的每个节点只包含一个后继指针;
- 单链表的头结点和尾结点比较特殊,头结点用来记录链表的基地址,是链表遍历的起点,尾结点的后继指针不指向任何结点,而是指向一个空地址NULL。
- 单链表的插入、删除操作时间复杂度为O(1),随机查找时间复杂度为O(n)。
与单链表相关算法
环形链表
题目大意: 在一个给定的链表中,判断是否存在环。
解题思路:
这里会用到两个指针,快指针和慢指针,快指针每次移动两步,慢指针每次移动1步,假设链表中存在环,快指针在移动过程中会再次遇到慢指针。否则快指针会优先到链表最后一个位置结束。
function hasCycle(head) {
if (!head || !head.next) return false
let s = head;
let f = head.next;
while(s!==f && f?.next ) {
f = f.next.next;
s = s.next;
}
return f === s;
}
环形链表 II
题目大意: 在一个给定的链表中,判断是否存在环,如果存在环,返回环形开始的节点。
解题思路: 这道题是在上一题基础延伸,同样需要借助两个指针,快指针和慢指针,它们起始都位于链表的头部,快指针每次移动两步,慢指针每次移动1步,如果链表中存在环,则快指针与慢指针在环中相遇。下面的分析可以参考下图。
假设在环外节点距离为a,慢指针进入环走了距离b与快指针相遇,我们可以得到快指针走过的距离为 a + n(b+c) + b
=> a + (n+1)b + nc
。
因为快指针是慢指针速度的2倍,可以得到 a+(n+1)b+nc=2(a+b)
=> a=c+(n−1)(b+c)
,(n-1)个(b+c)
刚好是一个环,我们可以看出相遇点到环的入口与链表的起点到环的入口的距离一致。
因此只要选中一个指针从头开始,另一指针从相遇点同时向后移动,再次相遇点就是环形链表的入口。
function detectCycle (head) {
if (!head || !head.next) return null;
let slow = fast = head;
while (fast?.next) {
slow = slow.next;
fast = fast.next.next;
if (slow === fast) {
slow = head;
while (slow !== fast) {
slow = slow.next;
fast = fast.next;
}
return slow;
}
}
return null;
}
快乐数
题目大意: 给一个正整数,每一次将该数替换为它每个位置上的数字的平方和。然后重复这个过程直到这个数变为 1,最后可以变成1就是快乐数。
输入: n = 19
输出: true
解释:
12+ 92 = 82
82 + 22 = 68
62 + 82 = 100
12 + 02+ 02 = 1
解题思路: 我们可以把输入的数字看做当前节点的值,指针的下一位为转换后的值。这样就可以看做是一个链表,最后能得到1数,就意味着链表有结束节点,否则就会形成一个环形链表。最后只要按照上面环形链表的判断方式就可以了。(这里有一个边界的思考可以看leetcode官网题解)
function getNext(num) {
let sum = 0;
while(num > 0) {
const d = num % 10;
sum += d*d;
num = Math.floor(num / 10);
}
return sum;
}
function isHappy (n) {
// 快慢指针
let slow = n;
let fast = getNext(n);
while(slow !== fast && fast !== 1) {
slow = getNext(slow);
fast = getNext(getNext(fast));
}
return fast === 1;
};
反转链表
题目大意: 给定一个单链表的头节点 head
,请你反转链表,并返回反转后的链表。
解题思路: 这里我借助了递归,将当前节点的下一个节点记录一下,然后将当前节点的下一个指向位置更换为前一个节点,然后重复上面步骤,直到当前节点为空返回前一个节点,就完成了反转。
function reverse(prev, cur) {
if (!cur) return prev;
const tem = cur.next;
cur.next = prev;
return reverse(cur, tem);
}
function reverseList (head) {
return reverse(null, head);
};
反转链表 II
题目大意: 给定一个单链表的头指针 head 和两个整数 left 和 right ,其中left <= right
反转从位置 left 到位置 right 的链表节点,返回反转后的链表
解题思路: 这道题我采用了一个常规的方式,先找到 left 和 right 区间的链表,将其反转,然后将原left前一位的节点的指向到反转后的节点,反转后最后一位节点指向原right下一位的节点。这样就得到指定区域反转后的链表了。
function reverse(prev, cur) {
if (!cur) return prev;
tem = cur.next;
cur.next = prev;
reverse(cur, tem);
}
function reverseBetween (head, left, right) {
const VNode = new ListNode(-1); // 虚拟节点
VNode.next = head; // 指向头节点
let prev = VNode;
// 找到left节点的前一项
for(i = 0; i < left - 1; i++) {
prev = prev.next;
}
let rightNode = prev;
// 找到right节点
for(i = 0; i < right - left + 1; i++) {
rightNode = rightNode.next;
}
// 知道left节点
const leftNode = prev.next;
// 存一下right节点的下一个节点
const cur = rightNode.next;
// 将待反转的链表与原链表断裂
prev.next = null;
rightNode.next = null;
reverse(null, leftNode);
// 将反转后链表与原链表接连
prev.next = rightNode;
leftNode.next = cur;
return VNode.next;
};
文中部分内容参考:概念,文中图片资源来源于leetcode。