1:按照左右半区的方式重新组合单链表
难度:❤️
给定一个单链表的头部节点head,链表长度为N,如果N为偶数,那么前N/2个节点算作左半区,后N/2个节点算作右半区;如果N为奇数,那么前N/2个节点算作左半区,后N/2 + 1个节点算作右半区。左半区从左到右依次记为L1 -> l2 -> …,右半区从左到右依次记为R1 -> R2 ->…,请将单链表调整成L1 -> R1 -> L2 -> R2 -> …的形式。
1.架构
链表的分区域后,合并思想
2.笔记
1.解题思路
首先关注题目要求,找出其特点:
-
将mid = head, right= head.next,将mid进行预处理:如果right每次向右移动两个节点且不为null,则mid右移一位。最终mid将指向左区域的尾结点,故mid+1为右区域首节点。
-
将left(左区域首节点)和right(右区域首节点)进行merge时,须要使用一个临时变量(next),记录被断开的节点。
-
第一次merge完,left和right需要重新赋值,直到left.next = null
left = right.next right = next
-
每次找到左区域的头节点(left)和右区域的投节点(right),然后进行merge
merge的过程就是将left和right拼接
2.遇到的问题
- 为什么next每次移动2,左区域长度就+1
- 如何merge左右两个区域
3.我与大神的代码
代码:
public class Solution{
public static void relocate(Node head){
if (head == null || head.next == null) {
return;
}
//划分左右两区域
Node mid = head;
Node right = head.next;
while (right.next != null && right.next.next != null) {
mid = mid.next;
right = right.next.next;
}
//将左右两区域断开 用作合并完成 退出的条件
mid.next = null;
//合并左右两区域
mergeLR(head, mid.next);
}
private static void mergeLR(Node left, Node right){
//left和right分别代表左右区域首节点
Node next = null;
while (left.next != null) {
next = right.next;
right.next = left.next;
left.next = right;
left = right.next;
right = next;
}
left.next = right;
}
}
class Node{
public int value;
public Node next;
public Node(int value){
this.value = value;
}
}
2:合并两个有序的单链表
难度:❤️
给定两个有序单链表的头节点head1和head2,请合并两个有序链表,合并后的链表依然有序,并返回合并后链表的头节点
1.架构
采用范围(指针)合并的思想
2.笔记
1.解题思路
2.遇到的问题
- pre变量的作用
- next变量的作用
- cur1和cur2初始化的含义
3.我与大神的代码
代码:
public class Solution{
public static Node merge(Node head1, Node head2){
if (head1 == null || head2 == null) {
return head1 != null ? head1 : head2;
}
//cur1总是指向头节点值小的那个 cur2则相反
//这是为了保证第一次能进if (cur1.value <= cur2.value)分支
Node head = head1.value <= head1 ? head1 : head2;
Node cur1 = head == head1 ? head1 : head2;
Node cur2 = head == head1 ? head2 : head1;
Node pre = null;
Node next = null;
while (cru1 != null && cur2 != null) {
if (cur1.value <= cur2.value) {
pre = cur1;
cur1 = cur1.next
}
else{
next = cur2.next;
cur2.next = cur1.next;
cur1.next = cur2;
pre = cur2;
cur2 = next;
}
}
pre.next = cur1 == null ? cur2 : cur1;
return head;
}
}
3:向有序的环形单链表中插入新节点
难度:❤️
一个环形单链表从头节点head开始不降序,同时由最后的结点指回头节点。给定这样一个环形单链表的头节点head和一个整数num,请生成节点值为num的新节点,并插入到这个环形链表中,保证调整后的链表依然有序。
1.架构
双指针确定插入的范围,然后合并
2.笔记
1.解题思路
插入过程分为2种情况
- 插入中间
- 插入到结尾
- num比所有值大
- num比所有值小
如果是2.1直接返回头节点,如果是2.2则须将插入的值作为新的头节点返回
2.遇到的问题
3.我与大神的代码
代码:
public class Solution{
public static Node insertNum(Node head, int num){
Node node = new Node(num);
//如果head为空 则将自己指向本省
if (head == null) {
node.next = node;
return node;
}
Node pre = head;
Node cur = head.next;
while (cur != head) {
if (num >= pre.value && num <= cur.value) {
break;
}
pre = cur;
cur = cur.next;
}
//insert
pre.next = node;
node.next = cur;
//is new head?
return num > head.value ? head : node;
}
}
class Node{
public int value;
public Node next;
public Node(int value){
this.value = value;
}
}
总结:
到此为止,链表的遍历须涉及到两个重要变量,pre和next。pre代表cur前一个结点,next代表cur后一个结点
1.打印两个有序链表的公共部分
难度:❤️
1.解题思路
由于链表是有序的,所以只需同时遍历两个链表即可
2.遇到的问题
3.代码
class Node {
public int value;
public Node next;
public Node(int value) {
this.value = value;
}
}
public class Solution {
public void printCommonPart(Node head1, Node head2) {
System.out.println("Common part:");
while (head1 != null && head2 != null) {
if (head1.value > head2.value) {
head2 = head2.next;
}
else if (head1.value < head2.value) {
head1 = head1.next;
}
else {
System.out.println(head1.value);
head1 = head1.next;
head2 = head2.next;
}
}
System.out.println();
}
}
4.收获
学会了链表的遍历方法
2.在单链表和双链表中删除倒数第K个节点
难度:❤️
1.解题思路
要想删除节点,必须要知道前一个结点(除了链表头节点)
观看视频
2.遇到的问题
- 为什么第二次遍历, k = 0的位置是要删除节点的前一个节点的位置
3.代码
class Node {
public int value;
public Node next;
public Node(int value) {
this.value = value;
}
}
public class Solution {
public Node removeLastKthNode(Node head, int k) {
Node cur = head;
//第一次遍历 k--
while (cur != null) {
k--;
cur = cur.next;
}
//判断k
if (k == 0) {
return head.next;
}
if (k < 0) {
//第二次遍历 k++ 找到要删除的节点的前一个节点
cur = head;
while (++k != 0) {
cur = cur.next;
}
//k = 0
//这里的cur表示的是倒数第k - 1个节点
cur.next = cur.next.next;
}
return head;
}
//单链表换成双链表
public Node removeLastKthNode(DoubleNode head, int k) {
Node cur = head;
//第一次遍历 k--
while (cur != null) {
k--;
cur = cur.next;
}
//判断k
if (k == 0) {
head.last = null;
return head.next;
}
if (k < 0) {
//第二次遍历 k++ 找到要删除的节点的前一个节点
cur = head;
while (++k != 0) {
cur = cur.next;
}
//k = 0
//这里的cur表示的是倒数第k - 1个节点
DoubleNode newNext = cur.next.next;
cur.next = cur.next.next;
//注意删除最后一个节点的情况
if (newNext != null) {
newNext.last = cur;
}
}
return head;
}
}
4.收获
学会了链表结点的遍历和删除操作
3.删除链表的中间节点和a/b处的节点
难度:❤️
1.解题思路
视频讲解
其中视频出现的错误:
- k取整的表达式,必须先把a,b强转成double进行除法运算,然后最后将结果强转为整数
2.遇到的问题
3.代码
public class Solution {
public Node removeMidNode(Node head) {
if (head == null || head.next == null) {
return head;
}
Node pre = head;
if (pre.next.next == null) {
return head.next;
}
Node cur = pre.next.next;
while (cur.next != null && cur.next.next != null) {
pre = pre.next;
cur = cur.next.next;
}
//delete
pre.next = pre.next.next;
return head;
}
public Node removeByRatio(Node head, int a, int b) {
if (head == null || a > b) {
return head;
}
//求链表的长度N
int n = 0;
Node cur = head;
while (cur != null) {
n++;
cur = cur.next;
}
//计算要删除第几个节点 向上取整
int k = (int) Math.ceil(((double)a / (double)b) * n );
if (k == 1){
return head.next;
}
//找到该节点的前一个节点
if (k > 1) {
cur = head;
while (--k != 1) {
cur = cur.next;
}
//delete
cur.next = cur.next.next;
}
return head;
}
}
4.收获
学会了求链表的长度
删除节点一定要找到前一个节点
4.反转单向和双向链表
难度:❤️
1.解题思路
找到节点的前继节点和后继节点
2.遇到的问题
3.代码
public class Solution {
public Node reverseLinkedList(Node head){
Node pre = null;
Node next = null;
//head兼顾遍历变量的作用
while (head != null) {
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
public Node reverseLinkedList(Double head){
Double pre = null;
Double next = null;
//head兼顾遍历变量的作用
while (head != null) {
next = head.next;
head.next = pre;
head.last = next;
pre = head;
head = next;
}
return pre;
}
}
4.收获
学会了如何反转链表及注意到的事项
5.反转部分单向链表
难度:❤️
1.解题思路
视频讲解
2.遇到的问题
- 部分链表反转后连接问题
- 返回新头节点问题
3.代码
public class Solution {
public Node reversePart(Node head, int from, int to) {
Node cur = head;
Node fPre = null;
Node tPos = null;
int n = 0;
while (cur != null) {
n++;
fPre = from - 1 == n ? cur : fPre;
tPos = to + 1 == n ? cur : tPos;
cur = cur.next;
}
if (to < 1 || to > from ||from > n || head) {
return head;
}
//对内部链表进行反转
Node pre = null;
Node next = null;
cur = fPre == null ? head : fPre.next;
Node temp = cur;
while (cur != tPos) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
//将反转后的链表连接到原始链表
//判断是不是新的头节点
if (fPre == null) {
temp.next = tPos;
return pre;
}
fPre.next = pre;
temp.next = tPos;
return head;
}
}
4.收获
巩固了链表的反转操作
6.环形单链表的约瑟夫问题
原问题:❤️
进阶问题:❤️❤️❤️
1.解题思路
- 根据报数得到编号
- B = (A - 1) % i + 1
- 其中A代表报数(假设无穷大),B代表对应的编号,i表示当前环长度
- 根据具体报数s得到i环中的编号和i - 1环中的编号关系
- old = (new + s - 1) % i + 1
- 其中old为i环对应的编号,new为i - 1环对应的编号,s为报数
- 结合两式得
- old = (new + m - 1) % i + 1
- 该式子即为递归函数
- 递归函数返回值是指,在最深的那层递归(i = 1),返回最后存活的节点在长度为1(i = 1)的环中的编号,然后依次返回上层递归,即返回存活节点在长度为2(i + 1)的环中的编号,在环i + 2…i + n中的编号,即找到了存活节点的编号
2.遇到的问题
- 如何得到上述两个式子
- 递归函数的作用
3.代码
public class Solution {
//方法一:不停的删除报到m数的节点 最后返回存活的数
publc Node josefphusKill1(Node head, int m) {
if (head == null || head.next = head || m < 1) {
return head;
}
Node last = head;
while (last.next != head) {
last = last.next;
}
int count = 0;
while (last != head) {
if (++count == m) {
last.next = head.next;
count = 0;
}
else {
last = head;
}
head = last.next;
}
return head;
}
//方法二:直接找到最后生存的节点
publc Node josefphusKill2(Node head, int m) {
if (head == null || head.next = head || m < 1) {
return head;
}
//求链表i的长度
int i = 0;
cur = head;
while (cur != null) {
i++;
cur = cur.next;
}
//在i环中 节点的编号是old
//在i - 1环中 节点的编号是new
//可以得到递归函数(其中s为要删除的这样一个报数):old = (new + s - 1) % i + 1
int live = getLive(i, m);
//找到存活的节点后 返回即可
cur = head;
while (--live != 0) {
cur = cur.next;
}
cur.next = cur;
return cur;
}
private int getLive(int i, int m){
//base case: 只剩下i = 1个节点的环 该节点就是最终存活的
if (i == 1) {
return 1;
}
return ( getLive(i - 1, m) + m - 1) % i + 1
}
}
4.收获
- 函数表达式与递归函数结合
- 编号问题的处理
7.判断一个链表是否是回文结构
原问题:❤️
进阶问题:❤️❤️
1.解题思路
根据回文的特点
- 从左往右读 = 从右往左读
此时有两个突破方向
- 栈
- 出栈等同于从右往左读
- 反转局部链表
- 也等同于从右往左读
2.遇到的问题
- 如何找到链表的 N/2 处的节点
- 如何将局部链表反转后恢复
- 只需记住反转链表第一个节点的前一个结点,即mid的处位置
3.代码
public class Solution {
//method-1:
public boolean isPalindrome1 (Node head){
//利用stack 比较压栈后出栈后的节点
Stack<Node> stack = new Stack<>();
//压栈
Node cur = head;
while(cur != cur) {
stack.push(cur);
cur = cur.next
}
//出栈
cur = head;
while (cur != null) {
if (cur.value != stack.pop().value) {
return false;
}
}
return true;
}
//method-2:
public boolean isPalindrome2 (Node head){
if (head == null || head.next == null) {
return true;
}
//利用stack 比较压栈后出栈后的节点
Stack<Node> stack = new Stack<>();
//寻找右半部分链表第一个节点 奇数则中间数不比较 然后压栈
//不过只压入一半的链表
Node right = head.next;
Node cur = head;
while (cur.next != null && cur.next.next != null) {
right = right.next;
cur = cur.next.next;
}
while (right != null) {
stack.push(right);
right = right.next;
}
//出栈
cur = head;
while (!stack.isEmpty()) {
if (cur.value != stack.pop().value) {
return false;
}
cur = cur.next;
}
return true;
}
//method-3:
public boolean isPalindrome3 (Node head){
if (head == null || head.next == null) {
return true;
}
//寻找中间节点 time:O(n) space:O(1)
Node cur = head;
Node mid = head;
while (cur.next != null && cur.next.next != null) {
mid = mid.next;
cur = cur.next.next;
}
//反转右部分链表节点 time:O(n) space:O(1)
Node pre = null;
Node next = null;
cur = mid.next;
while (cur != null) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
mid.next = pre;
//比较左 右部分链表节点
Node left = head;
Node right = mid.next;
boolean res = true;
while (right != null){
if (left.value != right.value) {
res = false;
break;
}
right = right.next;
}
//恢复被反转的链表部分
pre = null;
next = null;
cur = mid.next;
while (cur != null) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
mid.next = pre;
}
}
4.收获
学会寻找链表的2/N处的位置
8.将单向链表按某值划分成左边小、中间相等、右边大的形式
难度:❤️❤️
1.解题思路
- 原问题
- 将问题扁平化为数组的快排问题
- 进阶问题
- 空间复杂度为O(1)
2.遇到的问题
- 如何进行分区
- 分区后如何连接
3.代码
public class Solution {
public Node listPartition1 (Node head, int pivot){
if (head == null) {
return head;
}
//扁平化数组---快排问题
//求链表长度
int n = 0;
Node cur = head;
while (cur != null) {
n++;
cur = cur.next;
}
Node[] nArr = new Node[n];
cur = head;
for (int i = 0; i < n; i++) {
nArr[i] = cur;
cur = cur.next;
}
partition(nArr, pivot);
for (int i = 1; i < n; i++) {
nArr[i - 1].next = nArr[i];
}
nArr[i - 1].next = null;
return nArr[0];
}
private void partition (Node[] nArr, int pivot){
int small = -1;
int big = nArr.length;
int index = 0;
while (index != big) {
if (nArr[index] < pivot) {
swap(nArr, ++small, index++);
}
else if (nArr[index] = pivot) {
index++;
}
else {
swap(nArr, index++, --big);
}
}
}
private void swap (Node[] nArr, int i, int j){
Node temp = nArr[i];
nArr[i] = nArr[j];
nArr[j] = temp;
}
public Node listPartition2 (Node head, int pivot) {
Node cur = head;
Node next = null;
Node smallHead = null;
Node smallTail = null;
Node equalHead = null;
Node equalTail = null;
Node bigHead = null;
Node smallTail = null;
//分为small equal big三个区域
while (cur != null) {
next = cur.next;
cur.next = null;
if (cur.value < pivot) {
if (smallHead == null) {
smallHead = cur;
smallTail = cur;
}
else {
smallTail.next = cur;
smallTail = smallTail.next;
}
}
else if (cur.value == pivot) {
if (equalHead == null) {
equalHead = cur;
equalTail = cur;
}
else {
equalTail.next = cur;
equalTail = equalTail.next;
}
}
else {
if (bigHead == null) {
bigHead = cur;
bigTail = cur;
}
else {
bigTail.next = cur;
bigTail = bigTail.next;
}
}
cur = next;
}
//将三个区域连接起来
if (smallTail != null) {
smallTail.next = equalHead;
//equalTail可能为空
equalTail = equalTail == null ? smallTail : equalTail;
}
if (equalTail != null) {
equalTail.next = bighead;
}
bigTail.next = null;
return smallHead != null ? smallHead : equalHead != null ? equalHead : bigHead ;
}
}
4.收获
学会了处理链表的分区问题
9.复制含有随机指针节点的链表
难度:❤️❤️
1.解题思路
- 原问题
- 时间复杂度为O(n)
- 利用HashMap数据结构,key为原节点,value为复制原节点后的新节点
- 利用HashMap很容易找到新节点的next节点和rand节点
- 进阶问题
- 时间复杂度围为O(1)
- 利用将复制后的节点设置为原节点的next,这样也容易找到新节点的rand节点
- 比如:
- 原链表:1 -> 2 -> 3 -> null
- 新链表:1 -> 1’ -> 2 -> 2’ -> 3 -> 3’ -> null
2.遇到的问题
- 如何理解题意
- 比如复制是什么含义
- 新节点的next和rand不能指向原来的节点,只能指向新复制出来的节点
- 比如复制是什么含义
- 如何找到新节点的next节点和rand节点
3.代码
class Node {
public int value;
public Node next;
public Node rand;
public Node (int data) {
this.value = data;
}
}
public class Solution {
//方法一 space: O(n)
public Node copyListWithRand1 (Node head){
if (head == null) {
return head;
}
HashMap<Node, Node> map = new HashMap<>();
Node cur = head;
while (cur != null) {
map.put(cur, new Node(cur.value));
cur = cur.next;
}
//设置next和rand
cur = head;
while (cur != null) {
map.get(cur).next = map.get(cur.next);
map.get(cur).rand = map.get(cur.rand);
}
return map.get(head);
}
//方法二 space: O(1)
public Node copyListWithRand2 (Node head){
if (head == null) {
return head;
}
//将每个节点的副本添加为节点的next
Node cur = head;
Node next = null;
while (cur != null) {
next = cur.next;
cur.next = new Node(cur.value);
cur.next.next = next;
cur = next;
}
//接下来关键找到新节点的rand(也为新节点)
cur = head;
next = null;
Node curCopy = null;
while (cur != null) {
next = cur.next.next;
curCopy = cur.next;
curCopy.rand = cur.rand != null ? cur.rand.next : null;
cur = next;
}
//拆分链表
cur = head;
next = null;
curCopy = null;
Node res = head.next;
while (cur != null) {
next = cur.next.next;
curCopy = cur.next;
cur.next = next;
curCopy.next = next != null ? next.next : null;
cur = next;
}
return res;
}
}
4.收获
对陌生数据结构的处理,主要关注新的属性,本题就是rand,关键在于如何找到新节点的next和rand
10.两个单链表生成相加链表
1.解题思路
-
方法一
- 遍历两个链表,将他们的节点组成数字,然后进行相加,更具相加得到的数生成新的链表
- 但是这里可能存在溢出问题(如果链表长度很长),所以此方法并不推荐
-
方法二
- 利用栈,将链表中的节点压栈,出栈时就将节点值相加(即个位数相加),然后生成新的节点
- 这种方法要注意以下几点
- 个位数相加存在进位
- 进位数不可能 大于1
- 生成新节点要和前一个节点连接
-
方法三
- 方法二是通过栈来进行”逆序“操作,但如果将链表进行逆序,就可以减少栈的空间
2.遇到的问题
- 新节点之间的连接问题
3.代码
public class Solution {
public Node addList1(Node head1, Node head2) {
if (head1 == null || head2 == null) {
}
//压栈 进行尾数相加
Stack<Integer> stack1 = new Stack<>();
Stack<Integer> stack2 = new Stack<>();
Node cur = head1;
while (cur != null) {
stack1.push(cur.value);
cur = cur.next;
}
cur = head2;
while (cur != null) {
stack2.push(cur.value);
cur = cur.next;
}
Node node = null; //新建节点
Node pre = null; //前一个节点 用于连接新节点
int n = 0; //当前新节点值
int n1 = 0; //第一个链表节点值
int n2 = 0; //第二个链表的节点值
int ca = 0; //进位数
while (!stack1.isEmpty() || !stack2.isEmpty()) {
pre = node;
n1 = !stack1().isEmpty() ? 0 : stack1().pop;
n2 = !stack2().isEmpty() ? 0 : stack2().pop;
n = n1 + n2 + ca;
node = new Node(n % 10);
ca = n / 10;
node.next = pre;
}
//进位不可能超过2
if (ca == 1) {
//最后还要生成进位节点
pre = node;
node = new Node(1);
node.next = pre;
}
return node;
}
public Node addList2(Node head1, Node head2) {
head1 = reverseList(node1);
head2 = reverseList(node2);
Node c1 = head1;
Node c2 = head2;
Node node = null;
Node pre = null;
int n = 0;
int n1 = 0;
int n2 = 0;
int ca = 0;
while (c1 != null || c1 != null) {
n1 = c1 != null ? c1.value : 0;
n2 = c2 != null ? c2.value : 0;
n = n1 + n2 + ca;
pre = node;
node = new Node(n % 10);
node.next = pre;
c1 = c1 != null ? c1.next : null;
c2 = c2 != null ? c2.next : null;
ca = n / 10;
}
if (ca == 1) {
pre = node;
node = new Node(ca);
node.next = pre;
}
reverseList(head1);
reverseList(head2);
return node;
}
private Node reverseList(Node head) {
Node pre = null;
Node next = null;
Node cur = head;
while (cur != null) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
}
}
4.收获
逆序(反转)链表的两种方式:
1.利用栈
2.对链表本身进行逆序 不过最后要将原链表恢复
11.两个单链表相交的一系列问题
什么是单链表相交?
单链表相交就是链表共享某一连续部分,并且持续到尾结点end(但是尾结点可能进入循环)
1.解题思路
- 先判断两个链表的类型
- list1
- 无环
- 有环
- list2
- 无环
- 有环
- list1
- 无环-无环
- 该情况只需判断end节点是否相等,不相等直接返回null,相等则继续往前找
- 无环-有环
- 这种情况必不共享部分,因为如果共享,则有环的情况,两个链表的end部分必有环
- 有环-无环
- 同上
- 有环-有环
- 判断两个链表进入环的第一个节点是否相等(实际上也是判断end节点是否相等)
- 如果相等,则需往前找第一个相同的节点,如果不相等,则需往环里找到第一个相同的节点
2.遇到的问题
-
题意 ”相交部分“指的时哪里
-
n可能为负值
- 所以要用n = Math.abs(n)
-
getLoopNode()函数的调用时机
3.代码
public class Solution {
//如果有结尾节点 则只需判断结尾节点
//如果没有结尾节点 即结尾部分是个环形链表 则将进入环形链表的第一个节点当作结尾节点 然后比较结尾节点是否相等
//总之还是比较结尾节点
public Node getLoopNode (Node head){
if (head == null || head.next == null || head.next.next == null) {
return null;
}
Node slow = head; //慢指针每次移动一个节点
Node fast = head; //快指针每次移动两个节点
while (fast != slow) {
if (fast.next == null || fast.next.next == null) {
//如果无环 fast首先到终点
return null;
}
slow = slow.next;
fast = fast.next.next;
}
//此时fast与slow相遇:
//将fast设为head 每次移动一个节点 fast与slow再一次相遇的节点则为进入环的第一个节点
fast = head;
while (fast != slow) {
slow = slow.next;
fast = fast.next;
}
return fast;
}
public Node noLoop(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
int n = 0;
Node c1 = head1;
Node c2 = head2;
while (c1 != null) {
n++;
c1 = c1.next;
}
while (c2 != null) {
n--;
c2 = c2.next;
}
//end1 != end2 说明无共享部分
if (c1 != c2) {
return null;
}
c1 = n > 0 ? head1 : head2;
c2 = c1 != head1 ? head1 : head2;
Math.abs(n);
while (n != 0) {
n--;
c1 = c1.next;
}
//第一次相等的节点
while (c1 != c2) {
c1 = c1.next;
c2 = c2.next;
}
return c1;
}
public Node bothLoop(Node head1, Node loop1, Node head2, Node loop2) {
Node c1 = null;
Node c2 = null;
//结尾节点相等的话 则要往前找共享的第一个节点
if (loop1 == loop2) {
c1 = head1;
c2 = head2;
int n = 0;
while (c1 != loop1) {
n++;
c1 = c1.next;
}
while (c2 != loop2) {
n--;
c2 = c2.next;
}
if (c1 != c2) {
return loop1;
}
cur1 = n > 0 ? head1 : head2;
cur2 = cur1 != head1 ? head1 : head2;
n = Math.abs(n);
while (n != 0) {
n--;
cur1 = cur1.next;
}
while (cur1 != cur2) {
cur1 = cur1.next;
cur2 = cur2.next;
}
return cur1;
}
//结尾节点不相等 则往环里寻找第一个相等的节点
else {
c1 = loop1.next;
while (c1 != loop1) {
if (c1 == loop2) {
return loop1;
}
c1 = c1.next;
}
return null;
}
}
public Node getIntegersectNode(Node head1, Node head2) {
if (head1 == null || head2 == null) {
return null;
}
Node loop1 = getLoopNode(head1);
Node loop2 = getLoopNode(head2);
if (loop1 == null && loop2 == null) {
return noLoop(head1, head2);
}
if (loop1 != null && loop2 != null) {
return bothLoop(head1, loop1, head2, loop2);
}
return null;
}
}
4.收获
了解了有环链表和无环链表的特点,并学会求进入环形链表的第一个节点
慢指针、快指针操作有环链表
12.将单链表的每K个节点之间逆序
1.解题思路
- 利用栈将链表反转并拼接
- 直接对链表本身进行操作
2.遇到的问题
- 如何拼接反转后的局部链表
- 增加辅助变量left和right,还要注意拼接第一组的链表
3.代码
public class Solution {
public Node reverseKNodes1(Node head, int K) {
if (head == null || k < 2) {
return head;
}
Stack<Node> stack = new Stack<>();
Node newHead = head;
Node cur = head;
Node pre = null;
Node next = null;
while (cur != null) {
next = cur.next;
stack.push(cur);
if (stack.size() == K) {
pre = resign1(stack, pre, next);
//change head
newHead = newHead == head ? cur : newHead;
}
cur = next;
}
return newHead;
}
//left代表前一组的最后一个节点
//right代表后一组的第一个节点
//返回值表示返回逆序后该组链表的最后一个节点
private Node resign1(Stack<Node> stack, Node left, Node right) {
Node cur = stack.pop();
Node next = null;
//连接前一组 null
if (left != null) {
left.next = cur;
}
while (!stack.isEmpty()) {
next = stack.pop();
cur.next = next;
cur = next;
}
//连接后一组
cur.next = right;
return cur;
}
public Node reverseKNodes2(Node head, int K) {
if (head == null || k < 2) {
return head;
}
int count = 1; //
Node cur = head;
Node pre = null;
Node next = null;
Node start = null;
while (cur != null) {
next = cur.next;
if (count == K) {
start = pre == null ? head : pre.next;
head = pre == null ? cur : head; //
resign2(pre, start, cur, next);
pre = cur;
// start = next; //
count = 0; //
}
count ++;
cur = next;
}
return head;
}
//left:前一组的最后一个节点
//right:后一组的第一个节点
//start:待逆序组的第一个节点
//end:待逆序组的最后一个节点
private void resign2(Node left, Node start, Node end, Node right) {
Node pre = start;
Node cur = start.next;
Node next = null;
while (cur != right) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
if (left != null) {
left.next = end;
}
start.next = right;
}
}
4.收获
加强了链表局部反转(逆序)的一些细节操作,如局部反转需从第2个节点开始连接第1个节点,最后将第一个节点的next指向下一组的第一个节点。需要2个变量完成拼接,left和right
13.删除无序单链表中值重复出现的节点
要求:
方法一:时间复杂度为O(n)
方法二:空间复杂度为O(1)
1.解题思路
- 方法一
- 未限制空间复杂度,可以使用HashSet数据结构(因为存储的是无重复元素)
- 方法二
- 未限制时间复杂度,可以使用O(n^2)的解法
- 思路就是第一次遍历第一个节点,然后在剩余节点中删除重复出现的节点,然后遍历第二个节点,在剩余节点中删除重复出现的节点…
- 未限制时间复杂度,可以使用O(n^2)的解法
2.遇到的问题
- 为什么想到使用HashSet数据结构
- 方法二的遍历及删除细节操作
3.代码
public class Solution {
public void removeRep1(Node head) {
if (head == null) {
return;
}
HashSet<Integer> set = new HashSet<>();
Node cur = head;
Node pre = null;
while (cur != null) {
if (set.contains(cur.value)) {
pre.next = cur.next;
}
else {
set.add(cur.value);
pre = cur;
}
cur = cur.next;
}
}
public void removeRep2(Node head) {
if (head == null) {
return;
}
Node target = head;
Node cur = null;
while (target != null) {
cur = target.next;
pre = target;
while (cur != null) {
if (cur.value == target.value) {
pre.next = cur.next;
}
else {
pre = cur;
}
cur = cur.next;
}
target = target.next;
}
}
}
4.收获
双遍历链表并删除节点的细节代码操作,pre从head开始
HashSet的用法
14.在单链表中删除指定值的节点
1.解题思路
- 方法一
- 利用栈,将等于指定值的节点不入栈
- 方法二
- 对链表本身操作,正常删除指定节点
2.遇到的问题
- pre从null开始的遍历删除操作代码如何编写
3.代码
public class Solution {
//方法一
public Node removeValue1(Node head, int num) {
if (head == null) {
return head;
}
Stack<Node> stack = new Stack<>();
while (head != null) {
if (head.value != num) {
stack.push(head);
}
head = head.next;
}
while (!stack.isEmpty()) {
stack.peek().next = head;
head = stack.pop();
}
return head;
}
//方法二
public Node removeValue2(Node head, int num) {
if (head == null) {
return head;
}
//头节点没有判断到
Node cur = head.next;
Node pre = head;
while (cur != null) {
if (cur.value == num) {
pre.next = cur.next;
}
else {
pre = cur;
}
cur = cur.next;
}
return head.value == num ? head.next : head;
}
}
4.收获
灵活利用栈的特性;
删除链表中的指定值,pre从null开始
15.将搜索二叉树转换成双向链表
1.解题思路
- 方法一
- 利用队列,将BST的中序遍历节点添加到队列
- 然后遍历队列 拼接双向队列
- 方法二
- 递归二叉树,递归函数作用是拼接左右孩子到父节点,返回值为链表的尾结点
- 这里的尾节点进行了特殊处理,原来的尾结点的next是指向null,这里指向头节点,便于找到头节点
2.遇到的问题
- 递归函数如何设计
- 递归函数的流程(作用)
- 递归函数的返回值
- 为什么使用后序遍历
- 因为要将父节点与左右孩子连接,所以必须要知道左右孩子(节点)
3.代码
class Node {
public int value;
public Node left;
public Node right;
public Node(int data) {
this.value = data;
}
}
public class Solution {
public Node convert1(Node head) {
if (head == null) {
return head;
}
Queue<Node> queue = new Queue<>();
inOrderToQueue(queue, head);
head = queue.poll();
Node cur = head;
Node pre = head;
//头节点的left为null
pre.left = null;
while (!queue.isEmpty()) {
cur = queue.poll();
pre.right = cur;
cur.left = pre;
pre = cur;
}
//尾结点的right为null
pre.right = null;
return head;
}
private void inOrderToQueue(Queue<Node> queue, Node head) {
if (head == null) {
return;
}
inOrderToQueue(queue, head.left);
queue.offer(head);
inOrderToQueue(queue, head.right);
}
public Node convert2(Node head) {
if (head == null) {
return head;
}
Node temp = process();
head = temp.right;
temp.right = null;
return head;
}
private Node process(Node head) {
if (head == null) {
return null;
}
Node leftE = process(head.left);
Node rightE = process(head.right);
//因为尾结点进行了特殊处理: 将尾结点的right指向头节点 于是可快速的找到头节点
Node leftS = leftE == null ? null : leftE.right;
Node rightS = rightE == null ? null : rightE.right;
if (leftE != null && rightE != null) {
//形成新的链表 需进行拼接
leftE.right = head;
head.left = leftE;
head.right = rightS;
rightS.left = head;
//将新链表的尾结点的right指向头节点 并返回新链表的尾结点
rightE.right = leftS;
return rightE;
}
else if (leftE != null) {
//右孩子为空
leftE.right = head;
head.left = leftE;
//将新链表的尾结点(head)的right指向头节点 并返回head
head.right = leftS;
return head;
}
else if (rightE != null) {
head.right = rightS;
rightS.left = head;
rightE.right = head;
return rightE;
}
else {
head.right = head;
//返回end节点
return head;
}
}
}
4.收获
链表与二叉树结合的问题;
二叉树递归函数的设计;
对链表的尾结点进行特殊的技巧操作
16.单链表的选择排序
1.解题思路
观看视频
2.遇到的问题
- 每次循环cur如何赋值
- 如果getSmallestPre方法的参数中,head就是该链表中最小的节点,那么又该如何删除
- 头节点是无法删除的,因为头节点没有前继节点(preNode = null)
3.代码
public class Solution {
public Node listSelection(Node head) {
if (head == null) {
return head;
}
Node cur = head;
Node small = null; //未排序链表中值最小的节点
Node smallPre = null; //small前一个结点
Node tail = null; //排序链表中的尾结点
while (cur != null) {
small = cur;
smallPre = getSmallestPre(cur);
if (smallPre != null) {
small = smallPre.next;
smallPre.next = small.next;
}
cur = cur == small ? cur.next : cur;
if (tail == null) {
head = small;
}
else {
tail.next = small;
}
tail = small;
}
return head;
}
//删除最小结点必须找到该节点的前一个节点
//在以head为头节点的链表中寻找
private void getSmallestPre(Node head) {
Node small = head;
Node smallPre = null;
Node cur = head.next;
Node pre = head;
while (cur != null) {
if (cur.value < small.value) {
small = cur;
smallPre = pre;
}
pre = cur;
cur = cur.next;
}
return smallPre;
}
}
4.收获
选择排序的思想;
链表中找最小值并删除
链表总结篇
通过这20几道的算法题,可以将链表类算法题目进行如下总结:
-
链表的结构
- 拆分
- 将链表拆分成多个链表
- 整合
- 将多个链表整合
- 反转
- 将链表(或局部)进行反转
- 拆分
-
链表的遍历
- 链表的长度
- 寻找最小值节点
-
链表的局部操作
- 删除节点
- 增加节点
- 链表排序
-
链表的类型
- 直链
- 环链
-
链表的操作方法
-
双指针
-
pre
-
cur
或者
-
-
针对环形链表
-
last
-
head
或快慢指针
-
fast
-
low
快慢指针用于判断有无环或者进入环的第一个节点
-
-
总之,链表的操作细节麻烦之处在于链表的连接,写这部分代码时必须仔细
本文详细探讨了20余道链表相关的算法题目,包括按照左右半区重组链表、合并有序链表、环形单链表插入新节点、遍历链表找公共部分、删除链表节点等。每道题目都包含了解题思路、遇到的问题以及代码实现,旨在全面覆盖链表操作的各种场景,帮助读者深入理解和掌握链表操作技巧。
169万+

被折叠的 条评论
为什么被折叠?



