链表基本结构
public class ListNode {
// 当前节点值
int val;
// 所指向的下一节点
ListNode next = null;
ListNode(int val) {
this.val = val;
}
}
// 构建基本链表
int[] arr = {1, 2, 3, 4, 5};
ListNode head = new ListNode(arr[0]);
ListNode tail = head;
for (int i = 1; i < arr.length; i++) {
tail.next = new ListNode(arr[i]);
tail = tail.next;
}
问题关键点
- 链表调整函数的返回值类型,往往是节点类型(注意头节点)
- 处理链表的过程中,先采用画图的方式理清逻辑
- 周围环境需要预先保存(前一节点和后一节点)
- 注意边界
1. 环形链表插值
有一个整数 val,如何在节点值有序的环形链表中插入一个节点值为 val 的节点,并且保证这个环形单链表依然有序。
给定链表的信息,及元素的值 A 及对应的 nxt 指向的元素编号同时给定 val,请构造出这个环形链表,并插入该值。
思路
时间复杂度 O(N)、额外空间复杂度 O(1)
令 pre 节点和 cur 节点同步移动,若插入值满足大于 now 节点且小于 pre 节点则将其插入。
注意考虑头尾边界。
实现
public ListNode insert(int[] arr, int[] nxt, int val) {
if (arr.length == 0) {
return new ListNode(val);
}
ListNode head = new ListNode(arr[0]);
ListNode tail = head;
ListNode ins = new ListNode(val);
// 构建链表
for (int i = 0; i < arr.length - 1; i++) {
tail.next = new ListNode(arr[nxt[i]]);
tail = tail.next;
}
// 插入值
// 若插入值小于头
if (val < head.val) {
ins.next = head;
return ins;
}
ListNode pre = head;
ListNode now = head.next;
while (now != null && val > now.val) {
pre = now;
now = now.next;
}
pre.next = ins;
ins.next = now;
return head;
}
2. 访问单个节点的删除
实现一个算法,删除单向链表中间的某个结点,假定你只能访问该结点。
思路:复制后一个结点的值给当前结点,并将后一个结点删除
问题:复制时可能存在问题(非整型结点情况)、简单复制会影响外部依赖、尾结点处理问题等。
没有较好的解决方法。
下题则为简单删除:
给定带删除的头节点和要删除的数字,请执行删除操作,返回删除后的头结点。链表中没有重复数字
思路
顺序遍历并删除即可。
实现
public ListNode removeNode(ListNode pNode, int delVal) {
if (pNode == null) return null;
if (pNode.val == delVal) return pNode.next;
ListNode pre = pNode;
ListNode cur = pNode.next;
while (cur != null) {
if (cur.val == delVal) {
pre.next = cur.next;
break;
}
pre = cur;
cur = cur.next;
}
return pNode;
}
3. 链表的分化
对于一个链表,我们需要用一个特定阈值完成对它的分化,使得小于等于这个值的结点移到前面,大于该值的结点在后面,同时保证两类结点内部的位置关系不变。
给定一个链表的头结点 head,同时给定阈值 val,请返回一个链表,使小于等于它的结点在前,大于等于它的在后,保证结点值不重复。
思路
简单做法:转化为数组完成,类似荷兰国旗问题,再构建链表。
最优解:遍历过程中把所有节点分为两个小链表,最后组合起来即可。
实现
// 题意要求解法 小于等于在前 大于在后
public ListNode listDivide0(ListNode head, int val) {
if (head == null || head.next == null) return head;
// 为了保证 minHead 与 min 起始相同,先弄一个临时结点 0
ListNode cur = head, min = new ListNode(0), minHead = min,
max = new ListNode(0), maxHead = max;
while(cur != null) {
if (cur.val <= val) {
min.next = cur;
min = cur;
} else {
max.next = cur;
max = cur;
}
cur = cur.next;
}
// 链接大小节点
min.next = maxHead.next;
max.next = null;
head = minHead.next;
return head;
}
4. 打印两个链表的公共值
现有两个升序链表,且链表中均无重复元素。请设计一个高效的算法,打印两个链表的公共值部分。
给定两个链表的头指针 headA 和 headB,请返回一个 vector,元素为两个链表的公共部分。
请保证返回数组的升序。两个链表的元素个数均小于等于 500。保证一定有公共值
思路
链表都不为空时从两个链表的头节点开始遍历:
- 若相等则打印节点值并均后移节点
- 若不等则后移比较小的那个节点
两者其中一个为空时停止遍历。
实现
public int[] findCommonParts(ListNode headA, ListNode headB) {
if (headA == null || headB == null) return null;
ArrayList<Integer> res = new ArrayList<>();
while (headA != null && headB != null) {
if (headA.val == headB.val) {
res.add(headA.val);
headA = headA.next;
headB = headB.next;
} else if (headA.val > headB.val){
headB = headB.next;
} else headA = headA.next;
}
int[] finalRes = new int[res.size()];
for (int i = 0; i < res.size(); i++) {
finalRes[i] = res.get(i);
}
return finalRes;
}
5. 链表的 k 逆序
有一个单链表,请设计一个算法,使得每 K 个节点之间逆序,如果最后不够 K 个节点一组,则不调整最后几个节点。
例如链表 1->2->3->4->5->6->7->8->null,K=3 这个例子。
调整后为,3->2->1->6->5->4->7->8->null。
因为 K==3,所以每三个节点之间逆序,但其中的 7,8 不调整,因为只有两个节点不够一组。
给定一个单链表的头指针 head, 同时给定 K 值,返回逆序后的链表的头指针。
思路
方法一:利用栈实现,逐一压入节点,若栈中存在 k 个节点则弹出。
问题:最后不足 k 个的节点处理需要注意。
方法二:直接在每收集了 k 个节点时进行部分链表逆序
实现
// 方法二的实现
public ListNode inverse1(ListNode head, int k) {
if (k < 2) {
return head;
}
//当前位置
ListNode cur = head;
//开始位置
ListNode start = null;
//开始的前一个
ListNode pre = null;
//end 的后一个
ListNode next = null;
int count = 1;
while (cur != null) {
// 获取当前的下一个
next = cur.next;
// 如果 count 为 k 执行逆序操作
if (count == k) {
// 判断 pre 是否为空,为空 start = head 不为空 start = pre 的下一个节点
start = pre == null ? head : pre.next;
// 判断 pre 是否为空 为空 head 变为当前节点(逆序后当前 cur 为 head 了) 不为空
// head = head 只进行一次判断
head = pre == null ? cur : head;
// 传入前一个节点(起到连接的作用) 首节点 当前节点,当前节点的下一个节点
resign(pre, start, cur, next);
pre = start; // start (第一个值赋给 pre )
count = 0;
}
count++;
cur = next;
}
return head;
}
// 部分逆序
public void resign(ListNode left, ListNode start, ListNode end, ListNode right) {
ListNode pre = start;
ListNode cur = start.next;
ListNode next;
// 逆序开始
// 保留首节点 pre cur ( pre 下一个节点 要转换的 即 cur.next = pre ) 和 next 节点
while (cur != right) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
// 如果不是空
if (left != null) {
left.next = end;
}
start.next = right; // 可以是 right 或者 next
}
6. 链表指定值清除
现在有一个单链表。链表中每个节点保存一个整数,再给定一个值 val,把所有等于 val 的节点删掉。
给定一个单链表的头结点 head,同时给定一个值 val,请返回清除后的链表的头结点,保证链表中有不等于该值的其它值。请保证其他元素的相对顺序。
思路
构建新节点,仅保留与指定值不相等的节点。
注意边界条件。
实现
public ListNode clear(ListNode head, int val) {
ListNode cur = head;
ListNode newHead = null, newTail = null;
while (cur != null) {
if (cur.val != val) {
if (newHead == null) {
newHead = cur;
newTail = newHead;
} else {
newTail.next = cur;
newTail = newTail.next;
}
}
cur = cur.next;
}
if (newTail != null)
newTail.next = null;
return newHead;
}
7. 链表的回文结构
请编写一个函数,检查链表是否为回文。
给定一个链表 ListNode pHead,请返回一个 boolean,代表链表是否为回文。
测试样例:
{1,2,3,2,1} 返回:true
{1,2,3,2,3} 返回:false
思路
方法一:将前一半的节点压入栈中,并将当前节点继续遍历,每遍历一个都与栈弹出的节点相比较,若不同则不是。额外空间复杂度 O(N/2)。
方法二:将链表后半部分逆序,从链表两端开始往中间遍历比较,若不同则不是。不需要额外空间。
方法二需要注意比较完毕时将链表结构调整回原始未逆序结构。
实现
// 方法一:一半入栈
public boolean isPalindrome(ListNode head) {
Stack<Integer> stack = new Stack<>();
ListNode fastCur = head;
ListNode slowCur = head;
int flag = 0;
// 判断奇偶
while (slowCur != null) {
slowCur = slowCur.next;
flag++;
}
slowCur = head;
while (fastCur != null && fastCur.next != null) {
stack.push(slowCur.val);
slowCur = slowCur.next;
fastCur = fastCur.next.next;
}
// 判断最中间是否入栈
slowCur = flag % 2 == 0 ? slowCur : slowCur.next;
while (slowCur != null) {
if (slowCur.val != stack.pop())
return false;
slowCur = slowCur.next;
}
return true;
}
// 方法二
public boolean isPalindrome1(ListNode head) {
ListNode cur = head;
ListNode fastCur = head;
ListNode slowCur = head;
int flag = 0;
// 判断奇偶
while (cur != null) {
cur = cur.next;
flag++;
}
// slow 到达中部 fast 到达末尾
while (fastCur != null && fastCur.next != null) {
slowCur = slowCur.next;
fastCur = fastCur.next.next;
}
ListNode pre = slowCur;
ListNode next;
// 若是奇数正中间那个则跳过比较
slowCur = flag % 2 == 0 ? slowCur : slowCur.next;
cur = slowCur;
// 将链表后半截逆序指向中间
while (cur != null) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
// 中间断开
if (slowCur != null)
slowCur.next = null;
// 从两端开始往中间比较
ListNode leftCur = head;
ListNode rightCur = pre;
// 判断是否回文
while (leftCur != null && rightCur != null) {
if (leftCur.val != rightCur.val) {
return false;
}
leftCur = leftCur.next;
rightCur = rightCur.next;
}
cur = pre;
pre = null;
// 恢复链表结构
while (cur != null) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
while (head != null) {
System.out.println(head.val);
head = head.next;
}
return true;
}
8. 复杂链表的复制
输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点)。
class RandomListNode {
int label;
RandomListNode next = null;
RandomListNode random = null;
RandomListNode(int label) {
this.label = label;
}
}
// next: A -> B -> C
// random: A -> C, B -> A, C -> B.
思路
- 第一次遍历:将复制的节点 A’ 插入到 A 与 B 之间,其后的节点也按照这个方式进行复制。
- 再次遍历:通过 A 的 random 指针找到 C,再从 C 的 random 指针找到 C’,从而将 C’ 赋值给 A‘ 的 random 指针,后续节点操作同上。
- 分割复制的链表与原链表。
实现
public RandomListNode Clone(RandomListNode head) {
if (head == null) {
return null;
}
RandomListNode cur = head;
RandomListNode next;
// 复制所有结点
while (cur != null) {
next = cur.next;
cur.next = new RandomListNode(cur.label);
cur.next.next = next;
cur = next;
}
cur = head;
RandomListNode copyCur;
// 给复制的结点 random 指针赋值
while (cur != null) {
next = cur.next.next;
copyCur = cur.next;
copyCur.random = cur.random != null ? cur.random.next : null;
cur = next;
}
RandomListNode res = head.next;
cur = head;
// 将复制的链表与原链表分割
while (cur != null) {
next = cur.next.next;
copyCur = cur.next;
cur.next = next;
copyCur.next = next != null ? next.next : null;
cur = next;
}
return res;
}
9. 链表判环
如何判断一个单链表是否有环?
有环的话返回进入环的第一个节点的值,无环的话返回 - 1。
如果链表的长度为 N,请做到时间复杂度 O (N),额外空间复杂度 O (1)。
给定一个单链表的头结点 head,请返回所求值。
思路
普通解法:若没有额外空间复杂度限制则可以使用哈希表实现,若后续节点在表中出现过则有环。
最优解:
- 从头节点开始使用快慢两个指针进行遍历
- 若快指针到达 null 则无环
- 若快指针与慢指针相遇,则有环。此时使快指针回到头节点并以一次一步的速度与慢指针同时出发,再次相遇的节点则为入环点。
实现
public int chkLoop(ListNode head) {
if(head == null || head.next == null) return -1;
ListNode slowNode = head.next;
ListNode fastNode = head.next.next;
// 检验是否存在环
while (fastNode != slowNode) {
if (slowNode == null) return -1;
slowNode = slowNode.next;
if (fastNode.next == null) return -1;
fastNode = fastNode.next.next;
}
// 两者再次相遇的地方即为环的入口
fastNode = head;
while (fastNode != slowNode) {
slowNode = slowNode.next;
fastNode = fastNode.next;
}
return slowNode.val;
}
10. 无环单链表判相交
现在有两个无环单链表,若两个链表的长度分别为 m 和 n,设计一个时间复杂度为 O (n + m),额外空间复杂度为 O (1) 的算法,判断这两个链表是否相交。
给定两个链表的头结点 headA 和 headB,请返回一个 bool 值,代表这两个链表是否相交。保证两个链表长度小于等于 500。
思路
注意:
(1)一旦两个链表相交,那么两个链表中的节点一定有相同地址。
(2)一旦两个链表相交,那么两个链表从相交节点开始到尾节点一定都是相同的节点。
首先搞清楚什么叫两个链表相交,相交的意思是 从某个节点开始,两个链表的节点重合
一直到结束。
所有长链表的前面多出来的部分可以不用比较 直接遍历跳过。
解法一:
- 首先遍历两个指针得到两者的长度
- 两者回到头节点,偏长的那个指针先遍历偏长的长度,两者再一起遍历,什么时候相同就是相交。
解法二:可以直接判断最后两个指针是否相同
实现
// 解法一
public boolean chkIntersect(ListNode headA, ListNode headB) {
int lengthA = 0, lengthB = 0;
ListNode curA = headA;
ListNode curB = headB;
while (curA != null) {
lengthA ++;
curA = curA.next;
}
while (curB != null) {
lengthB ++;
curB = curB.next;
}
curA = headA;
curB = headB;
boolean flag = lengthA > lengthB;
int steps = Math.abs(lengthA - lengthB);
if (flag) {
while (steps-- > 0) {
curA = curA.next;
}
} else {
while (steps-- > 0) {
curB = curB.next;
}
}
while (curA != null) {
if (curA == curB) {
return true;
}
curA = curA.next;
curB = curB.next;
}
return false;
}
// 解法二,可以直接判断最后两个指针是否相同
public boolean chkIntersect1(ListNode headA, ListNode headB) {
if (headA == null || headB == null)
return false;
ListNode curA = headA;
ListNode curB = headB;
while (curA.next != null) {
curA = curA.next;
}
while (curB.next != null) {
curB = curB.next;
}
return curA == curB;
}
11. 有环单链表相交判断
如何判断两个有环单链表是否相交?相交的话返回第一个相交的节点,不相交的话返回空。
如果两个链表长度分别为 N 和 M,请做到时间复杂度 O (N+M),额外空间复杂度 O (1)。
给定两个链表的头结点 head1 和 head2。
请返回一个 bool 值代表它们是否相交。
思路
- 若入环点相同则相交
- 若入环点不同则遍历其中一个环,环内遍历时若与另一个入环点相遇则相交。类似于 α 的两个小触角为入环点的情况。
实现
public boolean chkInter(ListNode head1, ListNode head2) {
if(head1 == null || head2 == null) return false;
// 入环点
ListNode loopGate1 = chkLoop(head1);
ListNode loopGate2 = chkLoop(head2);
ListNode cur;
if (loopGate1 == loopGate2) {
// 环入口结点已相交
return true;
} else {
// 找环内的结点
cur = loopGate1.next;
while (cur != loopGate1) {
if (cur == loopGate2) return true;
cur = cur.next;
}
}
return false;
}